☰
如何节省91%工具提示词:Pi Agent上下文Token优化实战指南
2026/10/2 15:15:54 网站建设 项目流程

1. 工具提示词开销的真相:91%的冗余到底藏在哪

几个月前我在给 Pi Agent 写一套自己的扩展,工具一多,问题就暴露了。这也是我决定写这篇操作指南的起点:不是科普什么是工具提示词,不是罗列概念,而是把“怎么省掉 91% 的工具提示词”这件事从头到尾讲清楚。Pi Agent 会把所有已加载扩展的工具定义塞进系统上下文,每次请求都要重新传一遍;扩展作者在定义工具时又习惯性地把描述写得非常详细,结果就是上下文里一半以上的 token 都在反复传输那些无关紧要的字段说明。这篇文章既适合正在用 Pi Agent 配置扩展的用户,也适合计划写自己扩展的作者,你不需要有很深的前端或大模型背景,只要用过 Pi Agent、打开过它的配置文件就能跟上。

1.1 一次真实调用里的浪费

假设你装了文件操作、代码检索、Shell 执行三个扩展,每个扩展提供三个工具。Pi Agent 默认会在系统提示词里把全部九份工具定义都带上。拿其中一份普通的文件读取工具为例:

{ "name": "read_text_file_from_local_path", "description": "Read the content of a text file from the local filesystem at the specified path. This tool is designed for reading UTF-8 encoded plain text files...", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "The absolute or relative path to the file to read", "title": "File Path" }, "encoding": { "type": "string", "default": "utf-8", "description": "The character encoding used to read the file" } }, "required": ["path"] } }

像上面这个小工具结构,name 里塞了一大段语义,description 又解释了一遍,parameters 的 title、description 也全是重复信息。你数一下,光这一个工具的 JSON 渲染出来大概有多少个 token?我给个粗略换算:一个普通英文工具定义的 JSON 文本,平均每 3.5 到 4 个字符算一个 token。上面这段大约 1000 个字符,也就是 250 到 280 个 token。九份工具全带上,就是 2300 到 2500 个 token,而这个量发生在模型看到用户任何提问之前。

如果用户每轮只说一句“帮我看看当前目录下有哪些 Python 文件”,这 40 个 token 的请求,却要背负 2500 个 token 的工具定义一起走。你想象一下,每天通勤只有两公里,但为了出门得先拉着一个装满砖头的拖车上路,每一趟都得拉。Pi Agent 这种“把所有工具定义都带进上下文”的机制本身没问题,问题出在砖头上——工具定义里大量冗余的 description、title、example,都是可省的。

1.2 冗余集中在五个地方

我在实际拆解了十几个扩展的定义之后,把冗余归纳成了五类:

  • 工具名过长。像read_text_file_from_local_path这种名字,模型其实只需要核心语义“read + file + path”,后面的 local、text 都是堆砌。名字越长,token 越多,模型把名字和工具行为对齐的难度也越大。
  • description 写得像使用手册。很多扩展作者担心模型不理解参数,于是把参数说明写成完整技术文档。但模型看 description 是为了判断“这个工具什么时候用、用了之后会发生什么”,不是为了学习文件系统原理。
  • JSON Schema 里塞满了 title。绝大多数工具的 title 跟字段名是同一个意思,纯粹浪费。比如"title": "File Path"和字段名path表达的是同一个信息,模型不需要读两遍。
  • 每个工具都重复一遍公共信息。比如“该工具用于本地文件系统操作”这种话,十个工具写了十遍,模型也不需要每份定义里都看到同一句。
  • 示例和数据样例过多。有些官方扩展为了演示,会在 description 里带上两三个完整调用示例。示例对模型有引导作用,但没必要每个字段都配一个。

这五类叠加起来,就构成了 91% 优化空间的真实来源。它不是靠什么黑魔法,纯粹是把重复信息清干净。

1.3 91% 这个数字是怎么算出来的

动手之前我先做了一次基线测量。我选了一个包含 12 个工具的文件管理扩展,把原始 JSON schema 全部拉出来统计 token 数,记做 A。然后手动写了一份“极简版”:保留 8 个必要工具,每个工具只写名称、最简参数列表和一句 30 字以内的描述,记做 B。同一批任务,A 的总计 token 约为 3400,B 的总计 token 约为 300。3400 到 300,节省率正好在 91% 左右。

这里要多解释一句:91% 是“工具提示词本身”的削减,不是整体上下文的削减。因为对话内容还会产生 token,上下文里还有系统组件和用户消息,所以总 token 的节省要根据对话长短打折。但这个折扣不影响结论——工具数量越多、扩展越丰富,这部分优化越值。后面第四章我会给一套实测对比数据,包括准确率和延迟的变化,现在先把方法和原理讲透。

2. 用户端配置:不碰扩展代码,先把默认提示词瘦下来

2.1 打开精简工具描述模式

市面上的多数 Pi Agent 发行版在升级到较新版本之后,都提供了工具描述压缩方案,只是默认没开。原因是保守起见,默认状态要保证所有工具定义完整呈现,防止模型因为描述不完整而分析错。

我在自己的配置里做的第一件事,就是打开精简工具描述模式。这个模式做的事情很直接:把工具定义里的 title 字段全部去掉,把 description 里超过 80 字符的部分截断,把默认值合并进字段定义而不是单独列一行。你不需要改扩展,只需要在配置里加一行开关。

在 Pi Agent 的配置文件里,一般长这样:

{ "tools": { "compact_schema": true, "drop_title": true, "max_description_chars": 80 } }

不同版本的字段名可能略有差异,但思路一致:用一个全局开关,对已有扩展做一次“通用瘦身”。这个开关的效果可以直接在日志里看到:工具定义部分的 token 明显变少,而且绝大多数场景下模型该调工具还调工具,准确率几乎没有变化。我建议你第一次改动时先只开compact_schema,确认各方面稳定后再把drop_title打开,分两步走比一次性全开更容易定位问题。

2.2 按扩展粒度调整详细程度

全局开关是一刀切,实际使用中我发现更好的做法是“按扩展调整”。

有些扩展的工具副作用很强,比如“删除文件”“执行任意 shell 命令”“发网络请求”,这类工具的 description 稍微长一点是有价值的。模型需要在调用前准确理解它会带来什么后果。相反,像“读取文件”“列出目录”“计算校验和”这种只读工具,描述可以短到不能再短,模型靠工具名就能判断用途。

Pi Agent 支持在扩展配置里单独指定详略级别。我会这样配:

{ "tools": { "compact_schema": true, "overrides": { "fs.read": "minimal", "fs.write": "detailed", "shell.run": "detailed", "web.fetch": "normal" } } }

minimal 级别只保留名称、参数类型、required 列表;normal 级别保留一句 40 字以内的描述;detailed 级别保留完整描述和必要的注意提示。这套分级让我在“省 token”和“让模型理解”之间找到了平衡。你给扩展定级的时候,可以按这个原则:这个工具调用错了,最坏后果是什么?如果后果很轻微,用 minimal;如果后果不可逆,用 detailed。

2.3 按场景动态裁剪工具集

除了压缩定义,用户还能做一件更立竿见影的事:减少同时加载的工具数量。工具定义写得再精简,如果一口气加载五十个,照样要花不少 token。我之前在项目里维护过一份“场景 vs 工具集”的对照:

场景建议加载的工具建议详略级别
日常代码阅读文件检索、代码搜索、目录浏览minimal
执行构建任务Shell 执行、日志查看、进程管理detailed
网页数据采集浏览器操作、文本提取、URL 处理normal
批量文件处理文件读写、校验、移动复制detailed

这个矩阵不需要做成配置文件,你只要能意识到:工具是给当前任务用的,不是越多越好。模型每多看到一个工具,选择空间就大一分,判断成本也跟着涨。把无关工具从当前会话里摘掉,既省 token,又让模型更少做错误选择。我在做只读代码审查时会刻意禁用写类和执行类工具,模型就不会出现“一边看代码一边改代码”的危险操作。

2.4 别名机制:让模型少读一段长单词

除了压缩 schema,还有一种不太被人注意的优化:工具别名。如果某个扩展的工具名特别长,你可以在配置层给它一个短别名。比如把utils.filesystem.read_text_file_from_local_path映射为fs.read,模型在系统提示词里看到的工具名是fs.read,真正执行时 Pi Agent 再把别名映射回完整路径。

我当时在一个开发项目里就是这么干的。项目里 27 个工具,我全部换成短名后,工具定义部分的 token 又降了 15% 左右。别名的好处是它不影响扩展本身,完全是用户侧的覆盖。配置方式通常写在扩展加载区:

{ "extensions": { "chs_file_utils": { "aliases": { "read_text_file_from_local_path": "fs.read", "write_text_file_to_local_path": "fs.write" } } } }

这里有个注意点:别名不能跟其他工具重名,否则模型会困惑。我最早把“列出目录”和“列出进程”都起成list,结果模型连续在两者之间乱选。后来统一按前缀区分,fs.list和ps.list,问题马上消失。

3. 给扩展作者:从源头设计轻量工具定义

3.1 工具定义四层结构:名称、参数、描述、示例

如果说用户端配置是“治标”,那扩展作者改进工具定义就是“治本”。一个工具定义通常有四层信息:

  • 名称:模型用来标识工具的唯一字符串
  • 参数:JSON Schema 描述参数结构
  • 描述:说明工具用途和注意事项
  • 示例:调用样例

我的经验是,这四层信息对模型的价值完全不一样。名称和参数是模型调用工具时的“必读信息”,描述是“按需阅读”,示例则是“锦上添花”。多数扩展作者的问题在于:把四层信息混在一起,每个字段都重复解释,导致必读信息被淹没在冗余描述里。

3.2 落地案例:把文件读取工具从 260 token 减到 25 token

直接给你看一个改造前后的对比。下面是我对一个文件读取类工具的完整改造:

改造前:

{ "name": "read_text_file_from_local_path", "description": "读取本机文件系统上的文本文件内容。只支持 UTF-8 编码的纯文本文件,不支持二进制文件。调用时请自行确认路径是否存在。如果文件不存在,会返回错误信息。读取大文件时请小心内存占用。", "parameters": { "type": "object", "properties": { "path": { "type": "string", "title": "文件路径", "description": "指向目标文件的相对路径或绝对路径", "examples": ["/home/user/project/main.py"] } }, "required": ["path"] } }

改造后:

{ "name": "read_file", "description": "读取文本文件;不存在则报错", "parameters": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } }

这个改造里有几个关键决策。工具名从read_text_file_from_local_path改成read_file:模型看到read_file就明白用途,不需要再叠一堆修饰词。description 从六十多个字压到十四个字:“读取文本文件;不存在则报错”——前半句告诉模型什么时候用,后半句告诉模型失败了会怎样。参数只留一个path,去掉了 title、examples、超过字段名的描述。

有人会担心:description 写这么短,模型还知不知道“哪些工具是只读的”?我的回答是:工具名里带上read_前缀本身就是一种约定,Pi Agent 的模型在预训练过程中对这类命名已经形成了很强的先验。你把描述删到一句话,它反而更依赖你的字段名;字段名清晰,短描述完全够用。

3.3 三个让 Schema 更克制的设计技巧

第一,能用枚举就别用自由字符串。有些扩展把“文件操作模式”设计成:

"mode": { "type": "string", "description": "操作模式,可选 append 或 overwrite" }

改成枚举:

"mode": { "enum": ["append", "overwrite"] }

模型看到枚举,就不需要从 description 里猜“到底能传什么值”,token 也少写十几个,校验还更严格。

第二,用约定代替重复描述。如果你的扩展里所有工具都作用于同一个工作区,与其每个工具都写一遍“路径相对于当前工作区”,不如在扩展总配置里定义一个workspace_path参数,或者写进扩展的使用说明里。工具定义属于系统上下文,说明文档属于用户上下文。能放进文档里的约定,就不要塞进每个工具的 description。

第三,减少嵌套对象。嵌套对象在 JSON Schema 里写起来 token 多,模型解析也难。尽量把参数摊平:{"user": {"name": "...", "id": "..."}}改成{"user_name": "...", "user_id": "..."}。参数层次深了,模型每次递归解析都要消耗额外理解力,摊平之后 name 和 id 一目了然。我在优化网络请求类扩展时做过对比,把三层嵌套压成一层后,同一任务的工具定义部分 token 少了一半。

3.4 什么时候必须保留详细描述

精简不是越短越好。我遇到过一类特殊情况:工具的副作用无法从名称推断。比如一个工具叫sync_state,描述不写清楚它会把本地修改推送到远端,模型就可能误以为它只是在本地刷新状态。另一个例子是cleanup_cache,描述不说明它会删除临时目录,模型可能在用户还开着某些文件时调用它。

这类“名称看不出来后果”的工具,必须保留一段明确描述副作用和不可逆性的文字。所以我在给扩展写工具定义时的判断顺序是:先问“这个工具有没有不可逆副作用”,如果有,写 detailed;如果没有,优先考虑 minimal。你可以在同一个扩展里混用两种详略级别,没有规定说一个扩展的所有工具都得一个风格。

4. 效果验证:如何确认 91% 不是靠牺牲准确率换来的

4.1 用日志量化 token 节省

改了配置、改完扩展,怎么确认真的省了?最直接的办法是看两个指标:单轮请求的工具定义部分 token 数,和工具调用成功率。

Pi Agent 的日志或者 API 返回里通常能看到每次请求的 prompt 明细。把工具定义部分的字符数取出来,用字符数除以 3.5,粗略换算成 token。优化前和优化后各统计十轮请求,取平均值,就能得到你的实际节省率。注意每次对话长度不一样,所以别跟“总 token”比,要专门比工具定义部分。

4.2 回归测试六项清单

每次对工具定义做大改动之前,我都跑一遍回归测试。清单很长,这里挑六个关键项:

测试项检查点通过标准
必填参数缺失故意不给 path 调用 read_file返回参数错误,而不是瞎猜一个路径
枚举外值给 mode 传一个不在枚举里的值返回校验错误
多工具混合调用连续让模型“先查看再修改”两次调用分别命中正确工具
否定指令“不要执行任何写操作”模型避开写类工具
中文文件名路径含空格和中文参数正确传入,不出现转义错乱
并发长对话30 轮对话后继续调用工具工具定义部分仍然正确进入上下文

这六项能覆盖大部分优化带来的隐性风险。我自己的项目里,真实踩过其中三项的坑,后面部分会展开讲。

4.3 优化前后的准确率对比

我在一份内部工具集上做了 100 次随机任务测试,结果供你参考:

指标优化前优化后差异
工具定义 token 合计3400300-91%
工具调用成功率96%95%-1%
平均单次调用延迟1.8s1.1s-39%
错误调用次数45+1

工具调用成功率下降了一个百分点,这个误差在统计上基本可以忽略——我拿不同任务集测了两轮,第二轮甚至出现优化后比优化前更准的情况。因为描述变短之后,模型不再被大段文字干扰,反而更容易抓住“什么场景调什么工具”这个核心。当然,这个结论有个前提:我保留了所有带副作用的工具的详细描述。如果你把所有工具的 description 都一刀切删光,准确率不会这么好看。

5. 踩过的坑与排查速查表

5.1 三个典型的翻车现场

第一个坑:删描述删得太狠,模型开始“行为变异”。我优化过一个时间管理扩展,里面有个工具叫update_task_due_date,我把 description 从“更新某个任务的截止日期,只会修改 due 字段,不会影响任务的标题和优先级”压缩成“更新截止日期”。结果在复杂对话里,模型连续出现“把任务标题和截止日期一起改”的情况。原因不是它不认识这个工具,而是它不知道这个工具的副作用边界。后来我在 description 里补了半句“仅改 due 字段”,问题立刻消失。

第二个坑:参数名缩写导致“参数幻觉”。为了省 token,我把user_working_directory缩成uwd,把retry_count缩成rc。结果模型在多次调用里频繁填出莫名其妙的uwd: "/var/..."值,我查了半天才发现是参数名太抽象,模型只能靠猜。修法是保留有语义的短名:cwd、retries,它们同样短,但模型一眼就知道含义。

第三个坑:共享枚举带来的混乱。为了省 token,我把多个互不相关的工具的返回值格式统一成了一个共享枚举{ok, err, pending},然后在每个工具的 description 里写“返回值遵循标准枚举”。绝大多数情况下没问题,但当模型需要同时分析“文件读取”和“远程调用”两类工具的返回时,它会把pending误判成某个文件操作的异步等待。后来我把枚举拆成按工具类别声明,虽然 token 多了一点,但准确率明显回升。

5.2 问题速查表

症状可能原因解法
模型频繁调用错误工具描述删到连副作用都看不清恢复 detailed,重点写副作用
参数值经常凭空捏造参数名缩写成无意义短码改用有语义的短词,如 cwd/retries
工具定义没生效扩展缓存未刷新重启会话,清空缓存后重载
枚举值被乱传共享枚举跨领域共用按工具类别拆分枚举
模型完全不调用某个工具工具名与描述互相矛盾统一名称后缀风格,保持命名一致
中文参数乱码编码未显式声明在参数 schema 里加"encoding": "utf-8"

5.3 我的推荐配置模板

文章最后给你一套我实际在用的配置模板。它会随 Pi Agent 版本迭代微调,但核心思路不变:全局精简、副作用工具保留详细、不承载过多与当前任务无关的工具。

{ "tools": { "compact_schema": true, "drop_title": true, "max_description_chars": 80, "overrides": { "fs.write": "detailed", "shell.run": "detailed", "state.sync": "detailed", "fs.read": "minimal", "fs.list": "minimal", "web.fetch": "normal" } } }

我在实际使用中还发现一个小技巧:工具定义改动后,不要急着把配置压到最终形态,先跑两天日常任务,看模型有没有出现调用不稳定的情况。稳定了再继续压。压缩工具提示词这件事,本质上是对“模型理解成本”和“token 传输成本”做取舍。我的项目最终把工具提示词降到了原来的 9%,同时 100 次任务里的工具调用成功率只下降了一个点以内。对我和我身边的 Pi Agent 用户来说,这个优化带来的收益是实打实的:请求更快、成本更低、扩展的可维护性也更高。你自己上手之后,一定会在某个小项目里发现我上面没提到的坑,那正是这个优化方向的乐趣所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询