作者:互联网 时间: 2026-07-16 18:04:08
答案:是的,但不是"每轮"——是"每轮没有显式调用工具时"强制要求。

在 ga.py:549-561 的 turn_end_callback 中:
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 += "nnn[SYSTEM] 必须在回复文本中包含<summary>!nn"summary = f"{tool_name}, args: {clean_args}"# ← 用工具调用信息作为 fallback
触发逻辑:
<summary>...</summary> 标签[SYSTEM] 必须在回复文本中包含<summary>!tool_name, args)作为 fallback 摘要所以是"每轮都要求,但只在缺失时强制提醒"。
<summary> 标签提取(每轮)位置: ga.py:549-561
# turn_end_callback — 每轮结束后执行rsumm = re.search(r"<summary>(.*?)</summary>", _c, re.DOTALL)if rsumm: summary = rsumm.group(1).strip()else:next_prompt += "nnn[SYSTEM] 必须在回复文本中包含<summary>!nn"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 列表
作用:
<summary> 标签内容self.history_info 列表(格式: [Agent] {summary})位置: ga.py:564-574
if turn % 175 == 0 and (not _plan):next_prompt += f"nn[DANGER] Turn {turn}. Must call ask_user to summarize progress..."elif turn % 7 == 0:next_prompt += f"nn[SYSTEM] Turn {turn}. Call update_working_checkpoint to save key context..."elif turn % 25 == 0:next_prompt += f"nn[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 模式 |
位置: ga.py:537-547
def _get_anchor_prompt(self, skip=False):h = self.history_info; W = 30earlier = 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')}"
注入结构:
<earlier_context>[] 第1轮摘要(折叠后)[] 第2轮摘要(折叠后)...(超过30轮的部分被折叠)</earlier_context><history>[] 第N-29轮摘要[] 第N-28轮摘要...[] 第N轮摘要(最近30轮)</history>Current turn: 42<key_info>当前任务的关键信息</key_info>
这个 prompt 在每轮工具调用后都会被注入(通过 _get_anchor_prompt() → next_prompt)。
_fold_earlier 的折叠逻辑位置: ga.py:523-535
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 = lineflush()return "n".join(parts[-70:])# ← 只保留最近 70 行
折叠规则:
[USER] 开头的行 → 刷新缓冲区,保留该行[Agent] 行 → 计数合并(N turns) 代替(N turns)效果: 30 轮之前的对话历史被折叠成类似 [Agent] 直接回答了用户问题(5 turns) 的紧凑格式。
trim_messages_history(llmcore.py:97-110)def trim_messages_history(history, sess):cap = sess.context_win * 3# 上下文窗口上限(字符数)target = int(cap * sess.trim_keep_rate)# 保留比例(默认 0.6)# 如果超过上限 → 从头部删除消息,直到低于 targetwhile 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)
compress_history_tags(llmcore.py:40-71)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 轮触发一次
每轮 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 压缩标签内容
<summary> 标签是显式约定: 强制 LLM 在回复中总结自己的行为,比隐式的"让模型自己记住"更可靠_fold_earlier 把 30 轮之前的对话折叠成一行,大幅减少上下文占用<summary> 标签依赖 LLM 配合: 如果 LLM 不写 <summary> 标签,系统只能 fallback 到工具调用信息,丢失了语义总结AUTOCOMPACT_BUFFER_TOKENS 的自动触发机制,GenericAgent 只在 trim_messages_history 中被动删除旧消息_fold_earlier 丢失细节: 折叠后的 (N turns) 丢失了中间轮次的具体内容,只保留最后一条| 维度 | 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 计数 + 文本折叠"三层组合**:
<summary> 标签,追加到 history_infoupdate_working_checkpoint 保存关键上下文_get_anchor_prompt 把最近 30 轮的摘要 + 折叠后的更早历史注入到 prompt 中trim_messages_history 从头部删除旧消息但这不是真正的"上下文压缩"——它没有 Token 感知、没有自动触发、没有工具结果折叠。它更像是一个"手动档"的上下文管理:靠 Turn 计数和正则表达式,而不是靠 Token 计数和智能压缩。
这也正是 docs/ClaudeCode_对比与改造路线图.md 中 P0-2(Token 感知的上下文压缩) 被列为最高优先级改造项的原因。