工具工程化
🎯 学习目标
- 设计集中式工具注册表(Schema、handler、权限级别)
- 按场景配置 auto / required / none 等 tool_choice 策略
- 实现按角色或任务阶段加载的动态工具子集
- 评估 Deferred Loading 对 Token 与误调用的影响
引言
两三个工具的 Demo 可以硬编码在路由里;上到十几个工具后,需要注册表、策略与加载策略才算工程化。本节讲如何把工具当成可治理的 API 产品,而不是 Prompt 附件。
章节正文
第 1 步:集中式工具注册表
type ToolDef = {
name: string
schema: object
handler: (args: unknown) => Promise<unknown>
permission: 'read' | 'write' | 'admin'
requiresConfirm?: boolean
}
const registry = new Map<string, ToolDef>()
export function registerTool(def: ToolDef) {
registry.set(def.name, def)
}
export function listToolsForLlm(filter?: { permission?: string[] }) {
return [...registry.values()]
.filter(t => !filter?.permission || filter.permission.includes(t.permission))
.map(t => ({ type: 'function', function: { name: t.name, parameters: t.schema } }))
}注册表是单一事实来源:文档、OpenAPI、MCP list 都可从同一处生成,避免 Schema 漂移。
第 2 步:tool_choice 策略矩阵
| 场景 | 策略 | 原因 |
|---|---|---|
| 通用问答 | auto | 降低成本与延迟 |
| 必须查实时库存 | required | 禁止模型瞎编 |
| 公开客服无工具 | none | 缩小攻击面 |
| 固定审批步骤 | 指定 function | 流程可控 |
const toolChoice =
session.mode === 'analytics' ? 'required' :
session.toolsEnabled ? 'auto' : 'none'策略应来自会话配置,而非写死在 Prompt 里「请你一定要用工具」。
第 3 步:动态工具集
按用户角色加载子集:
function toolsForUser(user) {
const base = ['search_docs', 'calculator']
if (user.role === 'engineer') base.push('run_readonly_sql', 'query_logs')
if (user.role === 'admin') base.push('deploy_service') // 需确认
return base
}按任务阶段切换:调研阶段只有 search;执行阶段才开放 write 类工具。减少 Prompt 噪音与误调用比「给模型全部能力」更重要。
第 4 步:Deferred Loading 实现要点
两阶段注册:
ToolCatalog:name + oneLineDescription(常驻 system 或 meta 工具)ToolDetail:完整 schema + examples(选中后注入)
可配合小模型或规则做工具路由器,在主模型调用前先 narrowing:
const candidates = await routeTools(userMessage, catalog)
const tools = candidates.map(name => registry.get(name).toOpenAI())测量指标:平均 tools Token 数、误调用率、首轮正确工具命中率。
第 5 步:版本化与兼容
工具 Schema 变更应带版本:search_docs_v2 或 schema 内 version 字段。Breaking change 时并行保留旧工具一段窗口,日志监控旧工具调用量。
为每个工具编写契约测试:给定典型 args,断言 handler 输出形状符合 schema,防止上线后模型大量报错。
动手练习
- 实现 Map 注册表 + listToolsForLlm,至少注册 5 个 mock 工具。
- 为同一用户问题分别用 auto 与 required,记录工具调用率与回答 factual 程度(可人工打分)。
- 实现 engineer vs guest 两套动态工具集,验证 guest 无法调用 write 工具即使 Prompt 注入要求。
- 为 10 个工具写 Deferred Loading 伪代码,估算全量 schema 字符数 vs 按需加载字符数。
- 给一个工具 Schema 做 breaking change,设计双版本共存与下线计划。
常见问题
Q:工具太多,模型还是选错怎么办?
先减工具(动态子集),再加路由器,再优化 description,最后才考虑换更大模型。多数问题是工具集过大而非模型不够聪明。
Q:required 会导致模型强行调用不相关工具吗?
会。required 只应在「无工具就必错」的子流程使用,并配合单一或极少数工具。更常见是 auto + 强 system 约束 + 结果校验。
Q:注册表放前端还是后端?
handler 必须在后端;Schema 可对前端只读暴露用于 UI 展示。绝不在浏览器暴露 write 类 handler 或密钥。
本节小结
工具系统工程化 = 注册表 + 调用策略 + 动态子集 + 按需加载 + 版本治理。把工具当 API 产品维护,Agent 才 scale 得起来。