先说结论:在 Pi Agent 这类 AI 编程代理里,工具提示词不是写得越详细越好。我把自己的一个扩展工具集整体过了一遍,工具声明从 1320 个 token 压到 122 个,整整省掉 91%,工具命中率反而从 68% 涨到 94%。这篇文章想把这些思路原样分享给 Pi Agent 用户和扩展作者。不管你是刚开始接触 Pi Agent、正在调自己的扩展工具,还是计划给社区写扩展,都可以按里面的五步压缩法和测试清单,把自己的工具提示词快速瘦一遍。
1. 为什么说工具提示词是 Pi Agent 的隐形成本
1.1 工具提示词到底在 Agent 里扮演什么角色
先说一个很多人没意识到的事实:Pi Agent 的核心运行机制,是让模型在对话过程中主动调用你注册的工具。模型看到的“工具”,并不等同于背后的 Python 脚本或 shell 命令,而是一段描述文本。这段文本通常包含工具名称、触发逻辑、参数含义、返回值约定。模型完全是靠这段文本,来决定“要不要调用”“调用哪个”“参数填什么”。
所以工具提示词直接影响两件事:一个是工具的使用率,一个是调用准确率。我自己见过太多人把功夫花在脚本实现上,工具提示词随便写两行,结果模型要么死活不调用工具,要么调用错了工具,参数还填得乱七八糟,最后反过来说“Agent 不好用”。
这里可以类比成给一个新同事写工作交接说明:你说得越乱,他越容易在关键时刻做错决定;你把关键信息浓缩成几行,他反而能快速进入状态。模型没有“常识”去猜你想干什么,你给它什么文本,它就按什么文本决策。
1.2 “省掉 91%”的计算过程与口径说明
先亮出我的计算口径,免得读者觉得“91%”是个博眼球的数字。我写了一个简单的统计脚本,把 Pi Agent 启动时的完整系统上下文拆成三块:固定系统提示词、工具声明区、会话历史。然后分别统计每一块的 token 数。
我手上的这个仓库管理扩展,优化前的工具声明区是 1320 个 token,占整个固定上下文(系统提示词 + 工具声明)约 37%;优化后同一组工具只占 122 个 token,占比降到 4.1%。用公式算就是(1320 - 122) / 1320 = 0.907,四舍五入就是“省掉 91%”。
| 项目 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 工具声明 token 数 | 1320 | 122 | 减少 90.8% |
| 固定上下文总 token | 3560 | 2362 | 减少 33.7% |
| 工具命中率(100 次测试) | 68% | 94% | 提升 26% |
我为什么特别在意这个比例?因为在 Pi Agent 这种循环调用工具的场景里,工具声明是每一轮都固定带上的,它不像对话历史可以截断,也不像代码文件可以按需读取。这部分是不折不扣的固定成本,省掉它就直接降低了每一次调用大模型的费用和响应延迟。
1.3 那些我以为必要的描述,其实全是冗余
优化前我也写过很长很“完整”的工具提示词,总觉得自己把边界情况写清楚,模型就不会犯错。后来把提示词摊开一分析,发现冗余主要来自四个地方:
- 功能说明书式写作:每个工具开头都是“这是一个强大的工具,用于……”,介绍了两三句话才进入正题。
- 参数说明重复:在 description 里把 message 参数的规则写了一遍,在参数 schema 里又写了一遍,内容完全一样。
- 防御性条款堆叠:“如果……则……”“当……时注意……”写了一大堆,实际上模型根本不会逐条读。
- 示例泛滥:每个参数都配两个示例,参数之间还搞排列组合。
我自己统计了一下,冗余占比最高的就是“防御性条款”和“示例泛滥”,这两块占掉原来提示词的一半以上。更讽刺的是,这些描述并没有减少工具误用,反而因为文本过长,把真正重要的触发信号稀释掉了。
2. 我的五步压缩法:从冗长描述到最小提示词
2.1 一句话说清“工具在什么时候用”
压缩工具提示词的第一步,是给每个工具写一句“职责声明”,句式可以固定为:动词 + 对象 + 触发场景。
举个例子:优化前我写“本工具封装了底层 Git 命令,能够返回一个包含当前工作区状态的列表,用户可据此了解项目是否有未提交的变更……”;优化后只写一句“读取本地仓库 Git 状态。当用户询问变更、分支、提交信息时使用”。
这里的关键在于:模型第一眼只需要判断“这个工具和我现在要解决的问题有没有关系”。你把第一句话写成“这个工具是干什么的”,它就能秒速做判断。后面的细节,等模型决定调用之后,脚本和返回值自然会补上。
我在实际测试中还有一个观察:当描述首句足够清晰时,模型会倾向于优先使用这个工具;当首句模棱两可时,模型要么完全不调用,要么把多个相似工具都调一遍来“碰运气”。
2.2 参数能从 schema 删掉就别留着
很多扩展作者在设计工具参数时,追求面面俱到,每个可能的配置项都暴露给模型。结果就是模型每次调用前都要纠结:这个参数要不要填?填什么才对?
我的原则很简单:如果一个参数在 90% 的场景里都用默认值,就把它从工具参数里删掉,改成扩展配置文件里的环境变量。比如 git_commit 的 author 参数,默认来源就应该是全局 Git 配置,模型完全不参与判断。
参数数量直接关系到模型填参的错误率。参数越少,模型出错概率越低。我在优化 git 系列工具时,把所有非必要参数从参数表里移除后,参数校验日志里的错误提示数量下降了近一半。你保留的参数应该只有那些“用户真的会在对话里指定”的字段。
2.3 把防御性限制改写成触发条件
这里有个比较反直觉的经验:与其告诉模型“不能做什么”,不如告诉它“在什么条件下使用”,然后让脚本去处理“不能”的部分。
比如我原来的描述是:“该工具只负责创建提交,不负责暂存文件。如果暂存区为空,该工具将返回错误。提交信息请使用简洁的祈使句。”优化后改成:“把已暂存文件提交为一次 commit,使用简短祈使句信息。”
为什么这样改?“不做什么”这种事情,模型经常记不住。而且否定表达式在语义模型里的决策权重很低,你花 20 个 token 写“不要暂存文件”,模型转头就忘了。真正要解决的是:脚本自己检查暂存区,如果为空就返回错误信息给模型,让模型根据错误信息去引导用户先执行 git add。边界检查放脚本里,别放提示词里。
2.4 公共约束只写一次
我见过最浪费 token 的做法,是把一个公共约束在每个工具的描述里各写一遍。比如“所有工具都需要先经过 auth 验证”这句话,扩展里有 6 个工具就重复 6 次。
在 Pi Agent 的 manifest 体系里,有一个专门区域可以用来放置这些公共约定。把所有工具共享的约束写在这个区域,既能减少重复 token,又能让模型把注意力集中在工具之间的差异上。
我在优化时把“作用于当前目录的 Git 仓库”“输出格式统一为 UTF-8 文本”“无需指定仓库路径”这三条公共信息抽出来后,每个工具的描述都瘦了一大圈,而模型的整体决策质量反而提升了。公共约束集中管理,还有利于后续修改,不用一个文件一个文件去翻。
2.5 用一个示例收掉所有解释
描述讲不清的参数格式,用单个示例最有说服力。与其写“日期格式必须为 YYYY-MM-DD,月份和日期需要补零,时区为 UTC”,不如给一个example: "2025-06-18"。模型看到具体的值会自动类推,而且这个示例只需要 4 个 token。
但注意:示例只保留最典型的一个,不要给一整个参数矩阵。我最初为了展示“各种情况都能处理”,给日期参数写了三个示例,给提交信息写了两个风格完全不同的示例。结果模型反而无所适从,有时候模仿第一个示例,有时候模仿第三个。一个工具的一个参数,一个例子就够了。
3. 实操:把“仓库管理扩展”的工具提示词压掉 91%
3.1 扩展结构:manifest、工具定义与文档该放哪
在动手写工具提示词之前,先明确扩展的文件结构。Pi Agent 的扩展通常是一个目录,里面至少要有 manifest.yaml、工具定义文件和实际执行的脚本。我的建议是:
repo-mgr/ ├── manifest.yaml ├── tools/ │ ├── git_status.json │ ├── git_log.json │ ├── git_commit.json │ └── ... ├── docs/ │ └── README.md └── scripts/ ├── git_status.sh ├── git_log.sh └── git_commit.sh工具定义文件里只放机器要用的字段:名称、描述、参数 schema。更完整的用法说明、边界条件、常见问题,放到docs/README.md里,供人类阅读或按需引用。这样就避免了“把所有信息都塞进每轮调用都要发送的上下文”这种浪费。
这里的关键思路是分层:模型平时只需要知道“什么情况下用哪个工具、参数怎么填”这些决策信息,至于工具内部的实现细节、错误码含义、配置项大全,那是人和脚本自己看的东西。
3.2 优化前与优化后的工具定义对照
说再多理论,不如直接看一份真实的工具定义对比。这是仓库管理扩展里git_commit工具优化前的定义:
{ "name": "git_commit", "description": "在当前 Git 仓库中创建一个新的提交。该工具只负责创建提交,不负责暂存文件;调用前请确保相关文件已经通过 git add 添加到暂存区。如果暂存区为空,该工具将返回错误并附上当前状态说明。提交信息请使用简洁的祈使句,例如 'Fix login bug'。若不指定 author 参数,则使用全局 Git 配置的默认用户。该工具会自动跳过 pre-commit hooks,若需要执行 hooks 请使用其他脚本。当用户要求提交代码、创建 commit、或者说要“保存当前更改”时使用本工具。", "parameters": { "type": "object", "properties": { "message": { "type": "string", "description": "提交信息,建议使用英文祈使句,不超过 100 字符" }, "author": { "type": "string", "description": "提交作者,格式为 Name <email>,不填则使用全局 git config" } }, "required": ["message"] } }优化后的定义:
{ "name": "git_commit", "description": "把已暂存文件提交为一次 commit,使用简短祈使句信息。当用户说“提交/commit”时使用。", "parameters": { "type": "object", "properties": { "message": { "type": "string", "description": "提交信息,英文祈使句,不超过 100 字符", "example": "Fix login bug" } }, "required": ["message"] } }这段对比里,描述从约 130 个词降到了 20 个词,参数从 2 个降到 1 个。你可能会问:“author 参数去哪了?”我并没有删掉 author 功能,只是把它移出了模型决策面。扩展脚本会读取GIT_AUTHOR环境变量,找不到就用全局 Git 配置。这样模型根本不需要判断要不要填 author,填错了反而出问题。
3.3 测试提示词效果的四个检查点
优化完提示词之后,不能直接拍脑袋说“变好了”,要做系统性的测试。我整理了两个工具集对比测试时用的四个检查点:
工具是否被按需调用:统计 100 次对话里,目标工具被正确调用的次数。如果命中率低于 90%,先回触发词去查,别急着加描述。
参数是否一次填对:检查脚本侧的参数校验日志,看 message 参数里有没有夹杂多余的解释性文字。模型经常会把“请写上修复登录问题的提交信息”理解成“Fix login bug please remember to also update tests”——精简参数描述能显著改善这个问题。
返回错误是否被模型理解:当工具返回非 0 退出码时,模型是把错误原文直接抛给用户,还是会基于错误信息给出下一步建议?这个行为也受工具提示词影响。如果工具描述里写了“错误信息可能包含建议操作”,模型就会主动转译。
模型是否擅自改写工具描述:有些模型的运行机制会在上下文里自动压缩工具描述。如果原始描述信息密度低,压缩后可能丢掉关键信息。优化后的描述变短,被模型自己改写时的失真比例也会下降。
这四个检查点里,第一个和第二个最重要。工具命中率上不去,描述写得再精美也没用;参数老填错,描述简洁只会让问题更容易暴露。
3.4 可直接抄走的 manifest 模板
最后给一份可以直接改来用的 manifest 模板。下面的 YAML 是我目前仓库管理扩展的基础结构:
name: repo-mgr version: 1.0.0 description: 仓库管理工具集 shared_context: | 以下工具都作用于当前工作目录的 Git 仓库,执行前无需额外指定仓库路径。 所有工具输出均为 UTF-8 文本,脚本失败时请基于 stderr 内容给出建议。 使用 Git 命令前不需要手动执行 auth 验证。 tools: - name: git_status use_when: 用户询问文件变更、工作区状态、分支领先落后 action: bash scripts/git_status.sh params: {} - name: git_log use_when: 用户需要查看提交历史、找某次提交的 hash 或作者 action: bash scripts/git_log.sh params: - name: limit desc: 最多显示多少条,默认 20 - name: git_commit use_when: 用户说“提交/commit/保存更改” action: bash scripts/git_commit.sh params: - name: message desc: 提交信息,英文祈使句,不超过 100 字符 example: "Fix login bug"注意shared_context这块,它专门放公共约束。这样每个工具描述只需要写“这个工具什么时候用、参数是什么”,不必重复整段公共约定。这个结构也是我省掉 91% token 的基础。
4. 扩展作者避坑实录:提示词出问题的典型场景
4.1 模型死活不用你的工具,先查触发词
我遇到最多的问题是:“我明明把工具注册好了,模型就是不调用。”排查半天,十有八九是触发词设计得不好。
触发词不具体,是头号原因。比如你只写“获取仓库状态”,用户说“帮我看看今天的代码改了啥”时,模型不一定能把“改了啥”映射到“git status”上。改成“查看工作区是否有未提交的改动”之后,触发信号就强了很多。
另外一个坑是描述里的否定信息过多。比如“除非用户明确要求,否则不要获取远程变更”,模型对“除非”这种条件的处理能力很弱。更好的做法,是把“获取远程变更”单独做成一个显式标签的独立工具,让用户主动说“拉取更新”时才调用。
4.2 工具被乱调用、参数错位,多半是描述“撞车”
如果你的扩展里有多个工具都包含“获取 Git 信息”这种描述开头,那模型真的会拿不准。“查日志”和“看变更”在用户口语里经常混用,如果两个工具的描述长得差不多,模型就很容易同时触发多个工具,或者把一个工具的参数套到另一个工具上。
我自己的对策是:每个工具的use_when必须能被一句话区分。比如 git_status 的触发条件是“用户关心当前工作区有没有改动”,git_log 的触发条件是“用户想查历史提交记录”,两个场景在语义上不重叠。如果实在有交集,就明确写“当用户提及‘历史’时优先使用 git_log”。
参数错位通常是因为参数名太通用。我踩过的坑是给 git_log 的 limit 参数和另一个工具的 count 参数都表示“数量”,模型偶尔会把用户说的“20 条”填到 count 里。把参数名改成max_count之后,这种情况就消失了。参数名直接映射工具域的语义,别偷懒用通用词。
4.3 换个项目就不灵,别急着改描述
这类问题很隐蔽:在测试项目里工具调用模型表现很好,换了一个仓库之后,开始频繁误判。我排查过几次,发现原因几乎都是描述里含有项目特有信息。
比如我把某个仓库的路径写进了工具描述里,或者把一个业务概念当成了触发词。换到别的仓库后,这些信息就成了干扰项。正确做法是:描述中永远不出现具体项目名词,所有可变信息都参数化或环境变量化。跨项目回归测试是扩展作者最容易忽略的一环,我建议每改一次工具描述,至少换两个不同主题的仓库各跑一遍。
4.4 用最小复现实验给提示词做回归
如果问题实在不好定位,我的最后一招是做最小复现实验。把扩展的工具集缩到只剩两个有嫌疑的工具,造一组固定对话样本,然后跑 10 轮,对比不同描述下的调用日志。
这个方法的原理是控制变量。因为 Agent 的行为同时受系统提示词、对话历史、工具返回值等多重因素影响,你不做最小化,就说不清到底哪个变量导致了误判。实际操作上,我会写一个简单的 shell 脚本,把同一句用户消息分别发给加载了不同 manifest 的 Pi Agent 实例,比对工具调用日志里的差异。
这个办法帮我摆脱了至少一半的“玄学问题”。你以为是大模型抽风,其实只是描述文本里藏着干扰项。
最后再分享一个我从这套优化流程里得到的小习惯:每添加一个新工具时,先强迫自己用 20 个字符以内的“一句话职责”写好描述,跑通基本调用之后,再根据实际误判去补充信息。大多数情况下,补充信息的冲动都源于“我觉得模型可能做不好”,而不是模型真的做不好。工具提示词的维护,本质上就是不断的清理和克制。