涉及源码 :Python
src/query_engine.py、src/transcript.py;对照 Rustrust/crates/runtime/src/compact.rs与ConversationMessage会话模型。
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_messages 与 TranscriptStore
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_stop 带 transcript_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: bool、removed_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 实现里,CompactionConfig 与 should_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、APIcompact_now()(Rust CLI/commands 已接线)。
5.2 策略(What)
CompactionStrategy枚举或 trait :TailDrop|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_count、new_token_estimate、compacted_session或等价快照,便于日志与测试断言。- 版本号 :
session.version或 schema,便于迁移(RustSession带 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 平滑升级到模型摘要。