本文承接任务规划与多工具编排,系统讲解 AI Agent 的长期记忆与检索增强生成(RAG)实践:哪些信息应该留在对话上下文里,哪些应该持久化到外部存储,以及如何通过检索让 Agent 在长期任务中保持准确、一致和低成本。
目录
-
一、上下文窗口是 Agent 的第一道墙
-
二、记忆分层:短期、工作与长期
-
三、长期记忆的写入:记住什么、怎么更新
-
四、RAG:让 Agent 按需召回知识
-
五、上下文组装:把记忆与检索织进提示词
-
六、时效、一致性与成本控制
-
七、隐私与安全边界
-
八、上线前检查清单
-
结语
引言
在前三篇文章中,我们分别讨论了 AI Agent 的提示词传输、流式输出与异常处理,以及如何通过 Function Calling 让大模型安全调用业务系统,再把复杂目标拆解为可执行、可验证的多步工作流。完成这些之后,一个更隐蔽的问题浮出水面:
用户上周说过 “我更喜欢简洁的日报”,昨天让 Agent 生成过一份销售分析报告,今天又发起新的对话 ——Agent 还记得吗?
如果每次对话都从零开始,Agent 会反复询问已经告诉过它的信息;如果把所有历史都塞进上下文,模型很快会触达上下文窗口上限,成本也随之失控。长期记忆与检索增强生成(RAG)解决的,正是 “记住什么、忘掉什么、按需想起什么” 这三个问题。
本文将从工程实践出发,梳理记忆的分层设计、持久化方案与检索召回策略,并结合代码说明如何让 Agent 在长期任务中保持准确、一致和低成本。
一、上下文窗口是 Agent 的第一道墙
1.1 为什么不能把一切都塞进上下文
大模型拥有上下文窗口,但窗口不是无限大的;即使窗口足够大,也不意味着应该全部用满。原因有三:
-
成本:输入 Token 费用与上下文长度成正比,长上下文直接推高每次调用的成本;
-
干扰:大量无关信息会稀释模型对关键信息的注意力。研究观察到 “lost in the middle” 现象:模型对长文本中部内容的利用明显弱于开头和结尾;
-
时效:历史记录中的旧结论可能已被推翻,保留过期信息反而会导致错误。
因此,Agent 必须把 “对话上下文” 和 “长期记忆” 分开对待。
1.2 什么该进上下文,什么该持久化
| 维度 | 对话上下文 | 长期记忆 |
|---|---|---|
| 容量 | 受上下文窗口限制 | 几乎无限(外部存储) |
| 生命周期 | 当前会话 | 跨会话、跨任务 |
| 访问方式 | 全量进入模型 | 按需检索召回 |
| 成本 | 随 Token 线性增长 | 检索时按需付费 |
| 一致性 | 天然一致 | 需要处理过期与冲突 |
一个实用的分配原则是:
-
留在上下文中:系统提示词、当前任务目标、最近几轮对话、本轮用户输入、工具执行结果;
-
持久化到长期记忆:用户偏好、长期事实、历史任务的结论、跨会话反复需要的信息;
-
交给 RAG 检索:领域知识库、产品手册、历史文档、FAQ 等静态或半静态资料。
一句话总结:上下文放 “正在进行的事”,长期记忆放 “反复需要的事”,RAG 检索放 “需要时才知道的事”。
二、记忆分层:短期、工作与长期
工程上可以把 Agent 的记忆分为三个层级:
| 记忆层级 | 生命周期 | 存储位置 | 更新方式 | 典型用途 |
|---|---|---|---|---|
| 短期记忆 | 单次会话 | 对话上下文 | 每次交互追加、截断或压缩 | 理解当前对话脉络 |
| 工作记忆 | 单次任务 | 任务表、步骤表 | 步骤执行时更新 | 记录任务进度与中间结果 |
| 长期记忆 | 跨会话、跨任务 | 向量库、KV、关系库 | 事后总结、显式写入 | 用户偏好、领域知识、历史结论 |
2.1 短期记忆:会话内的滚动窗口
短期记忆就是第一篇文章讨论过的对话上下文。会话变长后,可以采用截断、摘要或两者的组合:
-
截断:只保留最近 N 轮消息;
-
摘要:把较早的对话压缩为一段要点,替换掉原文;
-
滚动窗口 + 摘要:最近几轮保留原文,更早的合并成摘要 —— 这是当前最主流的方案。
2.2 工作记忆:任务执行中的状态
工作记忆在上一篇 “任务规划与多工具编排” 中已经出现:任务表、步骤表、确认记录、调用日志。它不依赖模型上下文,而是持久化在数据库里,让长任务可以在服务重启后恢复。
2.3 长期记忆:跨会话的知识沉淀
长期记忆是本文的重点。它的常见形态包括:
-
键值记忆:适合用户偏好这类 “一个键对应一个值” 的信息,例如 “报告语言 = 中文”;
-
结构化记忆:适合属性明确的实体,例如客户、项目的字段化信息;
-
语义记忆(向量):适合无法用键精确描述的开放信息,例如 “用户更关注华东区业务” 这类需要语义匹配的内容。
三类存储并非互斥。实践中常以 KV 或关系库保存确定性信息,以向量库保存语义化信息,再统一通过一个 “记忆服务” 对外暴露读写接口。
三、长期记忆的写入:记住什么、怎么更新
3.1 记忆从哪里来
长期记忆不是把每句对话都存下来,而是从对话中提取可复用的信息。常见来源有三类:
-
用户显式声明:”以后都用中文写报告”—— 直接、可信,优先级最高;
-
模型隐式推断:从用户行为中推断偏好,例如用户连续三次要求压缩篇幅;
-
任务事后总结:任务结束时把结论、产出和关键参数写入记忆,供后续任务复用。
提取动作可以由单独的模型调用完成,也可以在任务结束时统一执行。下面是一次提取结果的示例:
{
"extracted": [
{
"type": "preference",
"key": "report_language",
"content": "用户偏好使用中文撰写报告",
"importance": 0.8
},
{
"type": "fact",
"key": "customer_a_domain",
"content": "客户 A 为制造业客户,主要使用 ERP 系统",
"importance": 0.6
},
{
"type": "task_result",
"key": "sales_analysis_2026_08",
"content": "8 月销售下降主要因华东区大客户流失",
"importance": 0.7
}
]
}
3.2 写入:新增、覆盖与失效
记忆更新只有三种操作,关键是选对操作:
-
新增:记忆库中不存在该信息时写入;
-
覆盖:同一键出现新值,且新值来自更高优先级来源(用户显式纠正 > 模型推断 > 旧记录)时更新;
-
失效:信息确认过期(如订单已关闭、客户已流失)时标记删除,而不是让旧值和新值同时存在。
3.3 重要性评分与淘汰策略
存储空间和检索质量都要求记忆库 “精而不滥”。一个简单的记忆服务可以维护重要性、访问时间和有效期:
import time
from dataclasses import dataclass
@dataclass
class Memory:
key: str
content: str
importance: float # 0-1,越高越不容易被淘汰
source: str # user / assistant / tool / summary
created_at: float
updated_at: float
last_access_at: float
ttl: int | None = None # 秒;None 表示长期有效
def expired(self, now: float) -> bool:
return self.ttl is not None and now - self.updated_at > self.ttl
class MemoryStore:
def __init__(self, max_items: int = 200):
self._items: dict[str, Memory] = {}
self._max_items = max_items
def remember(self, key, content, importance,
source="user", ttl=None):
now = time.time()
existing = self._items.get(key)
if existing:
existing.content = content
existing.importance = max(existing.importance, importance)
existing.updated_at = now
existing.ttl = ttl
else:
self._items[key] = Memory(
key=key, content=content, importance=importance,
source=source, created_at=now, updated_at=now,
last_access_at=now, ttl=ttl,
)
self._evict_if_needed()
def recall(self, key):
item = self._items.get(key)
if item is None or item.expired(time.time()):
return None
item.last_access_at = time.time()
return item.content
def _evict_if_needed(self):
if len(self._items) <= self._max_items:
return
now = time.time()
self._items = {k: v for k, v in self._items.items()
if not v.expired(now)}
while len(self._items) > self._max_items:
key = min(self._items,
key=lambda k: (self._items[k].importance,
self._items[k].last_access_at))
del self._items[key]
上面代码的核心思想只有两点:新信息覆盖旧信息,来源可信度更高时可以提升重要性;容量不足时,优先淘汰低重要性、久未访问的条目。 生产环境建议把这段逻辑替换为 Redis + 向量库,并加上多租户隔离与审计。
四、RAG:让 Agent 按需召回知识
RAG(Retrieval-Augmented Generation,检索增强生成)解决的是 “知识不在对话里、也不在记忆里,但在某个资料库里” 的问题。它让模型在生成前先从外部知识源检索相关片段,再基于这些片段作答。
4.1 RAG 的基本流程
-
切分(Chunking):把长文档切成适合检索的片段;
-
向量化(Embedding):把每个片段编码成向量;
-
索引(Indexing):建立向量索引,支持相似度检索;
-
召回(Retrieval):把用户问题编码成向量,检索最相似的片段;
-
重排(Rerank):对召回结果做精细排序,剔除无关内容;
-
组装(Assembly):把精选片段与对话上下文一起交给模型。
4.2 切分不是简单按字数切
切分质量直接影响检索效果。常见做法包括:
-
固定长度 + 重叠:按固定 Token 数切分,相邻片段保留一定重叠,避免语义在切点被截断;
-
语义切分:按段落、标题或语义完整性切分,适合结构化的手册与文档;
-
父子片段:检索小片段定位位置,生成时把包含它的更大段落一并给出,兼顾精度与上下文。
4.3 向量不是万能的:混合检索
纯向量检索对语义相近的表述很有效,但对精确匹配(订单号、型号、法规条文)反而不如关键词检索。生产系统通常采用混合检索:
-
向量召回负责 “语义相似”;
-
关键词检索(如 BM25)负责 “精确命中”;
-
两者结果合并去重后,再交给重排模型排序。
def build_context(question, user_id, recent_messages, top_k=5):
# 1. 长期记忆:用户画像等确定性信息
profile = memory_service.get_profile(user_id)
# 2. 混合检索:向量召回 + 关键词召回 + 重排
vector_hits = vector_store.search(question, top_k=top_k \* 2)
keyword_hits = bm25_index.search(question, top_k=top_k \* 2)
merged = deduplicate(vector_hits + keyword_hits)
reranked = reranker.rerank(question, merged)[:top_k]
# 3. 按 Token 预算组装上下文
return {
"system": SYSTEM_PROMPT,
"profile": profile,
"knowledge": format_snippets(reranked),
"history": recent_messages[-6:],
"question": question,
}
4.4 什么时候该检索
检索不是每轮都必须发生。可以按规则触发(问题包含 “是什么、怎么用、最新、对比” 等信号),也可以像工具调用一样,把 “是否检索、检索什么” 交给模型决策。后者更灵活,但要在检索工具中绑定权限与数据范围,避免模型检索到无权访问的内容。
五、上下文组装:把记忆与检索织进提示词
检索到内容之后,如何摆放也影响效果。建议按照以下顺序与预算组装上下文:
| 区块 | 内容 | 建议预算 |
|---|---|---|
| 系统提示词 | 人设、规则、能力边界 | 固定 |
| 长期记忆 | 用户画像、关键偏好 | 5% – 10% |
| 检索片段 | RAG 召回的知识 | 30% – 40% |
| 最近对话 | 近几轮历史 | 20% – 30% |
| 当前输入 | 本轮用户意图 | 必须保留 |
5.1 检索、记忆与工具调用如何分工
当 Agent 同时具备记忆、RAG 和 Function Calling 时,需要明确三者各管一段:
| 能力 | 解决什么问题 | 典型数据 | 时效 |
|---|---|---|---|
| 长期记忆 | 个性化的、反复需要的信息 | 用户偏好、历史结论 | 跨会话 |
| RAG | 静态、海量的知识资料 | 手册、FAQ、历史文档 | 资料更新时刷新 |
| 工具调用 | 实时、确定性的业务数据 | 天气、订单、库存 | 实时 |
判断顺序可以是:先查记忆(用户相关)→ 再查知识(RAG)→ 必要时调用工具(实时数据)。例如用户问 “我上个月的销售报告结论是什么”,答案在记忆中;问 “公司退货政策是什么”,答案在 RAG 中;问 “现在订单 10086 到哪了”,答案必须通过工具实时获取。
5.2 检索结果也要做 “质量门”
召回片段不应原样塞进上下文。建议:
-
过滤与问题相似度过低的片段;
-
对片段做去重与去噪音(广告、导航、页脚);
-
标注来源,让模型在引用时给出出处;
-
若召回内容自相矛盾,优先采纳更新、更权威的来源,并如实说明。
六、时效、一致性与成本控制
6.1 记忆会过期
偏好可能改变,事实可能过时。长期记忆需要三类失效机制:
-
TTL(生存时间):为临时性信息设置有效期,到期自动失效;
-
事件驱动失效:业务状态变更时主动删除或覆写相关记忆,例如订单关闭后清除订单相关缓存;
-
用户显式修正:用户纠正 Agent 时,立即覆盖旧记忆并提高该条记忆的优先级。
6.2 一致性与溯源
记忆与资料可能互相冲突。缓解手段包括:
-
每条记忆与检索片段都记录来源与更新时间;
-
生成时要求模型在依据不足时明确说 “我不确定”,而不是编造;
-
对 “事实类” 回答开启引用溯源,用户可点击查看依据。
6.3 让成本可控
长期记忆与 RAG 的目标之一就是降本:
-
缓存:相同问题的检索结果与模型回答按用户维度缓存;
-
分层召回:先在记忆与高频知识里查,查不到再检索全量知识库;
-
控制检索量:只召回必要数量的片段,而不是越多越好 —— 片段太多反而稀释注意力;
-
摘要压缩:长期任务中定期把已完成部分压缩为摘要,释放上下文预算。
6.4 可观测性
和任务编排一样,记忆与检索也需要留痕:检索了什么、召回了哪些片段、使用了哪些记忆、各自耗时多少。统一的 memory_id、chunk_id 与 call_id 贯穿全链路,才能定位 “为什么答错”。
七、隐私与安全边界
长期记忆跨会话保留用户信息,安全边界比单次对话更严格:
-
最小化存储:不存储明文密码、密钥、身份证号、完整支付信息;确需使用时临时获取;
-
多租户隔离:记忆与知识库按用户 / 租户分区,检索时强制注入数据范围条件,不能跨租户召回;
-
权限绑定:检索和写入都走服务端鉴权,模型生成的 “我要看某人的记忆” 不是授权;
-
用户权利:向用户提供查看、导出、删除个人记忆的入口,删除操作同步清理向量库副本;
-
脱敏与审计:记录记忆的写入来源、操作者与时间,敏感内容入库前脱敏。
八、上线前检查清单
-
是否明确了 “进上下文 / 进长期记忆 / 走 RAG 检索” 的边界?
-
短期记忆是否具备滚动窗口与摘要压缩能力?
-
长期记忆是否有提取、新增、覆盖、失效的完整生命周期?
-
记忆更新是否遵循 “用户显式纠正优先” 的冲突消解规则?
-
是否设置了重要性评分与容量上限,避免记忆库无限膨胀?
-
RAG 是否使用混合检索(向量 + 关键词)与重排,而非仅向量召回?
-
检索是否触发于明确信号,且绑定权限与数据范围?
-
上下文组装是否有固定的顺序与 Token 预算?
-
记忆与资料是否记录来源与时间,支持过期、失效与溯源?
-
是否实现多租户隔离、敏感信息脱敏与用户删除入口?
-
是否用真实长对话与知识变更场景评估准确率、一致性与成本?
结语
上下文窗口是 Agent 的第一道墙,但墙外不是一片空白 —— 长期记忆负责 “记住反复需要的信息”,RAG 负责 “按需想起知识库里的信息”,工具调用负责 “获取实时确定的信息”。三者协同,才能让 Agent 在长期任务中既准确、又一致、还便宜。
与任务编排同理,模型负责判断 “该想起什么”,确定性代码负责 “记住什么、检索什么、谁能看”。把记忆写入、失效、权限与审计固化在应用层,Agent 才能安全地越用越懂用户。
下一篇可以继续讨论多 Agent 协作:当一个任务大到单智能体难以胜任时,如何把目标分发给多个各司其职的智能体,以及它们之间如何通信、避免冲突并汇总出统一结果。
本文来自投稿,不代表知派立场,如若转载,请注明出处:https://www.zinpai.com/news/4625.html