Skip to content

Commit 9e38339

Browse files
docs(design): 上下文裁剪方案设计 / add context trimming scheme design doc
Co-authored-by: Yales Peter <noisystreet@users.noreply.github.com>
1 parent 06301e2 commit 9e38339

3 files changed

Lines changed: 173 additions & 2 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# 上下文裁剪方案设计
2+
3+
**状态**:与当前实现同步的设计说明(会话同步管道以源码为准;分层 ReAct 专用裁剪另见下文关联文档)。
4+
**受众**:维护 `agent::message_pipeline``agent::context_window` 与 Agent 主循环的开发者。
5+
**权威实现**`src/agent/message_pipeline.rs`**`apply_session_sync_pipeline`** 顺序契约)、`src/agent/context_window.rs`**`prepare_messages_for_model`****`maybe_summarize_with_llm`**)。
6+
**配置与环境变量**:键名与默认值以 **`docs/配置说明.md`**`config/default_config.toml` 为准。
7+
8+
---
9+
10+
## 1. 目的与范围
11+
12+
### 1.1 文档目的
13+
14+
归纳 CrabMate **发往模型前**对会话 **`messages`** 的裁剪与压缩策略的设计要点:动机、分层、固定顺序、观测方式及演进边界,便于评审配置变更与新增管道步骤。
15+
16+
### 1.2 范围
17+
18+
| 包含 | 不包含(另见) |
19+
|------|----------------|
20+
| 进程内 `Vec<Message>` 同步变换(tool 压缩、条数/字符裁剪、孤立 tool、合并 assistant) | HTTP 层 **`stream_chat`** 内与其它绕过管道的拼装(应避免新增此类路径) |
21+
| 可选「中间段 LLM 摘要」(**`maybe_summarize_with_llm`**| 供应商网关适配(**`conversation_messages_to_vendor_body`**、vendor adapter)的完整枚举——仅描述其与裁剪的衔接关系 |
22+
| **`GET /status`** 计数器与日志字段 | 前端展示策略 |
23+
24+
### 1.3 非目标
25+
26+
本文件****替代 `message_pipeline.rs` 模块内注释;步骤顺序以源码 **`MessagePipelineStage`****`apply_session_sync_pipeline`** 为准。
27+
28+
---
29+
30+
## 2. 为何需要裁剪
31+
32+
1. **上下文上限**:消息总长超过模型窗口会导致报错、截断或隐性丢失末尾内容。
33+
2. **成本与延迟**:输入 token 与费用、首字延迟正相关。
34+
3. **工具输出体积**`role: tool` 正文(命令输出、大 JSON)往往远大于自然语言轮次,若不先压缩再删条,易出现「条数很少但仍撑爆窗口」。
35+
4. **信噪比**:远期轮次对当前决策贡献通常低于近期轮次;在保留尾部的前提下压缩中部,是常见权衡。
36+
37+
---
38+
39+
## 3. 两阶段架构(必须区分)
40+
41+
设计上分为两步,**职责不同,勿混为一谈**
42+
43+
### 3.1 会话同步(Session sync)
44+
45+
- **入口****`message_pipeline::apply_session_sync_pipeline`**(由 **`context_window::prepare_messages_before_model_call_sync`** / **`prepare_messages_for_model`** 调用)。
46+
- **作用对象**:进程内会话 **`Vec<Message>`**(就地修改)。
47+
- **目标**:控制长度、压缩工具噪声、维护对话结构合法性(孤立 tool、相邻 assistant 等)。
48+
49+
### 3.2 供应商出站(Vendor body)
50+
51+
- **入口****`conversation_messages_to_vendor_body`** / **`normalize_stripped_messages_for_vendor_body`** 等(见 `message_pipeline.rs`)。
52+
- **作用对象**:从会话切片构造 **`ChatRequest.messages`****不写回**会话 `Vec`
53+
- **典型处理**:跳过 UI 分隔线 / 长期记忆注入展示条、按网关处理 **`reasoning_content`**、OpenAI 兼容 **normalize**、可选 **system→user** 折叠。
54+
55+
**原则**:会话侧裁剪解决「体积与结构」;出站路径解决「供应商兼容」。新增裁剪逻辑时默认落在 **会话同步**,除非明确只做 HTTP 层语义且不应改写会话 truth。
56+
57+
---
58+
59+
## 4. 会话同步管道顺序(契约)
60+
61+
以下顺序**固定**,实现见 **`apply_session_sync_pipeline`**;新增步骤须同步更新:
62+
63+
1. **`MessagePipelineStage`** 枚举
64+
2. **`message_pipeline.rs` 模块文档**编号列表
65+
3. **`docs/开发文档.md`**「上下文窗口策略」
66+
67+
当前顺序(与源码一致):
68+
69+
1. 记录起点快照(观测)
70+
2. **`compress_tool_message_contents`****`tool_message_max_chars`**
71+
3. **`trim_messages_by_count`****`max_message_history`**
72+
4.**`context_char_budget > 0`****`trim_messages_by_char_budget`** → 再次 **`compress_tool_message_contents`**
73+
5. **`drop_orphan_tool_messages`**
74+
6. **`merge_consecutive_assistants_in_place`**
75+
76+
**设计意图简述**
77+
78+
- **先压 tool 再按条数删**:避免「轮次少了但单条 tool 仍极大」。
79+
- **字符预算后再压一遍 tool**:删旧消息后剩余 tool 仍可能触线。
80+
- **最后处理 orphan 与 merge**:在删条之后统一修正结构。
81+
82+
---
83+
84+
## 5. 可选 LLM 摘要(中间段压缩)
85+
86+
**`context_summary_trigger_chars > 0`** 且非 system 文本总字符超过阈值时,**`maybe_summarize_with_llm`** 可发起**无 tools**`chat/completions`,将「中间段」折叠为一条 **user** 摘要,尾部保留 **`context_summary_tail_messages`** 条(细节见 **`context_window.rs`**)。
87+
88+
**设计要点**
89+
90+
- **额外成本与失败语义**:摘要是一次独立 LLM 调用,失败策略需在实现中保持可预期(勿静默丢历史)。
91+
- **幻觉风险**:摘要可能遗漏或歪曲约束;通过尾部保留条数、触发阈值与提示词约束缓解。
92+
- **与同步管道的关系**:摘要通常在同步裁剪**之后**执行(见 **`prepare_messages_for_model`** 调用链),具体顺序以源码为准。
93+
94+
---
95+
96+
## 6. 设计要点清单(评审用)
97+
98+
实现或改配置时建议逐项核对:
99+
100+
### 6.1 对话合法性
101+
102+
- **`tool_calls` / `role: tool`** 配对完整;删中间消息后不残留悬空 tool。
103+
- 裁剪后仍满足 **`normalize_messages_for_openai_compatible_request`** 一类契约。
104+
105+
### 6.2 预算口径
106+
107+
- **条数****`max_message_history`**)与 **近似字符****`context_char_budget`**)互补:条数控制轮次规模,字符抑制超长单条;二者与真实 token 仅为近似关系。
108+
- 若未来引入 **token 级预算**,应与现有顺序分层定义,避免多重裁剪冲突。
109+
110+
### 6.3 保留优先级(策略层)
111+
112+
- **System / 用户目标 / 近期轮次**通常优先于远古轮次。
113+
- **失败证据、验收结果、审批结论**是否必须留在窗口——业务上若关键,应在规则或摘要提示词中显式强调。
114+
115+
### 6.4 工具正文 vs 自然语言
116+
117+
- 优先压缩 **`crabmate_tool`** 信封内 **`output`**(首尾采样 + **`output_truncated`** 等元数据),非信封路径按前缀截断;便于模型知晓「保留比例」而非误以为全文可用。
118+
119+
### 6.5 多入口一致性
120+
121+
- Web / CLI / 分阶段 / 分层等路径应统一经 **`prepare_messages_for_model`**(或等价入口),避免同一会话在不同入口「模型所见不同」。
122+
123+
### 6.6 可观测性
124+
125+
- **`MESSAGE_PIPELINE_COUNTERS`****`trim_count_hits`****`trim_char_budget_hits`****`tool_compress_hits`****`orphan_tool_drops`****`GET /status`** 暴露累计值(进程级,非单会话)。
126+
- **日志****`crabmate::message_pipeline=trace`** 输出逐步 **`session_sync_step`**
127+
128+
### 6.7 安全与脱敏
129+
130+
- 裁剪与日志不得输出完整密钥或整条 Authorization;工具正文日志遵循 **`secrets-and-logging`** 仓库规则。
131+
132+
### 6.8 厂商与出站衔接
133+
134+
- **`reasoning_content`****`fold_system`** 等在出站阶段处理;会话侧勿过早丢弃下游仍需要的字段,除非已与 **`LlmVendorAdapter`** 策略对齐。
135+
136+
---
137+
138+
## 7. 配置索引(查阅用)
139+
140+
具体默认值、环境变量前缀与 clamp 范围以 **`docs/配置说明.md`** 为准,常用键包括:
141+
142+
- **`tool_message_max_chars`** / **`CM_TOOL_MESSAGE_MAX_CHARS`**
143+
- **`max_message_history`** / **`CM_MAX_MESSAGE_HISTORY`**
144+
- **`context_char_budget`** / **`CM_CONTEXT_CHAR_BUDGET`**
145+
- **`context_summary_trigger_chars`****`context_summary_tail_messages`****`CM_CONTEXT_*`**
146+
147+
---
148+
149+
## 8. 与其它设计文档的关系
150+
151+
| 文档 | 关系 |
152+
|------|------|
153+
| **`docs/design/context_window_management_react_pruning.md`** | 面向 **`hierarchy`** Operator **ReAct** 的长循环裁剪(**状态:待实现**);与本文全局 **`message_pipeline`** 互补,落地时应复用 **`context_window`** / **`message_pipeline`** 能力,避免第二套条数字符逻辑。 |
154+
| **`docs/design/agent_state_management.md`** | 更广义的会话/产物状态;与裁剪正交。 |
155+
| **`docs/开发文档.md`** | 维护者索引与「上下文窗口策略」段落;本文展开设计要点,细节仍以源码为准。 |
156+
157+
---
158+
159+
## 9. 演进建议(非承诺)
160+
161+
1. **分层 ReAct**:实现 **`context_window_management_react_pruning.md`** 时优先 token 估算与 **`prepare_messages_for_model`** 挂钩。
162+
2. **契约测试**:固定 fixture 覆盖「条数边界、超长 tool、孤儿 tool、摘要触发」等回归场景。
163+
3. **长期记忆 / 向量检索**:与裁剪顺序协调,避免重复注入或互相覆盖(参见 **`docs/待办清单.md`** 记忆相关条目)。
164+
165+
---
166+
167+
## 10. 修订记录
168+
169+
| 日期 | 摘要 |
170+
|------|------|
171+
| 2026-05-01 | 初版:会话同步契约、两阶段架构、设计要点与关联文档。 |

docs/design/context_window_management_react_pruning.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**状态**:设计稿(待实现)
44
**受众**:核心维护者、Agent 编排模块贡献者
5-
**关联文档**`docs/规划执行验证架构.md``docs/HIERARCHICAL_MULTI_CM_ARCHITECTURE.md``docs/design/agent_state_management.md`
5+
**关联文档**`docs/规划执行验证架构.md``docs/HIERARCHICAL_MULTI_CM_ARCHITECTURE.md``docs/design/agent_state_management.md`;全局会话同步管道与裁剪要点见 **`docs/design/context_trimming_scheme.md`**(本文侧重分层 ReAct 专用裁剪,落地时应与其对齐)。
66

77
---
88

docs/开发文档.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ flowchart TB
349349
| `workflow_reflection`(默认) | `N`(配置 `1..=20`,默认 `2`| 仅在工作流路径置位「需要终答规划」后校验;不合格则追加重写 user,**至多 `N`**(用尽 → SSE `plan_rewrite_exhausted`|
350350
| `always` | `N` | 每次终答均校验;同上重写上限 |
351351

352-
- **上下文窗口策略**(`src/agent/context_window.rs` + **`src/agent/message_pipeline.rs`**):每次 P 步前 `prepare_messages_for_model` 先经 **`message_pipeline::apply_session_sync_pipeline`**(从 **`AgentConfig`** 派生 **`MessagePipelineConfig`**;单测可用 **`apply_session_sync_pipeline_with_config`**)做同步变换。**顺序契约**以 `message_pipeline.rs` 模块文档编号列表为准(与 `MessagePipelineStage` 一一对应)。**`/status` 计数、日志与配置告警**见上文**架构设计**「**上下文管道(观测)**」。再可选 **`maybe_summarize_with_llm`**。变换含:**`tool` 消息正文压缩**(`tool_message_max_chars`;若正文为 **`tool_result::encode_tool_message_envelope_v1`** 的 `crabmate_tool` 形状且内层 **`output`** 超长,则 **`tool_result::maybe_compress_tool_message_content`** 对其做**首尾采样**并写入 **`output_truncated`** / **`output_original_chars`** / **`output_kept_head_chars`** / **`output_kept_tail_chars`**,便于模型引用「原文规模 + 保留片段」;非信封正文仍按前缀截断)、**按条数保留**(沿用 `max_message_history`)、可选 **`context_char_budget` 按近似字符删旧消息**;若 `context_summary_trigger_chars > 0` 且非 system 总字符超阈值,则额外发起**无 tools** 的 `chat/completions` 将「中间段」压成一条 user 摘要,尾部保留 `context_summary_tail_messages` 条。Web/CLI 侧 `messages` 会随裁剪/摘要变化(工具截断不改变条数)。发往供应商的 **`ChatRequest.messages`** 由 **`message_pipeline::conversation_messages_to_vendor_body`**(或已 strip 时的 **`normalize_stripped_messages_for_vendor_body`**)统一拼装,**`llm::api::stream_chat`** 在 HTTP 前对请求体再跑一遍同一出站路径以兜底直连 `ChatRequest`。**`tool_result_envelope_v1`**(默认 true):经 `execute_tools::emit_tool_result_sse_and_append` 写入历史的 `role: tool` 为单行 JSON,顶层键 **`crabmate_tool`**,字段含 **`v`/`name`/`summary`/`ok`/`exit_code`/`error_code`/`output`**(`summary` 与 SSE `ToolResultBody.summary` 及 `summarize_tool_call*` 同源);经消息管道压缩后另可有 **`output_truncated`** 等元数据。`per_coord::last_workflow_validate_layer_count` 等需解析工具 JSON 的路径使用 **`tool_result::tool_message_payload_for_inner_parse`** 剥离信封后读内层。**`per_coord` 非空时**,函数结束时会调用 **`PerCoordinator::invalidate_workflow_validate_layer_cache_after_context_mutation`**,避免 `per_coord` 内缓存的 `workflow_validate` **`layer_count`** 在删旧消息后仍指向已不存在的工具结果。配置见 `config/default_config.toml` 与 `CM_CONTEXT_*` / `CM_TOOL_MESSAGE_MAX_CHARS` / **`CM_TOOL_RESULT_ENVELOPE_V1`**。**会话工作区变更集**:若 **`session_workspace_changelist_enabled`**(默认 true),在 **`maybe_summarize_with_llm`** 之后调用 **`workspace_changelist::sync_changelist_user_message`**(**`push`** 到 `messages` **末尾**,使含 `tool` 结果的回合里摘要紧邻「最近上下文」;下一轮 P 步开头先 **`strip`** 再跑同步管道);**`types::messages_for_api_stripping_reasoning_skip_ui_separators`** 与长期记忆的「最后 user 查询」启发式均跳过该条。
352+
- **上下文窗口策略**(`src/agent/context_window.rs` + **`src/agent/message_pipeline.rs`**):每次 P 步前 `prepare_messages_for_model` 先经 **`message_pipeline::apply_session_sync_pipeline`**(从 **`AgentConfig`** 派生 **`MessagePipelineConfig`**;单测可用 **`apply_session_sync_pipeline_with_config`**)做同步变换。**顺序契约**以 `message_pipeline.rs` 模块文档编号列表为准(与 `MessagePipelineStage` 一一对应)。**方案级设计说明(裁剪动机、两阶段架构、评审清单)见 `docs/design/context_trimming_scheme.md`。** **`/status` 计数、日志与配置告警**见上文**架构设计**「**上下文管道(观测)**」。再可选 **`maybe_summarize_with_llm`**。变换含:**`tool` 消息正文压缩**(`tool_message_max_chars`;若正文为 **`tool_result::encode_tool_message_envelope_v1`** 的 `crabmate_tool` 形状且内层 **`output`** 超长,则 **`tool_result::maybe_compress_tool_message_content`** 对其做**首尾采样**并写入 **`output_truncated`** / **`output_original_chars`** / **`output_kept_head_chars`** / **`output_kept_tail_chars`**,便于模型引用「原文规模 + 保留片段」;非信封正文仍按前缀截断)、**按条数保留**(沿用 `max_message_history`)、可选 **`context_char_budget` 按近似字符删旧消息**;若 `context_summary_trigger_chars > 0` 且非 system 总字符超阈值,则额外发起**无 tools** 的 `chat/completions` 将「中间段」压成一条 user 摘要,尾部保留 `context_summary_tail_messages` 条。Web/CLI 侧 `messages` 会随裁剪/摘要变化(工具截断不改变条数)。发往供应商的 **`ChatRequest.messages`** 由 **`message_pipeline::conversation_messages_to_vendor_body`**(或已 strip 时的 **`normalize_stripped_messages_for_vendor_body`**)统一拼装,**`llm::api::stream_chat`** 在 HTTP 前对请求体再跑一遍同一出站路径以兜底直连 `ChatRequest`。**`tool_result_envelope_v1`**(默认 true):经 `execute_tools::emit_tool_result_sse_and_append` 写入历史的 `role: tool` 为单行 JSON,顶层键 **`crabmate_tool`**,字段含 **`v`/`name`/`summary`/`ok`/`exit_code`/`error_code`/`output`**(`summary` 与 SSE `ToolResultBody.summary` 及 `summarize_tool_call*` 同源);经消息管道压缩后另可有 **`output_truncated`** 等元数据。`per_coord::last_workflow_validate_layer_count` 等需解析工具 JSON 的路径使用 **`tool_result::tool_message_payload_for_inner_parse`** 剥离信封后读内层。**`per_coord` 非空时**,函数结束时会调用 **`PerCoordinator::invalidate_workflow_validate_layer_cache_after_context_mutation`**,避免 `per_coord` 内缓存的 `workflow_validate` **`layer_count`** 在删旧消息后仍指向已不存在的工具结果。配置见 `config/default_config.toml` 与 `CM_CONTEXT_*` / `CM_TOOL_MESSAGE_MAX_CHARS` / **`CM_TOOL_RESULT_ENVELOPE_V1`**。**会话工作区变更集**:若 **`session_workspace_changelist_enabled`**(默认 true),在 **`maybe_summarize_with_llm`** 之后调用 **`workspace_changelist::sync_changelist_user_message`**(**`push`** 到 `messages` **末尾**,使含 `tool` 结果的回合里摘要紧邻「最近上下文」;下一轮 P 步开头先 **`strip`** 再跑同步管道);**`types::messages_for_api_stripping_reasoning_skip_ui_separators`** 与长期记忆的「最后 user 查询」启发式均跳过该条。
353353
- **系统提示词规则拼接**`[agent] cursor_rules_enabled` / `CM_CURSOR_RULES_ENABLED`**默认开启**):在 `config::load_config` 阶段,`system_prompt`/`system_prompt_file` 读取后可追加 `cursor_rules_dir`(默认 `.cursor/rules`)下 `*.mdc`,并可选附加工作区根 `AGENTS.md``cursor_rules_include_agents_md`);按文件名排序拼接,附加段受 `cursor_rules_max_chars` 限制,超出截断并加提示。该拼接结果即后续 `messages_chat_seed` 使用的首条 `system`
354354
- **长期记忆与向量检索**:已实现 **会话级** SQLite 存储 + 可选 **fastembed**(CPU)余弦检索;注入条目标记 **`crabmate_long_term_memory`** 不进入供应商请求体。`/status` 暴露 `long_term_memory_*` 字段。外部 **Qdrant/pgvector** 仍为后续工作。`LongTermMemoryScopeMode` 当前仅 `conversation`;与 P0 鉴权关系见 `README.md`
355355
- **处理结束原因**

0 commit comments

Comments
 (0)