Skip to content

[提案] 输出侧决策逻辑入内核:轮次结构分类 + verbosity 档位(L0-L4) + effort 路由(billion-context 做 wire 落位) #355

Closed

Description

@ranxianglei

来源: 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)

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

  1. ranxianglei commented on Sep 22, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已实现并提交评审:PR #356 — #356

    审核结论(先于实现)

    • 查重:open/closed 全量检索无重复项。
    • Triage:本 issue 为 owner 已拍板的功能(定位与硬约束在正文中明确),直接进入实现。实现前逐行核对了参考实现 billion-context PR#1096(src/output-steering.ts + 测试基线),确认三块决策逻辑可在内核的「协议无关」约束下完整复现;issue 建议的 API 形态(纯函数、吃结构摘要、吐决策对象)直接采用。

    落地内容(PR #356)

    新增根模块 src/output-steering.ts(+ barrel 导出、README/DESIGN 文档),内核不感知任何协议字段名:

    1. 轮次结构分类 classifyTurn(messages): TurnKind — 纯结构判断,只看末条 user 消息的块构成(new_user_ask / mechanical_continuation / error_continuation / unknown),零内容正则。各 wire 的等价映射作为归一化契约写入模块 JSDoc,由适配器把自家协议的工具结果块映射成 StructuralBlock(bili 保留 per-wire 分类器作薄 normalizer)。
    2. verbosity 档位 L0–L4 — VERBOSITY_LEVELS 文案字节稳定(测试以精确字面量做冻结基线锚定);L0 不注入、默认 L2;applySteeringToPrompt 只做原位替换或系统提示尾部追加,绝不前置;同档重复应用是 no-op(幂等测试覆盖 compress 重试二次经过路径)。
    3. effort 路由(clamp-only) — EFFORT_LADDER + clampEffortToFloor(current, floor="low") → EffortValue | null:绝不抬高;字段缺失/无法识别/已在 floor 及以下时返回 null → 适配器不注入(客户端没发的字段绝不注入)。
    4. 入口 decideOutputSteering(messages, config?) → { turnKind, verbosityLevel, lowerEffort };config { enabled, verbosityLevel, effortRouting } 默认关闭,非法值降级默认并出 warning;按 provider route 覆盖留在适配器侧。

    行为变更披露(相对 bili 参考实现的差异,按要求显式声明)

    1. 无法识别的 effort 字符串:bili 参考实现会把任意非 low 字符串强制降为 low——对未知的 sub-floor 值这实际是升档,潜在违反「只降不升」硬边界。内核版改为原样不动(更严格的 clamp-only)。已知档位值行为完全一致;xhigh 补入 ladder(bili 里该值经强制路径同样落到 low,结果相同)。
    2. 数值预算 floor(Anthropic thinking.budget_tokens→1024、Google thinkingBudget→128、动态 -1 不动)按设计保留在 wire 落位侧——它们是协议字段属性,不是决策属性。
    3. 默认 sentinel <acp_output_steering> ≠ bili 的 <bili_output_steering>:billion-context 接入时必须传入自己的 sentinel,否则固定在 bili 旧块下的会话会累积两个 steering 块(前缀缓存失效)。已写入模块 JSDoc。

    验证(rebase 到当前 origin/master,v0.0.82 基座 b30723f)

    • npm run typecheck 干净;npm test 904 pass / 0 fail(含移植 bili 参考全部适用用例的新回归套件,429 行:字节级 L1–L4 文案锚点、幂等/重试路径、clamp 矩阵、config 解析);npm run build 绿。
    • diff 纯增量:5 文件 +778 行,0 删除。

    后续(按 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,需人工评审。

  2. added a commit that references this issue on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions