Agent summary 上下文总结机制分析
分析日期: 2026-07-05 分析范围:
agent_loop.py、ga.py、llmcore.py
一、核心问题:每轮都强调 summary 吗?
答案:是的,但不是"每轮"------是"每轮没有显式调用工具时"强制要求。
1.1 触发条件
在 ga.py:549-561 的 turn_end_callback 中:
python
def turn_end_callback(self, response, tool_calls, tool_results, turn, next_prompt, exit_reason):
_c = re.sub(r'```.*?```|<thinking>.*?</thinking>', '', response.content, ...)
rsumm = re.search(r"<summary>(.*?)</summary>", _c, re.DOTALL)
if rsumm:
summary = rsumm.group(1).strip() # ← 提取了 summary 标签内容
else:
# 没有 <summary> 标签 → 强制要求
next_prompt += "\n\n\n[SYSTEM] 必须在回复文本中包含<summary>!\n\n"
summary = f"{tool_name}, args: {clean_args}" # ← 用工具调用信息作为 fallback
触发逻辑:
- 每轮结束后,从 LLM 的回复中提取
<summary>...</summary>标签 - 如果提取到了 → 用提取到的内容作为本轮摘要
- 如果没提取到 → 在下一轮的 prompt 中强制要求
[SYSTEM] 必须在回复文本中包含<summary>! - 同时用工具调用信息(
tool_name, args)作为 fallback 摘要
所以是"每轮都要求,但只在缺失时强制提醒"。
二、上下文总结的三层机制
2.1 第一层:<summary> 标签提取(每轮)
位置 : ga.py:549-561
python
# turn_end_callback --- 每轮结束后执行
rsumm = re.search(r"<summary>(.*?)</summary>", _c, re.DOTALL)
if rsumm:
summary = rsumm.group(1).strip()
else:
next_prompt += "\n\n\n[SYSTEM] 必须在回复文本中包含<summary>!\n\n"
summary = f"{tool_name}, args: {clean_args}"
summary = smart_format(summary.replace('\n', ''), max_str_len=80)
self.history_info.append(f'[Agent] {summary}') # ← 追加到 history_info 列表
作用:
- 每轮提取 LLM 回复中的
<summary>标签内容 - 截断到 80 字符
- 追加到
self.history_info列表(格式:[Agent] {summary}) - 这个列表最终会被注入到 working memory prompt 中
2.2 第二层:Turn 计数驱动的 checkpoint 提示
位置 : ga.py:564-574
python
if turn % 175 == 0 and (not _plan):
next_prompt += f"\n\n[DANGER] Turn {turn}. Must call ask_user to summarize progress..."
elif turn % 7 == 0:
next_prompt += f"\n\n[SYSTEM] Turn {turn}. Call update_working_checkpoint to save key context..."
elif turn % 25 == 0:
next_prompt += f"\n\n[SYSTEM] Turn {turn}. Write checkpoints/key findings to a **file**..."
elif turn % 10 == 0:
next_prompt += get_global_memory() # ← 重新注入 L0+L1+L2 记忆
完整的 Turn 周期表:
| Turn 周期 | 触发动作 | 目的 |
|---|---|---|
| 每 7 轮 | [SYSTEM] Call update_working_checkpoint |
保存关键上下文到 working memory |
| 每 10 轮 | 重新注入 get_global_memory() |
刷新 L0+L1+L2 记忆 |
| 每 25 轮 | [SYSTEM] Write checkpoints to file |
将关键发现写入文件持久化 |
| 每 175 轮 | [DANGER] Must call ask_user |
强制向用户汇报进度(非 plan 模式) |
| Plan 模式每 5 轮 | [Plan Hint] 必须 file_read(plan.md) |
确认当前步骤 |
| Plan 模式 190 轮 | [DANGER] 已达上限 |
强制退出 plan 模式 |
2.3 第三层:Working Memory 注入(每轮)
位置 : ga.py:537-547
python
def _get_anchor_prompt(self, skip=False):
h = self.history_info; W = 30
earlier = f'<earlier_context>\n{self._fold_earlier(h[:-W])}\n</earlier_context>\n' if len(h) > W else ""
h_str = "\n".join(h[-W:]) # ← 最近 30 轮的摘要
prompt = f"\n### [WORKING MEMORY]\n{earlier}<history>\n{h_str}\n</history>"
prompt += f"\nCurrent turn: {self.current_turn}\n"
if self.working.get('key_info'):
prompt += f"\n<key_info>{self.working.get('key_info')}</key_info>"
if self.working.get('related_sop'):
prompt += f"\n有不清晰的地方请再次读取{self.working.get('related_sop')}"
注入结构:
csharp
### [WORKING MEMORY]
<earlier_context>
[Agent] 第1轮摘要(折叠后)
[Agent] 第2轮摘要(折叠后)
...(超过30轮的部分被折叠)
</earlier_context>
<history>
[Agent] 第N-29轮摘要
[Agent] 第N-28轮摘要
...
[Agent] 第N轮摘要(最近30轮)
</history>
Current turn: 42
<key_info>当前任务的关键信息</key_info>
这个 prompt 在每轮工具调用后都会被注入 (通过 _get_anchor_prompt() → next_prompt)。
三、_fold_earlier 的折叠逻辑
位置 : ga.py:523-535
python
def _fold_earlier(self, lines):
FALLBACK = '直接回答了用户问题'
parts, cnt, last = [], 0, ''
def flush():
if cnt:
if FALLBACK in last:
parts.append(f'[Agent]({cnt} turns)') # ← 合并为一行
else:
parts.append(f'{last}({cnt} turns)') # ← 保留最后一条内容 + 计数
for line in lines:
if line.startswith('[USER]'):
flush(); parts.append(line); cnt = 0; last = ''
else:
cnt += 1; last = line
flush()
return "\n".join(parts[-70:]) # ← 只保留最近 70 行
折叠规则:
- 遇到
[USER]开头的行 → 刷新缓冲区,保留该行 - 连续的
[Agent]行 → 计数合并 - 如果最后一条是"直接回答了用户问题" → 用
(N turns)代替 - 否则 → 保留最后一条内容 +
(N turns) - 最终只保留最近 70 行
效果 : 30 轮之前的对话历史被折叠成类似 [Agent] 直接回答了用户问题(5 turns) 的紧凑格式。
四、LLM 层的上下文管理
4.1 trim_messages_history(llmcore.py:97-110)
python
def trim_messages_history(history, sess):
cap = sess.context_win * 3 # 上下文窗口上限(字符数)
target = int(cap * sess.trim_keep_rate) # 保留比例(默认 0.6)
# 如果超过上限 → 从头部删除消息,直到低于 target
while len(history) > 9 and cost() > target:
history.pop(0)
while history and history[0].get('role') != 'user':
history.pop(0) # ← 确保删除后第一条是 user 消息
触发时机 : 每次 BaseSession.ask() 后自动调用(llmcore.py:572-573)
4.2 compress_history_tags(llmcore.py:40-71)
python
def compress_history_tags(messages, keep_recent=10, max_len=800, force=False, interval=5):
# 每 interval 轮触发一次
# 压缩 <thinking>/<tool_use>/<tool_result> 等标签中的内容
# 保留最近 keep_recent 条消息不压缩
触发时机 : trim_messages_history 中调用,每 5 轮触发一次
五、完整的数据流图
arduino
每轮 LLM 回复
│
├─ 1) turn_end_callback 提取 <summary> 标签
│ ├─ 有标签 → 提取内容(截断 80 字符)
│ └─ 无标签 → 强制要求 + 用工具调用信息 fallback
│ → 追加到 self.history_info
│
├─ 2) Turn 计数检查
│ ├─ turn % 7 → 提示 update_working_checkpoint
│ ├─ turn % 10 → 重新注入全局记忆
│ ├─ turn % 25 → 提示写入文件 checkpoint
│ └─ turn % 175 → 危险警告
│
├─ 3) _get_anchor_prompt 构造 working memory
│ ├─ 取最近 30 轮 history_info
│ ├─ 超过 30 轮的部分 → _fold_earlier 折叠
│ ├─ 注入 key_info + related_sop
│ └─ → 拼接到 next_prompt 中
│
└─ 4) LLM 层 trim_messages_history
├─ 检查上下文大小是否超限
├─ 超限 → 从头部删除旧消息
└─ compress_history_tags 压缩标签内容
六、设计评价
6.1 优点
- 多层冗余: 三层机制(summary 标签 + turn 计数 + working memory)互为补充,不会因为单一机制失效而丢上下文
<summary>标签是显式约定: 强制 LLM 在回复中总结自己的行为,比隐式的"让模型自己记住"更可靠- 折叠算法节省 token :
_fold_earlier把 30 轮之前的对话折叠成一行,大幅减少上下文占用 - Turn 计数的渐进式提醒: 7/25/175 轮的提醒频率设计合理------不是每轮都打扰,但在关键节点提醒
6.2 缺点
- 没有真正的 Token 感知: Turn 计数是粗粒度的,不知道每轮实际消耗了多少 token。可能 7 轮就超了上下文窗口,也可能 200 轮还很宽松
<summary>标签依赖 LLM 配合 : 如果 LLM 不写<summary>标签,系统只能 fallback 到工具调用信息,丢失了语义总结- Working Memory 是纯文本拼接: 没有结构化的摘要,只是把历史摘要字符串拼在一起
- 没有自动压缩触发 : 不像 Claude Code 有
AUTOCOMPACT_BUFFER_TOKENS的自动触发机制,GenericAgent 只在trim_messages_history中被动删除旧消息 _fold_earlier丢失细节 : 折叠后的(N turns)丢失了中间轮次的具体内容,只保留最后一条
6.3 与 Claude Code 的对比
| 维度 | GenericAgent | Claude Code |
|---|---|---|
| 压缩触发 | Turn 计数(7/25/175) | Token 阈值(AUTOCOMPACT_BUFFER_TOKENS) |
| 压缩方式 | 文本折叠 + 标签截断 | 6 层管道(auto/micro/session/snip/reactive/cached) |
| 摘要生成 | LLM 回复中的 <summary> 标签 |
后台 forked agent 独立生成 |
| Token 感知 | ❌ 无(只靠字符数估算) | ✅ 精确 token 计数 |
| 自动压缩 | ❌ 只在超限时被动删除 | ✅ 接近阈值时主动触发 |
| 工具结果折叠 | ❌ 无 | ✅ Micro Compact 折叠 Read/Bash/Grep 结果 |
七、总结
GenericAgent 的上下文总结机制是**"显式标签 + Turn 计数 + 文本折叠"三层组合**:
- 每轮 : 从 LLM 回复中提取
<summary>标签,追加到history_info - 每 7 轮 : 提示 Agent 调用
update_working_checkpoint保存关键上下文 - 每轮 :
_get_anchor_prompt把最近 30 轮的摘要 + 折叠后的更早历史注入到 prompt 中 - 超限时 :
trim_messages_history从头部删除旧消息
但这不是真正的"上下文压缩"------它没有 Token 感知、没有自动触发、没有工具结果折叠。它更像是一个"手动档"的上下文管理:靠 Turn 计数和正则表达式,而不是靠 Token 计数和智能压缩。
这也正是 docs/ClaudeCode_对比与改造路线图.md 中 P0-2(Token 感知的上下文压缩) 被列为最高优先级改造项的原因。