让 Agent 记住用户:Memory、RAG 与上下文工程
前几篇文章,我们已经逐步搭建了 Agent 的基本认知:
- 第一篇:理解什么是 Agent,以及它与普通聊天机器人的区别。
- 第二篇:理解工具调用(Tool Calling)和执行循环(Agent Loop)。
- 第三篇:理解任务规划(Planner)、任务执行(Executor)与任务调度(Scheduler)。
- 第四篇:通过 Python 编写一个最小的 Agent Runtime,把规划、执行、调度和状态管理连接起来。
到这里,我们已经能够理解一个 Agent 如何接收目标、生成任务、调用工具,并根据执行结果推进任务。
但一个新的问题出现了。
假设用户昨天告诉 Agent:“我平时不喜欢太早出门,安排旅行时尽量不要把活动放在早上七点以前。”
今天,用户又说:“帮我规划一次东京三日游。”
如果 Agent 每次都把今天的请求当成全新的任务,它可能完全忘记昨天的偏好。
再假设,用户上传了一份体检报告。几天后,用户问:“我上次报告里的这个指标有没有变化?”
如果 Agent 无法找到之前的报告,即使它很擅长理解语言,也无法准确比较两次报告。这说明,Agent 除了执行任务,还需要管理历史信息、检索相关知识,并决定当前应该向大模型提供哪些内容。
这就涉及三个重要概念:
- 记忆(Memory):保存和使用过去对话、用户偏好或任务经历等信息。
- 检索增强生成(Retrieval-Augmented Generation,简称 RAG):先从外部知识中检索相关内容,再让大模型基于这些内容生成答案。
- 上下文工程(Context Engineering):有目的地组织、筛选和安排提供给大模型的信息。
这三个概念经常一起出现,但它们解决的问题并不相同。
一、为什么 Agent 需要记忆?
1. 大模型为什么会“忘记”?
大模型(Large Language Model,简称 LLM)在生成回答时,会根据当前收到的输入内容进行计算。
这些输入通常包括:
- 当前用户的问题。
- 本轮对话中提供的历史消息。
- 系统指令和开发者指令。
- 检索得到的知识。
- 工具调用结果。
- 其他与当前任务有关的数据。
这些内容共同构成了模型当前能够使用的上下文(Context)。
上下文窗口(Context Window)是模型单次处理输入和生成输出时能够容纳的内容范围。不同模型支持的范围可能不同。
即使某个模型的上下文窗口很大,也不意味着它会自动记住所有历史对话,更不意味着它能够在未来的请求中可靠地找回某条信息。
如果应用程序没有把过去的信息再次提供给模型,模型就可能无法利用这些信息。
因此,Agent 不能仅仅依赖大模型自身的上下文能力来完成长期记忆管理。
2. 记忆究竟解决什么问题?
我们用一个简单的旅行助手举例。
用户第一次说:“我喜欢樱花,不喜欢早起,旅行预算大约一千元。”
Agent 可以提取出以下信息:
user_memory = { "preferences": { "favorite_scenery": ["樱花"], "avoid": ["太早出门"] }, "constraints": { "travel_budget": 1000 }}
几天后,用户提出新的旅行规划请求。应用程序可以先读取与当前任务相关的记忆,再把这些信息提供给大模型,这样,Agent 就不必要求用户每次重新输入相同的偏好。不过,并不是所有信息都应该永久保存。
例如:
- “我喜欢樱花”可能是相对长期的偏好。
- “这次旅行预算是一千元”可能只适用于当前旅行。
- “帮我查询一下今天的天气”通常不需要保存为长期记忆。
因此,记忆系统不仅需要保存信息,还需要考虑信息的用途、有效期、更新方式和适用范围。
二、Agent 的记忆可以怎样分类?
工程中并不存在唯一适用于所有 Agent 的记忆分类。为了便于入门,我们可以先按用途理解以下几类。
1. 对话记忆(Conversation Memory)
对话记忆用于保存和利用对话历史。
例如:
用户:帮我查北京明天的天气。Agent:明天北京可能下雨。用户:那上海呢?
如果 Agent 只收到最后一句“那上海呢?”,就可能不知道用户问的是上海明天的天气。
这时,系统需要提供必要的历史对话,或者把历史对话整理成明确的当前任务。
对话记忆的核心价值是:让 Agent 能够理解当前问题与此前对话的关系。
但不一定要把所有历史消息原封不动地发送给模型。对话很长时,可以保留最近的消息,并将较早的内容压缩成摘要。
2. 用户记忆(User Memory)
用户记忆用于保存相对稳定、未来可能重复使用的用户信息。
例如:
user_memory = { "user_id": "user_001", "preferences": { "language": "中文", "travel_style": "慢节奏", "avoid_early_morning": True }}
用户记忆通常适合保存:
- 用户明确表达且可能长期有效的偏好。
- 用户允许应用程序保存的个性化设置。
- 未来任务可能需要的非敏感信息。
3. 任务记忆(Task Memory)
任务记忆用于保存某个任务的执行过程和结果。
例如,前一篇文章中的 Agent Runtime 可能记录:
task_memory = { "goal": "比较北京和上海的天气", "completed_tasks": [ { "task_id": "T1", "result": { "city": "北京", "weather": "晴" } }, { "task_id": "T2", "result": { "city": "上海", "weather": "下雨" } } ]}
这些信息可以帮助系统继续尚未完成的任务,也可以为后续的执行记录和问题排查提供依据。
不过,任务记忆与运行时状态并不完全相同。
运行时状态(Runtime State)通常描述当前一次执行正在进行到哪里;任务记忆则可以用于保存跨越当前执行过程、未来可能需要再次使用的信息。
例如:
- 当前任务是否正在执行,属于运行时状态。
- 上次任务最终得出了什么结论,可能属于任务记忆。
- 为了故障恢复而保存的完整执行快照,则属于持久化状态管理的一部分。
它们可以使用同一套存储系统,但在设计上最好明确各自的用途。
4. 长期记忆(Long-term Memory)
长期记忆强调信息可以跨越多次请求或多个会话继续使用。
例如,用户连续多次告诉 Agent 自己喜欢慢节奏旅行,系统可以将这一偏好保存在数据库中,并在未来规划旅行时读取。
需要注意,长期记忆不是把所有历史消息永久堆积起来。
更实用的方式是:
- 从历史交互中识别值得保留的信息。
- 检查信息是否有明确来源。
- 判断信息是否已经过期或被更新。
- 按用户或业务实体保存。
- 在相关任务发生时检索。
- 在信息不再需要时允许更新或删除。
长期记忆的价值不在于保存得多,而在于能够在合适的时间,可靠地取出合适的信息。
三、记忆应该存在哪里?
记忆不仅是一种模型能力,更是应用程序中的数据管理问题。
1. 先从 Python 字典开始
最简单的实现方式,是将记忆保存在内存中的字典里。
memory_store = {}def save_memory(user_id: str, memory: dict) -> None: memory_store[user_id] = memorydef get_memory(user_id: str) -> dict | None: return memory_store.get(user_id)
这里的 MemoryStore 可以理解为记忆存储(Memory Store),也就是负责保存和读取记忆的模块。
这段代码可以帮助我们理解接口设计,但它有明显限制:
- 程序重启后,内存中的数据通常会丢失。
- 多个应用进程之间不一定共享同一份字典。
- 缺少持久化、权限管理和数据更新策略。
- 不能直接作为可靠的生产级记忆系统。
因此,内存字典适合教学和原型验证,而不是长期保存重要用户信息。
2. 使用数据库保存结构化记忆
结构化数据(Structured Data)指的是字段和数据类型相对明确的数据。
例如,用户偏好可以设计为数据库记录:
UserMemory├── id├── user_id├── memory_type├── content├── source├── created_at└── updated_at
字段含义如下:
| 字段 | 含义 |
|---|---|
id | 记忆记录的唯一标识 |
user_id | 记忆所属用户 |
memory_type | 记忆类型,例如偏好或任务摘要 |
content | 记忆内容 |
source | 记忆来源,例如用户明确表达 |
created_at | 创建时间 |
updated_at | 最近更新时间 |
例如,在健康报告 Agent 中,报告原文、解析结果和指标记录应该优先保存为可追踪的业务数据,而不是全部塞进一段模糊的用户记忆。
因为业务数据需要明确的主键、数据关系、权限控制和更新规则。把重要业务事实只保存在大模型生成的摘要里,会增加数据不一致和无法核验的风险。
3. 使用向量数据库保存语义检索信息
向量(Vector)在这里指的是一组数字,可以用来表示文本等内容的语义特征。
嵌入(Embedding)是将文本转换成向量的过程。
例如,以下两句话用词不同,但表达的意思相近:
- “我不喜欢太早出门。”
- “旅行时尽量安排晚一点开始的活动。”
经过合适的嵌入模型处理后,这些文本可能得到语义上接近的向量。
向量数据库(Vector Database)可以保存向量,并根据向量之间的相似程度检索相关内容。
这适合处理“我之前说过什么类似的话”之类的问题。
但要注意:向量相似不等于事实正确,也不等于两条信息完全相同。检索结果仍然需要结合原始文本、时间、来源和业务规则进行判断。
此外,并不是所有记忆都需要向量数据库。
如果只需要根据 user_id 查询用户的语言偏好,用普通关系数据库往往更简单、更可靠。
四、RAG 是什么?它和 Memory 有什么区别?
现在,我们再来理解检索增强生成(Retrieval-Augmented Generation,RAG)。
这个名称可以拆成三部分:
- 检索(Retrieval):从外部数据源中查找相关内容。
- 增强(Augmented):把检索结果补充到模型的输入中。
- 生成(Generation):让大模型根据当前问题和提供的内容生成答案。
1. 为什么需要 RAG?
假设用户上传了几十份体检报告,并提出:“比较我最近两次报告中的血糖指标。”
如果把几十份报告全部塞进模型输入,不仅可能占用大量上下文空间,还可能引入无关信息。
更合理的方式是:
用户问题 ↓确定需要查询的指标与时间范围 ↓从数据库中检索相关报告 ↓取得最近两次报告中的血糖记录 ↓将相关数据提供给模型或业务分析模块 ↓生成比较结果
这里的重点是:先找到相关数据,再组织回答。
如果数据是结构化的,例如报告日期、指标名称和测量值,那么直接使用数据库查询通常比语义检索更合适。
如果数据是大量非结构化文本,例如报告解释、知识文档和长篇资料,语义检索就可能发挥作用。
2. RAG 的基本流程
一个常见的文本 RAG 流程可以分为两个阶段。
阶段一:准备知识库。
原始文档 ↓文本提取 ↓文本分块(Chunking) ↓生成文本向量(Embedding) ↓保存文本、元数据与向量
文本分块(Chunking)是把长文档切分成较小片段的过程。
这样做是为了让系统更容易检索到与问题相关的部分,而不是每次都返回整篇文档。
元数据(Metadata)是描述文档的信息,例如文档编号、所属用户、创建时间、标题和来源。
这些信息能够帮助系统过滤不相关的数据,并追踪检索结果来自哪里。
阶段二:回答用户问题。
用户问题 ↓理解问题并生成检索条件 ↓检索相关片段 ↓过滤和整理检索结果 ↓构建模型输入 ↓生成回答
这里的检索可以使用关键词、向量相似度或混合检索等方式。
混合检索(Hybrid Search)是把不同的检索方式结合起来。例如,关键词检索擅长匹配明确的指标名称,向量检索则可能更擅长寻找语义相近的表达。
检索方式应根据数据类型和实际效果选择,而不是默认所有项目都必须使用向量数据库。
3. Memory 和 RAG 的区别
这两个概念经常被混为一谈,但可以从它们的主要目的来区分。
| 对比维度 | Memory(记忆) | RAG(检索增强生成) |
|---|---|---|
| 主要目的 | 保存并复用过去的交互、偏好或任务经历 | 从外部知识源检索与当前问题相关的内容 |
| 常见数据 | 用户偏好、对话摘要、任务结果 | 文档、知识库、业务资料 |
| 信息来源 | 用户交互、任务执行和应用状态 | 文档库、数据库或其他外部数据源 |
| 关键问题 | 什么值得记住?什么时候使用? | 应该检索哪些内容?怎样提高相关性? |
| 是否必须使用向量数据库 | 不需要 | 也不一定需要 |
| 典型场景 | 记住用户偏好,恢复任务进度 | 从大量资料中查找答案依据 |
二者可以组合使用。
例如,用户问:“结合我之前的偏好,推荐一个适合我的旅行方案。”
系统可以:
- 从 Memory 中读取用户的旅行偏好。
- 从 RAG 知识库中检索景点、交通和活动资料。
- 将用户偏好、检索资料和当前请求一起交给规划模块。
- 生成符合约束的旅行方案。
这说明 Memory 和 RAG 可以协作,但它们不是同一个模块。
五、上下文工程:不是信息越多越好
现在,我们已经知道如何保存记忆,也知道如何检索知识。
接下来还有一个重要问题:
当我们找到了很多信息,究竟应该把哪些内容交给大模型?
这就是上下文工程(Context Engineering)要解决的问题。
1. 什么是上下文工程?
上下文工程是指围绕当前任务,有目的地选择、组织、压缩和安排模型输入信息。
可以把它理解为:不仅要让模型“有信息可用”,还要让它“在正确的位置获得完成任务所需的信息”。
例如,用户提出:“帮我比较北京和上海明天的天气。”
系统可能获得以下信息:
- 当前用户的问题。
- 用户几个月前讨论过的旅行计划。
- 用户喜欢樱花的偏好。
- 北京天气查询工具的定义。
- 上海天气查询工具的定义。
- 当前日期。
- 与天气无关的历史对话。
- 本次任务已经完成的查询结果。
这些内容并不都同样重要。
如果把所有信息都塞进模型输入,不仅浪费上下文空间,还可能干扰模型判断。
合理的做法是:根据当前任务,挑选真正有用的信息。
2. 上下文通常包括什么?
在 Agent 应用中,可以把上下文划分成几个部分:
当前模型输入├── 系统指令与开发者指令├── 当前用户请求├── 当前任务与执行状态├── 相关用户记忆├── 检索得到的知识├── 可用工具定义├── 必要的工具执行结果└── 必要的历史对话
不同任务需要的上下文不一样。
例如:
- 天气查询需要城市、日期和天气工具定义。
- 体检报告趋势分析需要报告日期、指标名称、数值和单位。
- 旅行规划可能需要预算、偏好、景点资料和交通信息。
因此,上下文工程不应被实现成一个固定的“把所有内容都加进去”的模板,而应该根据任务类型进行组装。
3. 上下文排序(Context Ordering)
上下文排序指的是决定不同信息在模型输入中如何组织。
例如:
1. 任务目标与必须遵守的约束2. 当前任务状态3. 相关工具定义4. 可信的检索结果5. 必要的历史对话6. 当前需要模型完成的具体操作
这只是一个示例顺序,不是所有模型和任务都适用的固定规则。
不同模型对位置和格式的敏感程度不同,因此关键是通过实际测试确定合适的输入组织方式。
对于 Agent 开发者来说,至少应该保证:
- 当前任务目标清晰。
- 重要约束没有被遗漏。
- 工具定义与当前操作相关。
- 工具结果能够与对应任务关联。
- 历史信息不会覆盖当前的明确要求。
4. 上下文预算(Context Budget)
上下文预算是指为模型输入中的不同信息分配有限的空间。
假设某个应用为一次请求设置了二十四千个词元的输入预算。
词元(Token)是模型处理文本时使用的基本单位,它不等于一个汉字或一个英文单词。实际占用量取决于模型的分词方式。
可以把预算暂时划分为:
| 内容 | 示例预算 |
|---|---|
| 系统指令和任务要求 | 3,000 |
| 当前任务与状态 | 2,000 |
| 工具定义 | 3,000 |
| 检索结果 | 8,000 |
| 历史对话摘要 | 3,000 |
| 输出预留及其他空间 | 5,000 |
| 合计 | 24,000 |
这些数字仅用于演示预算规划,并不代表所有模型都应该采用这样的分配。
实际系统还需要考虑模型对输入和输出的限制,以及系统指令、工具定义、消息格式等内容占用的空间。
为什么要做预算?
因为上下文空间有限,而不同信息的重要程度不同。
如果检索结果已经占用了大量空间,系统就可能需要:
- 减少不相关的文档片段。
- 压缩较早的对话历史。
- 只保留当前任务需要的工具定义。
- 删除已经完成且不再有用的中间信息。
- 优先保留影响任务正确性的约束和执行结果。
这里最重要的不是死记某个数字,而是形成一个工程习惯:每一类上下文都应该有明确的用途、优先级和容量限制。
5. 上下文压缩(Context Compression)
上下文压缩是将较长的信息整理成更短、但仍然保留关键信息的表示方式。
例如,原始对话可能是:
用户:我想去东京玩三天。用户:我喜欢樱花和安静的地方。用户:我不喜欢早起。用户:预算大约一千元。用户:希望每天不要安排太多景点。
可以将它压缩成:
travel_preferences = { "destination": "东京", "duration_days": 3, "interests": ["樱花", "安静的地方"], "avoid": ["早起"], "budget": 1000, "pace": "慢节奏"}
对于结构明确的信息,使用结构化数据往往比自由文本摘要更容易验证和更新。
但压缩并不意味着可以随意丢弃细节。
例如,健康报告中的具体数值、日期、单位和参考范围,不能为了节省上下文而被随意改写或省略。需要进行精确计算的数据,应尽量保留结构化原始记录。
六、把 Memory、RAG 和上下文工程连接起来
现在,我们把三个概念放在同一个 Agent 中理解。
假设用户提出:
“根据我以前的旅行偏好,帮我规划一次东京三日游。”
系统可以采用下面的流程:
用户请求 ↓识别当前任务 ↓读取 Memory取得用户偏好、预算等信息 ↓使用 RAG 检索景点与交通资料 ↓检查资料来源、相关性与时间有效性 ↓Context Engineering组织目标、约束、记忆和检索结果 ↓Planner生成旅行任务计划 ↓Scheduler选择当前可以执行的任务 ↓Executor调用搜索、天气或日历工具 ↓更新 AgentState ↓判断是否需要继续执行或重新规划 ↓生成最终结果
注意,这不是说 Memory、RAG 必须位于 Agent Runtime 的某个固定位置。
在不同架构中,它们可以作为独立服务、工具、图节点或运行时中的辅助模块。
重要的是明确每个模块的职责,以及它们怎样把信息交给后续步骤。
1. 在 AgentState 中怎样体现?
回顾第四篇文章,我们设计了 AgentState 来保存当前任务的执行信息。
现在,可以在此基础上加入与上下文组装有关的字段:
from typing import Anyfrom dataclasses import dataclass, field@dataclassclass AgentState: goal: str tasks: list[dict] = field(default_factory=list) # 当前任务的执行状态 current_task: str | None = None tool_results: list[dict] = field(default_factory=list) # 从用户记忆中读取的相关信息 retrieved_memories: list[dict] = field(default_factory=list) # 从知识库中检索到的资料 retrieved_documents: list[dict] = field(default_factory=list) # 当前为模型组织好的输入信息 context: dict[str, Any] = field(default_factory=dict) final_result: str | None = None
这里的 retrieved_memories 保存本次任务读取到的相关记忆,retrieved_documents 保存知识检索结果,context 保存经过筛选和整理后准备交给模型的信息。
还要注意:AgentState 中保存这些字段,并不意味着数据已经永久写入数据库。
运行时状态(Runtime State)与持久化存储(Persistence)是两个不同的概念。
如果希望程序重启后仍然能够恢复记忆或任务,就需要使用数据库、文件存储或其他持久化机制。
2. 一个最小的上下文组装函数
我们先不引入复杂的记忆框架,而是用 Python 体现核心思路:
def assemble_context( goal: str, memories: list[dict], documents: list[dict], tool_results: list[dict],) -> dict: return { "goal": goal, "relevant_memories": memories, "relevant_documents": documents, "tool_results": tool_results, }
这个函数做的事情很简单:把已经筛选好的信息组织成统一的数据结构。
真实项目还需要在调用之前完成:
- 根据用户身份过滤数据。
- 根据当前任务筛选相关记忆。
- 检查文档是否属于当前用户有权访问的范围。
- 限制检索结果数量和总长度。
- 删除不必要的历史内容。
- 保留来源和时间等可追踪信息。
因此,assemble_context() 不是完整的上下文工程实现,而是其中的一个小步骤。
真正的难点通常不在于把字典拼起来,而在于判断哪些信息应该进入字典。
七、结合健康报告 Agent:为什么不能只依赖记忆?
我们用一个实际的业务例子收尾。
假设我们正在开发一个健康报告 Agent。它需要支持:
- 上传体检报告。
- 解析报告中的指标。
- 保存结构化指标数据。
- 查询历史报告。
- 比较指标变化。
- 向用户解释结果。
当用户问:
“最近两次报告里的某个指标有什么变化?”
系统应该怎样处理?
1. 先查询业务数据库
报告日期、指标名称、测量值、单位和参考范围,都属于应该有明确记录的业务数据。
因此,系统应优先通过数据库查询取得准确记录,而不是依赖模型对历史聊天的回忆。
例如:
history = [ { "report_date": "2026-06-01", "indicator": "示例指标", "value": 5.2, "unit": "示例单位" }, { "report_date": "2026-09-01", "indicator": "示例指标", "value": 5.8, "unit": "示例单位" }]
这里的数据仅用于说明数据结构,不代表任何真实医学指标或临床判断。
2. 再由程序完成确定性计算
如果只是计算两个数值之间的差值,可以由 Python 完成:
old_value = history[0]["value"]new_value = history[1]["value"]difference = new_value - old_value
这属于确定性计算(Deterministic Computation):在输入相同且计算逻辑不变时,程序会得到相同的计算结果。
相比让大模型自行计算,使用明确的程序逻辑更容易测试和验证。
当然,实际健康指标还需要确认单位一致、检测方法可比、日期正确,并遵循适用的专业规则。不能仅凭数值变大或变小,就直接推导疾病诊断。
3. 再使用记忆和上下文工程
如果用户以前明确表示希望用通俗语言解释报告,系统可以读取这一偏好。
随后,系统将以下内容组合起来:
- 当前问题。
- 最近两次报告的真实记录。
- 计算后的数值变化。
- 相关指标的专业解释资料。
- 用户的表达偏好。
- 必须遵守的安全限制。
这些信息共同构成当前任务的上下文。
最后,大模型可以负责把经过验证的数据与专业资料组织成易懂的解释。
这个设计把不同类型的工作分配给合适的模块:
- 数据库负责保存和查询业务事实。
- Python 负责明确的计算规则。
- RAG 负责检索相关资料。
- Memory 负责复用合适的历史偏好或任务信息。
- 上下文工程负责组织当前模型需要的信息。
- 大模型负责语言理解、解释和生成。
- Agent Runtime 负责组织整个执行过程。
这就是一个更容易测试、追踪和维护的 Agent 应用。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)