在前两篇文章里,我们把 Agent 的核心结构拆清楚了:它需要目标、上下文、决策能力、执行能力和反馈闭环;而 Tool Use 是让它真正能行动的关键接口。
但有一个问题一直没有正面回答:当任务变长、工具调用变多、对话轮次增加之后,Agent 为什么开始犯低级错误、忘记之前的结论、甚至原地打转?
这篇文章就专门回答这个问题,以及如何设计状态管理来解决它。
先给结论
- Agent 的“失忆”不是模型变笨了,而是上下文窗口的物理限制和信息质量问题共同导致的。
- Context Window 是 Agent 的工作记忆,它有容量上限,而且越靠后的信息越重要。 如何填充这个窗口,比窗口多大更关键。
- Agent 需要三类记忆:工作记忆(当前上下文)、情节记忆(历史执行记录)、语义记忆(可检索的知识库)。 大多数失败的 Agent 系统只实现了第一类。
- 状态管理的本质是:在每一轮推理前,确保模型拿到的是“对当前决策最有价值的信息”,而不是“完整的历史记录”。
- 好的状态管理不是让 Agent 记住更多,而是让它在任何时刻都知道自己在哪、做了什么、下一步应该做什么。
失忆是怎么发生的
先把失忆的机制讲清楚,后面的设计决策才有依据。
Context Window 的本质
语言模型没有持久化的内部状态。每次调用模型,它看到的只有当前传入的 messages 列表。它没有“上一次调用时记得什么”,也没有“后台一直在运行的记忆”。
所谓的“对话历史”,本质上是每次调用时把之前的消息都重新传给模型。模型每次都是从头看完整个历史,然后生成下一条消息。
这意味着两件事:
第一,context window 是有上限的。 不管是 128k 还是 200k tokens,它是有限的。当历史消息 + 工具结果 + 当前输入超过这个上限,要么截断,要么报错。
第二,模型对上下文的注意力不是均匀分布的。 研究表明,模型对上下文开头和结尾的注意力更高,对中间部分的注意力较弱——这就是所谓的“Lost in the Middle”问题。当你的关键信息被淹没在大量工具调用结果的中间,模型很容易“看不见”它。
工具调用是上下文的主要消耗者
在一个典型的 Agent 执行流里,每次工具调用会往 messages 里追加至少两条消息:
[assistant] → tool_call: {name: "search_web", arguments: {...}}
[tool] → tool_result: {content: "...搜索结果,可能几百到几千 tokens..."}
如果一个任务需要 20 次工具调用,而每次工具结果平均 500 tokens,光工具结果就消耗了 10,000 tokens。再加上系统提示词、用户消息、模型的中间推理,context window 消耗得非常快。
更麻烦的是:这些工具结果大多数是“用完就没用了”的。第 3 次搜索的结果,在第 15 次推理时可能完全不相关,但它仍然占据着宝贵的 context window 空间。
失忆的三种典型表现
1. 重复工具调用
Agent 忘记了几轮前已经查过某个信息,再次调用相同的工具,拿到同样的结果,形成循环。
2. 目标漂移
任务执行到一半,Agent 开始偏离原始目标,被某个工具结果里的细节带跑,忘记最初要做什么。
3. 前后矛盾
Agent 在第 5 轮得出了“方案 A 不可行”的结论,但在第 12 轮上下文被稀释后,又重新考虑方案 A,甚至给出相反的建议。
这三种表现背后的根本原因是一样的:关键信息在上下文中的信噪比太低,或者直接被挤出了窗口。
记忆的三个层次
要解决失忆问题,先要搞清楚 Agent 需要什么类型的记忆。
借用认知科学里的分类,Agent 的记忆可以分三层:
工作记忆(Working Memory)
对应 context window。这是模型每次推理时能直接“看到”的信息,也是唯一能直接影响模型输出的记忆类型。
特点:容量有限、访问速度最快、每次推理都会重建。
工作记忆里应该放什么?只放“当前这一步决策需要的信息”,不是“完整的历史记录”。
情节记忆(Episodic Memory)
记录 Agent 的历史执行轨迹:做了什么、用了哪些工具、拿到了什么结果、做出了哪些决策。
特点:体量大、需要外部存储、按需检索。
情节记忆不住在 context window 里,而是存在外部(数据库、文件、向量存储)。当 Agent 需要回顾历史时,通过检索把相关片段取回来放进工作记忆。
语义记忆(Semantic Memory)
存储领域知识、用户偏好、任务相关的结构化信息。比如“这个用户偏好 Python”、“项目使用 PostgreSQL”、“API 的鉴权方式是 Bearer Token”。
特点:相对稳定、高密度、跨任务复用。
语义记忆通常以结构化格式存储,可以在任务开始时直接注入 context,也可以按需检索。
大多数 Agent 系统只实现了工作记忆
这是最常见的问题。开发者把完整对话历史塞进 messages,然后奇怪为什么 Agent 在长任务里表现越来越差。
真正健壮的 Agent 需要三层记忆协同工作:工作记忆处理当前推理,情节记忆支持历史回顾,语义记忆提供稳定的知识基础。
状态管理的核心设计
有了记忆分层的框架,现在来看具体的设计模式。
模式一:任务状态对象(Task State)
与其让 Agent 从对话历史里“回想”当前进展,不如显式维护一个结构化的任务状态对象,在每轮推理时注入 context。
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
class TaskStatus(Enum):
IN_PROGRESS = "in_progress"
BLOCKED = "blocked"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class SubTask:
id: str
description: str
status: TaskStatus
result: Optional[str] = None
tool_used: Optional[str] = None
@dataclass
class TaskState:
goal: str # 原始目标,永远不变
current_focus: str # 当前正在做什么
completed_subtasks: list[SubTask] = field(default_factory=list)
pending_subtasks: list[SubTask] = field(default_factory=list)
key_findings: list[str] = field(default_factory=list) # 关键发现,蒸馏后保留
blockers: list[str] = field(default_factory=list) # 当前阻碍
decisions_made: list[str] = field(default_factory=list) # 已经做出的决策
def to_context_string(self) -> str:
"""把任务状态转成注入 context 的字符串"""
lines = [
f"## 当前任务状态",
f"**目标**:{self.goal}",
f"**当前焦点**:{self.current_focus}",
"",
]
if self.completed_subtasks:
lines.append("**已完成步骤**:")
for st in self.completed_subtasks:
result_summary = f" → {st.result[:100]}..." if st.result else ""
lines.append(f" - ✅ {st.description}{result_summary}")
if self.pending_subtasks:
lines.append("**待执行步骤**:")
for st in self.pending_subtasks:
lines.append(f" - ⬜ {st.description}")
if self.key_findings:
lines.append("**关键发现**:")
for f in self.key_findings:
lines.append(f" - {f}")
if self.decisions_made:
lines.append("**已确定决策**:")
for d in self.decisions_made:
lines.append(f" - {d}")
if self.blockers:
lines.append("**当前阻碍**:")
for b in self.blockers:
lines.append(f" - ⚠️ {b}")
return "\n".join(lines)
每轮推理时,把 task_state.to_context_string() 注入 system prompt 或 user message 的开头。这样模型无论看不看历史消息,都能立刻知道任务的当前状态。
def build_messages(task_state: TaskState, user_input: str, history: list) -> list:
system = f"""你是一个任务执行 Agent。
{task_state.to_context_string()}
---
根据以上任务状态,继续执行任务。如果当前焦点已完成,更新任务状态并推进到下一步。
"""
messages = [{"role": "system", "content": system}]
# 只保留最近 N 轮历史,不是全部
recent_history = history[-10:] # 后面会详细说这个策略
messages.extend(recent_history)
messages.append({"role": "user", "content": user_input})
return messages
这个模式的核心思想是:把隐式的“对话历史”转化为显式的“结构化状态”。状态是稠密的、精确的,而历史是稀疏的、充满噪音的。
模式二:上下文压缩(Context Compression)
当工具结果很长时,不要把原始结果全部存入 messages,而是先压缩再存储。
async def compress_tool_result(
tool_name: str,
raw_result: str,
task_context: str,
max_tokens: int = 500
) -> str:
"""
在工具结果进入 context 之前,用一次轻量级 LLM 调用把它压缩成
与当前任务相关的关键信息摘要。
"""
if len(raw_result.split()) < 200:
# 短结果不需要压缩
return raw_result
response = await client.chat.completions.create(
model="gpt-4o-mini", # 压缩用便宜模型就够了
max_tokens=max_tokens,
messages=[
{
"role": "system",
"content": (
"你的任务是提取工具返回结果中与当前任务直接相关的关键信息,"
"去除无关内容,输出简洁的摘要。"
"保留具体的数据、结论、错误信息。不要添加分析,只做提取和压缩。"
)
},
{
"role": "user",
"content": (
f"当前任务上下文:{task_context}\n\n"
f"工具名称:{tool_name}\n"
f"工具返回结果:\n{raw_result}"
)
}
]
)
return response.choices[0].message.content
这个模式有几个注意点:
压缩不是摘要。 摘要会丢失细节,压缩是在保留关键信息的前提下减少体积。在 prompt 里要明确说“保留具体数据、结论、错误信息”。
只压缩长结果。 短结果压缩反而增加开销,设一个阈值,低于阈值直接使用原文。
压缩本身有成本。 额外的 LLM 调用意味着延迟和费用。用便宜的模型(gpt-4o-mini、haiku)做压缩,用强模型做推理,是合理的分工。
模式三:滑动窗口历史(Sliding Window History)
不要无限追加历史消息,维护一个固定长度的滑动窗口。
class ContextWindow:
def __init__(self, max_messages: int = 20, max_tokens: int = 8000):
self.messages: list[dict] = []
self.max_messages = max_messages
self.max_tokens = max_tokens
self.archived: list[dict] = [] # 滑出窗口的消息存档
def add(self, message: dict):
self.messages.append(message)
self._trim()
def _trim(self):
"""
裁剪策略:
1. 如果消息数量超过上限,把最老的消息移入 archived
2. 工具调用消息(assistant + tool)要成对保留或成对移除,
避免出现孤立的 tool_result 没有对应的 tool_call
"""
while len(self.messages) > self.max_messages:
# 找到第一对完整的 tool_call + tool_result 或普通消息
if self.messages[0]["role"] == "assistant" and \
hasattr(self.messages[0].get("content"), "__iter__") and \
self.messages[1]["role"] == "tool":
# 成对移除 tool_call + tool_result
self.archived.append(self.messages.pop(0))
self.archived.append(self.messages.pop(0))
else:
self.archived.append(self.messages.pop(0))
def get_messages(self) -> list[dict]:
return self.messages
def search_archived(self, keyword: str) -> list[dict]:
"""在归档历史中检索相关消息,用于情节记忆检索"""
return [m for m in self.archived
if keyword.lower() in str(m.get("content", "")).lower()]
滑动窗口有一个容易踩的坑:tool_call 和 tool_result 必须成对出现。如果把一个 assistant 消息(里面包含 tool_call)移走,但保留了它对应的 tool 消息,API 会报错。裁剪时要检查消息的配对关系。
模式四:显式规划 + 检查点(Planning + Checkpoints)
对于需要多步执行的复杂任务,在开始执行前先让 Agent 生成一个结构化计划,然后在每个检查点更新计划状态。
async def create_execution_plan(goal: str, context: str) -> dict:
"""任务开始前,先生成结构化执行计划"""
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "你是一个任务规划专家。根据给定目标和上下文,生成结构化执行计划。"
},
{
"role": "user",
"content": f"""
目标:{goal}
上下文:{context}
请生成执行计划,格式如下(JSON):
{{
"goal_summary": "一句话总结目标",
"steps": [
{{
"id": "step_1",
"description": "步骤描述",
"depends_on": [],
"success_criteria": "什么情况下这步算完成",
"tools_likely_needed": ["tool_name"]
}}
],
"key_constraints": ["约束条件"],
"definition_of_done": "整体任务完成的判断标准"
}}
"""
}
]
)
return json.loads(response.choices[0].message.content)
async def checkpoint(plan: dict, completed_step_id: str,
result_summary: str, agent_notes: str) -> dict:
"""每完成一步,调用检查点更新计划状态"""
# 更新步骤状态
for step in plan["steps"]:
if step["id"] == completed_step_id:
step["status"] = "completed"
step["result_summary"] = result_summary
break
# 让模型评估是否需要调整计划
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": f"""
当前执行计划:
{json.dumps(plan, ensure_ascii=False, indent=2)}
刚完成步骤:{completed_step_id}
执行结果摘要:{result_summary}
执行者备注:{agent_notes}
请判断:
1. 是否需要调整后续步骤?(比如某个结果改变了后续的方向)
2. 如果需要调整,给出调整后的步骤列表
3. 整体任务是否已经完成?
以 JSON 格式返回:
{{
"needs_replan": true/false,
"updated_steps": [...] or null,
"task_completed": true/false,
"next_step_id": "step_x" or null,
"notes": "调整原因或完成说明"
}}
"""
}
]
)
checkpoint_result = json.loads(response.choices[0].message.content)
if checkpoint_result["needs_replan"] and checkpoint_result["updated_steps"]:
plan["steps"] = checkpoint_result["updated_steps"]
return plan, checkpoint_result
显式规划的最大价值是:它把“任务目标”固化成了一个可以随时查阅的结构化文档,而不是埋在对话历史的某条消息里。哪怕 context window 只剩下最近 5 条消息,只要规划对象还在,Agent 就不会迷失方向。
一个完整的状态管理架构
把上面几个模式组合起来,一个完整的状态管理系统大致是这样的:
图 1:一个完整的 Agent 状态管理架构,把任务规划器、任务状态对象、上下文构建器、工作记忆、工具接口层,以及情节记忆与语义记忆连接成一个闭环。
各层职责:
任务规划器:任务开始时生成结构化计划,中途在检查点评估是否需要调整。
任务状态对象:实时记录目标、当前焦点、已完成步骤、关键发现、已确定决策。每轮推理前注入 context,是 Agent 方向感的核心锚点。
上下文构建器:负责在每轮推理前把“任务状态 + 近期历史 + 必要的语义记忆”组装成 messages 列表,控制总 token 量不超过预算。
工作记忆(Messages):滑动窗口,只保留最近 N 条,超出的归档到情节记忆。
情节记忆:存储完整的历史执行轨迹,支持关键词或向量检索,在需要回溯时按需取回相关片段。
语义记忆:稳定的结构化知识,任务开始时注入,或在特定触发条件下更新。
实际工程中的几个决策点
决策点 1:压缩 vs 截断
当 context 快满时,有两种选择:直接截断最老的消息,或者先压缩再保留。
一般建议:对工具结果做压缩,对推理过程做截断。工具结果里可能有不可再现的数据(比如搜索结果、API 响应),压缩后保留摘要比直接扔掉要好。推理过程(模型的 assistant 消息)通常可以从任务状态里重建,截断成本更低。
决策点 2:什么时候做检查点
不是每步都需要做检查点,过于频繁的规划重评估会增加延迟和成本。实践中通常在以下时机触发:
- 完成一个预定义的“阶段性目标”
- 工具调用失败或返回了意外结果
- 模型的输出里出现了不确定性信号(“我不确定是否应该……”)
- 已完成的步骤数达到计划步骤数的 50%
决策点 3:语义记忆放多少进 context
语义记忆是稳定的,但不是无限的。放太多会占用工作记忆空间,放太少又起不到作用。
一个可行策略:把语义记忆分成“核心知识”和“扩展知识”两部分。核心知识(用户偏好、项目基础配置)永远注入;扩展知识通过向量检索,只在与当前任务相关时才取入 context。
决策点 4:任务状态对象应该多详细
任务状态对象的粒度需要平衡:太粗糙提供不了足够信息,太细致又会膨胀占用 context 空间。
实践中的经验:key_findings 和 decisions_made 的每一条都应该是一句话的结论性陈述,不是原始数据。原始数据放情节记忆,结论放状态对象。
常见的状态管理失误
1. 用日志代替状态
把所有工具调用结果堆进 context,期望模型自己从中提炼状态。这本质上是把状态管理的工作外包给了模型,而模型并不擅长做这件事——它的注意力不是均匀分布的。
2. 状态对象和 messages 双轨但不同步
维护了任务状态对象,但忘记在工具调用完成后更新它,导致状态对象和实际执行历史出现偏差。解决方法是在工具执行的 postprocessing 钩子里自动更新状态。
3. 压缩时过度摘要
压缩工具结果时把具体数据也丢掉了,只保留了高层总结。结果模型后续需要具体数据时找不到,只好再次调用工具。保留具体数字、错误码、关键字段,丢弃的是格式化标记和冗余文字。
4. 没有“失败记忆”
任务状态里只记录成功的步骤和发现,不记录失败的尝试和原因。导致 Agent 后续会重复相同的失败路径。blockers 和失败记录和成功记录一样重要,甚至更重要。
总结
Agent 在长任务里失忆,根本原因是:它赖以推理的工作记忆(context window)是有限的,而执行过程产生的信息是无限增长的。不做主动管理,这个矛盾会随着任务复杂度线性恶化。
解决方案不是“让 context window 更大”,而是建立有结构的状态管理:
- 用任务状态对象固化目标和方向,让 Agent 在任何时刻都知道自己在哪
- 用上下文压缩提高工作记忆的信息密度
- 用滑动窗口历史控制 context 体积不失控
- 用显式规划和检查点在长任务中保持执行的连贯性
- 用情节记忆和语义记忆把工作记忆装不下的信息外化存储,按需取回
这些机制组合起来,不是让 Agent 记住更多,而是让它在每一轮推理时拿到的信息都是“当前决策最需要的那些”。
方向感不来自记忆的完整性,而来自状态的清晰度。
下一篇:多 Agent 协作——当一个 Agent 不够用时,如何设计任务分解和协调机制