工具描述不是写给人看的文档,是写给模型看的约束条件。这两件事的区别,我是在同一批 12 个工具上改到第三版才分清的。
一开始我以为 description 越详细越好,把每个参数的业务含义、返回值示例、异常码全写了进去。结果模型挑工具时错得更多——它把大段描述当成了"背景说明",直接跳过,只看函数名。
12 个工具对应一套订单后台:查订单、改地址、退款、发券、查库存、批量导出、改价、加备注、拆单、合单、查物流、催发货。
第一版 description 平均 180 字,写了业务背景、权限说明、调用前置条件。60 次单轮任务跑下来:
最典型的一次:任务写"把这个订单的收货地址改到杭州仓",模型调了 export_orders,参数里填了 region: "杭州"——它把"改地址"和"按区域导出"混成了一个动作。
我的第二反应是加强语气。每条 description 前面加"你必须先……",参数说明里加"严禁传入……"。
选错率从 43% 降到 39%,几乎没动。参数编造率还升到 34%。
原因后来想明白了:否定式约束在工具列表里是噪声。模型一次看到 12 个工具、每个工具带 3 条"不要做 X",它记住的是 X 本身,不是那个"不要"。
真正有效的改动是三件具体的事,跟措辞强弱无关。
一是工具名自带动词和对象。export_orders 改成 export_orders_to_csv,update_order 拆成 update_order_shipping_address 和 update_order_price。模型不再需要在两个语义重叠的工具之间做取舍。
二是description 第一句写"什么时候用",第二句写"什么时候不用",各一句,不超过 40 字。
三是枚举值直接列在参数里,不让模型自己映射。region 从前是自由字符串,改成 4 个固定取值,中文对照写进 description 末尾。
改完后的结构大致是这样:
{
"name": "update_order_shipping_address",
"description": "修改订单收货地址。仅当用户明确给出新地址时使用;只改联系方式请用 update_order_contact。region 取值:east=华东, south=华南, north=华北, west=西部分拣。",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"region": {"type": "string", "enum": ["east", "south", "north", "west"]},
"detail": {"type": "string", "maxLength": 120}
},
"required": ["order_id", "region", "detail"]
}
}跑测试的方式很土,但够用:同一批 60 条任务指令,每条跑一遍,记录三件事——选中了哪个工具、传了什么参数、是否命中预期。
import json, subprocess
CASES = [c.strip() for c in open("cases.txt", encoding="utf-8") if c.strip()]
stat = {"tool_ok": 0, "arg_ok": 0, "fabricated": 0}
for i, prompt in enumerate(CASES, 1):
out = subprocess.run(
["python", "run_agent.py", "--prompt", prompt, "--tools", "tools_v3.json"],
capture_output=True, text=True, timeout=120,
)
call = json.loads(out.stdout)["tool_calls"][0]
ok_tool = call["name"] == EXPECTED[i]
ok_arg = set(call["arguments"]) <= ALLOWED[call["name"]]
stat["tool_ok"] += ok_tool
stat["arg_ok"] += ok_arg
stat["fabricated"] += not ok_arg
print(f"{i:02d} {ok_tool!s:5} {ok_arg!s:5} {call['name']}")
print(stat)三版对比:
版本 | description 平均字数 | 工具选对率 | 参数合法率 | 编造参数 |
|---|---|---|---|---|
v1 文档式 | 180 | 35% | 68% | 19/60 |
v2 加语气词 | 210 | 39% | 66% | 20/60 |
v3 条件式 + 拆工具 | 62 | 87% | 93% | 2/60 |
选对率从 35% 到 87%,靠的不是模型变强,是把"需要模型推理的判断"变成了"可以照抄的枚举"。12 个工具、60 条用例、三版 schema,一共跑了 180 次,前后花了两天。
剩下的失败集中在两类。
一是跨工具任务。指令是"先退款再补一张等额券",模型能选对第一个工具,但不会主动串第二个。单轮多工具调用在 12 个工具的规模下,有串行依赖的任务成功率只有一半左右。
二是参数值的来源。模型知道要填 order_id,但没拿到订单号时会编一个格式完全正确的假号。这里我踩了个坑:最初给 order_id 加了 ^SO\d{10}$ 的正则校验,看起来是防守,实际帮了它——格式对了就过,业务层反而查不出来。后来把 pattern 去掉,让无效值在业务层报错,再由 Agent 把错误原文回灌给模型重试。宁可多一轮,也不要让它"格式正确地错"。
这套东西现在跑在 PRIVO 的桌面端里,同一份工具定义可以换模型复用,换模型时失败率会变化,工具选对率基本不动——说明约束写在 schema 里比写在 prompt 里更耐迁移。
60 条用例是我按业务自己写的,不是真实用户分布,长尾指令覆盖不到。另外 12 个工具的结论不能外推到 40 个工具的场景:我试过 30 个工具,v3 的选对率掉到 61%,工具一多,名字再清楚也会互相干扰。工具规模到 20 个以上时,分组和分层调用比继续优化 description 更划算。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。