一个查询工具的参数从 date 改成 date_range(起止两天),实现改了、调用方改了,唯独工具描述里还写着"date: 单日"。于是模型每次只传一个日期,工具按缺省解释成"今天"——查询结果永远只覆盖一天,而没人报错。
工具描述是模型唯一的说明书。它和实现之间的漂移不会引发编译错误,只会引发"行为微妙不对"——更麻烦的是它常常同时影响多个模型:描述是共享的,所有模型都照着同一份过期说明行动,于是"多个模型都错"会被误读成"这个任务本身难"。
而这在 Agent 场景里比崩溃更难查。
1. 漂移的四种形态
| 形态 | 表现 |
|---|---|
| 参数名变更未同步 | 模型传旧名,工具用默认值 |
| 语义变宽/变窄 | "可选"实际必填 |
| 示例过期 | 示例里的值已不合法 |
| 边界未更新 | 上限提高到 1000,描述仍写 100 |
第一项最危险:旧参数名往往不报错,而是被忽略,于是工具"成功返回"了一个错误范围的答案。
第三项几乎无人维护:描述里的示例是最容易被遗忘的部分第二项(语义变宽/变窄)最难发现:"可选"变"必填"时,模型仍会省略该参数,工具于是报参数缺失——而这个错误看起来像"模型不会用",实际是描述没更新。
,而模型非常依赖示例——它会把示例当模板模仿。
2. 描述里必须写清的六件事
| 项 | 为什么 |
|---|---|
| 做什么(一句话) | 决定模型何时调用 |
| 参数类型与范围 | 决定能否构造合法调用 |
| 必填/可选 | 决定调用完整性 |
| 示例 | 模型模仿的模板 |
| 失败时的返回 | 决定重试还是放弃 |
| 副作用 | 决定是否需要确认 |
第四项(示例)的作用常被高估也被低估:模型会把示例当"合法输入的模板"来模仿,所以示例过期比没有示例更糟——它会让模型持续构造一种已被废弃的调用。
第五项最常被省略:"失败返回什么"是模型决策的依据。如果描述不写,模型只能靠试,于是产生"改参数重试"的死循环。
第六项在有副作用的工具上必须写:"会写入数据库"与"只读"对应完全不同的使用策略。
// 描述应当包含的六段(示例)
name: query_orders
description: >
按日期范围与状态查询订单。只读,不修改任何数据。
返回最多 100 条;超出时返回 truncated=true,需要缩小范围重试。
parameters:
date_from: { type: string, format: date, required: true, desc: "起始日期(含)" }
date_to: { type: string, format: date, required: true, desc: "结束日期(含),不得早于 date_from" }
status: { type: string, enum: [PAID, SHIPPED, CLOSED], required: false }
returns: >
items: 订单数组;truncated: 是否被截断;error_code: 失败时为 INVALID_RANGE / TOO_MANY
example: '{"date_from":"2026-11-01","date_to":"2026-11-07","status":"PAID"}'
3. 让漂移可见的三种做法
| 做法 | 效果 |
|---|---|
| 描述与实现同文件 | 改一个看得见另一个 |
| 生成式描述(从类型导出) | 无法漂移 |
| 断言测试(描述含关键字) | 变更即失败 |
第二项最彻底:从代码的类型定义生成描述,两者同源就没有漂移。第一项(同文件)是最便宜的做法,成本几乎为零:把描述写在实现旁边,改参数时视线扫过就会看到它。它不能防住"忘了看",但能防住"根本不知道有这份描述"。
代价是把描述的表达力限制在类型能表达的范围(比如"副作用"就表达不出来)。
第三项是轻量替代:在测试里断言"描述包含 required 字段名、包含 example、包含 returns",改实现时若忘了改描述,测试会红。
// 断言测试:把"描述完整性"变成可检查项
def test_query_orders_description():
spec = registry.get("query_orders")
assert set(spec.parameters) == {"date_from", "date_to", "status"}
assert "truncated" in spec.returns # 关键返回必须在描述里
assert spec.example and "date_from" in spec.example
assert spec.side_effect == "read_only" # 副作用必须声明
// 实现改了参数名而描述没改 → 这个测试失败
4. 模型"用错"的三种情况与判断
| 现象 | 是描述问题还是模型问题 |
|---|---|
| 总传同一个错参数 | 描述问题(旧名/示例) |
| 明知有两个参数只传一个 | 描述未写必填 |
| 收到错误后重复同样调用 | 描述未写失败返回 |
这个实验还有一个变体:把描述改写一遍(不改实现)再试。如果换描述就对了,那实现没问题,问题在描述的表达——而这类问题在"调提示"里永远修不好,因为工具描述与系统提示是两份不同的东西。
判据是"换个模型/换个提示是否还错":换一个也错,是描述问题;只有一个模型错,才是模型问题。这个实验很便宜,但常被跳过——于是"改提示"成了默认动作,而真正该改的是描述。
漂移还有一类"跨层"来源:工具描述、系统提示、调用示例三份材料各写一套要求。三处不一致时,模型会取"最具体"的那一份——通常是最旧的示例。所以三者应当由同一份定义生成,或至少在发布时对齐一次。
// 三份材料的一致性检查(发布前手动过一遍)
工具描述:date_from/date_to 必填,enum 三个值
系统提示:只提"按日期查询订单",未提参数名
示例(few-shot):'{"date":"2026-11-01"}' ← 过期!
// 结论:示例最具体 → 模型优先模仿它 → 旧参数名被持续使用
5. 版本与兼容
| 变更 | 兼容做法 |
|---|---|
| 加可选参数 | 直接加,描述同步 |
| 改参数名 | 保留旧名做别名一版 |
| 改语义 | 新工具名,旧的标 deprecated |
| 删参数 | 先标 deprecated 一版 |
第二项与第四项的原则是"给一版过渡期":第二项(保留旧名)的实现要"双向":旧名仍可用,但返回里提示 deprecated,让还在用旧名的调用方(包括模型)能看到迁移信号,而不是静默继续。
模型侧的调用模板(提示、示例、缓存的工具定义)不会自动更新,所以直接删改会让"已经稳定的调用方式"突然失败。
第三项最稳:语义变了就换新工具名,因为旧名的语义已经被大量调用固化。
还有一个"工具名"层面的漂移:名字里的动词与实现不符。比如 get_order 实际会写入"最近访问时间",那它不是只读的——而模型会按只读工具的用法去批量调用它,副作用被放大。
// 命名与副作用必须一致
get_order → 只读,可批量
find_orders → 只读,可批量
create_order → 写,需确认
update_order → 写,需确认
touch_order → 写(更新时间戳),需注意批量副作用
6. 排错:五种"工具行为不符预期"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 结果范围总是偏小 | 旧参数名被忽略 | 检查参数名与描述 |
| 只返回今天的数据 | 语义变更未写 | 更新描述与示例 |
| 模型反复重试 | 未写失败返回 | 补 returns |
| 工具被错误时机调用 | 描述未写"何时用" | 补一句话定义 |
| 换了模型就好 | 模型差异 | 分开处理,不改描述 |
第一项(范围偏小)还有一个"看起来正确"的版本:工具返回 200 与合理的结果,只是范围不是你想要的。这类错误在日志里完全正常,只有对着业务数据核对才能发现。
第四项在工具数多时最明显:两个工具都能"查数据",模型会随机选一个。解法是在描述里写清"何时用我、何时用另一个"。
最后一道保险是"运行时自检":工具在收到参数时校验"是否全部为已知参数",遇到未知参数直接报错而不是忽略。这样旧参数名会立刻暴露,而不是静默走默认值。
// 严格参数校验:未知参数直接拒绝,不留静默失败
def query_orders(params: dict):
unknown = set(params) - {"date_from", "date_to", "status"}
if unknown:
return {"error_code": "UNKNOWN_PARAM", "fields": sorted(unknown),
"hint": "参数已变更为 date_from/date_to(见工具描述)"}
...
// 效果:描述未同步时,错误立刻出现且指向明确,而不是查出一份范围偏小的数据
7. 边界:什么情况不必苛求描述
| 场景 | 为什么 | 做法 |
|---|---|---|
| 单工具、单参数 | 无歧义 | 简短即可 |
| 内部脚本调用(人不读) | 模型不参与 | 描述可以粗 |
| 实验期原型 | 频繁变 | 不追求准确 |
| 有强的后置校验 | 错误会被拦住 | 描述可略 |
判断口径一句话:这个描述会不会被模型当作唯一依据。最后一条与"评测"有关的边界:工具描述的质量必须用任务集验证,而不是靠人读。用 20 条真实任务跑一遍,统计"选错工具"与"传错参数"的比例——这两个数比任何文档评审都准。
会,就必须与实现同源或加断言;不会(有人读代码),描述只是注释。
8. 自测
- 四种漂移形态里,哪一种最危险?为什么"旧参数名被忽略"比"报错"更难查?
- 描述必须写清的六件事里,哪两件最常被省略?它们分别影响模型的什么决策?
- 为什么"从类型生成描述"能根治漂移?它的表达力边界在哪里?
- "换个模型是否还错"这个实验为什么能区分"描述问题"与"模型问题"?
- 改工具语义时为什么建议换新工具名而不是原地改?
- 判断"是否需要苛求描述"的那个问题是什么?