claw-code 源码详细分析:Compaction 前置课——上下文压缩在接口层要预留哪些旋钮,避免后期全局返工?

涉及源码 :Python src/query_engine.pysrc/transcript.py;对照 Rust rust/crates/runtime/src/compact.rsConversationMessage 会话模型。


1. 为什么要「接口层先留旋钮」

上下文压缩(compaction)在工程上一定会演进:从截断 → 按 token 触发 → 模型摘要 → 分层记忆 → 外部向量库。若早期把逻辑写死在「字符串列表 pop」里,后面每升一级都要改:

  • 调用点散落各处,无法统一观测「何时压、压掉什么、压完 token 多少」;
  • 持久化 / 重放 与内存结构绑死,迁移成本爆炸;
  • 测试 无法注入策略,只能集成测大段流程。

正确姿势是:在公开 API 上固定「策略输入(Config)+ 生命周期钩子(何时调用)+ 结果对象(Result)」 ,实现可以先是 no-op 或 tail-truncate,但边界不动


2. Python 移植层:已经埋下的旋钮与钩子

2.1 配置项:QueryEngineConfig.compact_after_turns

python 复制代码
# 15:21:src/query_engine.py
@dataclass(frozen=True)
class QueryEngineConfig:
    max_turns: int = 8
    max_budget_tokens: int = 2000
    compact_after_turns: int = 12
    structured_output: bool = False
    structured_retry_limit: int = 2

意义 :把「窗口大小」从代码魔法数提升为 可注入配置 ,便于测试与 CLI/环境覆盖。
注意 :默认 max_turns=8 小于 compact_after_turns=12,在不做配置覆盖时,往往先触达轮次上限 ,压缩逻辑很少被执行------这是移植期优先级取舍,也提醒:多个闸门要一起在配置里审

2.2 生命周期钩子:每轮成功后 compact_messages_if_needed()

python 复制代码
# 91:96:src/query_engine.py
        self.mutable_messages.append(prompt)
        self.transcript_store.append(prompt)
        self.permission_denials.extend(denied_tools)
        self.total_usage = projected_usage
        self.compact_messages_if_needed()

意义 :压缩(或未来摘要)的触发点 钉在「一轮提交完成、状态已更新」之后 ,而不是散落在 UI 或网络回调里------后续换实现只需改方法体或委托给 CompactionEngine

2.3 双缓冲一致性:mutable_messagesTranscriptStore

python 复制代码
# 129:132:src/query_engine.py
    def compact_messages_if_needed(self) -> None:
        if len(self.mutable_messages) > self.config.compact_after_turns:
            self.mutable_messages[:] = self.mutable_messages[-self.config.compact_after_turns :]
        self.transcript_store.compact(self.config.compact_after_turns)
python 复制代码
# 15:17:src/transcript.py
    def compact(self, keep_last: int = 10) -> None:
        if len(self.entries) > keep_last:
            self.entries[:] = self.entries[-keep_last:]

意义 :接口层已经承认 「会话里不止一种载体」 (此处两条线同步截断)。将来若引入「摘要 system 消息 + 原文 recent」,也要在 同一钩子里 更新所有衍生视图,避免 persist/replay 分叉。

2.4 可观测:message_stoptranscript_size

python 复制代码
# 122:127:src/query_engine.py
        yield {
            'type': 'message_stop',
            'usage': {'input_tokens': result.usage.input_tokens, 'output_tokens': result.usage.output_tokens},
            'stop_reason': result.stop_reason,
            'transcript_size': len(self.transcript_store.entries),
        }

意义 :为运维/前端预留 压后尺寸 信号;后续可扩展 compacted: boolremoved_turns 等而不改事件主类型。


3. Python 当前实现的边界(避免误当「产品级 compaction」)

  • 策略 :纯 尾部截断,无摘要、无 role 区分、无工具结果特判。
  • 触发条件 :仅 消息条数 > compact_after_turns,无 真实 token 、无 软预算
  • 产物 :不生成 独立 summary 块 ;不写入 「continuation」系统提示(与 Rust 侧对比见下)。
  • 持久化StoredSession 只存截断后的 messages(见 result/06.md),无法从磁盘恢复被截断内容

这些在移植期合理;返工风险 在于:若业务代码直接依赖「只有 user 字符串列表」,将来要加 ConversationMessage { role, blocks } 时会痛。旋钮上已经预留 config + 单钩子下一步 是把 mutable_messages 的元素类型抽象成 统一 Message ,而不是到处传 str


4. Rust runtime:更完整的「旋钮组」参考

同仓库 Rust 实现里,CompactionConfigshould_compact / compact_session 展示了 接口层应暴露的另一维度

rust 复制代码
# 8:21:rust/crates/runtime/src/compact.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompactionConfig {
    pub preserve_recent_messages: usize,
    pub max_estimated_tokens: usize,
}

impl Default for CompactionConfig {
    fn default() -> Self {
        Self {
            preserve_recent_messages: 4,
            max_estimated_tokens: 10_000,
        }
    }
}
rust 复制代码
# 37:47:rust/crates/runtime/src/compact.rs
pub fn should_compact(session: &Session, config: CompactionConfig) -> bool {
    let start = compacted_summary_prefix_len(session);
    let compactable = &session.messages[start..];

    compactable.len() > config.preserve_recent_messages
        && compactable
            .iter()
            .map(estimate_message_tokens)
            .sum::<usize>()
            >= config.max_estimated_tokens
}

与 Python 对照

旋钮 Python QueryEngineConfig Rust CompactionConfig
保留最近 K 条 compact_after_turns(截断窗口 = K) preserve_recent_messages
按 token 触发 无(仅有 max_budget_tokens 用于 stop_reason) max_estimated_tokens
压缩产物 无摘要 CompactionResult { summary, formatted_summary, compacted_session, removed_message_count }
续写语义 get_compact_continuation_message 注入 system 前文

学习点 :产品级 compaction 通常要 同时 暴露 「保留窗口」「触发阈值(token/字符)」;仅条数一项不足以避免浪费上下文或过早压碎长工具输出。


5. 接口层建议预留的旋钮清单(防全局返工)

下列项不必第一天全实现,但 建议在类型与配置结构上留位(或文档契约上承诺),避免日后改签名。

5.1 触发(When)

  • preserve_recent_messages / keep_last_turns:永远原样保留的尾部轮次或消息数。
  • max_context_tokens / compact_threshold_tokens:估算 token 超阈再压(Rust 已有雏形)。
  • min_messages_before_compact:避免极短会话反复压。
  • cooldown_turns:压完 N 轮内不再压,防止震荡。
  • 手动触发 :斜杠命令 /compact、API compact_now()(Rust CLI/commands 已接线)。

5.2 策略(What)

  • CompactionStrategy 枚举或 traitTailDrop | SummarizeModel | Hybrid | ExternalMemory,便于 A/B。
  • 可插入的 summarize_fn:输入被移除消息区间,输出 summary 文本 + 可选结构化「决策/待办」。
  • 角色策略 :user/assistant/tool/system 是否可进入摘要、工具结果是否单独截断。
  • Pinned messages :用户或系统 钉住 的条目不参与压缩。

5.3 产物与提示(How it affects the model)

  • Summary 消息角色 :通常 system 或专用 context 槽,避免与 user 混淆(Rust 用 MessageRole::System 包 continuation)。
  • Continuation 文案可配置 :如 COMPACT_DIRECT_RESUME_INSTRUCTION 类常量,便于多语言与产品调性。
  • 多次压缩合并merge_compact_summaries 避免摘要无限膨胀(Rust 已实现层级合并思路)。

5.4 一致性与持久化

  • CompactionResult 式结果 :至少包含 removed_countnew_token_estimatecompacted_session 或等价快照,便于日志与测试断言。
  • 版本号session.version 或 schema,便于迁移(Rust Session 带 version 字段方向)。
  • 原子写 :压完再 save,避免半压状态落盘。

5.5 可观测与合规

  • 事件compaction_started / compaction_finished / compaction_skipped(含 reason)。
  • 审计:谁触发、用了哪版策略、摘要是否含 PII(脱敏钩子)。
  • max_turns / budget 关系:文档化优先级(谁先触发、是否重置计数)。

5.6 测试钩子

  • 纯函数 should_compact(session, config)(Rust 已拆出),Python 可对照抽取,便于表驱动单测。
  • 固定随机种子 / fixture 会话 ,对 compact_session 做 golden file。

6. 小结

  • claw-code Python 已在 QueryEngineConfig + 每轮末尾 compact_messages_if_needed 上做了 最小正确预埋 :配置可调、触发点单一、双缓冲同步、流式里带 transcript_size
  • 缺口触发维度单一(仅条数)无摘要与续写语义消息模型过窄(str) ------接产品前应优先 抽象 Message 与 CompactionResult ,并参考 Rust CompactionConfig / should_compact / compact_session 补全 token 与产物旋钮。
  • 避免全局返工 的核心不是一次写全功能,而是 把「何时压、压什么、压完长什么样、如何持久化与观测」变成稳定 API 面,实现可从 tail-truncate 平滑升级到模型摘要。

相关推荐
m0_613856292 小时前
mysql如何利用事务隔离级别解决特定业务冲突_mysql隔离方案选型
jvm·数据库·python
收获不止数据库3 小时前
达梦9发布会归来:AI 时代,我们需要一款什么样的数据库?
数据库·人工智能·ai·语言模型·数据分析
xskukuku3 小时前
VSCode中的Codex插件如何调用第三方API
vscode·ai·codex
我的xiaodoujiao3 小时前
API 接口自动化测试详细图文教程学习系列16--项目实战演练3
python·学习·测试工具·pytest
ID_180079054733 小时前
Python 实现亚马逊商品详情 API 数据准确性校验(极简可用 + JSON 参考)
java·python·json
时空系4 小时前
第10篇:继承扩展——面向对象编程进阶 python中文编程
开发语言·python·ai编程
胖墩会武术4 小时前
Obsidian 与 Obsidian Skills 小白入门
人工智能·ai·obsidian·obsidian skills
CHANG_THE_WORLD5 小时前
python 批量终止进程exe
开发语言·python
liann1195 小时前
3.2_红队攻击框架--MITRE ATT&CK‌
python·网络协议·安全·网络安全·系统安全·信息与通信
云天AI实战派5 小时前
AI 智能体问题排查指南:ChatGPT、API 调用到 Agent 上线失灵的全流程修复手册
大数据·人工智能·python·chatgpt·aigc