summary 上下文总结机制

Agent summary 上下文总结机制分析

分析日期: 2026-07-05 分析范围: agent_loop.pyga.pyllmcore.py


一、核心问题:每轮都强调 summary 吗?

答案:是的,但不是"每轮"------是"每轮没有显式调用工具时"强制要求。

1.1 触发条件

ga.py:549-561turn_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

触发逻辑:

  1. 每轮结束后,从 LLM 的回复中提取 <summary>...</summary> 标签
  2. 如果提取到了 → 用提取到的内容作为本轮摘要
  3. 如果没提取到 → 在下一轮的 prompt 中强制要求 [SYSTEM] 必须在回复文本中包含<summary>!
  4. 同时用工具调用信息(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 行

折叠规则:

  1. 遇到 [USER] 开头的行 → 刷新缓冲区,保留该行
  2. 连续的 [Agent] 行 → 计数合并
  3. 如果最后一条是"直接回答了用户问题" → 用 (N turns) 代替
  4. 否则 → 保留最后一条内容 + (N turns)
  5. 最终只保留最近 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 优点

  1. 多层冗余: 三层机制(summary 标签 + turn 计数 + working memory)互为补充,不会因为单一机制失效而丢上下文
  2. <summary> 标签是显式约定: 强制 LLM 在回复中总结自己的行为,比隐式的"让模型自己记住"更可靠
  3. 折叠算法节省 token : _fold_earlier 把 30 轮之前的对话折叠成一行,大幅减少上下文占用
  4. Turn 计数的渐进式提醒: 7/25/175 轮的提醒频率设计合理------不是每轮都打扰,但在关键节点提醒

6.2 缺点

  1. 没有真正的 Token 感知: Turn 计数是粗粒度的,不知道每轮实际消耗了多少 token。可能 7 轮就超了上下文窗口,也可能 200 轮还很宽松
  2. <summary> 标签依赖 LLM 配合 : 如果 LLM 不写 <summary> 标签,系统只能 fallback 到工具调用信息,丢失了语义总结
  3. Working Memory 是纯文本拼接: 没有结构化的摘要,只是把历史摘要字符串拼在一起
  4. 没有自动压缩触发 : 不像 Claude Code 有 AUTOCOMPACT_BUFFER_TOKENS 的自动触发机制,GenericAgent 只在 trim_messages_history 中被动删除旧消息
  5. _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_对比与改造路线图.mdP0-2(Token 感知的上下文压缩) 被列为最高优先级改造项的原因。

相关推荐
带刺的坐椅2 小时前
Agent Harness 实战指南:构建生产级 AI Agent 的"马具"框架
java·ai·llm·agent·solon-ai
星航夜空的帆舟2 小时前
ADK 会话与状态管理:三级状态、内存服务与多存储后端
go·agent
阿里云大数据AI技术2 小时前
DataWorks Data Agent的演进与工程化实践
人工智能·agent
weigangwin3 小时前
LlamaIndex 第一次试用:别先写 RAG Demo,先验上下文合同
python·ai·agent·rag·检索·llamaindex·观测性
闲研随记3 小时前
【文献阅读】BunnyFinder:探寻以太坊共识机制中的激励机制漏洞
区块链·agent·漏洞·以太坊·ppo·incentive flaws
阿拉斯攀登3 小时前
Embedding 模型选择与领域适配
人工智能·chatgpt·embedding·agent·ai编程·loop·rag
饼干哥哥3 小时前
用爱马仕多Agent组建跨境电商公司,出海赚美金
openai·agent·ai编程
SelectDB6 小时前
阶跃星辰 Agent 可观测实践:为什么 Trace 数据底座选择 SelectDB?
大数据·数据库·agent
i晟6 小时前
Hermes Agent 自进化架构深度解析(二):记忆系统——2200 字符如何成为自我整理的引擎
agent