site logo

Marico's space

为什么你的 AI Agent 问题不在推理,而在记忆:生产级 Agent 状态实用指南

AI技术与应用 2026-08-27 11:29:01 2

折腾 AI Agent 有一段时间了,调 system prompt、试 chain-of-thought、跑 ReAct、把 GPT-4o 和 Claude 3.5 Sonnet 比了个遍,结果呢?Agent 还是在第三轮对话就失忆、还是前后矛盾、用户第二天回来感觉像在跟一个新人聊天。

说句可能被喷的实话:你的 Agent 根本不是推理有问题,是记忆有问题。

现在的大模型在给定的上下文窗口内推理能力已经相当强了。一个"聪明的 Agent"和"聊三句就崩"的 Agent 之间的差距,几乎从来不是模型逻辑能力的问题,而是 Agent 记住了什么、以什么方式记忆、以及这些记忆能不能从 Demo 活到生产环境。

这篇文章深入讲讲生产级 Agent 状态的架构、模式和权衡,目标是:从原型到能接真实用户的系统。

1. 推理的神话

在解决记忆问题之前,先把这个推理叙事彻底破掉。

基准测试到底测的是什么

我们评估 LLM 推理能力时,测的东西其实很窄:给一个 prompt 和有限上下文,模型解决一个定义好的问题的能力。Chain-of-thought 论文、MATH 基准、GPQA、LiveCodeBench——这些都是无状态评估。模型看到问题、推理、给出答案。没有持续、没有积累。

真实的 Agent 工作完全不同。Agent 跨多轮对话多工具、以及增量到达的信息分布工作。推理的挑战不是"模型会不会思考"——而是"模型能不能在已经知道的东西上思考,同时还要决定下一步做什么"。

无状态模型,有状态的世界

看这个对话:

用户(第1轮): "我要订下周二从北京到上海的机票,预算400以内。"
Agent: 调用搜索工具 → 找到航班 → 返回结果
用户(第2轮): "哪个航班经停时间最短?"
Agent: 调用另一个工具或基于之前结果推理
用户(第3轮): "算了,改成去杭州吧。"
Agent: ...它还记得原始需求是什么吗?

在第3轮,Agent 需要协调一个修改后的目标之前的工具结果中间结论跨轮表达的用户偏好。这不是推理缺陷,是状态管理缺陷。再多 prompt 工程也修不了这个——信息压根就没在那里。

为什么"给更多上下文"不管用

你可以把每轮对话都塞进上下文窗口,期待模型自己记住。这在生产环境里会因三个原因挂掉:

  1. 上下文窗口很贵。 每个 token 都要花钱。一段10轮对话加上工具返回结果,每轮调用轻松吃掉 15,000–50,000 tokens。上了量这就是无底洞。

  2. 上下文窗口很吵。 取出 40K tokens 的对话历史不代表模型关注的是对的 40K tokens。长上下文中注意力会稀释。第1轮的关键细节早就被埋了。

  3. 上下文窗口跨会话不持久。 用户明天回来,上周对话已经没了,除非你主动把它存到某处再取出来。

解决方案不是更大的窗口,是更好的记忆架构。

2. Agent 状态到底是什么

Agent 状态不是单一东西。它是几个不同但相互作用层的复合体。混淆这些层是大多数生产事故的根源。

2.1 工作记忆(情景记忆)

驱动当前交互的短生命周期、会话绑定的状态。包括:

  • 当前目标和子目标
  • 工具调用历史(调了什么、返回了什么)
  • 中间结论和计划
  • 用户到目前为止表达的用户意图

这些通常驻留在上下文窗口中,每轮刷新。它快、灵活、但生命周期短暂。

2.2 语义记忆(陈述性记忆)

Agent 需要反复引用的世界知识:

  • 用户画像和偏好("张三喜欢靠窗座位、出差较多")
  • 领域事实(公司政策、产品目录、API 文档)
  • 已学到的流程("订机票时总要先查退改签政策")

这些存在于上下文窗口之外,通常在数据库或向量存储中,按需检索。

2.3 程序记忆(技能型记忆)

Agent 的动作及其结果清单:

  • 工具定义和 schema
  • 成功的动作序列("订机票流程:搜索 → 比价 → 选座 → 支付 → 确认")
  • 错误恢复模式("如果支付失败返回码 402,换个支付方式重试")

这些通常编码在函数/工具定义中,但也应包含随时间改进的学习启发式规则。

2.4 来源记忆(溯源)

每条信息从哪里来的——对信任和调试至关重要:

  • 哪个工具产生了哪个事实?
  • 这条信息上次更新是什么时候?
  • 置信度是多少?

没有来源记忆,Agent 会自信满满地复述那些它"记得"但无法验证的细节。区别在于 Agent 说"根据您的偏好..."和说"您3月3日告诉我您更喜欢..."。

3. 生产级记忆架构

生产级 Agent 记忆系统有四层,每层服务不同的延迟和持久化需求。

Layer 1: 上下文缓冲(毫秒级)

注入每个 LLM 调用的活跃工作记忆。这不是原始对话历史——是 Agent 维护和更新的精心的缓冲区

type ContextBuffer = { // 当前会话状态 sessionId: string; turnCount: number; // 活跃目标栈 currentGoal: GoalState; subgoals: Subgoal[]; completedSubgoals: Subgoal[]; // 工具调用账本(精简版,不是原始日志) toolLedger: ToolEvent[]; // 本轮提取的事实 extractedFacts: Fact[]; // 对话摘要(滚动,用于裁剪时) rollingSummary: string;
};

关键洞察:Agent 写入这个缓冲区来工作,而不只是接收。每次工具调用后,Agent 应该用结构化结果更新缓冲区,而不是把原始输出一股脑丢进上下文。

Layer 2: 检索索引(10–100毫秒)

语义记忆和程序记忆在这里。当 Agent 需要召回超出当前上下文的东西时,它查询这层。

interface MemoryRetriever { // 语义召回 — 找相关事实 recallSemantics(query: string, userId: string): Promise<Fact[]>; // 情景召回 — 找相似的历史交互 recallEpisodes(similarTo: string, limit: number): Promise<Episode[]>; // 程序召回 — 找相关工具/模式 recallProcedures(taskType: string): Promise<ToolDefinition[]>;
}

实现选项:

  • 向量嵌入用于语义召回(pgvector、阿里云 OpenSearch、腾讯云向量数据库)
  • 基于嵌入的情景检索用于情景召回(对话哈希存储,关键时刻嵌入)
  • 元数据过滤的工具注册表用于程序召回(按能力标记工具,而非仅按名称)

Layer 3: 持久化层(秒级)

带生命周期管理的长期存储。这是记忆生存、衰老、最终被修剪的地方。

interface MemoryStore { // 带 TTL 和优先级写入 write(entry: MemoryEntry): Promise<void>; // 压缩 — 合并冗余记忆 compact(userId: string): Promise<number>; // 过期 — 移除陈旧记忆 expire(maxAgeDays: number): Promise<number>; // 溯源审计 getProvenance(memoryId: string): Promise<ProvenanceRecord>;
} interface MemoryEntry { id: string; type: 'fact' | 'preference' | 'event' | 'procedure'; content: string; metadata: Record<string, unknown>; source: SourceReference; createdAt: Date; expiresAt?: Date; confidence: number; // 0–1,用于不确定的记忆 accessCount: number; // 用于基于访问频率的淘汰
}

Layer 4: 遗忘机制

这是所有人都跳过的层。没有遗忘的记忆是负担。 陈旧偏好、过时目标、冗余事实会把有效信息淹没。生产系统需要明确的遗忘策略:

  • 时间衰减:超过有用寿命的记忆被标记移除
  • 置信度衰减:置信度低且未被强化的记忆被淘汰
  • 竞争性驱逐:当存储达到阈值时,访问最少、置信度最低的记忆优先移除
  • 用户主动纠错:当用户说"我其实不再喜欢那个了"时,系统应该主动覆盖旧记忆,而不是简单累积新记忆

4. Agent 记忆循环

这一切在实践中怎么运作?以下是每轮 Agent 运行的生产级记忆循环:

┌─────────────────────────────────────────────────┐
│ TURN START │
│ │
│ 1. 用户输入到达 │
│ ↓ │
│ 2. 查询检索 │
│ ┌─→ 语义召回(事实、偏好) │
│ ├─→ 情景召回(相似的历史轮次) │
│ └─→ 程序召回(相关工具) │
│ ↓ │
│ 3. 上下文组合 │
│ ┌─→ 当前工作记忆(缓冲区) │
│ ├─→ 检索到的记忆 │
│ ├─→ 滚动摘要(压缩后的历史) │
│ └─→ 系统提示词 + 工具定义 │
│ ↓ │
│ 4. LLM 调用 │
│ (Agent 在组合后的上下文中推理) │
│ ↓ │
│ 5. 动作执行 │
│ 工具调用 → 结果 → 验证 │
│ ↓ │
│ 6. 记忆更新 │
│ ┌─→ 将新事实写入持久化层 │
│ ├─→ 更新工作缓冲区 │
│ ├─→ 压缩/过期陈旧记忆 │
│ └─→ 更新来源溯源 │
│ ↓ │
│ 7. 响应 │
│ 格式化并返回给用户 │
└─────────────────────────────────────────────────┘

逐步分解

第2步 — 查询检索: 在 LLM 看到任何内容之前,系统先获取相关记忆。检索查询不是用户的原始输入——是从用户输入加当前目标生成的元查询。这个两步流程(生成查询 → 检索记忆)防止无关记忆污染检索结果。

async function buildRetrievalQuery( userInput: string, workingMemory: ContextBuffer
): Promise<string> { // 使用轻量级模型生成聚焦的检索查询 const queryGen = await llm.call({ model: 'fast-model', // 更便宜、更快的模型用于查询生成 messages: [ { role: 'system', content: RETRIEVAL_QUERY_SYSTEM_PROMPT }, { role: 'user', content: `目标: ${workingMemory.currentGoal.description}\n输入: ${userInput}` } ] }); return queryGen.content;
}

第3步 — 上下文组合: 这是大多数 Agent 失败的地方。上下文不是"把所有东西拼接起来",而是带优先级的结构化组装

  1. 系统提示词(固定,高优先级)
  2. 当前目标和子目标(来自工作记忆)
  3. 检索到的事实(按相关度排序)
  4. 工具定义(仅与当前子目标相关的)
  5. 滚动摘要(压缩历史,最多 500 tokens)
  6. 最近的工具结果(最近 3–5 次调用,完整细节)

滚动摘要是关键。不是保留原始对话历史,而是在每 5–10 轮时用 LLM 本身将历史压缩成简洁摘要:

async function compressHistory( rawHistory: Message[], workingMemory: ContextBuffer
): Promise<string> { const summary = await llm.call({ model: 'summary-model', messages: [ { role: 'system', content: COMPRESS_SYSTEM_PROMPT }, { role: 'user', content: formatMessages(rawHistory) } ], maxTokens: 500 }); return summary.content;
}

第6步 — 记忆更新: Agent 动作之后,系统提取并持久化新知识:

async function updateMemories( turnResult: AgentTurnResult, workingMemory: ContextBuffer, memoryStore: MemoryStore
): Promise<void> { // 从本轮提取新事实 const newFacts = await extractFacts(turnResult); for (const fact of newFacts) { await memoryStore.write({ id: crypto.randomUUID(), type: classifyFact(fact), content: fact.text, metadata: fact.metadata, source: { type: 'extraction', confidence: fact.confidence }, createdAt: new Date(), confidence: fact.confidence }); } // 更新工作缓冲区 workingMemory.extractedFacts.push(...newFacts); workingMemory.turnCount++; // 定期压缩 if (workingMemory.turnCount % 10 === 0) { await memoryStore.compact(workingMemory.sessionId); }
}

5. 提取问题

Agent 记忆最难的部分不是存储——是知道存储什么。原始对话是噪音。你不能什么都持久化。你需要从噪音中提取信号。

提取什么

信号类型 示例 优先级
用户偏好 "我喜欢早班机"
明确事实 "我的订单号是 ORD-12345"
目标状态 "我想要退款,不要换货"
工具结果 "机票搜索返回了3个400以下的结果"
上下文线索 对话语气、紧急信号
闲聊 "谢谢!" "知道了" "等等"

提取管道

原始轮次输出 ↓
┌──────────────┐
│ 分类器 │ 这个值得记住吗?
└──────┬───────┘ ↓ 是
┌──────────────┐
│ 提取器 │ 提取结构化事实
└──────┬───────┘ ↓
┌──────────────┐
│ 验证器 │ 与现有记忆核对
│ (去重) │ 避免存储"北京到上海"当
└──────┬───────┘ "北京到杭州"已存在时 ↓
┌──────────────┐
│ 优先级器 │ 分配置信度、TTL、类型
└──────┬───────┘ ↓
┌──────────────┐
│ 持久化器 │ 写入记忆存储
└──────────────┘

提取器提示词

const EXTRACTION_PROMPT = `
你正在从用户和 AI Agent 之间的对话中提取事实。 只提取以下类型的信息:
1. 用户偏好(出行、饮食、日程等)
2. 明确陈述的事实(订单号、日期、姓名)
3. 活跃目标及其状态
4. 限制未来决策的工具结果 不要提取:
- 随意的对话填充词
- Agent 本轮已完成的请求
- 已存储在现有记忆中的信息(你会收到这些) 该用户的现有记忆:
{{existingMemories}} 对话:
{{conversation}} 返回一个 JSON 数组的事实。每条事实:{type, content, confidence (0-1), expiresAt (永久则为 null)}。
`;

6. 溯源与信任

一个无法引用来源的 Agent 就是一个会自信幻觉的 Agent。生产系统需要溯源追踪——每条记忆必须可追溯到其来源。

为什么溯源重要

  1. 自我修正:当 Agent 说"根据您之前的请求..."时,它应该能指出是哪条之前的请求。如果用户纠正了,系统知道该更新什么。

  2. 调试:当 Agent 做出错误决策时,你需要知道是推理出了问题还是记忆出了问题。这是不同类型的 bug,需要不同的修复方式。

  3. 用户信任:用户能察觉 Agent 什么时候在编造。说"您3月3日提到..."和说"我觉得您可能说过..."给用户的信任感完全不同。

溯源 schema

interface ProvenanceRecord { memoryId: string; sourceType: 'user-stated' | 'tool-result' | 'inferred' | 'system-injected'; sourceId: string; // 引用原始消息、工具调用或系统事件 timestamp: Date; confidence: number; overwrittenBy?: string; // 如果该记忆被取代了
}

当 Agent 检索记忆时,溯源记录应该是检索上下文的一部分,而非隐藏的元数据。Agent 需要知道它如何知道某事,以便表达适当的确定性。

7. 内存高效的上下文管理

即使有好的提取和检索,你也会碰到上下文限制。以下是生产系统如何处理。

带摘要锚点的滑动窗口

不是在开头截断对话历史(丢失最老的、可能是关键性的上下文),而是使用摘要锚点

[第1–12轮摘要:"用户订了北京→上海的机票,订单号 ORD-999。偏好靠窗座位。"]
[第13–24轮摘要:"用户要求改到杭州。已发起退款。"]
[第25轮:...]
[第26轮:...]
[第27轮:...]

摘要保留了早期轮次的意义,而没有token 成本。你可以按轮次计数或上下文预算触发定期摘要。

上下文预算

interface ContextBudget { totalTokens: number; // 例如 GPT-4o 的 128_000 reservedForSystem: number; // ~2,000 — 提示词、工具定义 reservedForTools: number; // ~8,000 — 相关工具结果 reservedForSummary: number; // ~1,000 — 滚动摘要 budgetForRetrieval: number; // 剩余 — 基于检索得分动态分配
}

每轮,上下文组合器检查预算并决定:能包含多少检索到的记忆?如果预算紧张,只包含相关度阈值以上的记忆。

8. 跨会话记忆

最难的情况:用户几天后回来。上次的工作记忆已经没了。Agent 如何重建联系?

会话链接

interface SessionLink { currentSessionId: string; previousSessionId: string; linkReason: 'same_user' | 'similar_goal' | 'referenced_context'; bridgeSummary: string; // "上次我们正在订去上海的机票..." linkedAt: Date;
}

新会话开始时,系统检查可能的链接:

  1. 身份链接:同一用户 ID → 加载其完整记忆画像
  2. 目标链接:当前目标与近期会话相似 → 加载该会话的桥接摘要
  3. 上下文链接:用户提到历史交互中的内容 → 检索相关记忆

桥接摘要

桥接摘要是跨会话连续性最重要的单一产物。它是上一会话发生的事情的 2–3 句话压缩,在会话关闭时生成:

"上一会话:您正在订下周二北京到上海的机票。我们找到了三个400以下的选项。您当时在美联航红眼航班和国航早班机之间犹豫。对话结束前未做出选择。"

这个桥接被注入下一会话的第一轮,让 Agent 立即获得连续性,而无需重建整个对话。

9. 常见失败模式

回音室

当 Agent 自己的输出成为其记忆的一部分时,会产生反馈循环。它"记住"的是自己生成的内容,而非用户陈述的内容。缓解:来源标记。每条记忆必须携带 sourceType——Agent 自己的推理输出未经用户确认不应写入长期记忆。

囤积问题

记住一切的 Agent 反而记不住有用的。没有压缩和遗忘,检索索引变得嘈杂,相关记忆被淹没。缓解:基于 TTL 的驱逐基于置信度的修剪

虚假连续性

当 Agent 假设存在连续性但实际没有——混淆对话、将会话归属错误。缓解:默认情况下话会话作用域记忆,只在验证后才进行明确的跨会话链接。

提取盲区

当提取器错过重要信息,因为它们嵌入在间接陈述中时。"我不太想坐东航"是偏好,但简单的提取器可能会在对话噪音中遗漏。缓解:多轮提取——第一轮提取明确事实,第二轮提取隐含偏好,每轮用不同的提示词。

10. 工具和实现模式

记忆中间件模式

用记忆中间件层包装你的 Agent 框架,拦截每轮:

class AgentWithMemory { constructor( private agent: BaseAgent, private memoryStore: MemoryStore, private retriever: MemoryRetriever, private contextBuffer: ContextBuffer ) {} async execute(userInput: string, sessionId: string): Promise<AgentResponse> { // 1. 检索相关记忆 const retrievalQuery = await this.buildRetrievalQuery(userInput); const memories = await this.retriever.recall(retrievalQuery, sessionId); // 2. 组合丰富上下文 const enrichedContext = this.composeContext( this.contextBuffer, memories, userInput ); // 3. 执行 Agent const response = await this.agent.execute(enrichedContext); // 4. 更新记忆 await this.updateMemories(response, sessionId); return response; }
}

与流行框架的集成

框架 记忆方案 说明
LangGraph 带持久化的 StateGraph 最适合复杂多步 Agent;用 Checkpointers 做持久化
LangChain ConversationBufferMemory、VectorStoreRetrieverMemory 好用的原语,但需要手动组合
LlamaIndex QueryEngine + 带记忆的 ChatEngine 检索集成强,状态管理弱
CrewAI 通过共享上下文的 Agent 记忆 更简单,但对记忆生命周期的控制更少
自研(生产推荐) 手撸中间件 对上述每一层都有完全控制

对于生产系统,趋势是走向自研中间件而非开箱即用的记忆方案。框架提供构建块,但本文描述的架构——明确的分层、溯源、压缩、跨会话链接——需要目前没有框架能提供的编排能力。

11. 衡量记忆健康度

无法衡量就无法改进。追踪这些指标:

  • 召回精度:检索到的记忆中,有多少实际与 Agent 的决策相关?(通过用户反馈或自动化评估测量)
  • 记忆留存率:提取的记忆中有多少能存活超过预期 TTL?
  • 上下文利用率:上下文窗口实际用于有用信息的比例 vs 噪音?
  • 跨会话连续性得分:用户回来时,Agent 是否表现出对之前交互的认知?(自动化或抽样人工评估)
  • 记忆压缩比:压缩期间有多少原始事实被合并或修剪?高比值意味着初始提取质量差。

常见问题

Q: 我直接用向量数据库当记忆行不行?

不行。向量数据库给你的是检索,不是记忆。记忆需要提取、溯源追踪、压缩、TTL 管理、跨会话链接——向量存储默认都不提供。向量数据库是记忆系统的一个组件,不是记忆系统本身。

Q: 记忆应该预取还是按需取?

语义记忆(用户偏好、领域事实)预取——延迟可接受且相关度高,每轮都取。情景记忆(历史对话)按检索查询按需获取。程序记忆(工具定义)不要预取——只加载与当前目标相关的工具以节省上下文 tokens。

Q: 生产 Agent 的最小可行记忆系统是什么?

三件事:(1) 结构化上下文缓冲区,Agent 每轮写入,(2) 检索层用于语义记忆(用户偏好 + 领域事实),(3) 滚动摘要机制处理上下文窗口限制。先把这三件事做对,再考虑跨会话链接、溯源追踪或压缩。那些是优化,不是基础。

模型的推理能力已经在线。缺的永远是好用的记忆系统。把记忆系统搭好,推理问题大部分会自己消失。