ADR 0042:压低首轮 token——固定前缀瘦身与少走回合
状态:Proposed
关联:建立在 ADR 0041(段组装器、场景段在系统提示词末尾、消息头)之上,0041 落地后开工。
背景
2026-10-28 测试群一轮真实 trace(context_budget)里,单次模型调用约 15k 输入 token,其中每次都要带的固定部分约 8.9k:
| 部分 | token | 内容 |
|---|---|---|
| 工具 schema | 2.8k | 24 个常驻工具的完整参数定义(群聊里也含 grep / read_file / list_dir / bash 等开发工具) |
| bootstrap | 2.4k | workspace AGENTS.md(约 4k 字)、SOUL.md(约 2k 字)、TOOLS.md |
| 系统规则 | 1.6k | 安全、编排、工作流、风格 |
| 技能目录 | 1.3k | 全部技能描述 |
| 工具索引 | 1.1k | 69 个延迟工具逐条一行 |
| 输出 schema | 0.9k | 结构化回复 JSON schema |
同一轮里,一次「踢人」请求走了 21 次模型调用(累计输入 358k):模型找不到合适工具,在群聊里反复 grep 插件源码;每用一个新工具还要 discover → load_tool → 调用三个来回,每个来回都重发完整前缀。
目标:降低首轮(含新会话第一句)的输入 token 与计费,同时不丢能力。
决策
0. 原则
- 常驻只放「知道有这个能力」所需的最少信息,细节按需取。
- 能力不丢的判据是评测:每一项改动都先跑
eval:agent/eval:chat基线,改后对比;工具调用成功率与回答质量不下降,且既有任务(踢人、查成员、设提醒、公告、日程)仍能在调用上限内完成,否则不合入。 - 授权边界不变:按需加载、自动加载都只能取当前回合已授权投影里的工具(
DeferredCapabilityPlan的 ceiling),不扩大任何权限;工具自身的权限与审批照旧。
1. 跨会话逐字节稳定的前缀(首轮计费的最大杠杆)
- 系统提示词的
system/environment/role_rules/platform四类段,在同一实例、同一 provider/模型下跨会话、跨群逐字节相同;任何会话相关内容(会话规则、记忆路径、命令前缀等)只能进scene段或消息。 - 在支持缓存断点的 provider 上,于
scene段之前打断点(ADR 0041 §5 已给出切分与偏移,本 ADR 负责接到packages/im/ai的请求组装:wrapSystemForPromptCache支持两段系统提示词)。 - 效果:一个新群的第一句话,前面约 8k 也命中缓存,按缓存价计费。
- 守护:测试断言两个不同会话(群 / 私聊 / 不同群)构建出的前四类段字节相同。
2. 常驻工具按场景分档
| 场景 | 常驻工具 | 其余 |
|---|---|---|
| 群聊、普通私聊 | 聊天必需:回复相关(表情回应、pick_sticker)、记忆检索、会话信息、discover、load_tool | 进工具索引,按需加载 |
| 主人私聊 | 上述 + 开发工具(文件读写、grep、list_dir、bash 等,按 sandbox 策略) | 同上 |
- 分档由宿主按回合的 origin / 角色决定,写成
deferredTools的场景默认;alwaysLoadedTools配置仍可显式追加。 - 非主人回合里不常驻开发工具:它们仍在工具索引里,但只对有权限的回合可加载(现有权限不变),群聊里模型不会再拿
grep去翻源码。
3. 发现即调用
- 模型直接按名字调用一个在已授权延迟目录里、但未加载的工具:运行时自动加载并执行(不再要求先
load_tool),在本会话后续回合保持已加载(受maxLoadedPerSession限制)。 discover自动加载最匹配的至多 3 个工具,完整 schema 通过下一次模型请求的tools字段提供;结果正文只保留检索摘要与已加载名称,不再重复 schema(#1102)。模型可在同一回合直接调用,无需额外load_tool。- 不在授权目录里的名字照旧报「未知工具」,不泄露其存在。
- 每用一个新工具省 1–2 个来回。
4. 目录折叠
- 工具索引:按领域一行(如
群管理:踢人、禁言、头衔、公告…(7)),领域名来自工具所属 skill / 插件分组;细节用discover。 - 技能目录:每个技能只留名字 + 一句触发描述(≤ 40 字),完整描述在
load_skill/discover时给出。 - 守护:
eval:agent中「需要某延迟工具才能完成」的用例全部通过。
5. bootstrap 按场景注入
AGENTS.md/SOUL.md/TOOLS.md支持小节作用域标注:<!-- scope: master-private -->、<!-- scope: group -->等,标注作用到下一个同级或更高级标题前;无标注 = 全场景(向后兼容)。- 宿主只注入与本回合场景匹配的小节;
zhin trace显示被跳过的小节标题。 - 这些文件是用户 workspace 的内容:框架只提供机制和文档,不替用户改文件;
create-zhin模板可附带示例标注。
6. 紧凑输出 schema
- 走 prompt 模式结构化输出的 provider(
compat.structuredOutput: prompt,如 DeepSeek):完整 JSON Schema 换成「最小示例 + 字段规则」;原生结构化输出的 provider 不受影响(schema 走 API 参数)。 - 守护:
decodeStructuredReply解析成功率在eval:chat上不下降。
7. 少走回合
- 按场景的迭代上限:群聊 6、私聊 10、主人私聊保持 15(
maxIterations的场景默认,modelHarness仍可覆盖)。剩 1 次时注入「先用现有结论回复」。 - 原地打转检测:同一工具族连续 3 次调用、结果无新信息(相同结果摘要、
grep无匹配、重复读同一文件)→ 注入一次提示要求换思路或直接回答;记zhin trace事件。 - 并行调用:编排规则明确「互不依赖的查询放在同一条消息里并行发」;已有
spawn_task并行规则合并表述,不增加净字数。
8. 度量
- 新增固定场景的基线测量:群聊首轮(新会话)、群聊第 N 轮、主人私聊带工具回合;记录名义输入 token、缓存读、调用次数。用
eval:chat+context_budget生成前后对比表,写进 PR。 fixed-prompt-cost(#949)预算随本 ADR 下调到实测值 + 余量,防止回涨。
预期
| 场景 | 现在 | 目标 |
|---|---|---|
| 群聊首轮单次调用(名义) | ~13–15k | ~6–7k |
| 群聊首轮实际计费(含缓存) | ~13–15k | ~2–3k |
| 带工具回合调用次数 | 5–20 | 3–8 |
数字为估算,以 §8 实测为准。
实现分工(ADR 合入后多 agent 并行,最后一个 PR)
| 工作项 | 内容 | 主要位置 |
|---|---|---|
| W1 稳定前缀 + 缓存断点 | §1 | prompt/segments、packages/im/ai 请求组装 |
| W2 场景分档 + 发现即调用 | §2、§3 | deferred-capability-plan.ts、工具执行入口、discover |
| W3 目录折叠 + 紧凑 schema | §4、§6 | 工具索引 / 技能目录渲染、structured-reply |
| W4 bootstrap 作用域 | §5 | workspace prompt 解析、trace |
| W5 少走回合 + 度量 | §7、§8 | agent loop、exploration budget、evals |
W2 与 W3 都碰工具目录渲染,分工时按文件划清;其余互不依赖。
待 Fin 确认的默认值
- 群聊 / 普通私聊常驻工具清单(§2)。
- 迭代上限:群聊 6、私聊 10、主人私聊 15。
- 打转阈值:同一工具族 3 次无新信息。
discover自动加载工具的上限:3 个(仍受会话加载上限约束)。- 技能一句话描述上限 40 字。
非目标
- 换模型 / 换 provider 来省钱;改 provider 定价策略。
- 删减安全规则或硬闸相关提示(只允许合并表述,不减语义)。
- 改写用户 workspace 里的
AGENTS.md/SOUL.md内容。
结果
- 首轮输入与计费显著下降,新会话也能吃到缓存。
- 群聊里不再出现模型拿开发工具翻源码的长回合。
- 能力以评测守护:工具仍可发现、可调用,权限与审批边界不变。