Repository navigation
工具发现重设计:会话内动态 tools 与 prompt cache 的根本矛盾 #879
Description
这是 #300 Tool Surface Economics 的一个实测续篇,不是重复开题:#300 当时缺的是「动态工具面在真实长任务上到底付了多少钱」的运行证据,而最近一组 Terminal-Bench 对照跑分把这个问题推到了不得不正面回答的位置。
相关的设计探索已有一份未合并的文档草案(PR #878,仅文档、无运行时改动),本文希望成为它的公开讨论入口。
1. 跑分暴露的问题
在 Terminal-Bench 一组 10 个任务上,用 Harbor 驱动 OpenPI(pi --print --mode json,普通 tool-calling,openai-completions)与 opencode 做同题对照,观察到的现象是:
- OpenPI 的 prompt cache 重叠命中约 59%,opencode 约 90%;opencode 从第 2 轮起基本稳定在 93–98%,OpenPI 则反复出现「整轮归零 / 腰斩」的 miss 回合,把整段 prompt 算进未缓存。
- 最干净的单点证据是
kv-store-grpc:在bg_start成功之前缓存一直正常(cacheRead 约 5k+),OpenPI 随后动态注入了bg_status/bg_list/bg_kill/bg_watch四个 lifecycle 工具,下一轮立刻变为 cacheRead 从约 5504 腰斩到 2432。 - 另外两个伴随现象:
nginx-request-logging中 OpenPI 护栏改写 transcript 后出现cacheRead=0的全量 miss;regex-log/log-summary这类「大 thinking / 大 tool 输出」之后一轮整段记为未命中、再下一轮恢复高命中。
先排除一个误解:这轮跑分不是 Code Mode。 Harbor 实际拉起的是 pi --print --mode json(mode json 只是 harness 的 JSON 事件流),scriptMode 为 false,系统提示是标准编码 agent(read/bash/edit/write)。所以「缓存低是因为走了 Code Mode」不成立——问题恰恰出在没走、但又在会话中途改 tools 的这条路上。
证据边界(避免过度归因,这点与 PR #878 的处理一致):
- 上述 59% / 90% 是今回跑分的对照数字,尚未在仓库内以固定 revision / model / 任务集复跑存档;请求按 provider 上报的
cacheRead统计,且各家cacheWrite上报不完整(本轮cacheWrite全程为 0 却能cacheRead),所以它适合作为「现象量级」而非精确价格结论。 - Issue Runtime tool-surface toggling (
context_pivot, background lifecycle tools) breaks the prompt prefix cache on models without mid-conversation tool support #644 曾把失效归因于工具显隐,作者随后复查并撤回了原主张:当时同时变化的还有系统段折回开头的问题。也就是说「工具数组本身」与「系统段变化」两条通道尚未拆开测量。本文的诉求正是把这件事测清楚,而不是预设结论。
2. 抽象问题:这不是 bg 的 bug
bg_* 只是最容易抓现行的一个。同一模式在 OpenPI 里是一整类:
- 能力组按意图加载:组加载本身改一次 tools;组内还有第二道门——
tasks_update/tasks_list随任务存在与否显隐,get_goal/update_goal随 goal 状态显隐; context_pivot按 token 阈值(约 30k)在每次agent_start重算、反复开关(Runtime tool-surface toggling (context_pivot, background lifecycle tools) breaks the prompt prefix cache on models without mid-conversation tool support #644 本地统计里开关 88 次、调用 0 次);- Plan / Setup 情节工具(
plan_ready、ask_user、configure_my_pi_setup等)随情节开始/结束进出活动集; - 子代理:父会话的活动面会被投影进子会话,父面中途变化会传导到子会话的工具装配(bug: subagent spawn fails when the parent holds runtime-activated tools #637 就是这类传导出问题的实例);
- MCP:任何「服务器连上后把 schema 塞进顶层 tools」的接法都会重演同一问题。
根因是同一个:工具定义位于 prompt 前缀最前面(tools → system → messages),会话内任何一次对模型可见 tools 的增、删、改序,在不支持中途锚定的 provider 上都会使此前已缓存的前缀失效;即使能锚定,OpenPI 工具附带的 promptSnippet / promptGuidelines 变化还可能通过系统段再打一次。 做 K 次动态变更,就付 K 次接近全量的 miss。会话越长、能力用得越全,损失越大。
原来隐含的取舍是「省首轮 schema tokens > 后期缓存收益」。这组跑分说明,在长会话上这个取舍不成立:为了前期不暴露工具付出的成本,后期以数量级更大的方式还回来了。这正是想请社区一起重新判断的地方。
3. 同行共识(供讨论,非定论)
- Pi(上游):MCP 默认
exposure: "codemode"——工具不声明进前缀,模型在 QuickJS 脚本里用searchTools()/describeTool()发现,mcp_servers摘要变化时追加到对话而非改 tools;另有deferred+tool_search路径。Pi 已经有「不改声明集」的发现面,只是 OpenPI 自己的工具几乎都还走经典 schema +setActiveTools显隐。 - Anthropic:
defer_loading让延迟工具不进初始 prefix、以tool_reference追加;官方明确把改 tools 数组列为打穿 prompt cache 的行为。 - OpenAI:建议 tools 保持稳定、用
allowed_tools/tool_choice限制可调用集(mask 而非 remove);tool search 把新发现工具加在上下文末尾。 - Manus:KV-cache 命中应作为第一指标,工具可用性用 mask 切换、不要从数组里删。
- MCP 官方:大工具集走「search → inspect → execute」的 progressive discovery,并明确禁止中途增删前缀 tools。
4. 改进方向(按代价从低到高,均待讨论)
- P0 — 冻结规则:同一会话内禁止对模型可见 tools 做 Added / Removed / Reorder。先停掉 churn 源:
bg_start之后再注入 lifecycle、context_pivot的按轮开关、Tasks/Goal 的状态显隐。能力组内改为「组加载时一次装齐 + 执行期检查」。 - P1 — 能力组边界定型:每个组只允许在首次加载边界改一次声明集,之后只增不减;「换 pack」视为新会话或显式边界。子代理工具只活在子会话自己的前缀里,主会话只保留固定的 spawn/await 合同。
- P2 — 大目录与 MCP 走 Pi 既有发现面:优先
deferred+tool_search或 Code Mode,使发现结果只追加在后缀上,不动顶层 tools;不再重复实现一套搜索/执行网关。 - 失败关闭用冻结集,不用显隐:在不支持中途锚定的 provider 上,退化应是「一次声明、之后冻结」,而不是退回动态显隐。
- 测量先行:用 faux provider / 本地回放按「只改 tools / 只改系统段」拆开请求字节(tools 字节 SHA、tool-surface fingerprint、input/cacheRead/cacheWrite),Terminal-Bench 验收同时报告 cache overlap、tools 数组是否恒等、成功率与总成本。
5. 我的思考与待讨论问题
我的倾向:这件事的本质是「工具发现的序列化策略」,不是某个工具要不要常驻。发现(discoverable)、可见(visible)、授权(authorized)必须分开,但「可见」这一层在会话内应当近似只读。省 token 只允许发生在会话开始或能力组首次加载这两个边界上,每个边界最多吃一次 miss。
待讨论:会话边界画在哪?子代理工具面如何与父面解耦?Code Mode 在 DeepSeek 等模型上写脚本调工具的可靠性如何?tool_search 在不支持锚定的模型上是否仍等于改顶层 tools?「常用」应由采用率 × 净收益定义,目前缺数据。
6. 明确不做什么
- 不把约 33 个工具默认全部常驻(从 31 个常驻工具到零常驻表面:OpenPI 基准、改进、迭代与 DeepSeek 0731 三臂诊断复盘 #20 已退掉;30–50 量级时选择质量会变差),只可作失败关闭选项。
- 不自建第二套「search + execute」网关或第二个代码执行器。
- 不指望切 Responses API 解决:换 API 不改变「中途改 tools 前缀」。
- 不用 prompt 代替运行时权限检查;发现结果不能扩权。
想请大家表态:(1)「会话内 tools 只允许在会话开始/组首次加载定型」是否成立;(2) P0 三处 churn 源能否先独立停掉;(3) 大目录默认面选 Code Mode 还是 deferred+tool_search,需要什么最小证据。
先讨论,不预设结论;本文也不构成实现授权。
Activity
tt-a1i commentedon Oct 17, 2026
方案草案:单一默认工具集,会话内冻结(讨论稿,不构成实现授权)
1. 结论
/plan、/goal、/openpi-setup):低频、由用户发起,每次进出接受一次缓存失效。模型的动作和运行时状态(bg_start、goal 状态、上下文阈值、提示词内容)一律不得改变 tools;状态不满足时由工具执行时检查并返回明确错误,或者向上下文末尾追加消息。deferred/tool_search/ Code Mode,也就是只往上下文末尾追加的发现面,不自己再造一套。2. 证据
A. 源码机制(
1940c38)patchOwnedTools→setActiveTools,见extensions/shared/tool-surface.ts。setActiveToolsByName()每次都会执行_rebuildSystemPrompt(activeNames)(pi-coding-agent/dist/core/agent-session.js:1104-1117)。所以 tools 数组和系统段在每次切换时必然一起变,Runtime tool-surface toggling (context_pivot, background lifecycle tools) breaks the prompt prefix cache on models without mid-conversation tool support #644 没能拆开的两条通道在当前 API 下本来就绑在一起。冻结 tools 可以同时消除这两条。projectedOwnerTools)。这和kv-store-grpc里 cacheRead 是"腰斩"而不是"归零"的现象一致(推断,未单独测量)。capabilities/index.ts:160-168,按提示词关键词触发);组内第二道门bg_*(首次bg_start后)、tasks_*(每批任务一显一隐)、goal(创建/清除);context_pivot(agent_start时按 30k 阈值,以及压缩后);plan / setup / interaction 这几个模式。B. 本地会话实测(维护者单人数据,0.2.0 之后共 236 个会话,混合多个 provider;只能看量级,不是基准)
openpi_load_tools之后的下一轮缓存大面积失效(前一轮命中 ≥50%,间隔 ≤120s,下一轮命中 <50%)read/bash/edit/rg)rg/fd出现的会话占比bg_*/tasks_*门控除了让缓存失效,还让模型不知道这些能力存在,实际效果接近把工具删掉了。生命周期门(如
bg_start之后)造成的缓存失效,在本地数据中没有样本,目前只有 Terminal-Bench 那一份证据。C. 每个工具的成本:只统计
{name, description, parameters}的 JSON 加promptSnippet/promptGuidelines,token 按字符数 ÷ 4 粗估read/bash/edit/write:合计约 0.9kconfigure_my_pi_setup一个就约 3.9k;除去它约 9.2kD. 其他 harness 怎么做(带 commit 链接和原文引用)
deepseek-harness(官方):plan mode 原文 "The tool catalog stays the same across modes for request-cache stability … those tools remain listed to keep the tool catalog unchanged"(packages/bundle/base/cordis.patch.yml)。TodoWrite默认关闭。defer_loading+tool_search,目录变化以追加通知的形式告诉模型;prompt_cache_key= session id。execute,MCP 放进 CodeMode 目录,目录变化以追加 diff 的形式告诉模型;后台任务是shell的background: true参数;没有 todo 工具。3. 成本账
设常驻工具占 T token、缓存读价格约为正常输入价的 r(常见是 0.1):
r·T。T≈3.5k 时每轮约等于 0.35k 正常输入 token。平均上下文 60k 的长会话,总成本多约 5%;平均上下文 15k 左右的小会话,多约 20%。(1−r)·C(C 是当时的上下文大小),外加一次全量 prefill 的延迟;同时还有"能力被藏起来、模型不用"的隐性损失(见 2B)。单一默认可行的前提是先删并、再常驻:33 个工具 / 13k 全部常驻不可取(#20 当时退掉常驻也是这个原因),8–10 个 / 约 3.5k 可以接受。为了防止工具集以后又慢慢涨回去,加一个常驻工具预算检查:测试中实测 OpenPI 常驻工具的总 token 数,超过预算(实测值 + 少量余量)就失败;新增常驻工具必须先在 Issue 里说明它的价值和成本。
4. 发现面失败关闭
provider 不支持只往末尾追加的发现方式时,退化行为应该是"会话开始声明一次、之后冻结",不能退回中途显示/隐藏。
5. 逐个工具的决定
openpi_load_tools+ 整套关键词机制shared/capability-intent.ts)、按关键词加载能力组和显示网关、隐藏的 skill 指引注入、编辑器里关键词高亮和闪烁的 UI 层、capabilities.discovery配置,以及能力组 /loadOpenPiCapabilities这套门控。是否委派、是否用后台任务,由模型根据用户原话判断;能力说明改由 Pi Skills 按需披露runtime_snapshotfd/rggrep底层就是 ripgrep,find底层就是 fd,功能重复;所有只读白名单(READ_ONLY_AGENT_TOOLS、PLAN_MODE_CHILD_TOOLS、workflow replay)本来就包含 Pi 的grep/find。损失的只有file_type/hidden/max_depth/按目录类型过滤这些边角参数,确实需要时可以提给 Pi 上游git_show/diff/log--no-ext-diff --no-textconv,plan mode 的 bash 策略正是依赖这一点才拒绝原始git diff/show(plan-mode/bash-policy.ts:12-20)。不在父会话常驻,只在/plan边界和只读子会话中出现subagent_*(6 个)workflow*(3 个)workflow_stop/workflow_statusbg_*(5 个)bg_status被轮询 418 次tasks_*(3 个)/goal发起/goal发起,算作用户显式边界,进出时各改一次 tools;goal 生命周期内不再变化plan_ready、ask_user/human_handoff/plan、/openpi-setup边界进出,模式内保持不变。现在plan_ready会在计划就绪后隐藏,这属于状态驱动的切换,要去掉;重复调用由工具执行时拒绝configure_my_pi_setupcontext_pivotopenpi_browserexposure: "deferred",并在session_start时按配置决定做完这些之后:
/plan、/goal、/openpi-setup这几个用户显式边界上变化。需要同步修改的约定
AGENTS.md "Package configuration contract" 里现在写的是:"expensive or privileged behavior must be opt-in"。建议改为:
同时删除
tool-surface.ts顶部"Ordinary parent sessions add no resident OpenPI tool"这一设计说明,以及 README / SETUP 里关于能力发现模式的描述。迁移注意
fd/rg时,需要同步更新只读白名单、workflow replay 的工具身份、plan mode 的提示词,以及CHILD_SAFE_PACKAGE_TOOL_NAMES。用户已有的角色文件(例如~/.pi/agent/agents/*.md)里如果写了fd/rg,应该忽略并提示,而不是直接失败。tasks时,旧会话里持久化的任务记录要能被安全忽略。capabilities.discovery字段要能被安全忽略。6. 明确不做什么
7. 验收证据
/plan、/goal、/openpi-setup边界单独断言"进出时各变一次"。欢迎对第 1 节的原则和第 5 节的决定提出异议;如果没有异议,就按第 5 节拆成实现 Issue。
fix(tools): repair CI, restore read-only child search, remove goal (#879
feat(tools): drop background terminals, default Code Mode off, compac…
feat: remove the workspace cleanup guard (#879)
fix(plan-mode): keep plan reminders prefix-stable for prompt caching (#…
fix(subagents): tell the model when a run exits after its turn (#879)