Agent 跑 50 步之后,上下文爆了怎么办
系列一搭好了系统,系列二第一篇讲了高级工具。这篇解决一个实战里最头疼的问题:Agent 执行步数多了之后,上下文窗口被撑爆,Token 费用飙升,模型还开始"失忆" 。
核心思路不是"硬抗",而是分层管理——把不同重要程度的信息放在不同的"记忆层"里。
一、问题拆解:上下文里到底装了什么
一个典型的 Agent 执行轨迹里,信息密度差异很大:
| 信息类型 | 占比 | 重要程度 | 是否需要保留 |
|---|---|---|---|
| 系统 Prompt | 5-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% 左右。
七、避坑指南
- 摘要不要过度压缩:摘要里必须保留"做了什么 + 得到了什么",否则 Agent 会重复执行。
- 向量记忆需要清理:长期运行后向量库会膨胀,定期清理过期/低价值的记忆。
- 关键事实单独维护:不要把所有东西都扔进向量库,重要的事实(如用户偏好、任务目标)单独列出来。
- 监控召回质量:定期抽样检查向量记忆检索出来的内容是否真的相关,不相关就调整 embedding 模型或检索策略。