tool 重设计(合并+收窄)
Day 42 给工具体检(标出五类反模式),Day 44 算出 tools 配额常超额——今天动手治:把重叠工具合并、给 description 写「何时用/何时不用」、给参数加 enum/required 收窄取值域。
阶段: B5 · tool/context engineering + 指标树(Day 41-50) 标签: #tool-redesign #enum-schema #single-responsibility #ab-ready
今日导引(由浅入深)
Day 42 给工具体检(标出五类反模式),Day 44 算出 tools 配额常超额——今天动手治:把重叠工具合并、给 description 写「何时用/何时不用」、给参数加 enum/required 收窄取值域。
这是 B5 里第一次从「度量与标注」转向「真正改代码」。它紧接前几天,因为没有 Day 42 的反模式清单和 Day 44 的预算超额,重设计就是无的放矢;它通向 Day 46,因为 v2 必须与 v1 并存才能跑受控 A/B(同任务集、同模型、同 judge,只切工具版本)。
今天的「最小可判定产出」:在 toolRegistry.ts 新增 v2 工具集 + 单测,pnpm test 全绿、tsc clean,不破当前 378 tests passing 基线。
1. 机理精读
定义。 工具重设计的三原则:
- 单一职责(single responsibility):一个工具只干一件事,杜绝重叠——消除模型「选哪个」的歧义。
- description 显式写「何时用 / 何时不用」:把调用时机判据写进描述,含负例(何时不用),直接降低误用率。
- schema 用 enum / required 收窄取值域:把自由参数约束成枚举/必填,缩小模型能填的非法/语义错值空间。
参考 Anthropic《Writing effective tools for agents》(2025-09)。
为什么收窄 schema 等价于缩小动作空间。 模型选工具 + 填参数,本质是在一个离散+连续混合的动作空间里采样。
自由 string 参数让连续维度无界,模型出错面随之膨胀;改成 enum: [...] 后,该维度坍缩成有限选项,非法值在调用前就被 validate() reject(见走读),合法值里也少了语义歧义。
所以「收窄 schema」不是装饰,而是从数学上把动作空间体积压小、把策略正确率抬高的杠杆。这正回应 Day 44 预算表里 tools 超额——更窄的 schema 往往也更短,顺带省 token。
为什么 description 写负例。 模型缺的不是「这工具能干嘛」,而是「这个 query 该不该用它」。
只写功能("查代币价格")会让模型在边界 case 误用;加一句「何时不用」("不要用它查钱包余额——用 getWalletBalance")等于在 description 里植入路由规则,把 Day 42 的「重叠/模糊」反模式从源头堵掉。
关键权衡:合并 vs 表达力。 把两个重叠工具合并成一个带 enum 的工具,减少了选择面,但若强行合并语义不同的动作,会造出胖参数(又回到反模式 3)。
所以合并的判据是「同一职责的多入口」而非「凑数减个数」。同时保留 v1 不动——这是为 Day 46 A/B 做的工程纪律:要归因到工具设计,必须只切这一个变量。
与相邻概念的边界。 重设计改的是设计层(工具的 spec 怎么写),机制层(validate / call / 错误契约)不动。
机制保证 enum 被强制执行,但「该不该加这个 enum」是今天的设计决策。它也不同于 Day 46 的 A/B:今天产出 v2 + 单测(证明 v2 自身正确),明天才用同 judge 跑 v1 vs v2 比通过率(证明 v2 更好)。
2. 推导 / 手算 / 代码走读
代码日:走读 src/agent/mcp/toolRegistry.ts 里支撑「收窄」的机制,确认 v2 设计可落地。
validate(schema, value)对 enum 的强制:if (schema.enum && !schema.enum.includes(value)) errs.push('value … not in enum …');- 给参数加
enum: ['eth','btc',…]后,模型传枚举外的值会在call()里被McpCallError(INVALID_PARAMS)挡下; - 即收窄即可执行,不是文档约定。
validate对 required 的强制:for (const req of schema.required ?? []) if (!(req in obj)) errs.push('required property missing');- required 让"漏填关键参数"变成可检测错误。
validate的其他收窄手段:还支持minLength、minimum/maximum——可进一步收窄 string/number 取值域。register(spec, handler):- 重名抛
already registered、name 非法抛错、inputSchema.type !== 'object'抛错; - 新增 v2 必须用不同 name(如
getPrice.v2),否则重名直接抛错——这天然保证 v1/v2 并存而不冲突。
- 重名抛
list():按 name 稳定排序返回全部 spec——v1 与 v2 会同时出现在 tools/list,A/B 时按版本筛选子集。- 现有测试基线:v1 单测在
src/agent/mcp/__tests__/toolRegistry.test.ts(覆盖validate/enum/required/McpCallError/JSON-RPC dispatch,已绿)。- v2 工具集 + 其单测是今天要「待建」的——v1 测试不动,新增 v2 用例,保持整体绿。
走读结论:机制层已完整支持 enum/required/min 约束,所以「收窄 schema」是纯设计+注册工作,无需改 validate;今天的代码量集中在新增 v2 McpToolSpec(合并重叠、写负例 description、加 enum)+ 对应单测。
v1→v2 spec 重设计范例(示意,体现三原则):
// v1(胖参数 + 模糊 description,命中反模式 2/3)
{ name: 'query', description: 'query crypto data',
inputSchema: { type:'object',
properties: { kind:{type:'string'}, arg:{type:'string'} } } }
// v2(单一职责 + 负例 description + enum/required 收窄)
{ name: 'getAssetMetric.v2',
description: '查某资产的单一指标。何时用:要 price/balance/supply 之一时。何时不用:要历史时间序列时(用 getSeries)。',
inputSchema: { type:'object', required:['asset','metric'],
properties: {
asset: { type:'string', minLength:1 },
metric: { type:'string', enum:['price','balance','supply'] } } } }
要点:metric 从自由 string 收成 enum(三选一,越界即被 validate reject);加 required 杜绝漏填;description 写明「何时不用」。这就是把动作空间体积压小的最小手术。
3. 今日实战
- 在
src/agent/mcp/toolRegistry.ts(或配套注册文件)新增 v2 工具集:- 合并 Day 42 标注的重叠工具为单一职责工具;
- 给每个 v2 工具 description 加「何时用 / 何时不用」(含负例);
- 给参数加
enum/required/minimum等收窄取值域。
- 保持 v1 不动(不同 name 注册),以便 Day 46 跑 A/B。
- 为 v2 写单测:覆盖「合法入参通过、枚举外值被 reject、缺 required 报错」。
- 跑
pnpm test确保现有单测(含src/agent/__tests__/eval/tokenizer.test.ts与src/agent/mcp/__tests__/toolRegistry.test.ts)仍绿,tscclean。
4. 今日实测 / 产出
- v2 工具注册 + 单测为「待建」(本日实现)。
- 目标产出:passing test——toolRegistry v2 单测全绿,整体保持当前 378 tests passing、tsc clean 的基线不破。
(诚实状态:v2 是本日要落地的代码,标「待建」;378 tests passing / tsc clean 是当前基线数字,逐字保留,v2 不得破坏它。)
5. 常见误区 / 陷阱
- 为减数量强行合并语义不同的工具:会造出胖参数,回到反模式 3——合并只针对「同职责多入口」。
- 改 v1 而非新增 v2:破坏 A/B 的单变量前提(Day 46 就无法把 Δ 归因到工具设计);且
register重名会直接抛错。 - enum 只写进 description 不写进 schema:那是文档约定,
validate()不强制;必须落到inputSchema.enum才会在call()里被 reject。 - description 只加"何时用"漏掉"何时不用":负例才是消除重叠/模糊误用的关键。
6. 学习资源(每条带 YYYY-MM)
- Anthropic, Writing effective tools for agents(2025-09)——三原则(单一职责 / 写负例 / enum 收窄)的主线来源。
- Anthropic, Effective context engineering for AI agents(2025-09)——更窄 schema 顺带降 token,呼应 Day 44 预算。
- MCP specification(2026-07-28 定稿,RC 2026-05-21)——stateless + schema 校验语义,v2 工具契约的规范背景。
- 本仓代码:
src/agent/mcp/toolRegistry.ts(validate的 enum/required 强制、register重名保护)、src/agent/mcp/__tests__/toolRegistry.test.ts(v1 单测基线)。
SOTA检查 (2026-06 更新)
- 当前主流方案:enum 收窄取值域 + description 写负例 + 单一职责,是 2026 工具设计的当前最佳实践;与 MCP 2026-07-28 的 schema 校验语义对齐。
- 是否仍 SOTA:是。收窄动作空间提升工具选择正确率,是受社区与 Anthropic 指南共同背书的现行做法。
- 过时黑名单(AVOID):胖 string 参数任由模型自由填(无 enum/required,出错面大);把多个职责塞进一个工具靠长 description 兜底;改 v1 而非并存 v2(毁掉 A/B 归因)。
- 下次复查点:MCP 2026-07-28 最终规范落地后,复核 v2 工具 spec 是否需对齐其 stateless/Tasks-extension 约束;OpenRouter 上 DeepSeek-V3 id 当周重验(Day 46 A/B 要用)。
衔接
- 昨天:Day 44 — context-budget 表(
tools配额超额,今天削它)。 - 今天:v2 工具集——合并重叠、description 写负例、enum/required 收窄;v1 并存以备 A/B;378 tests 不破。
- 明天:Day 46 — A/B eval 设计(固定同任务/同模型/同 judge,只切 v1↔v2,用 Δ 证明工具改进)。