Repository navigation
[提案] 输出侧决策逻辑入内核:轮次结构分类 + verbosity 档位(L0-L4) + effort 路由(billion-context 做 wire 落位) #355
Description
来源: billion-context#1093 (ranxianglei/billion-context#1093) 分析 output-side compression 时发现——经 owner 拍板将「决策逻辑」下沉到内核、billion-context 只保留 wire 侧落位,故在内核单独立项(bili 侧参考实现见 PR#1096)。
背景 / Motivation
billion-context 目前只压缩输入侧,完全不干预输出。输出 token 通常比输入贵 3–5×、逐 token 解码又慢,而 agentic loop 里大量输出是仪式性的(开场白、复述刚读过的代码、在例行续行轮上烧 xhigh effort)。两个确定性改写杠杆实测收益明确:
- 尾部简洁档位注入(verbosity steering,L0–L4):−22% ~ −39%,无质量损失;
- 机械续行轮 effort 降档(effort routing):−29%。
定位(owner 已拍板,硬约束)
这是「内核先行」的能力:轮次结构分类、verbosity 档位、effort 路由的决策逻辑属于 acp-kernel 管线;billion-context 只做 wire 侧落位(把内核产出的档位/降档决策写到具体协议的字段上)。内核就绪并发版后,billion-context 走标准跨仓流程升级接入,并移除自身重复的决策代码。
可作规格与回归基准的参考实现(bili 侧临时版本,含全部决策逻辑):
https://github.com/ranxianglei/billion-context/pull/1096/files (分支 2026-09-21_output-side-compression,核心在 src/output-steering.ts)
需要内核提供的三块决策逻辑
1. 轮次结构分类(turn classifier)
纯结构判断、不做内容正则匹配,只看「最后一条 user 消息」的块构成,返回枚举 new_user_ask / mechanical_continuation / error_continuation / unknown。以 Anthropic 形态为例(其余协议等价映射):
- 末条
role !== "user"→unknown - content 为非空字符串 →
new_user_ask;空字符串 →unknown - content 为块数组:遇
text/image/document立即new_user_ask;累积tool_result(sawToolResult)、tool_result.is_error === true(sawError);遍历结束:sawError →error_continuation,否则 sawToolResult →mechanical_continuation,否则unknown - 各 wire 的「工具结果块」等价物:
- OpenAI chat:末条
role === "tool" - Responses:
input中仅出现*_call_output(function/custom_tool/local_shell/apply_patch)且无 user signal(input_text/input_image/input_file或role:"user"的非空文本) - Google:
contents末条 user 的parts仅functionResponse(出现 text/inlineData/fileData/videoMetadata 即为 new_user_ask)
- OpenAI chat:末条
2. verbosity 档位(L0–L4)
五档确定性简洁指令文案,L0 = 不注入,默认 L2。文案必须跨版本字节稳定——改一个字会让所有固定在该档的会话前缀缓存整体失效。参考文案见上述 PR 中 VERBOSITY_LEVELS。
3. effort 路由(clamp-only)
仅在 mechanical_continuation 轮,把「客户端已发送」的 effort 字段降到该模型的下限。决策层只产出「是否降档 + 目标值」,具体字段写回是 billion-context 的落位职责(内核不应感知协议字段名):
- OpenAI chat
reasoning_effort→low - Responses
reasoning.effort→low - Anthropic
output_config.effort→low,且thinking.budget_tokens→ 1024(API floor) - Google
generationConfig.thinkingConfig.thinkingBudget→ 128(动态值-1不动)
硬边界(接入时必须保持,不可变)
- 只降不升(clamp-only):绝不抬高 effort;
- 客户端没发的 effort 字段绝不注入(不支持 effort 的模型会因多余字段 400);
- 不动
thinking.type(存在历史 thinking 块时切换 type 会 400 且破坏缓存层级);动态预算-1不动; - 注入只能加在系统提示尾部(前缀缓存要求头部字节稳定,绝不前置)。
配置 & 默认
- 配置开关 + 默认关闭。billion-context.json:
outputSteering: { enabled, verbosityLevel(默认 2), effortRouting },支持全局 + 按 provider route 覆盖。 - 幂等:同档重复应用必须是 no-op(compress 重试重发会二次经过该路径)。
建议的内核 API 形态(供讨论,非定论)
一个纯函数入口:吃「最终消息列表的结构摘要」(role + 块类型序列,不含内容),吐 { verbosityLevel: number, lowerEffort: boolean } 决策对象;billion-context 拿决策后按 wire 落位。这样内核保持协议无关、可被多个宿主复用。
后续
内核合入并发布到 npm 后:确认 npm view acp-kernel version 有新版本 → billion-context bump acp-kernel(精确版本 pin)→ 发 release → 把 PR#1096 里重复的决策逻辑改为调用内核。
Activity
ranxianglei commentedon Sep 22, 2026
[bot] 🏷 已实现并提交评审:PR #356 — #356
审核结论(先于实现)
src/output-steering.ts+ 测试基线),确认三块决策逻辑可在内核的「协议无关」约束下完整复现;issue 建议的 API 形态(纯函数、吃结构摘要、吐决策对象)直接采用。落地内容(PR #356)
新增根模块
src/output-steering.ts(+ barrel 导出、README/DESIGN 文档),内核不感知任何协议字段名:classifyTurn(messages): TurnKind— 纯结构判断,只看末条 user 消息的块构成(new_user_ask/mechanical_continuation/error_continuation/unknown),零内容正则。各 wire 的等价映射作为归一化契约写入模块 JSDoc,由适配器把自家协议的工具结果块映射成StructuralBlock(bili 保留 per-wire 分类器作薄 normalizer)。VERBOSITY_LEVELS文案字节稳定(测试以精确字面量做冻结基线锚定);L0 不注入、默认 L2;applySteeringToPrompt只做原位替换或系统提示尾部追加,绝不前置;同档重复应用是 no-op(幂等测试覆盖 compress 重试二次经过路径)。EFFORT_LADDER+clampEffortToFloor(current, floor="low") → EffortValue | null:绝不抬高;字段缺失/无法识别/已在 floor 及以下时返回null→ 适配器不注入(客户端没发的字段绝不注入)。decideOutputSteering(messages, config?) → { turnKind, verbosityLevel, lowerEffort };config{ enabled, verbosityLevel, effortRouting }默认关闭,非法值降级默认并出 warning;按 provider route 覆盖留在适配器侧。行为变更披露(相对 bili 参考实现的差异,按要求显式声明)
low——对未知的 sub-floor 值这实际是升档,潜在违反「只降不升」硬边界。内核版改为原样不动(更严格的 clamp-only)。已知档位值行为完全一致;xhigh补入 ladder(bili 里该值经强制路径同样落到low,结果相同)。thinking.budget_tokens→1024、GooglethinkingBudget→128、动态-1不动)按设计保留在 wire 落位侧——它们是协议字段属性,不是决策属性。<acp_output_steering>≠ bili 的<bili_output_steering>:billion-context 接入时必须传入自己的 sentinel,否则固定在 bili 旧块下的会话会累积两个 steering 块(前缀缓存失效)。已写入模块 JSDoc。验证(rebase 到当前 origin/master,v0.0.82 基座
b30723f)npm run typecheck干净;npm test904 pass / 0 fail(含移植 bili 参考全部适用用例的新回归套件,429 行:字节级 L1–L4 文案锚点、幂等/重试路径、clamp 矩阵、config 解析);npm run build绿。后续(按 issue 计划)
此 PR 合入后切 release 分支发版 →
npm view acp-kernel version确认 → billion-context 精确版本 pin 升级 + release → PR#1096 中重复的决策逻辑改为调用内核(wire 落位与其 sentinel 留 bili 侧)。注:本 PR 引入新公共 API 面(feat,非纯 fix),按 AGENTS.md §7.4 不走 auto-merge,需人工评审。
Merge pull request #356 from ranxianglei/2026-09-23_output-steering