首页
/ Codewhale 输出展示过滤器解析:不污染规范会话记录的表示层压缩与筛选机制(RFC 4468)

Codewhale 输出展示过滤器解析:不污染规范会话记录的表示层压缩与筛选机制(RFC 4468)

2026-09-08 17:07:51作者:舒璇辛Bertina

本篇技术指南围绕 Codewhale 仓库中已接受的设计文档 docs/rfcs/4468-output-presentation-filters.md 展开:解释 Codewhale 为何拒绝在"模型响应与规范会话记录之间"运行任意用户脚本,而是将输出压缩/筛选收敛到一个显式的展示(presentation)与导出(export)层。读完本文,你将理解 show_thinkingcodewhale exec --output-format stream-json 这两个"安全表面"的语义边界、它们背后的凭据完整性(receipt fidelity)约束,以及如何用结构化 JSON 行流在 Codewhale 外部自行实现有界、可审计的输出过滤管线。

背景:CIPHER 过滤器提案带来的需求与信任问题

在提交与评审 #4468 时,社区提出了一个称为 CIPHER 的输出压缩/过滤方向:希望在模型产生响应之后,用一个外部脚本来压缩或改写终端里呈现的内容,以服务可访问性(无障碍阅读)与自动化(harness 取数)需求。

RFC 承认这一需求是真实存在的——可访问性工具需要把冗长的思考块折叠成摘要,自动化 harness 需要只取 text 而跳过 thinking。但它同时指出:如果这类变换发生在持久化之前,被改写的内容就会进入规范会话记录(canonical session record),从而产生一系列无法挽回的后果。

RFC 给出的最终裁定非常明确(对应原文 Decision 一节):

Codewhale 不会在模型响应与规范会话记录之间运行任意用户脚本。模型原生的 assistant 与 thinking 块,始终是审计(audit)、回放(replay)、缓存计量(cache-accounting)、调试(debugging)以及 provider 签名(provider-signature)的唯一可信来源(source of truth)。

换句话说:模型输出的是"事实",任何压缩都是"派生视图",两者不允许混为一谈。 这一决策被接受进 v0.9.2 的产品边界。

核心设计:输出处理收敛到独立的表示/导出层

RFC 将"输出压缩"明确归类到一个独立的 presentation/export(展示/导出)层,第一版只支持既有安全表面,不引入任何新的任意命令执行能力:

表面 行为边界
TUI show_thinking = false 只在渲染层隐藏 thinking,不删除消息/凭据路径里的规范内容
codewhale exec --output-format text|stream-json 选择文档化的输出编码;结构化输出保留块身份,下游工具可自行挑选 thinkingtext
未来导出的 --view canonical|response-only|thinking-only 选择器 过滤后的导出必须自我标注为派生视图、携带规范会话引用,且绝不覆盖原会话

这一定位的关键词是 non-destructive(无破坏性)与 opt-in(显式选择):展示层可以决定"你看什么",但永远不改变"记录里有什么"。

为什么不能在持久化前改写:四项底层约束

凭据保真(Receipt fidelity)

会话本质是一份审计记录。如果内容在持久化前被替换:

  • 无法证明 provider 实际发出了什么;
  • 会破坏带签名的 Anthropic thinking 块(signed thinking blocks);
  • 用量/费用凭据(usage/cost receipts)会与可见记录不一致。

因此 RFC 明确拒绝了两种存储策略:只存变换后形式(会导致无法审计)、默认同时存规范+变换双份(会双倍放大敏感数据、造成回放权威歧义)。在代码层,用量字段的权威性体现在 crates/protocol/src/event_msg.rsTokenUsage 结构:reasoning_tokensreasoning_replay_tokensprompt_cache_hit_tokens 等字段按 provider 上报如实携带,None 表示"provider 未报告"而非零值——它们从不参考展示层有没有折叠过 thinking。

提示缓存与回放(Prompt cache and replay)

展示过滤器不会改变请求侧 token 用量。特别是 reasoning_replay_tokens 与 provider 专有的 signed-thinking 回放,必须使用规范形式。RFC 要求任何"压缩收益"的声明必须分开度量四类字节/token:

  1. 终端/导出字节(terminal/export bytes);
  2. 本地存储字节(local storage bytes);
  3. 请求侧回放 token(request-side replay tokens);
  4. provider 缓存命中行为(provider cache-hit behavior)。

v0.9.2 的边界只影响第 1 项。这也解释了为什么展示层的压缩不能宣称"省了推理 token"——那属于请求管线的事,与展示层无关。

流式语义(Streaming)

HookEvent::ResponseDelta 是纯观察者(observer-only)事件、以增量形式到达。若要做块级变换,就必须把 delta 缓冲到块结束,这会引入额外延迟并改变取消语义。在源码中,HookEvent::ResponseDelta 被定义为仅携带 response_id 与增量文本的观察事件(见 crates/hooks/src/lib.rs),它没有任何回写通道。

RFC 的结论是:展示消费者可以为自己的输出做缓冲,但引擎继续按规范形式增量发出并持久化。对应到实现,headless 执行路径在 crates/tui/src/core/engine/turn_loop.rs 明确要求对 exec / stream-json / app-server 的 stdout 保持 byte-clean,避免任何非 JSON 内容混入事件流。

信任与失败模式(Trust and failure)

由于没有新增任意命令执行,任何失败的过滤管线都不会破坏、延迟或替换会话。规范记录天然提供 fail-open 的兜底:即使外部消费者挂了,凭据依旧完好。外部消费者可以读取 stream-json 后,在 Codewhale 进程之外施加自己的有界变换。

第一个安全表面:show_thinking 只是显示开关

TUI 设置中的 show_thinking 是 RFC 列举的典型"展示层控制"。它只影响渲染出的会话抄本是否包含 thinking 内容,不影响规范消息/凭据路径。

在实现上,show_thinking 是 TUI 设置结构里的布尔字段,并接受 thinking 作为别名键(见 crates/tui/src/settings.rs"show_thinking" | "thinking" => "show_thinking" 的归一化与解析逻辑)。配置文档 docs/CONFIGURATION.md 将其描述为 on/off 开关:开启 show_thinking 时 thinking 初始即展开显示,空格键仍可切换。可访问性说明 docs/ACCESSIBILITY.md 对它的定位描述得最贴切——设为 false 可从 TUI 呈现中隐藏模型的 reasoning_content 块,而 canonical session/replay receipts 保持不变

值得注意的是,settings 实现中还刻意出现了一条注释级约束:"evidence-preserving —— show_thinking is deliberately left untouched"(见 crates/tui/src/settings.rs),即某些系统级路径不允许触碰该设置,从侧面印证了"它只是渲染偏好"这一设计意图。实践中建议把它当作纯显示偏好理解:需要彻底不看思考过程的场景用它,需要审计/复盘的场景直接看会话记录。

第二个安全表面:stream-json 结构化输出契约

stream-json 被 RFC 定义为可访问性与集成的正式接口,其契约要点如下:

  • 事件是 JSON lines(NDJSON),一行一个 JSON 对象;
  • response/thinking 的块身份保持显式;
  • 工具/下游可以从自己的派生视图中省略某个块,但不得把该视图描述为规范会话
  • 不会为了过滤而附加环境变量映射、provider 凭据、隐藏工具载荷或无关抄本内容;
  • 下游工具应自行限制输入、输出与处理时间。

在 CLI 帮助中,codewhale exec--output-format <FORMAT> 的说明为 textstream-json,并明确 stream-json 是自动化包装器使用的路径(见 crates/cli/src/lib.rs 中 exec 子命令的 after_help 示例:codewhale exec --auto --output-format stream-json "fix the failing test")。格式枚举定义在 crates/tui/src/lib.rsExecOutputFormat::{Text, StreamJson},其中 StreamJson 通过 #[value(name = "stream-json")] 绑定命令行取值。--json(汇总 JSON)与 stream-json 互斥,仓库里也有对应的回归测试(exec_json_conflicts_with_stream_json_output)。

实际运行时的事件形状(与 RFC 的"版本化"提醒)

RFC 原文给出了一条"response-only 呈现"的管道示例:

codewhale exec --output-format stream-json "..." \
  | jq -r 'select(.type == "message_delta") | .text // empty'

同时它郑重提醒:确切事件名是版本化的运行时输出,调用方应当检查自己安装版本对应的 fixture,而不是从 RFC 推断字段。这条提醒在实际代码中得到了印证——本仓库 v0.9.2 边界的 exec 事件流实现于 crates/tui/src/exec_agent.rscrates/tui/src/lib.rs,其中:

  • 每条流事件对象都会被打上版本标记:schema: "codewhale.exec-stream"schema_version: 1(见 exec_stream_value 的实现);
  • 事件以 println!("{}", serde_json::to_string(...)) 逐行输出到 stdout(emit_exec_stream_event),天然符合"JSON lines 且 UTF-8 字节干净"的约束;
  • 实际事件 type 包括 contenttool_usetool_resultagent_spawnedsandbox_deniedworkflow_eventsession_captureturn_usagemetadatadoneerror 等(见 ExecStreamEvent 枚举)——模型正文增量(Event::MessageDelta)在 StreamJson 模式下会被映射为 content 事件(见 crates/tui/src/exec_agent.rsMessageDelta 分支)。

因此,若要在当前版本实现 RFC 中"只取正文"的等价管道,应选择 type == "content" 的事件,例如:

codewhale exec --auto --output-format stream-json "explain this function" \
  | jq -r 'select(.type == "content") | .content'

引擎级事件流与 TUI/CLI 的字节一致性

如果只想要"块身份显式"的细粒度事件,而不满足于 exec 聚合后的 content,仓库还提供了引擎级协议层:stream-json、app-server 的 SSE、以及 TUI 事件通道在协议层面讲的是同一种 EventMsg,使 headless 与 TUI 对同一个 Op 观察到字节一致的事件形状。文档级说明见 crates/protocol/src/event_msg.rs

  • ResponseDelta 增量事件带 indexdelta 字段,并通过 channel 区分正文通道与推理通道(默认省略即 text 通道);kind_str 统一为 response_delta
  • MessageStarted / MessageComplete / ThinkingStarted / ThinkingComplete 给出块的起止生命周期;
  • 引擎侧的 MessageDelta/ThinkingDelta 到协议侧 ResponseDelta(channel 分别为 Text/Reasoning)的映射由 crates/tui/src/core/protocol_parity.rs 以编译期穷举匹配保证(新增引擎事件变体若没有协议孪生将无法通过编译)。

这条"协议奇偶校验"链路意味着:以 EventMsg 为格式的消费者拿到的是真正保留块身份的规范投影,用它做 thinking/text 的再选择是安全的。

无头 agent 的调用链与持久化语义

RFC 没有细讲但值得补充的是 stream-json 模式下的会话持久化行为。在 crates/tui/src/exec_agent.rs 中:

let should_persist_session = resuming_session || output_format == ExecOutputFormat::StreamJson;

stream-json 运行默认持久化会话(与文本模式不同),便于包装器在进程结束后仍可复现、续跑或审计该次运行。同时该文件明确注释:exec 的 stream-json 会有意省略推理增量ThinkingDelta 分支被留空),thinking 仍保留在 TUI 抄本既有的 Activity Detail 表面中——这正是"展示层做减法、记录层保完整"的又一次体现。

另外,metadata 事件承载了可机读的分类结果:docs/MODES.md 中说明 exec --auto --output-format stream-json 每行输出一个 JSON 对象供 harness 与后端包装器消费,进程退出码 0 表示成功、1 表示真实任务/agent 失败、75EX_TEMPFAIL)表示在会话内所有重试后仍因可重试的基础设施故障(provider/传输的 network/timeout)结束,终态 metadata 事件的 error_category 携带同样的分类。对应的映射函数 exec_failure_exit_code 位于 crates/tui/src/lib.rs,并特意将 rate_limit 排除在可重试码之外(配额耗尽不应被盲目重试打爆)。

被否决的备选方案:为什么它们走不通

RFC 逐条记录了被否决的路线,理解这些"不做什么"比理解"做什么"更能把握边界:

  1. 持久化前输出 hook(Pre-persistence output hook):会篡改审计/回放权威,直接违反凭据保真原则。
  2. 只改写 thinking(Mutate only thinking):带签名 thinking 与请求回放依然要求保真,改写即破坏。
  3. 默认同时存规范与变换双份(Store canonical plus transformed by default):重复的敏感数据 + 权威归属模糊,存储与合规成本不可接受。
  4. 提示模型自行缩写(Prompt the model to abbreviate):报告方实测采纳率为 0%,且它不是一个可靠的机械契约——不能把正确性押在模型的临场发挥上。
  5. 增设 [hooks.output_filter] 配置表:在重复 hook 模式的同时,仍无法解决信任与凭据问题。

特别值得展开的是第 1、5 条背后的工程约束:当前 hook 系统(crates/hooks/src/lib.rs)中的流式事件本就是观察者模型——ResponseDelta 只携带 response_iddelta,hook stdout 从不具备响应改写权限。这正是 RFC 验收项"hook stdout 不得获得响应变更权威"的代码基础。任何试图把 stdout 解释为"改写指令"的设计都会破坏该约束,因此被整体排除。

未来的加法式扩展:可审计的派生导出 API

RFC 为后续演进保留了接口,而不是把当前拒绝变成永久死路。未来可能出现的 derived-export API 必须满足:

  • 接受声明式、不可执行的选择器(declarative, non-executable selector);
  • 写出包含 源会话 id(source session id)、源内容哈希(source content hash)、选择器(selector)、派生输出哈希(derived output hash) 的凭据;
  • 任意可执行变换仍属于外部管线,除非后续安全评审定义好沙箱、披露(disclosure)、延迟与双形态保留(dual-form retention)语义。

可以看到,"哈希+选择器+引用"的设计意味着:即使未来允许派生导出,它产出的也始终是一份可被验证的派生视图,规范会话永远不会被覆盖。这与 v0.9.2 边界是一脉相承的。

验收清单与仓库证据的对照

RFC 的验收项可以在仓库中逐条找到证据:

RFC 验收项 仓库证据
规范会话持久化与推理回放保持不变 TokenUsagereasoning_replay_tokenscrates/protocol/src/event_msg.rs);stream-json 模式显式持久化会话(crates/tui/src/exec_agent.rs
show_thinking 被文档化为仅显示层 设置字段+别名解析(crates/tui/src/settings.rs);docs/CONFIGURATION.mddocs/ACCESSIBILITY.md
stream-json 被文档化为安全的机读过滤边界 CLI help 与示例(crates/cli/src/lib.rs);docs/MODES.mddocs/AGENT_RUNTIME.md
无 hook stdout 获得响应变更权威 HookEvent::ResponseDelta 仅为增量观察事件(crates/hooks/src/lib.rs
未引入新脚本/shell/凭据/网络能力 exec 的事件枚举仅声明可序列化事件、无执行通道(crates/tui/src/lib.rsExecStreamEvent

实践结论

RFC 4468 的价值在于它把一个看似简单的需求("过滤输出")拆成了两个真正独立的问题:记录什么呈现什么。Codewhale 的选择是前者永远由模型原生输出决定、只增不改;后者可以无限灵活,但必须显式地标记为派生视图并携带可回溯引用。

面向实践,你可以按这样的优先级使用本仓库能力:

  1. 只想在 TUI 里眼不见 thinking → 设置 show_thinking = false,零副作用;
  2. 要接 CI/harness/另一个 agent,只取正文结果codewhale exec --auto --output-format stream-json 逐行消费,按 type == "content" 过滤(先检查你安装版本的流 fixture 确认事件名);
  3. 要做有界的外部压缩/改写 → 在 Codewhale 之外对 stream-json 施加自己的变换,自行限制输入/输出/耗时,并把变换产物当作派生视图而非会话;
  4. 要完整保留 thinking/text 块身份的规范投影 → 对接引擎级 EventMsg/ResponseDelta 通道(crates/protocol/src/event_msg.rs),由协议奇偶校验保证与 TUI 观测一致。

这样,可访问性工具的折叠诉求、自动化管线的取数诉求,与审计复核所需的凭据完整性,才能同时被满足——这正是 RFC 4468 被纳入 v0.9.2 产品边界的全部理由。RFC 全文与后续演进请继续参阅 docs/rfcs/4468-output-presentation-filters.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393