Agent 上下文压缩策略:长对话下的记忆裁剪与摘要

一、对话超过 30 轮后,Agent 开始答非所问

Agent 的上下文窗口不是无限的。
即使 GPT-4 支持 128K Token,长对话仍会出问题。
上下文过长导致三个后果。
一是注意力稀释:模型开始忽略早期关键信息。
二是成本飙升:每轮追加全部历史,Token 消耗指数增长。
三是延迟升高:长上下文让推理时间从 2 秒变成 8 秒。

一个客服 Agent 的真实案例。
用户和 Agent 聊了 40 轮,描述了问题、提供了截图。
但 Agent 在第 41 轮时忘记了用户最初说的产品型号。
因为型号信息在对话开头,已被注意力机制稀释。

上下文压缩的本质是取舍。
保留什么信息?丢弃什么信息?怎么保留?

二、记忆管理的三层架构

Agent 的记忆系统分三个层次。
工作记忆:当前对话窗口,最新的 N 轮。
摘要记忆:对历史对话的结构化压缩。
长期记忆:跨会话的关键信息持久化。

flowchart TB
    A[用户新消息] --> B[当前对话缓冲区]
    B --> C{缓冲区满?}
    C -->|否| D[直接追加上下文]
    C -->|是| E[触发压缩]
    
    E --> F[滑动窗口: 保留最近 K 轮]
    F --> G[旧对话送入压缩器]
    G --> H[生成结构化摘要]
    H --> I[提取关键实体]
    
    I --> J[摘要存储]
    J --> K{关键实体变化?}
    K -->|是| L[更新长期记忆]
    K -->|否| M[保留现有记忆]
    
    L --> D
    M --> D

关键设计决策:
窗口大小:保留最近 10-15 轮完整对话。
摘要粒度:每 10 轮旧对话生成一次摘要。
实体提取:人名、产品名、问题类型等关键信息。

三、上下文压缩的 Python 实现

"""
context_compressor.py - Agent 上下文压缩
"""

import hashlib
import json
import logging
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime

logger = logging.getLogger(__name__)


@dataclass
class Message:
    """对话消息"""
    role: str          # user / assistant / tool
    content: str
    timestamp: float = field(default_factory=lambda: datetime.now().timestamp())


@dataclass
class ConversationSummary:
    """对话摘要"""
    summary_text: str            # 自然语言摘要
    key_entities: Dict[str, str] # 提取的关键实体
    decisions: List[str]         # 做出的决策
    pending_items: List[str]     # 未完成事项
    compressed_at: float         # 压缩时间戳


class ContextCompressor:
    """Agent 上下文压缩器"""

    def __init__(
        self,
        max_recent_rounds: int = 10,
        compress_every: int = 15,     # 每 15 轮旧对话触发压缩
        max_summary_length: int = 500, # 摘要最大长度(字符)
    ):
        self.max_recent_rounds = max_recent_rounds
        self.compress_every = compress_every
        self.max_summary_length = max_summary_length

        self.buffer: List[Message] = []         # 当前对话缓冲
        self.summaries: List[ConversationSummary] = []  # 历史摘要
        self.long_term: Dict[str, str] = {}      # 长期记忆

    def add_message(self, msg: Message):
        """添加新消息,自动触发压缩"""
        self.buffer.append(msg)

        if len(self.buffer) > self.max_recent_rounds + self.compress_every:
            self._compress()

    def _compress(self):
        """压缩旧消息为摘要"""
        # 保留最近 max_recent_rounds 条
        recent = self.buffer[-self.max_recent_rounds:]
        old = self.buffer[:-self.max_recent_rounds]

        if not old:
            return

        # 生成摘要(生产环境调用 LLM)
        summary = self._generate_summary(old)

        self.summaries.append(summary)
        self.buffer = recent

        # 更新长期记忆
        for key, value in summary.key_entities.items():
            self.long_term[key] = value

        logger.info(
            f"压缩完成:{len(old)} 条消息 → 摘要 "
            f"({len(summary.summary_text)} 字符)"
        )

    def _generate_summary(
        self, messages: List[Message]
    ) -> ConversationSummary:
        """
        生成对话摘要
        生产环境应调用 LLM 生成,此处为简化示例
        """
        # 提取关键信息(简化实现)
        entities = {}
        decisions = []
        pending = []

        conversation_text = ""
        for msg in messages:
            conversation_text += f"[{msg.role}] {msg.content}\n"

        # 简化的实体提取(正则匹配)
        import re
        # 查找产品名、ID等模式
        product_matches = re.findall(
            r'(产品|型号|订单)[::\s]*([A-Za-z0-9_-]+)',
            conversation_text
        )
        for match in product_matches:
            entities[match[0]] = match[1]

        # 查找决策
        decision_matches = re.findall(
            r'(决定|确认|选择)[::\s]*(.+)',
            conversation_text
        )
        for match in decision_matches:
            decisions.append(f"{match[0]}:{match[1]}")

        # 生成摘要文本(生产环境用 LLM)
        summary_text = (
            f"历史对话讨论要点:"
            f"涉及 {len(messages)} 条消息,"
            f"提取关键实体 {len(entities)} 项,"
            f"做出 {len(decisions)} 项决策。"
        )

        return ConversationSummary(
            summary_text=summary_text,
            key_entities=entities,
            decisions=decisions,
            pending_items=pending,
            compressed_at=datetime.now().timestamp(),
        )

    def build_context(self) -> str:
        """构建发送给 LLM 的完整上下文"""
        parts = []

        # 1. 长期记忆
        if self.long_term:
            parts.append("## 重要信息")
            for key, value in self.long_term.items():
                parts.append(f"- {key}: {value}")
            parts.append("")

        # 2. 历史摘要
        if self.summaries:
            parts.append("## 历史对话摘要")
            for i, summary in enumerate(self.summaries):
                parts.append(f"### 阶段{i+1}")
                parts.append(summary.summary_text[:self.max_summary_length])
            parts.append("")

        # 3. 最近对话
        if self.buffer:
            parts.append("## 最近对话")
            for msg in self.buffer:
                parts.append(f"**{msg.role}**: {msg.content}")
            parts.append("")

        return "\n".join(parts)

    def token_count(self) -> int:
        """估算 Token 数(约 4 字符 = 1 Token)"""
        context = self.build_context()
        return len(context) // 4

    def reset(self):
        """重置上下文"""
        self.buffer = []
        self.summaries = []
        self.long_term = {}


# ---- 使用示例 ----
def demo():
    compressor = ContextCompressor(max_recent_rounds=5, compress_every=10)

    # 模拟长对话
    messages = [
        Message("user", "我的产品型号是 X-200,出现了开机蓝屏问题"),
        Message("assistant", "了解了,X-200 蓝屏通常与驱动有关。请问最近更新了什么?"),
        Message("user", "昨天装了新的显卡驱动"),
        Message("assistant", "建议回滚驱动。先进入安全模式..."),
    ] * 5  # 模拟 20 轮对话

    for msg in messages:
        compressor.add_message(msg)

    print("压缩后的上下文:")
    print(compressor.build_context())
    print(f"\n估算 Token 数: {compressor.token_count()}")

四、压缩策略的边界与局限

摘要质量高度依赖 LLM 能力。
弱模型生成的摘要可能遗漏关键信息。
建议在关键场景使用更强的模型做摘要。

压缩可能丢失上下文完整度。
"上次说的那个方案"这种指代会断裂。
需要在摘要中保留明确的实体引用。

不适合压缩的场景:
短对话(< 10 轮)没必要压缩;
法律合规场景需要完整审计记录;
实时流式输出的聊天场景。

五、总结

长对话下 Agent 需要上下文压缩保持质量。
三层记忆架构:工作记忆、摘要记忆、长期记忆。
滑动窗口保留最近对话,旧对话压缩为结构化摘要。
压缩时提取关键实体和决策,避免信息丢失。
Token 消耗从 O(n) 降为 O(k),k 为窗口大小。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐