工具为什么要做成数据库驱动的工具中心?停用一个工具怎么保证模型真的调不到?
简化版
把工具写死在代码里,新增工具、调整说明、临时下线都要改代码发版;而工具说明本质上是 Prompt,需要改完马上看效果。所以把工具的编码、说明、入参说明、启用状态、排序登记到一张表里,每次执行时现查表构建工具池,执行逻辑仍然在代码里按编码分发,这就是数据库驱动的工具中心。「停用后模型真的调不到」要靠几处一致生效:构建工具池只取启用的;所有执行入口(Agent 回调、管理端手动执行)执行前再查一次状态,挡住运行中途被停用、模型手里还拿着旧清单的情况;页面预览和 Agent 用同一条查询,页面看到的就是模型能调的。另外要处理表和代码不同步:表里有、代码没实现的工具不交给模型;代码里有实现的工具不许删,只能停用。
详细版
mcp_tool / function_tool(工具注册表)
tool_code 工具编码:既是发给模型的工具名,也是执行器分发的依据,唯一
description 发给模型的说明,改它等于调 Prompt
input_schema 入参说明
status 启用 / 停用
sort 顺序
每次执行:
查 status = 启用 → 构建工具池(名称、说明、Schema)→ 交给模型
模型调用 → 执行入口再查一次(存在且启用)→ 按 tool_code 分发到代码里的实现 → 写调用日志
| 生效点 | 做什么 | 挡住什么情况 |
|---|---|---|
| 构建工具池 | 只取启用的工具 | 停用的工具出现在模型面前 |
| 执行入口 | 执行前再查一次是否存在且启用 | 运行中途被停用,模型拿着旧清单调用 |
| 手动执行入口 | 同一个执行方法 | 管理端调试绕过开关 |
| 页面预览 | 和构建工具池共用同一条查询 | 页面显示和模型实际拿到的不一致 |
完整版教学
一、工具写死在代码里的三个问题
最简单的做法是在代码里声明工具,框架自动生成工具定义。它能跑,但对应不了三个真实需求:
| 需求 | 写死在代码里 | 放进工具中心 |
|---|---|---|
| 新增一个工具 | 改代码、打包、发布 | 表里加一行,代码里加一个分发分支 |
| 调整工具说明 | 改代码、发布,看不到立即效果 | 页面改说明,下一次执行生效 |
| 临时下线一个工具 | 删代码或加开关代码 | 页面切「停用」 |
| 知道工具被调了多少次 | 没有 | 调用日志表按工具统计 |
注意工具中心改变的是「模型能看到哪些工具、看到什么说明」,执行逻辑仍然在代码里:表里的编码对应代码里分发器的一个分支。不能把可执行的逻辑也存进数据库,那会引入远比工具选错严重的安全问题。这种进程内的工具中心也不是 MCP 协议:MCP 是模型应用和独立工具服务之间的通信协议,见「MCP 的架构是什么?它在 AI Agent 系统中解决了什么问题?」。
二、工具编码的两个身份
工具编码要加唯一索引,因为它同时承担两个角色:
发给模型的工具名 模型返回 tool_calls 时用它说「我要调哪个」
分发器的路由值 执行器按它找到代码里对应的实现
编码重复,分发器就不知道该用哪一行的定义;编码和代码里的分支对不上,模型调得到、执行器却找不到实现。新增和编辑时都要查重,编辑时要把自己排除掉,否则保存一条记录会被自己拦下。
三、真开关的第一处:构建工具池
每次 Agent 执行前现查表:
select * from mcp_tool where status = '启用' order by sort asc, id asc
把结果包装成工具规格交给模型。模型只知道这几个工具,停用的根本不会出现在请求里。这条查询要成为「模型能调哪些工具」的唯一来源:管理页上的「工具池预览」也调它,页面上看到几个标签,模型就拿到几个工具。
记忆钩子:开关要在所有入口都生效,最好的办法是所有入口共用同一条查询。
四、真开关的第二处:执行时再查一次
只过滤清单还不够。一次 Agent 执行会往返几十轮,工具清单是开始时构建的:
第 1 轮 构建工具池:6 个工具,含 A
第 8 轮 管理员在页面上停用了 A
第 9 轮 模型手里还是第 1 轮的清单,照样调用 A
所以执行入口在真正分发之前再按编码查一次:不存在或已停用,直接拒绝并告诉模型「工具已停用」。管理端的「手动执行」也走同一个执行方法,调试时同样受开关约束。
五、表和代码不同步怎么办
工具中心有两份数据:表里的登记和代码里的实现。它们会不同步,要分情况处理:
| 情况 | 风险 | 处理 |
|---|---|---|
| 表里有,代码没实现 | 模型调了才报「没有实现」,白浪费一次调用 | 构建工具池时过滤掉,页面上标红提示 |
| 代码有实现,表里被删了 | 功能静默消失,页面也看不到这条记录 | 有实现的工具不许删,只能停用 |
| 编码拼写有空格 | 比对不上,工具静默失效 | 保存和比对时都去掉首尾空格 |
「只能停用、不能删」背后的考虑是:停用至少还能在页面上看见,排查时一眼能发现;删掉了,没人会想到去查一张不存在的记录。
六、页面开关要反映真实状态
开关是写库操作,页面上有一个容易忽视的细节:如果用双向绑定,点击瞬间开关就拨过去了,但接口可能失败,界面和数据库就对不上了。更稳的写法是开关的显示值只来自列表数据,点击时调接口,成功后刷新列表才变;失败也刷新一次,把开关拨回数据库里的真实状态。提示文案也要说清语义:「已从工具池移除」「模型下一次执行即可调用」。
七、工具中心还要提供什么
工具池预览: 实时列出启用工具,看到的就是模型能调的
定义预览: 查看真正发给模型的工具 JSON,改完说明立即验证
手动执行: 填入参 JSON 直接执行一次,调试工具本身
调用日志: 每次调用的入参、返回、成败、耗时,按工具统计成功率
手动执行和 Agent 调用最好共用一个执行入口,这样校验、分发、日志只有一套,两边行为完全一致。
八、常见误区与追问
- 误区:工具中心就是把工具逻辑也存进数据库。 表里只存定义和开关,执行逻辑必须在代码里,按编码分发。
- 误区:构建工具池时过滤了停用工具,开关就生效了。 运行中途停用的工具,模型还拿着旧清单,执行入口要再查一次。
- 误区:页面预览单独写一条查询。 预览和 Agent 必须共用同一条查询,否则页面显示和模型实际拿到的会不一致。
- 误区:不用的工具直接删掉最干净。 代码里还有实现的工具被删后功能静默消失,应该停用。
- 误区:页面开关用双向绑定就行。 接口失败时界面和数据库会不一致,显示值应以刷新后的列表为准。
- 追问:表里登记了但代码没实现的工具怎么处理? 构建工具池时过滤掉,并在页面上标出,避免模型白白调用一次。
- 追问:工具编码为什么要唯一? 它既是模型看到的工具名,也是分发器的路由值,重复就无法确定执行哪一行的定义。
九、加强记忆
工具中心把编码、说明、入参说明、状态、排序存进表,每次执行现查表构建工具池,执行逻辑仍在代码里按编码分发;编码唯一,因为它既是工具名又是路由值。真开关要多处一致:构建工具池只取启用的,执行入口再查一次挡住中途停用,手动执行走同一入口,页面预览和 Agent 共用同一条查询。表和代码不同步时,没实现的不给模型,有实现的不许删只能停用。页面开关以真实状态为准,失败要拨回。
项目实战落地
项目里怎么做的
《AI Agent 智慧医院智能导诊就诊系统》的分诊 Agent 能调哪些工具,完全由 mcp_tool 表说了算:
- 注册表:6 个工具,
tool_code加唯一索引,description原样发给模型,input_schema告诉模型要传哪些参数,status是「启用 / 停用」; - 一条查询两处用:
selectEnabled固定是status = '启用'按排序取,页面的「工具池预览」和分诊 Agent 构建工具池共用它,页面上看到的标签就是模型能调的工具; - 统一执行入口:
McpToolExecutorService.execute是手动执行和 Agent 调用共用的唯一入口:按编码查定义,不存在或未启用直接拒绝;解析参数(必须是 JSON 对象);按tool_code分发;成功失败都写mcp_tool_call_log; - 开关写库:行上的开关调
updateStatus只更新状态和更新时间两列,更新失败时重新拉列表,让开关回到数据库里的真实状态。
《AI Agentic RAG高级企业知识库平台》多做了两道处理:构建工具清单时同时要求「表里启用」和「代码里有实现」;执行时再查一次启用状态,挡住模型拿着旧清单调用已停用工具;代码里有实现的工具不允许删除,页面提示改用停用。
《AI 多Agent智能相亲交友匹配平台》的 function_tool 表按分组组织八个工具,每个 Agent 角色绑一个分组,执行时只拿到自己分组里启用中的工具。它把核对提前到了保存这一步:
- 保存时就核对实现。 登记工具时,编码必须能在代码的实现对照表里找到,找不到就拒绝保存,报错里把当前能登记的编码全部列出来。
- 编码登记后不能改。 编码是模型点名、步骤记录和调用日志找这个工具的依据,改了之后历史记录就对不上;要换编码,就新增一条、把旧的停用。
- 参数 Schema 保存时校验。 必须是合法 JSON、顶层是对象、
type为object、properties是对象,每一项不过都给一句不同的提示。
为什么这样取舍
- 执行入口也拦:手动执行和 Agent 调用共用一个入口,即使工具池构建之后、执行之前被停用,也会在真正分发前拦一道。
- 开关以数据库为准:页面开关状态和数据库不一致时,管理员会以为工具已经停了,实际上模型还在调。
面试官还会追问
- 执行器失败分支取错误信息时,为什么要用
getMsg()而不是getMessage()? - 在停用状态下点某个工具的「执行」按钮,这次调用会写进调用日志吗?为什么?
学完《AI Agent 智慧医院智能导诊就诊系统》,上面这些追问你都会迎刃而解。