
1. 工具提示词为什么成了 Pi Agent 的隐形开销第一次认真统计 Pi Agent 的 token 消耗时我盯着账单愣了几秒真正用于推理和生成的内容只占一小部分剩下的大头全被工具提示词吃掉了。所谓工具提示词就是每次调用模型时随请求一起塞进去的那一大段工具描述——每个工具叫什么、参数有哪些、什么时候该用、返回什么格式全都得写清楚。工具越多这段描述越长而且它是每一轮对话都要重新发一遍的。这件事的隐蔽性在于它不像对话历史那样肉眼可见地增长。你看着聊天窗口里只有几句问答但后台每次请求都带着一份完整的工具清单。假设你有 20 个工具每个工具的描述平均 150 token光工具定义就 3000 token。如果一轮任务要来回 10 次那就是 3 万 token 的纯开销跟你的实际需求毫无关系。标题里说的省掉 91%不是拍脑袋的数字。它来自一个很朴素的观察大部分任务根本用不到全部工具。一个查天气的请求不需要数据库工具、不需要文件读写工具、不需要代码执行工具。但传统做法是把所有工具一股脑塞进去让模型自己挑。模型挑得累你的钱包也累。1.1 工具提示词的三个成本维度很多人只盯着钱其实成本有三个层面而且后两个往往比钱更致命。第一是直接费用。按 token 计费的模型工具提示词是实打实要付钱的。输入 token 通常比输出便宜但架不住量大。一个高频使用的 Agent工具提示词可能占到总输入 token 的 60% 到 90%。第二是上下文窗口占用。模型的上下文长度是有限的工具提示词占得越多留给真实对话、文档、代码的空间就越少。你可能会遇到明明没聊几句模型就说记不住了的情况多半是工具描述把窗口挤爆了。第三是注意力稀释。这是最容易被忽略的。模型在处理长上下文时注意力是会被分散的。当工具清单长达几千 token模型在判断该用哪个工具时的准确率会下降出现选错工具、参数填错、该调用时不调用等问题。工具越多这种退化越明显。提示如果你发现 Agent 经常忘记某个工具的存在或者在小任务上调用了一堆无关工具先别怀疑模型能力去看看工具提示词是不是太长了。1.2 为什么全量塞入是默认做法这得从工具调用的实现说起。早期做 Agent 时最省事的方案就是把所有工具定义序列化成一段文本或结构化 schema拼在系统提示里。这样做的好处是简单、通用、不用维护额外逻辑。模型厂商的 SDK 也大多这么设计你传一个 tools 数组它帮你拼进去。问题在于这个设计假设了工具数量不多。当工具从 5 个涨到 50 个从单一领域扩展到跨领域这个假设就崩了。但很多框架没有及时跟进默认还是全量塞入。于是大家一边享受着 Agent 的便利一边默默承担着这份隐形开销。我见过一个内部工具平台注册了 80 多个工具每次请求的工具提示词接近 2 万 token。后来做了按需加载同样的任务 token 消耗直接降到原来的十分之一不到。这不是什么黑科技就是把用不到的东西别发这个常识落实了。1.3 省 token 和保能力之间的平衡点有人会担心工具少了模型会不会做不了复杂任务这个担心合理但解法不是全塞而是按需给。核心思路是两阶段调用第一阶段只给模型一个精简的工具目录比如只有工具名和一句话说明让它判断这个任务需要哪些工具第二阶段再把选中的工具完整定义发过去执行真正的调用。这样既保证了模型知道有哪些能力可用又避免了每次都发全量描述。这个思路听起来简单但落地时有不少细节要处理目录怎么设计才够模型判断、选中后怎么动态注入、多轮对话中工具集怎么保持一致、缓存怎么处理。这些正是后面要展开的内容。2. 拆解 Pi Agent 的工具加载机制要动手优化先得搞清楚 Pi Agent 到底是怎么加载和传递工具的。不同版本的实现细节可能有差异但核心链路大同小异。我按自己的理解把这条链路拆开讲你对照自己的代码看基本能定位到关键位置。2.1 一次请求里工具提示词的生命周期从你发起一个任务到模型返回结果工具提示词经历了这么几个阶段注册阶段扩展作者通过 API 注册工具提供名称、描述、参数 schema、执行函数。收集阶段Agent 运行时把所有已注册工具汇总成一个列表。序列化阶段把工具列表转成模型能理解的格式通常是 JSON schema 或特定文本模板。注入阶段把序列化后的工具描述拼进请求通常放在系统提示或专门的 tools 字段。传输阶段随请求发给模型。缓存阶段如果用了提示缓存这段工具描述会被缓存但缓存失效后仍需重新传输。关键点在第三步和第四步。序列化的方式直接决定了 token 量注入的位置决定了它是否会被缓存、是否每轮都重发。2.2 工具描述里哪些字段最费 token不是所有字段都一样贵。我做过一个粗略的统计按 token 占比排下来大概是这个顺序字段典型占比说明参数 schema40%-55%嵌套越深越费枚举值、默认值、示例都算工具描述文本20%-30%写得越详细越费但往往可以精简参数描述15%-25%每个参数的说明文字工具名称5%-10%短名称省不了多少但数量多了也可观返回格式说明5%-10%有些框架会额外加一段参数 schema 是大头。一个带嵌套对象、数组、枚举的参数定义轻松几百 token。如果你有十几个这样的工具光 schema 就上千了。2.3 扩展作者最容易忽略的注入点扩展作者通常只关心自己的工具能不能被调用很少关注它被注入时的形态。但恰恰是这里藏着优化空间。第一个注入点是工具描述的写法。很多人把描述写成产品文档恨不得把每个边界情况都讲清楚。但模型需要的是什么时候用我不是我的完整规格。描述精简到两三句话往往效果一样token 却省一半。第二个注入点是参数 schema 的冗余。比如给每个参数都加description但参数名本身已经足够清晰比如给枚举值加详细说明但枚举值本身就是自解释的比如加了examples字段但模型很少真正用到。这些都可以砍。第三个注入点是工具的分组。如果框架支持工具分组或命名空间把相关工具归到一起模型判断时更聚焦也便于按组加载。2.4 从注册到调用的完整数据流把上面这些串起来一个工具从注册到被调用的完整数据流是这样的扩展注册工具 - 运行时收集到工具池 - 按当前策略筛选全量 or 按需 - 序列化为模型格式 - 注入请求 - 模型判断是否调用 - 返回工具调用请求 - 运行时路由到对应工具 - 执行并返回结果 - 结果注入下一轮请求优化的切入点就在按当前策略筛选这一步。默认策略是全量我们要做的是把它换成按需。这一步改动不大但收益巨大。3. 按需加载把工具提示词砍到十分之一的实操理论讲完了进入动手环节。这一节我按先跑通最小可用版本再逐步优化的顺序来写你可以跟着一步步来。3.1 最小可用方案两阶段工具选择最直接的做法是加一个工具选择阶段。具体流程维护一份工具目录每个工具只有名称和一句话描述总 token 控制在几百以内。用户发起任务时先把目录发给模型让它返回需要的工具名列表。根据返回的列表从工具池里取出完整定义注入到真正的执行请求里。执行请求里不再包含目录只包含选中的工具。这个方案的关键是目录要足够精简同时信息量要够模型判断。我试过几种目录格式最后觉得工具名 动词开头的短句效果最好。比如weather_query: 查询指定城市的实时天气 file_read: 读取本地文件内容 db_query: 执行数据库查询语句一句话动词开头说清楚做什么。不要写这个工具用于...不要写参数不要写返回格式。模型判断需不需要时这些信息足够了。3.2 工具目录的设计让模型一眼选对目录设计有几个坑我踩过分享给你。坑一描述太抽象。比如写处理数据模型根本不知道是读、写、转换还是分析。要具体到动作和对象。坑二工具名太相似。get_user和fetch_user放一起模型容易混。要么合并要么在描述里明确区分场景。坑三目录太长。如果工具有上百个目录本身也会变成负担。这时候要分层先按领域分组让模型先选领域再选具体工具。两层下来每层都短。坑四没有兜底。模型可能选不出工具或者选错。要允许它返回无合适工具并准备一个兜底策略比如回退到全量加载或者提示用户补充信息。我现在的做法是目录控制在 30 个工具以内超过就分组。每组目录单独发给模型让它先选组。实测下来模型选组的准确率比直接选工具高不少因为组级别的语义更清晰。3.3 动态注入的实现细节与代码骨架下面是一个简化的实现骨架用 Python 写你可以对照自己的技术栈改。class ToolRegistry: def __init__(self): self.tools {} # name - full_definition def register(self, name, definition): self.tools[name] definition def get_catalog(self): # 只返回名称和一句话描述 return [ {name: name, brief: definition[brief]} for name, definition in self.tools.items() ] def get_full(self, names): # 返回选中工具的完整定义 return [self.tools[name] for name in names if name in self.tools] def select_tools(registry, task, model_client): catalog registry.get_catalog() prompt build_selection_prompt(catalog, task) response model_client.chat(prompt) selected parse_selection(response) # 解析出工具名列表 return registry.get_full(selected) def execute_task(registry, task, model_client): selected_tools select_tools(registry, task, model_client) # 用选中的工具执行真正的任务 return model_client.chat_with_tools(task, toolsselected_tools)这段代码的核心就是select_tools和execute_task的分离。选择阶段用精简目录执行阶段用完整定义。两次调用的总 token 通常远低于一次全量调用。3.4 缓存策略别让选择阶段变成新开销有人会问多了一次选择调用不是又多花 token 了吗确实但这次调用的输入很短只有目录和任务描述输出也很短只有工具名列表总开销远小于全量工具描述。而且选择结果可以缓存。缓存策略我推荐两级任务级缓存同一个任务的多轮对话工具集一旦选定就固定下来后续轮次不再重新选择。这能省掉大量重复选择。模式级缓存如果某些任务模式反复出现比如查天气总是用同一组工具可以把任务特征 - 工具集的映射缓存起来下次直接命中。要注意缓存的失效条件任务意图明显变化时要重新选择工具池有更新时要清缓存。我一般给缓存加一个较短的过期时间配合意图变化检测效果比较稳。3.5 实测数据从全量到按需的 token 对比我在一个中等规模的项目上做了对比测试工具池 45 个任务类型覆盖查询、文件操作、代码执行、数据分析四类。结果如下方案平均输入 token/轮相对全量全量加载8600100%按需加载无缓存140016%按需加载任务级缓存7809%省掉 91% 就是这么来的。注意这是输入 token输出 token 基本不变因为任务本身的工作量没变。但输入 token 往往是成本大头所以整体费用下降非常明显。除了费用还有个意外收获模型选工具的准确率提升了。全量时偶尔会选错或漏选按需后基本没再出现。原因前面说过注意力不被稀释了。4. 扩展作者视角让你的工具更容易被选中如果你是扩展作者上面这些是运行时的事你控制不了。但你能控制的是自己工具的描述质量。这一节专门讲怎么写出省 token 又容易被选中的工具定义。4.1 工具描述的三句话原则我给自己定的规矩是工具描述不超过三句话。第一句说做什么一句话讲清楚这个工具的核心功能。 第二句说什么时候用给出典型场景帮模型判断。 第三句说边界什么情况下不该用或者有什么限制。举个例子一个文件读取工具读取指定路径的文本文件内容。 当需要查看本地文件、配置文件或日志时使用。 不支持二进制文件大文件请分段读取。三句话信息完整token 可控。对比一下那种写了一大段的描述效果差不多但省了一半以上。4.2 参数 schema 的瘦身清单参数 schema 是 token 大户能砍就砍。下面是我常用的瘦身清单删掉冗余的 description参数名已经说清楚的不用再写描述。删掉 examples模型很少依赖示例除非参数格式特别反直觉。简化枚举枚举值自解释的不用加说明。扁平化嵌套能用平铺参数解决的不要用嵌套对象。去掉默认值说明默认值写在 schema 里就行不用在描述里重复。合并相似参数两个参数总是成对出现考虑合并成一个。我做过一次瘦身把一个工具的 schema 从 380 token 压到 120 token功能完全没受影响。4.3 命名与分组的约定命名要遵循两个原则一致和可预测。一致是指同类操作动词统一。比如都用get_、set_、list_、delete_不要一会儿fetch一会儿retrieve。模型看到动词就能猜到行为。可预测是指名称能反映领域。db_query比query好file_read比read好。加上领域前缀模型在目录里扫一眼就知道这个工具属于哪块。分组则是在注册时就规划好。如果框架支持命名空间把工具按领域分到不同组。这样按需加载时可以按组选粒度更合理。4.4 一个真实扩展的重构前后对比我重构过一个数据库扩展原来注册了 12 个工具每个描述都很详细总 token 约 4200。重构后合并成 5 个工具描述精简总 token 约 900。具体改动把db_connect、db_disconnect、db_reconnect合并成一个db_manage用参数区分操作。把db_query、db_execute、db_batch合并成一个db_run用参数区分模式。删掉所有参数的冗余描述只保留必要的格式说明。描述统一改成三句话结构。重构后不仅 token 降了模型调用也更准了。因为工具少了选择空间小了出错概率自然低。5. 那些让我多花了两周才想明白的坑优化过程中踩的坑不少挑几个有代表性的讲讲希望你能绕过去。5.1 选择阶段的误判与兜底最开始我太信任模型的选择能力结果遇到几种误判任务描述模糊用户说帮我处理一下模型选不出工具返回空列表。这时候要有兜底要么追问用户要么回退到全量。跨领域任务一个任务同时涉及查询和文件操作模型只选了一类。解法是允许它选多组或者在选择提示里明确可以多选。新工具未被识别刚注册的工具模型不熟悉容易漏选。可以在目录里给新工具加个标记或者在选择提示里强调优先考虑新工具。兜底策略我建议至少准备两层第一层是模型选不出时追问用户第二层是追问无果时回退全量。这样保证任务不会卡死。5.2 多轮对话中工具集漂移多轮对话里如果每轮都重新选择工具会出现工具集漂移第一轮选了 A、B第二轮选了 B、C第三轮又变了。这会让模型困惑也让缓存失效。解法是锁定工具集。第一轮选定后后续轮次沿用除非用户明确切换任务。判断任务是否切换可以看用户输入和上一轮的语义相似度或者让模型自己判断是否延续当前任务。我现在的做法是默认锁定用户说换个任务或明显转向时才重新选择。这样既稳定又省 token。5.3 缓存失效的隐蔽触发条件缓存失效有几个不容易发现的触发点工具池更新注册了新工具或改了描述缓存必须清。但很多人忘了在更新时清缓存。系统提示变化如果系统提示里包含了工具相关信息系统提示一变缓存就失效。模型版本切换不同模型对同一目录的理解可能不同切换模型时最好清缓存。我吃过一次亏更新了工具描述但没清缓存结果模型一直用旧描述新功能死活调不出来。排查了半天才发现是缓存问题。5.4 精简描述导致的能力退化精简是好事但精简过头会出问题。我试过把描述压到极致结果模型在某些边界场景下选错工具。比如两个工具都能查询数据描述太简就分不清该用哪个。经验是核心区分点不能省。如果两个工具容易混描述里必须明确区分场景。省 token 的前提是不影响判断这个平衡要自己把握。6. 把优化做成可持续的机制一次优化不难难的是让它持续有效。工具池会增长任务会变化模型会升级优化方案得跟着演进。6.1 建立 token 基线监控第一步是知道现状。我建议给每次请求记录几个指标输入 token、输出 token、工具提示词 token、选中工具数。有了这些数据才能判断优化是否有效、何时需要调整。监控不用很复杂一个简单的日志加定期统计就够。关键是持续别优化完就不管了。我一般每周看一次趋势发现工具提示词占比回升就排查原因。6.2 工具使用频率驱动的清理定期看工具的使用频率长期没人用的工具考虑下线或归档。工具池不是越大越好每个工具都是成本。我一般按季度清理一次把使用率低于阈值的工具标记出来确认无用后移除。移除前要通知扩展作者避免影响他们的功能。6.3 给扩展作者的接入规范如果平台有多个扩展作者最好定一份接入规范明确工具描述的字数上限参数 schema 的字段要求命名和分组的约定必须提供的目录用简短描述规范不用太严但要有。我见过没有规范的平台工具描述五花八门有的写几百字有的只有一行优化起来很痛苦。6.4 版本升级时的回归验证模型升级或框架升级后按需加载的效果可能变化。要准备一组回归测试用例覆盖典型任务升级后跑一遍确认工具选择准确率和 token 消耗没有退化。我一般准备 20 到 30 个用例覆盖各个领域和边界场景。跑一遍大概几分钟但能避免很多线上问题。7. 一些零散但有用的经验最后分享几个零散的点都是实操中攒下来的。关于目录的排序工具在目录里的顺序会影响模型选择。把常用工具放前面模型更容易注意到。但别太刻意否则可能引入偏差。关于选择提示的写法选择提示里明确说只返回工具名用逗号分隔比让它自由发挥更稳定。格式约束能减少解析错误。关于多语言如果任务描述可能是多种语言目录描述最好也用对应语言或者用英文保持中立。混用语言会增加模型判断难度。关于测试优化前后一定要做 A/B 对比别凭感觉。我见过有人觉得优化了实际 token 没降多少因为选择阶段的调用把省下的又花回去了。关于文档把优化方案和接入规范写成文档新人和新扩展作者能快速上手。口头约定靠不住文档才是长期资产。这套方案我在几个项目上跑下来token 消耗稳定在原来的十分之一左右模型表现还有提升。核心就一句话别把用不到的东西发给模型。听起来简单但真正做到需要理解工具加载的每个环节并在每个环节上做减法。希望这些经验能帮你少走点弯路。