Agent Memory与RAG
Agent Memory
为什么AI Agent需要记忆模块
AI Agent的记忆模块本质是借鉴人类记忆系统设计的,就像人类智能一样记住过去的经历,从而学习并将这些经验应用到新的情况中。而对于基于LLM(大语言模型)的智能体,通常面临着一下局限性:
- 无状态导致的对话遗忘
LLM的设计本质上是无状态的,这也就意味着,用户的每一次请求或API调用都是一次独立、无关联的计算,模型本身并不会记忆上一次对话的内容,从而导致以下几个问题:
-
- 上下文窗口限制导致的上下文丢失:LLM通过一个有限的上下文窗口来处理消息,所有的输入,包括Prompt和之前的对话片段都必须在这个窗口范围之内。一旦信息超出这个窗口,LLM就会“遗忘”它,从而导致早期上下文重要信息丢失。
- 难以处理多轮/复杂问题:对于需要跨越多轮对话、追踪状态或执行一系列子任务的复杂任务,LLM很难保持连贯性和进展,因为它会不断“忘记”之前的步骤和决策,无法从过往的失败或成功经验中学习改进。特别是在Agent的场景,工具的定义和工具的返回值都会存在于上下文中,同时由于Agent具有自主工作的能力,和LLM的平均交互的轮数也大大增加。
- 缺少个性化:由于无法记住特定用户的历史偏好、习惯或之前的互动,LLM难以提供真正个性化的体验。每次互动都像是第一次见面。
- 性能和成本受限:LLM在处理更长的上下文时,需要进行更多的计算来处理和理解所有输入信息。这会导致推理时间增加,响应速度变慢。尽管可以通过扩大LLM的上下文窗口,使得LLM能够处理更多输入消息,但随着LLM的上下文窗口越来越大,模型在超长上下文中检索关键信息的能够反而可能会下降。同时,上下文越长,输入的token数量就越多,每次API调用的成本也会更高,这在需要频繁交互或处理大量文本的应用来说,费用会迅速积累。
- 模型内置知识的局限性
除了遗忘对话历史,LLM 的另一个核心局限在于其知识是静态的、有限的。这些知识完全来自于它的训练数据,这些训练数据存在时间截止点,因此大模型无法获取超出时间截止点的数据,也就无法获取最新信息。
AI Agent记忆类型
短期记忆/工作记忆
短期记忆是智能体维护当前对话和任务的即时上下文系统,主要包括:
- 会话缓冲记忆:保留最近对话历史的滚动窗口,确保回答上下文相关性;
- 工作记忆:存储当前任务的临时信息,如中间结果、变量值等。
短期记忆受限于上下文窗口大小,适用于简单的对话和单一的任务场景
长期记忆
长期记忆是智能体用于跨会话、跨任务长期保存知识的记忆形式。它对应于人类的大脑中持久保存的记忆,例如事实知识、过去经历等,长期记忆的实现通常依赖于外部存储或知识库。
Agent是如何实现记忆的?
这里我通过一个 demo 来直观展示每一次用户发起请求,请求的内容都包含了什么,就很容易理解了。这里我使用spring-ai来做演示,模型使用的是deepseek。
首先我们需要开启 deepseek 的会话记忆功能,也就对应着下面代码中的MessageChatMemoryAdvisor.builder(chatMemory).build()
然后我们还需要自定义一个Advisors(PrintAdvisor.java),用来拦截deepseek的接收到的请求,对其进行处理
@Configuration
public class AutoConfig {
@Bean
public PrintAdvisor printAdvisor() {
return PrintAdvisor.builder().build();
}
@Bean
public ChatClient chatClient(DeepSeekChatModel model, ChatMemory chatMemory, PrintAdvisor printAdvisor) {
return ChatClient
.builder(model)
.defaultAdvisors(
printAdvisor,
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
}
}
简单解释一下 PrintAdvisor.java 这段代码,首先Advisor是基于AOP实现的,它能够在 AI 请求执行的前后对会话记忆进行拦截和处理。它提供了两个方法:before 和 after,分别对应着 AI 请求执行前后的处理。这里我主要是在 before 方法中,获取到了每次向 AI 发起请求时的 Prompt
public class PrintAdvisor implements BaseChatMemoryAdvisor {
private String contents = "";
private List<Message> messages;
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
Prompt prompt = chatClientRequest.prompt();
messages = extractMessages(prompt);
ObjectMapper msgMapper = new ObjectMapper();
try {
contents = msgMapper.writeValueAsString(messages);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return chatClientRequest;
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
return chatClientResponse;
}
@Override
public int getOrder() {
return 0;
}
public String getContents() {
return contents;
}
private List<Message> extractMessages(Prompt prompt) {
List<Message> extractedMessages = new ArrayList<>();
try {
// 使用反射获取prompt中的messages字段
Field messagesField = Prompt.class.getDeclaredField("messages");
messagesField.setAccessible(true);
Object messagesObj = messagesField.get(prompt);
if (messagesObj instanceof List) {
List<?> messageList = (List<?>) messagesObj;
for (Object obj : messageList) {
if (obj instanceof Message) {
extractedMessages.add((Message) obj);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return extractedMessages;
}
public PrintAdvisor() {}
public static Builder builder(){
return new Builder();
}
public final static class Builder {
public Builder() {}
public PrintAdvisor build() {
return new PrintAdvisor();
}
}
}
下面这段代码则是通过路径参数,向 AI 发送数据,也就是使用 deepseek 时,用户发送消息这一步骤
@RequestMapping("/chat")
public String chat(@RequestParam String prompt) {
String content = chatClient.prompt()
.user(prompt)
.call()
.content();
print();
return content;
}
下面这段代码则是我们通过 Advisor 获取到每次发送请求时的 Prompt
@RequestMapping(value = "/print", produces = MediaType.APPLICATION_JSON_VALUE)
public String print() {
return printAdvisor.getContents();
}
接下来,我们开始模拟用户发送消息的操作

这是我们的第一次对话,下面我们来看拦截得到的 Prompt,内容非常简单,就是我们发送的消息,以及消息发送者(USER),不难理解,因为是第一次对话,AI 还没有记忆

那么接下来我们开始进行第二次对话,由于我们在第一次对话中设定了今天是周一,所以 AI 很快就通过记忆推断出明天就是周二,这个结果是符合我们预期的

现在我们再来看发送请求时的 Prompt,此时内容会丰富很多,不仅包含当前问题,还会包含上一次对话的完整记录。也就是说,每次对话发送给 AI 的,不仅是本次的问题,而是截止当前的全部聊天记录。AI 的“记忆”机制,本质上就是通过持续将先前的对话内容纳入 Prompt 来实现的。AI 通过分析 Prompt 中上下文之间的关联,推导出当前对话的回应,从而达成了记忆之前内容的效果。

RAG:检索增强生成
什么是RAG
检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合了信息检索和文本生成的技术。它的核心思想是:在生成回答之前,先从外部知识库中检索相关信息(检索),然后将检索到的信息作为上下文提供给大语言模型(增强),从而生成更准确、更可靠的回答(生成)
RAG工作原理
RAG 的工作原理是从外部来源检索数据,将其处理到大语言模型的语境中,然后基于组合来源生成回答。该过程包括三个主要阶段:数据准备、检索和生成。
数据准备
- 加载文档:识别并获取要与大语言模型共享的源文档,并确保这些文档采用大语言模型能够理解的格式,然后将每个文档统一转换成文本文件,然后才能嵌入到向量数据库中。这个过程也称为ETL阶段,通过提取、转换和加载,确保对原始数据进行清理和整理,为存储、分析和机器学习做好准备
- 文档切片:通过“文本分割”或“分块”处理文档,以便模型进行检索。这意味着对更新后的文档进行解析,然后根据不同的特征以目录的形式将其编入相关的“块”。文档切片逻辑需要自己实现,不同格式的文档需要不同的切片策略
- 嵌入:嵌入技术使用专门的机器学习模型(向量嵌入模型),将数据转换为数值向量,使得能够运用数学运算来评估数据片段之间的相似性和差异性。借助嵌入技术,您可以将文本(或图像)转换为向量,从而捕获内容的核心含义,同时剔除不相关的细节。
在完成文档切片后,系统会将每个文本切片通过 Embedding模型 将数据转换成向量表示。在向量数据库中,此数值可帮助 RAG 架构指示内容块之间的关联,并对数据进行整理以优化检索。这种方式通过对数据进行结构化处理,从而将相似的概念存储在相邻的坐标中。而两个向量点之间的距离或接近程度,有助于模型判断应检索哪些信息,并将这些信息纳入用户查询的输出结果中 - 存储:经过处理后得到的切片文本机器对应的向量会被存入到向量数据库中,以便或后续快速索引
索引
当用户提问时,将用户问题用相同的Embedding模型向量化,根据计算得到的向量与数据库中存储的数据进行近似最近邻搜索,得到相似度最高的知识,并融入到Prompt中
生成
LLM通过融入知识库数据的Prompt,结合相关的上下文进行推理,最后向用户呈现响应内容
上下文工程
要让智能体在真实复杂场景中稳定地思考与行动,仅有记忆与检索还不够——我们需要一套工程化方法,持续、系统地为模型构造恰当的上下文。这是就需要用到我们的上下文工程,它关注的是在每一次模型调用前,如何以可复用、可度量、可演进的方式,拼装并优化输入上下文,从而提升正确性、鲁棒性与效率。
所谓上下文,是指在对大语言模型进行采样时所包含的那组 tokens,从本质上讲,上下文是提供给 LLM 的、用于完成下一步推理或生成任务的全部信息集合。手头的工程问题,是在 LLM 的固有约束之下,优化这些 tokens 的效用,以便稳定地得到预期结果。这就要求 LLM 需要在上下文中思考,即在任何一次调用时,都要审视 LLM 可见的整体状态,并预判这种状态可能诱发的行为。
为什么需要上下文工程
随着模型的速度越来越快、可处理的数据规模越来越大,但随着上下文窗口中的 tokens 增加,模型从上下文中准确回忆信息的能力反而下降,即上下文腐蚀。因此,上下文必须被视作一种有限资源,且具有边际收益递减。就像人类有有限的工作记忆容量一样,LLM 也有一笔“注意力预算”。每新增一个 token,都会消耗这笔预算的一部分,因此我们更需要谨慎地筛选哪些 tokens 应该被提供给 LLM。
这种现象源自 LLM 的架构约束,Transformer 让每个 token 能够与上下文中的所有 token 建立关联,理论上形成 (n^2) 级别的两两注意力关系。随着上下文长度增长,模型对这些两两关系的建模能力会被“拉薄”,从而自然地产生“上下文规模”与“注意力集中度”的张力。此外,模型的注意力模式来源于训练数据分布——短序列通常比长序列更常见,因此模型对“全上下文依赖”的经验更少、专门参数也更少。
因此,上下文工程是专注于设计、构建并维护一个动态系统,该系统负责在 Agent 执行任务的每一步,为其智能地组装出最优的上下文组合,在上下文窗口中填入恰到好处的信息,为下一步推理做准备,以确保任务能够被可靠、高效地完成。
上下文工程最佳实践
为应对上下文腐蚀带来的挑战,需要有一整套系统的应对框架,参考Lance的上下文应对策略,可将上下文工程分为四个部分:写入、选取、压缩、隔离

写入
写入主要是将上下文持续久化,超越上下文窗口的限制,在未来按需取用。通常情况下分为
- 会话内写入:Agent将中间思考、计划或临时数据写入一个会话内的草稿纸。这是一种轻量级的、非持久化的写入,用于管理当前任务的复杂性。
- 持久化写入:系统将具有长期价值的信息写入外部的记忆 系统,如向量数据库或知识图谱,实现跨会话的知识积累。

选取
选取是指在每次 LLM 调用前,从所有可用的信息源中,动态地拉取与当前子任务最相关的信息,以帮助Agent执行任务。主要包含三类:
- 确定性选取:指根据预设规则加载上下文。
- 模型驱动选取:当可用信息源过多时(如海量工具或文档),可以利用模型自身的能力进行筛选。
- 检索式选取:这是最主流的方式,其核心范式是通过相似度检索,从外部知识库中选取信息。因此,选取操作的成败,在很大程度上依赖于底层检索系统的质量。
压缩
压缩是指仅保留执行任务所需上下文的 Tokens,其目的是在信息进入上下文窗口之前,对上下文进行有损或无损的压缩,用更少的 Token 承载最核心的信号。
通常使用LLM提炼最相关的上下文信息,然后再进一步对上下文进行修剪,压缩总结保留其认为最重要的部分。

隔离
隔离是指将上下文进行拆分隔离,在多信息流之间设置边界,由子流程先行消化,仅上交要点,以帮助智能体执行任务。由主代理负责高层规划与综合,多个专长子代理在“干净的上下文窗口”中各自深挖、调用工具并探索,最后仅回传凝练摘要。
通过这种方式,最终实现关注点分离。庞杂的搜索上下文留在 Sub Agent 内部,Lead Agent 专注于整合与推理;由于 Lead Agent 只接收隔离上下文中最有价值的部分,可以很大程度的避免长上下文带来的潜在问题,例如:上下文干扰/上下文冲突,也侧面提高了上下文中的信息密度。

1342

被折叠的 条评论
为什么被折叠?



