Skip to content

工具工程化

🎯 学习目标

  • 设计集中式工具注册表(Schema、handler、权限级别)
  • 按场景配置 auto / required / none 等 tool_choice 策略
  • 实现按角色或任务阶段加载的动态工具子集
  • 评估 Deferred Loading 对 Token 与误调用的影响

引言

两三个工具的 Demo 可以硬编码在路由里;上到十几个工具后,需要注册表、策略与加载策略才算工程化。本节讲如何把工具当成可治理的 API 产品,而不是 Prompt 附件。

章节正文

第 1 步:集中式工具注册表

typescript
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流程可控
javascript
const toolChoice =
  session.mode === 'analytics' ? 'required' :
  session.toolsEnabled ? 'auto' : 'none'

策略应来自会话配置,而非写死在 Prompt 里「请你一定要用工具」。

第 3 步:动态工具集

按用户角色加载子集:

javascript
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 实现要点

两阶段注册:

  1. ToolCatalog:name + oneLineDescription(常驻 system 或 meta 工具)
  2. ToolDetail:完整 schema + examples(选中后注入)

可配合小模型或规则做工具路由器,在主模型调用前先 narrowing:

javascript
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,防止上线后模型大量报错。

动手练习

  1. 实现 Map 注册表 + listToolsForLlm,至少注册 5 个 mock 工具。
  2. 为同一用户问题分别用 auto 与 required,记录工具调用率与回答 factual 程度(可人工打分)。
  3. 实现 engineer vs guest 两套动态工具集,验证 guest 无法调用 write 工具即使 Prompt 注入要求。
  4. 为 10 个工具写 Deferred Loading 伪代码,估算全量 schema 字符数 vs 按需加载字符数。
  5. 给一个工具 Schema 做 breaking change,设计双版本共存与下线计划。

常见问题

Q:工具太多,模型还是选错怎么办?

先减工具(动态子集),再加路由器,再优化 description,最后才考虑换更大模型。多数问题是工具集过大而非模型不够聪明。

Q:required 会导致模型强行调用不相关工具吗?

会。required 只应在「无工具就必错」的子流程使用,并配合单一或极少数工具。更常见是 auto + 强 system 约束 + 结果校验。

Q:注册表放前端还是后端?

handler 必须在后端;Schema 可对前端只读暴露用于 UI 展示。绝不在浏览器暴露 write 类 handler 或密钥。

本节小结

工具系统工程化 = 注册表 + 调用策略 + 动态子集 + 按需加载 + 版本治理。把工具当 API 产品维护,Agent 才 scale 得起来。