先说个前阵子踩的坑。我用 Pi Agent 做跨模块重构会话跑到一半模型开始频繁丢上下文回答越来越敷衍。一开始我以为是长会话的老毛病后来把会话的 token 明细拉出来一看问题清楚得吓人系统提示词里光工具定义就占了 12800 多 token而真正被调用的只有六个工具。剩下三十多个躺在输入序列里一动不动每轮请求都在白吃预算。这一周我系统做了一次减法把会话里的工具提示词从 12800 token 压到 1152 token省掉了整整 91%。这篇文章就是对这次优化的完整操作复盘适合两类读者一类是 Pi Agent 的日常用户想知道怎么通过配置和会话习惯给上下文腾位置另一类是扩展作者想从源头设计出不费 token 的工具清单。我会先把机制讲清楚再给可直接照抄的操作步骤最后说哪些提示词坚决不能省。1. 工具提示词是怎么悄悄吞掉上下文预算的1.1 先定义清楚我们说的工具提示词是哪个东西工具提示词严格来说不是用户敲进去的那段 prompt而是 Pi Agent 框架根据注册好的工具清单自动生成的一段模型输入。每注册一个工具它就会被渲染成工具名 用途描述 参数 Schema三件套拼进系统提示词里。模型靠这段文字知道我现在有哪些东西可以用、每个东西怎么用、参数怎么传。举个例子一个简单的代码搜索工具在系统提示词里大概长这样{ name: search_code, description: 在代码库中执行正则搜索返回匹配文件和行号。定位符号定义、调用点、TODO 标记时使用。, parameters: { type: object, properties: { pattern: { type: string, description: 要搜索的正则表达式 }, path: { type: string, description: 可选限定搜索目录默认项目根目录 } }, required: [pattern] } }就这么一个最小化的工具渲染进提示词也要一百多个 token。你可能觉得一百多没什么但它不是只出现一次——它跟着系统提示词出现在每一轮请求里从第一轮到最后一轮一次都不会少。1.2 算一笔账40 个工具意味着 12.8k token 的固定开销上下文窗口就像一张工位工具提示词是固定摆着的一排健身器材聊天记录是桌上堆的文件代码是手里正在焊的架子。器材越多干活的空间越小。我先按真实使用情况把账算给你看场景工具数量单工具平均 token工具提示词合计只用核心工具123003,600装 2 个常用扩展253107,750装 5 个扩展我优化前的状态4032012,80012.8k token 是什么概念在常见的 200k 上下文窗口里看着占比不大但实际会话里还要叠加上系统规则、历史消息、读进来的代码文件长任务跑到中段可用余量就非常紧张了。再算一笔账假设这个会话有 300 轮请求工具提示词每轮都完整发送一次300 轮下来光是描述工具就消耗了约 3.8M token。这还只是输入侧的开销不包含输出。一句话工具提示词是全会话里最贵、又最容易被忽视的固定成本。1.3 四类冗余这笔钱是怎么白白花掉的我后来逐个工具拆开看发现这 12.8k token 里有四类明显的浪费理解了这四类后面所有操作都会变得很有针对性。第一类是描述冗余。很多工具的描述写得像文档摘要把接口细节、返回格式、示例都塞进去。模型其实只需要知道这个工具是干嘛的、什么时候该用。第二类是Schema 冗余。每个参数都写了 title、description、默认值说明字段名本身已经能说明意思的还要再解释一遍。参数越多浪费越严重。第三类是加载冗余。这是最大的头。40 个工具里大部分扩展工具跟当前任务毫无关系——写代码的会话挂着一套数据库工具做数据清洗的会话却还挂着六个 Git 操作工具。工具全量常驻是提示词膨胀的根本原因。第四类是跨会话冗余。每个新会话都要把同一套工具定义重新加载一遍。如果不同的会话类型编码、运维、数据分析共用同一份全量配置那每个会话都在重复交这笔学费。2. 动手之前先搞懂 Pi Agent 的加载规则2.1 三层工具集核心、扩展与按需加载很多人拿到 Pi Agent 的第一反应是把所有扩展都装上这恰恰是提示词膨胀的起点。以我当前用的版本为例它的工具加载分三层。第一层是核心工具集包括 shell、文件读写、搜索、patch 这类基础能力随 Agent 启动常驻一般 10 到 15 个。第二层是扩展工具集来自你安装的各种扩展包——GitHub 操作、Docker、数据库、云平台部署等等。第三层是按需工具只在特定条件下被临时加载用完即弃。这里的关键点在于扩展工具默认并不一定全量进入提示词。Pi Agent 会先做一个轻量的任务预判再决定把哪一组扩展工具真正加载进当前会话。这个预判的依据恰恰是扩展清单里的名称和描述。所以扩展作者怎么写名字、怎么写描述直接决定它能不能在需要时被准确唤醒也决定它会不会在不需要时白白占位。2.2 扩展清单是如何变成提示词的扩展包的根目录里通常有一个 manifest 文件声明这个扩展提供哪些工具。我贴一个简化版给你看name: git-helper version: 1.3.0 description: Git 常用操作封装 tools: - name: git_status description: 查看工作区状态包括暂存区、未跟踪文件和分支信息。 input: type: object properties: short: type: boolean description: 使用短格式输出默认 false。 output: text - name: git_log description: 查看提交历史支持按作者、时间过滤。 input: type: object properties: author: type: string description: 只显示该作者的提交。 since: type: string description: 起始日期格式 YYYY-MM-DD。 output: textPi Agent 在会话初始化时会把所有被判定为需要加载的工具逐条读取然后按照统一的模板渲染成一段连续文本拼进系统提示词。扩展装的越多、每个工具的描述和 Schema 越臃肿这段文本就越长。理解了这个渲染链路你就应该明白在 manifest 阶段做减法比在会话阶段做减法更彻底。2.3 用户手里有四张牌禁用、别名、模板与按需开关用户侧不是只能被动接受。我实际可操作的入口大致是四个第一项目级配置文件里可以手动禁用指定工具第二可以给常用工具设置短别名名字短了工具提示词和模型输出里的工具名都会变短第三创建会话时可以选不同的任务模板模板会决定挂载哪些扩展第四有一个开关控制是否启用按需加载核心机制就是我前面说的任务预判。这四张牌组合起来就是后面第三节的具体操作。做优化之前我强烈建议你先看一眼自己的配置文件长什么样不同版本的路径可能略有差异但逻辑基本一致能拿到工具清单的地方就能做减法。3. 用户端实操四个动作把提示词压到 9%3.1 第一步盘点当前会话到底加载了什么优化永远从测量开始。你可以直接在会话里输入/tools或打开工具面板看当前会话实际加载了哪些工具。我当时的清单里有 40 个工具其中 14 个是核心工具26 个来自 5 个扩展包。让我意外的不是装了很多而是里面有 12 个工具我近一个月一次都没用过网页截图工具、翻译工具、时区转换工具、两个功能高度重叠的代码搜索工具……我建议你做一个最简单的记录表列三列工具名、上次使用时间、当前会话是否用到。如果一行工具既在上次使用时间里查不到近期的记录又在当前会话是否用到里填了否那它就是第一批被砍掉的对象。3.2 第二步砍掉这些工具没有任何损失拿我手头这个版本举例项目根目录下的配置文件长这样tools: disabled: - web_fetch - browser_snapshot - translate_text - timezone_convert aliases: github_pr_create: pr_create loads: on_demand: truedisabled列表里的工具会直接被排除在渲染之外不会再生成对应提示词。我一次性禁掉了 8 个工具工具提示词立刻从 12800 掉到 9500 左右少了四分之一。这一步几乎没有风险因为砍掉的都是低频或与当前项目无关的能力。你担心的万一以后要用怎么办完全多余——配置文件里随时可以加回来而且 Pi Agent 的按需加载机制会在你真用到的时候通过预判把它带进来。3.3 第三步用会话模板做职责隔离砍完低频工具大头还在后面那 40 个工具里的高频工具也只是对某个特定场景高频。写 Go 服务的时候Docker 部署工具一次都用不上查数据库的时候Git 操作工具基本闲置。让一个会话背上全部高配工具等于每天通勤都开卡车。我的做法是建三套会话模板你可以直接抄模板名挂载的扩展适用场景coding核心工具 Git 扩展日常写代码、重构、代码审查data核心工具 数据库/数据处理扩展数据分析、脚本调试ops核心工具 Docker/K8s/云平台扩展部署、运维、排障这样每个会话的工具数量从 40 降到 15 左右提示词规模自然跟着砍半。一开始你会觉得切来切去麻烦但配合模板一键创建实际多花的时间不到五秒钟换来的却是每一轮请求都更轻快。3.4 第四步把按需加载真正打开如果你确认自己的配置里没有开启on_demand请一定把它打开。按需加载的意义在于即使某个扩展在模板里被挂载了如果本次任务完全没触发它的关键词它的工具提示词也不会被渲染进系统提示词。这相当于给工具集又加了一道动态闸门。实测下来打开按需加载之后我那个 coding 模板的会话工具提示词又掉了一截最终稳定在 1152 token 左右。四步全部做完从 12800 到 1152刚好省了 91%。这中间没有任何一步是伤筋动骨的全是配置级的调整属于做了就赚的优化。4. 扩展作者的设计守则让工具从源头就省 token前三节是用户侧的打法但工具提示词的膨胀真正的源头在扩展作者这边。一个写得臃肿的扩展会被几百个用户加载、渲染、浪费。下面这五条守则是我自己写扩展时定下的硬规矩每条都对应可量化的 token 节省。4.1 名字短而唯一工具名是模型识别工具的标识也是提示词里必须出现的字符串。好的名字控制在 6 到 12 个字符用命名空间前缀避免冲突。比如gh_pr_create就比create_pull_request短也比create_pull_request_with_title这类描述式命名干净得多。模型在调用工具时必须输出完整名字名字越长每轮调用都跟着多花 token。这不是一次性成本是叠加成本。反面典型我也见过一个请求历史数据的工具叫get_historical_telemetry_data_for_device_with_range42 个字符。改成device_telemetry_query十六个字符意思一点没丢。4.2 描述一句话说清做什么、何时用描述是工具提示词里弹性最大的部分。很多人习惯写长描述生怕模型不懂。但模型比你想象的更擅长抓重点你要做的不是解释是触发。我自己的模板是动作 对象 触发条件不超过 15 个词。常规写法约 70 token从远程仓库拉取一条 Pull Request 的完整详细信息包括标题、正文、提交历史、文件变更列表、审查意见、CI 检查状态以及合并状态。适用于需要了解 PR 全貌的场景。精简写法约 20 token读取 PR 详情用于查看变更和检查状态。前者包含的很多信息模型反正看不到返回值的实际结构就无法用写了等于白写。后者把做什么和何时用讲清楚了剩下的交给函数实现。我统计过单这一条守则就能让描述成本降低 60% 到 70%。4.3 参数 Schema删掉所有可以被默认值替代的字段Schema 是工具提示词里最容易失控的部分。写 Schema 的人有一种心理惯性把所有可配项都暴露出来显得工具很强大。但模型其实只需要知道必须传什么、可传什么、默认给什么。这是我实际压缩过的一个例子。压缩前{ type: object, properties: { owner: { type: string, description: 仓库所属的组织或个人用户名必填。 }, repo: { type: string, description: 仓库名称必填。 }, pull_number: { type: integer, description: PR 编号必填。 }, include_reviews: { type: boolean, description: 可选是否包含审查意见默认 false。 }, include_ci: { type: boolean, description: 可选是否包含 CI 状态默认 false。 } }, required: [owner, repo, pull_number] }压缩后{ type: object, properties: { owner: {type: string}, repo: {type: string}, pull_number: {type: integer}, include_reviews: {type: boolean, default: false}, include_ci: {type: boolean, default: false} }, required: [owner, repo, pull_number] }差别就在于删掉了所有重复描述的字段说明把可选参数用default兜底。字段名owner、repo本身已经自解释描述说明纯属画蛇添足。这个工具的 Schema 部分直接从原来的 230 token 砍到 90 token效果立竿见影。4.4 公共 Schema 抽取一处定义处处引用如果你一个扩展里有一组工具都要用到分页参数、认证信息、时间过滤条件千万别在每个工具的 Schema 里各写一遍。把这些公共对象提取出来用引用语法统一指向。渲染的时候提示词里只出现一次定义各工具按需引用。这一招在工具数量多的时候尤其有效——10 个工具共享同一个分页结构能省下数千 token。4.5 聚合工具用 action 枚举替代一打相似工具这是我自己最推崇的一招。很多扩展作者喜欢一个操作一个工具结果 GitHub 扩展一写就是 20 个工具。更好的做法是聚合一个工具、一个 action 枚举、内部去分发。比如仓库管理与其注册repo_list、repo_get、repo_create、repo_update、repo_delete五个工具不如注册一个{ name: github_repo_ops, description: GitHub 仓库操作。action 指定 list/get/create/update/delete。, parameters: { type: object, properties: { action: { type: string, enum: [list, get, create, update, delete] }, repo: {type: string}, payload: {type: object} }, required: [action] } }五个工具的提示词合并成一个描述成本直接省掉 60% 以上。模型侧的理解负担也小得多——它不需要在五个名字里做选择题只需要在一个工具的参数里选一个枚举值。这个模式唯一的代价是描述需要稍微写清楚聚合规则但这几十个 token 的投入比维护五个工具省得多。5. 实测记录同样的任务12806 token 到 1152 token5.1 测量方法用 tokenizer 说话优化这种事不能靠感觉变快了要有数字。我用的测量脚本很简单核心逻辑是把工具清单渲染成和系统提示词一样的文本然后用 tokenizer 编码数长度import json import tiktoken enc tiktoken.get_encoding(cl100k_base) def tools_to_prompt(tools: list[dict]) - str: lines [] for t in tools: lines.append(f## {t[name]}) lines.append(t.get(description, )) lines.append(json.dumps(t.get(parameters, {}), indent0, ensure_asciiFalse)) return \n.join(lines) def tool_prompt_tokens(tools: list[dict]) - int: return len(enc.encode(tools_to_prompt(tools))) # 优化前从配置和扩展清单里读出的全部工具 before_tools load_all_tools() # 40 个 # 优化后模板加载 按需预判之后的工具 after_tools load_effective_tools() # 15 个 print(before:, tool_prompt_tokens(before_tools)) print(after:, tool_prompt_tokens(after_tools))这里有个细节值得注意load_effective_tools()不是模拟出来的是我在会话里通过/tools拿到真实渲染结果。测量要测模型实际看到的不是配置文件里写的否则数字会虚高。5.2 优化前后对比我那次优化的完整数据贴在这里你可以当成一个基准参考指标优化前优化后变化会话加载工具数4015-62.5%工具提示词 token12,8061,152-91.0%单工具平均 token32077-76.0%首轮响应耗时同任务约 2.1s约 1.4s-33%长任务中途丢上下文的概率高未再出现-首轮响应耗时的下降很直观模型要处理的输入短了预填充时间自然变短。但更重要的收益是长任务稳定性——以前跑到 200 轮左右开始丢细节优化后同样的任务跑到 350 轮关键上下文依然完整。这就是省出上下文预算带来的实际红利。5.3 回归验证压缩会不会破坏效果省了 91%心里肯定会打鼓工具描述这么短模型还能不能正确调用我做了两轮回归。第一轮把过去两周跑过的 12 个典型任务用优化后的配置重跑一遍包括代码重构、多文件搜索、Git 操作、数据库查询。结果是 11 个任务输出完全符合预期1 个任务第一次调用参数传错但模型自己通过工具返回的错误信息纠正了。第二轮专门测边界场景让模型在必须从两个相似工具里选一个的情况下做判断这个我放在下一节细讲。结论是对于绝大多数日常场景91% 的压缩不会带来可感知的能力下降。但如果你的用例涉及危险操作、易混淆工具或复杂参数约束那就得看最后一节的边界清单。6. 不能省的提示词压缩的边界与反面案例任何一刀切的优化都会留下隐患。下面这四类提示词是我在实践中发现必须保留甚至主动加厚的地方。6.1 涉及危险操作的安全说明删除分支、清空目录、推送强覆盖这类工具描述里必须保留明确的安全警告。模型需要知道这个操作不可逆才能在用户意图含糊时停下来确认。我在一个工具集里做过实验把删除操作的安全说明从 80 token 压到 10 token结果模型在任务中直接把删除分支和创建分支搞混过一次。虽然最终没有造成事故但这说明安全信息不是冗余是护栏。生产环境的扩展护栏不能省。6.2 易混淆工具的区分信息如果你的扩展里有两个工具长得特别像比如read_file和read_binary或者parse_log和search_log描述里必须给出明确的区分线索。压缩描述时我通常保留一句话文件较大或二进制内容用前者纯文本检索用后者。这类区分信息一般也就二三十个 token但它决定了模型能不能选对工具。省了这几十个 token换来一次错误的工具调用光重试的 token 成本就翻倍了。6.3 带复杂约束的参数正则表达式、时间范围格式、枚举白名单、速率限制边界——这些约束必须留在 Schema 里。模型不擅长猜约束你删掉pattern或format限制它就会传一个格式错误的值然后被工具报错再重试。一次报错重试的成本轻松超过你省下的那几十个 token。我见过最离谱的例子一个工具的参数要求 ISO 8601 格式作者把格式说明删了模型连续传错三次。后来加回一行format: date-time问题立刻消失。6.4 什么时候应该主动加回去压缩不是终点是起点。我现在的做法是先按守则压缩然后跑一轮典型任务哪个工具调用不稳就给哪个工具单独加厚描述而不是整体回滚。通常加 30 到 50 个 token 的描述就够解决问题整体依然维持在 90% 的压缩率附近。这种按需回补的迭代方式比一次性写满再慢慢删要高效得多。我现在已经把这套守则写进了自己扩展仓库的贡献指南每个新工具的 description 字数上限设为 20 词Schema 里不出现与字段名重复的说明ray 公共类型统一抽取。新写的扩展从合并那一刻起就自动达标。如果你维护着自己的工具集建议也把这条底线定下来——它不耽误功能却能让你和所有使用你扩展的人每一轮请求都少付一点代价。