RAG 进阶:Agentic RAG 与多智能体协作(系列二·第2篇):长上下文优化

3 阅读6分钟

Agent 跑 50 步之后,上下文爆了怎么办

系列一搭好了系统,系列二第一篇讲了高级工具。这篇解决一个实战里最头疼的问题:Agent 执行步数多了之后,上下文窗口被撑爆,Token 费用飙升,模型还开始"失忆" 。

核心思路不是"硬抗",而是分层管理——把不同重要程度的信息放在不同的"记忆层"里。


一、问题拆解:上下文里到底装了什么

一个典型的 Agent 执行轨迹里,信息密度差异很大:

信息类型占比重要程度是否需要保留
系统 Prompt5-10%高始终保留
用户原始问题1%最高始终保留
中间推理过程40-60%中低可压缩/丢弃
工具返回结果20-40%中可摘要/向量存储
关键结论5-10%高始终保留

问题根源:大部分 Token 花在了"中间过程"上,而这些过程往往只需要在当下有用,不需要全程携带。


二、策略一:滑动窗口 + 关键信息保留

最简单的方案:只保留最近 N 轮对话,但把关键结论抽出来放在前面。

from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

def compress_context(messages: list, window_size: int = 10) -> list:
    """
    滑动窗口压缩:保留最近 N 条消息 + 关键摘要
    """
    if len(messages) <= window_size:
        return messages
    
    # 1. 保留系统消息
    system_msgs = [m for m in messages if isinstance(m, SystemMessage)]
    
    # 2. 保留用户原始问题(第一条 HumanMessage)
    first_user_msg = next((m for m in messages if isinstance(m, HumanMessage)), None)
    
    # 3. 保留最近 N 条消息
    recent_msgs = messages[-window_size:]
    
    # 4. 拼接
    compressed = system_msgs
    if first_user_msg:
        compressed.append(first_user_msg)
    compressed.extend(recent_msgs)
    
    return compressed

缺点:丢失了中间的工具调用结果。如果 Agent 在第 5 步查到的数据在第 50 步还要用,就找不到了。


三、策略二:LLM 摘要压缩(推荐)

让 LLM 自己把"前面发生了什么"总结成一段话,替换掉原始对话。

from langchain_openai import ChatOpenAI

summarizer = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def summarize_context(messages: list, max_messages: int = 20) -> list:
    """
    当消息超过阈值时,把前面的对话压缩成摘要
    """
    if len(messages) <= max_messages:
        return messages
    
    # 1. 保留最后 N 条原始消息
    recent = messages[-max_messages:]
    
    # 2. 把前面的消息压缩成摘要
    old_messages = messages[:-max_messages]
    old_text = "\n".join([f"{type(m).__name__}: {m.content}" for m in old_messages])
    
    summary_prompt = f"""以下是 Agent 之前的执行记录。请压缩成一段简洁的摘要,
保留关键事实、已完成的步骤、重要数据。控制在 200 字以内。

执行记录:
{old_text}

摘要:"""
    
    summary = summarizer.invoke(summary_prompt).content
    
    # 3. 用摘要替换旧消息
    from langchain_core.messages import HumanMessage
    summary_msg = HumanMessage(content=f"[执行摘要] {summary}")
    
    return [summary_msg] + recent

效果:50 条消息压缩后可能只剩 15 条,Token 减少 60-70%。

进阶技巧:摘要可以增量更新,而不是每次都重新压缩全部历史。

def incremental_summarize(current_summary: str, new_messages: list) -> str:
    """基于现有摘要 + 新消息,生成更新后的摘要"""
    new_text = "\n".join([f"{type(m).__name__}: {m.content}" for m in new_messages])
    
    prompt = f"""当前执行摘要:{current_summary}

新发生的步骤:
{new_text}

请更新摘要,保持简洁(200字以内)。"""
    
    return summarizer.invoke(prompt).content

四、策略三:向量记忆(长期记忆)

有些信息不是"摘要"能解决的——比如 Agent 在第 3 步查到的某个具体数据,第 40 步需要精确引用。这时候需要把工具结果存进向量库,按需检索。

from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
memory_store = InMemoryVectorStore(embeddings)

def store_tool_result(tool_name: str, args: dict, result: str, step: int):
    """把工具调用结果存入向量记忆"""
    # 元数据方便后续过滤
    metadata = {
        "tool": tool_name,
        "step": step,
        "args": str(args)[:200],
    }
    memory_store.add_texts([result], metadatas=[metadata])

def retrieve_relevant_memory(query: str, top_k: int = 3) -> list[str]:
    """根据当前问题检索相关的历史工具结果"""
    docs = memory_store.similarity_search(query, k=top_k)
    return [doc.page_content for doc in docs]

# 在 Agent 节点中使用
def research_node_with_memory(state: AgentState):
    query = state["query"]
    
    # 1. 先查向量记忆,看之前有没有相关数据
    relevant = retrieve_relevant_memory(query)
    
    # 2. 执行新的检索
    result = search_knowledge_base(query)
    
    # 3. 存入记忆
    store_tool_result("search_knowledge_base", {"query": query}, result, state.get("step_count", 0))
    
    context = "\n".join(relevant + [result])
    return {"research_data": context}

适用场景:

  • 多轮对话中用户提到过的事实("我之前说过我在上海")
  • Agent 之前查过的数据("上周的销售额是多少")
  • 跨任务的共享知识

五、策略四:分层上下文架构(终极方案)

把上面三种策略组合起来,形成一个三层记忆架构:

┌─────────────────────────────────────────────┐
│  L0 · 工作记忆(始终在上下文里)               │
│  - 系统 Prompt                               │
│  - 用户原始问题                               │
│  - 当前步骤的推理                            │
│  - 最近 5 条消息                             │
├─────────────────────────────────────────────┤
│  L1 · 摘要记忆(压缩后放入上下文)              │
│  - 之前执行步骤的摘要(200字)                 │
│  - 关键结论列表                              │
├─────────────────────────────────────────────┤
│  L2 · 向量记忆(按需检索,不常驻上下文)        │
│  - 所有工具调用结果                           │
│  - 历史对话中的事实                           │
│  - 跨任务共享知识                             │
└─────────────────────────────────────────────┘
class LayeredMemory:
    """分层记忆管理器"""
    
    def __init__(self):
        self.summary = ""  # L1 摘要
        self.vector_store = InMemoryVectorStore(embeddings)  # L2 向量记忆
        self.key_facts: list[str] = []  # 关键事实清单
    
    def add_step(self, step_info: dict):
        """记录一个执行步骤"""
        # 更新 L1 摘要
        self.summary = incremental_summarize(
            self.summary, 
            [AIMessage(content=str(step_info))]
        )
        
        # 存入 L2 向量记忆
        self.vector_store.add_texts(
            [step_info.get("result", "")],
            metadatas=[{"step": step_info.get("step"), "tool": step_info.get("tool")}]
        )
        
        # 提取关键事实(用 LLM 抽取)
        facts = self._extract_facts(step_info)
        self.key_facts.extend(facts)
    
    def build_context(self, current_query: str, recent_messages: list) -> list:
        """构建当前上下文"""
        context = []
        
        # L0: 系统 Prompt + 用户问题 + 最近消息
        context.extend(recent_messages)
        
        # L1: 摘要 + 关键事实
        if self.summary:
            context.insert(0, HumanMessage(content=f"[执行摘要] {self.summary}"))
        if self.key_facts:
            facts_text = "\n".join(f"- {f}" for f in self.key_facts[-10:])
            context.insert(0, HumanMessage(content=f"[关键事实]\n{facts_text}"))
        
        # L2: 检索相关记忆
        relevant = self.vector_store.similarity_search(current_query, k=3)
        if relevant:
            mem_text = "\n".join([f"[记忆] {d.page_content}" for d in relevant])
            context.insert(0, HumanMessage(content=mem_text))
        
        return context
    
    def _extract_facts(self, step_info: dict) -> list[str]:
        """用 LLM 从步骤中提取关键事实"""
        prompt = f"""从以下执行步骤中提取关键事实(不超过3条),
如果没有重要事实则返回空列表。

步骤:{step_info}

关键事实(JSON数组):"""
        # 解析 LLM 返回的 JSON
        result = summarizer.invoke(prompt).content
        # ... 解析逻辑
        return []  # 简化示例

六、效果对比

方案Token 节省实现难度信息保留度适用场景
滑动窗口30-50%低低简单对话
LLM 摘要50-70%中中大多数 Agent
向量记忆按需节省中高多轮/跨任务
分层架构60-80%高最高复杂长流程

经验值:对于一个 50 步的 Agent 任务,原始上下文可能需要 80K Token,用分层架构后可以压到 15-20K Token,费用降低 75% 左右。


七、避坑指南

  1. 摘要不要过度压缩:摘要里必须保留"做了什么 + 得到了什么",否则 Agent 会重复执行。
  2. 向量记忆需要清理:长期运行后向量库会膨胀,定期清理过期/低价值的记忆。
  3. 关键事实单独维护:不要把所有东西都扔进向量库,重要的事实(如用户偏好、任务目标)单独列出来。
  4. 监控召回质量:定期抽样检查向量记忆检索出来的内容是否真的相关,不相关就调整 embedding 模型或检索策略。