记忆与 Agent 集成
. 💡 在 Agent 中,除了对话历史,还需要记住什么?比如工具的返回结果。¶
Agent 的工作方式是“思考-行动-观察”循环(ReAct),在这个过程中,它会调用工具、获取结果,并基于此进行下一步推理。因此,除了用户与Agent的对话历史,Agent还必须记住以下内容:
-
当前任务的上下文与子目标:用户最初提出的完整指令可能被拆解为多个子任务。Agent 需要记住“我已经完成了哪几步,下一步要做什么”,以及“最终目标是什么”。否则在多步推理中容易迷失方向。
-
工具调用的历史与返回值:Agent 调用工具后,必须记住工具的名称、参数和返回结果。这些信息是后续推理的依据。例如,查询了天气,记住“上海今天25度”,后续才能推荐合适的衣服。
-
中间推理过程(scratchpad):Agent 在单次任务执行中,会记录自己的思考(Thought)、行动(Action)和观察(Observation)。这些是瞬时的“草稿”,用于在当前任务闭环内进行多步推理,但通常不会存入长期记忆(Memory)。
-
从历史对话中提取的实体与偏好:在多轮对话中,用户可能在不同时间点提及个人偏好(如“我吃素”)、名字、地点等。Agent 需要把这些“知识”记住,以便个性化回应。这属于长期记忆的范畴,常需要持久化到向量库。
-
任务执行状态与错误信息:当工具调用失败时,Agent 需要记住错误原因,避免重复相同的错误调用,或者切换到备选方案。
实践建议:在 LangChain 中,对话历史通常由 Memory 组件自动管理;而工具调用历史和 scratchpad 则由 AgentExecutor 内部维护,并通过 Prompt 注入。如果需要记忆工具返回结果以便在未来的独立会话中引用,则应将这些结果显式地存入长期记忆(如向量库)。
📝 Agent 的中途输出(scratchpad)是如何被记录和传递的?这与 Memory 有什么关系?¶
Scratchpad 是 Agent 在一次任务执行中产生的“草稿纸”,记录了它多步推理的完整轨迹,包括 Thought, Action, Action Input, Observation。它的生命周期仅限于单次 agent_executor.run() 调用。一旦 Agent 输出 Final Answer,scratchpad 就会被丢弃。
记录与传递机制:
-
当你调用
AgentExecutor.run()时,LangChain 内部会启动一个循环(最多max_iterations次)。 -
在每次循环中,Agent 根据当前 Prompt(包含用户输入、可用工具、以及所有之前的 scratchpad 内容)调用 LLM,LLM 返回下一步的 Thought/Action/Action Input。
-
如果 LLM 输出的是 Action,AgentExecutor 执行相应工具,得到 Observation。
-
这个 (Thought, Action, Observation) 三元组会被追加到
scratchpad列表中。 -
在下一次循环开始时,更新后的 scratchpad 会再次被格式化并注入 Prompt。因此,Agent 能看到自己“刚才想了什么、做了什么、结果如何”,从而进行连贯的多步推理。
与 Memory 的关系:
-
Memory(如
ConversationBufferMemory)存储的是跨轮次的对话历史。比如上一轮用户问了天气,这一轮问“那明天呢?”,Memory 使得 Agent 知道“那”指的是天气。 -
Scratchpad 是轮次内的推理轨迹。即使没有 Memory(如单次任务),Agent 也需要 scratchpad 来完成多步操作。两者共同构成 Agent 的上下文窗口。
-
在 Prompt 组装时,LangChain 通常将 Memory 的历史放在前面,scratchpad 紧接其后,再是用户的最新输入。这样 Agent 既了解对话背景,也清楚当前任务的进展。
“坑”与思考:如果 Memory 过长,会挤压 scratchpad 的可用空间,导致 Agent 在长对话中失去推理能力。因此需要限制 Memory 大小,或使用摘要记忆,为 scratchpad 留足 token 预算。
🧭 如果一个 Agent 执行过程中需要多轮推理,如何让它在后续步骤中不忘记最初的目标?¶
Agent “忘记目标”是长链推理中的经典问题。根本原因是 LLM 的注意力随着 Prompt 增长而衰减,或者 scratchpad 中的中间步骤噪声过大,淹没了原始指令。
防御策略:
-
在 Prompt 中固化目标:在构建 Agent Prompt 时,将用户原始问题放在一个显眼的位置,并使用
SystemMessage强调“你的最终目标是{original_question}”。每次循环,这个目标都会出现在上下文中,不会被后续的 observation 淹没。 -
利用 Memory 保持目标:将用户初始问题存入 Memory,并在每次 Agent 推理时,将 Memory 中的目标摘要作为额外上下文注入。例如,在
ConversationSummaryMemory的摘要中,明确包含“用户希望完成X任务”。 -
显式追踪子目标完成状态:要求 Agent 在每一步思考时,先回顾最终目标,再列出已完成和未完成的子目标。这可以通过 Prompt 工程设计实现:
-
限制推理步数并设置检查点:当步数超过阈值时,强制 Agent 暂停并总结已完成的工作,然后重新明确下一步计划。这相当于在 scratchpad 中插入一个人工检查点。
-
利用长期记忆存储任务状态:对于需要跨会话的复杂任务,可将任务状态(如 JSON 格式的已完成列表)存储在向量库或 Redis 中。每次 Agent 启动时,先检索当前任务状态,然后基于此继续工作。
实践反思:单纯依赖 LLM 的注意力来记住初始目标并不靠谱。应该在 Prompt 结构上下功夫,把目标做成“固定背景板”,每次推理都强制 LLM 先看目标,再看历史。这就像给人一份任务清单,每完成一项就划掉,始终知道终点在哪。
🗄️ 你如何设计一个 Agent 的记忆,使其能根据当前问题自动检索相关的长期记忆?¶
设计这种记忆的核心是 “检索增强记忆”。Agent 不应无差别地加载所有历史,而应根据当前问题,从长期记忆库中动态拉取最相关的片段,注入 Prompt。
设计步骤:
-
存储长期记忆:使用向量数据库(如 Chroma、Pinecone)存储对话片段、提取的实体、用户偏好等。每条记忆可以带元数据(时间戳、重要性评分等)。
-
自动检索:在 Agent 执行前,用当前用户输入(或改写后的查询)去向量库中检索最相关的 top-K 条记忆。
-
注入 Prompt:将检索到的记忆格式化为文本,放入 Prompt 的特定位置(例如
{long_term_memory})。 -
更新机制:在对话结束后,使用 LLM 从最新交互中提取值得长期保留的信息,存入向量库。旧记忆可通过时间衰减或重要性评分来淘汰。
LangChain 实现:
from langchain.memory import VectorStoreRetrieverMemory
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
# 1. 初始化向量库和检索器
vectorstore = Chroma(embedding_function=OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 2. 创建长期记忆
long_term_memory = VectorStoreRetrieverMemory(
retriever=retriever,
memory_key="long_term_memory",
input_key="input",
)
# 3. 在 Agent 中使用
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, memory=long_term_memory)
但是,VectorStoreRetrieverMemory 更适用于 Chain。对于 Agent,你可能需要自定义逻辑,在 AgentExecutor 的每次调用前手动检索记忆,然后拼接到 input 中。
关键点:
-
检索粒度:以“对话片段”为单位存储可能过于粗糙,建议提取为“事实”或“偏好”粒度,例如“用户喜欢中餐”、“用户住在北京”。
-
时效性:为记忆增加时间衰减,优先检索近期或反复提及的记忆。
-
反馈循环:Agent 的回答如果引用了某条长期记忆,可增加该记忆的权重。
个人经验:用 LLM 先对用户问题生成一个“搜索查询”,再用它检索记忆,效果比直接用原始问题好。这与 ConversationalRetrievalChain 的 condense_question 思路一致。
🛠️ 当 Agent 使用完一个工具后,如何把工具的输出作为记忆存入,并在后续对话中引用?¶
工具输出通常作为 observation 直接返回给 LLM 进行下一步推理,但默认不会自动存入 Memory。要在后续对话中引用,必须显式地将工具输出保存到 Memory 或长期记忆库。
实现策略:
-
利用 Agent 的 Memory 自动保存
ConversationBufferMemory等会保存整个对话历史,包括用户消息和最终的 AI 回复,但不会保存中间的FunctionMessage或ToolMessage(这些只存在于 scratchpad)。要让 Memory 包含工具调用细节,需要将工具输出转化为 AI 能理解的内容,并通过自定义回调或修改 Agent 执行逻辑来写入 Memory。 -
在 Prompt 中要求 LLM 总结工具输出 最简单的方法:在 Agent 的 Prompt 中要求它获得工具结果后,必须用自然语言复述关键信息,这样这部分复述就会出现在最终的 AI 回复中,从而被 Memory 记录。例如:“查询结果显示上海明天25度。基于此,我推荐...”。这样,Memory 就间接记住了天气。
-
使用专用的“知识库记忆”存储结构化结果 对于重要的工具输出(如用户订单号、病历ID),在工具执行后,由应用层逻辑将其存入向量库或键值存储,并关联用户ID。下次对话时,通过检索取出。这是最可靠的方式。
-
通过回调函数拦截并存储 LangChain 提供
on_tool_end回调。你可以编写一个自定义处理器,在工具调用结束后,将工具名、输入、输出存入 Memory 或数据库。例如:
class ToolMemoryHandler(BaseCallbackHandler):
def on_tool_end(self, output, **kwargs):
# 将 output 存储到某个持久化存储
persist_tool_output(session_id, output)
最佳实践:对于需要在多轮对话中反复引用的工具结果,主动将其结构化并存入长期记忆,而不只是依赖 LLM 的记忆。这样即使 LLM 上下文窗口被裁剪,关键事实也不会丢失。
📄 写一个带记忆的 Agent 的初始化代码,使用 ConversationalAgent。¶
在 LangChain 中,ConversationalAgent 已被 create_openai_functions_agent 或 create_react_agent 所取代,但经典的 ConversationalAgent 仍然可用。下面给出一个使用 ConversationalAgent + Memory 的完整示例。
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.tools import Tool
# 1. 定义工具
def search(query: str) -> str:
# 模拟搜索
return f"搜索结果:关于'{query}'的信息..."
tools = [
Tool(name="Search", func=search, description="当你需要查找实时信息时使用")
]
# 2. 初始化 LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)
# 3. 创建记忆(带消息格式)
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
# 4. 初始化 Agent
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION, # 对话式 Agent
memory=memory,
verbose=True,
handle_parsing_errors=True,
max_iterations=5,
early_stopping_method="generate",
)
# 5. 运行
response = agent.run("帮我搜索一下量子计算的最新进展")
print(response)
关键配置说明:
-
AgentType.CONVERSATIONAL_REACT_DESCRIPTION:专门为对话优化的 ReAct Agent,其 Prompt 中包含了对话历史的占位符。 -
memory=memory:将记忆注入 Agent。记忆会自动记录每次交互的 Human 输入和 AI 回复。 -
return_messages=True:现代聊天模型推荐使用消息列表格式,记忆会返回List[BaseMessage],而不是字符串。
踩坑经验:
-
这个 Agent 类型比较老,对于复杂的工具调用可能不如
create_openai_functions_agent稳定。 -
如果 Memory 过大,Prompt 会超限,建议配合
ConversationSummaryBufferMemory使用。 -
工具的描述(
description)要足够清晰,否则 Agent 不知道何时调用。
🔍 在 ReAct Agent 中,Observation 是如何被注入到 Prompt 中的?它算记忆的一部分吗?¶
Observation 的注入过程:
在 ReAct Agent 的执行循环中,每当 Agent 执行完一个工具(Action),工具返回的结果会被包装成 Observation。这个 Observation 紧接着会被格式化并追加到当前 scratchpad 中。然后,更新后的 scratchpad(包含所有历史 Thought/Action/Observation)与原始 Prompt 模板一起,重新发送给 LLM,用于下一轮推理。
具体流程:
-
LLM 输出 Thought: ... Action: ... Action Input: ...
-
AgentExecutor 执行 Action,得到结果(即 Observation)。
-
将本次的
Thought, Action, Action Input, Observation作为一个整体,追加到 scratchpad 字符串(或消息列表)中。 -
下一次调用 LLM 时,Prompt 中包含完整的 scratchpad,LLM 就能“看到”刚才发生了什么。
它算记忆的一部分吗?
不完全是。 Observation 是短期工作记忆,仅在当前任务闭环中存在。它不会被保存到 ConversationBufferMemory 等长期 Memory 中(除非 Agent 最终输出显式地复述了它)。如果把 Agent 比作一个人:
-
Memory 是人的长期记忆(记得昨天聊了什么)。
-
Scratchpad(包含 Observation)是人在解决当前问题时使用的草稿纸(写满了计算步骤,用完就扔)。
实际影响:
由于 Observation 不会进入 Memory,当下次对话再开启时,Agent 不记得之前调用工具的结果。如果需要在后续对话中引用这些结果,必须在应用层显式地将 Observation 写入 Memory 或长期知识库。
📏 如何限制 Agent 的记忆总长度,防止 Prompt 超限?¶
Agent 的记忆(包括对话历史和 scratchpad)随着运行不断膨胀,很容易超出 LLM 的上下文窗口。限制方法如下:
-
使用窗口或摘要记忆 用
ConversationBufferWindowMemory(k=5)只保留最近 5 轮对话;或用ConversationSummaryMemory压缩早期对话。这是最直接的控制手段。 -
限制 scratchpad 长度
-
设置
max_iterations:Agent 最多执行 N 步,超过后强制停止,防止无限推理。 -
在 Prompt 中要求 LLM 精简 Thought:例如“请用一句话写出你的思考”。
-
对 scratchpad 进行后期处理:如果中间步骤过多,在注入前只保留最近的 M 步,或对早期步骤进行摘要。
-
动态 Token 预算管理
编写自定义逻辑,在每次 Agent 循环前计算当前 Prompt 的总 token 数。如果接近上限,则:
-
自动裁剪 Memory 中的早期消息。
-
对 scratchpad 中过长的 Observation 进行截断或摘要。
-
如果依然超限,提前终止并返回“任务过于复杂,请简化问题”。
-
选择高上限模型或拆分任务
使用支持更大上下文的模型(如 gpt-4-1106-preview 支持 128K),或将复杂任务拆分为多个子任务,分别由不同的 Agent 实例执行,每个实例拥有独立的短期记忆。
经验之谈:我通常采用“窗口记忆 + max_iterations=5”组合,在多数场景下有效。如果 Agent 频繁因超限而失败,那往往是提示词不够精炼,或者工具返回的信息太多。应该优化工具,让它们只返回最关键的内容。
🧠 如果一个 Agent 长时间运行(如执行数百步),你有什么策略来压缩或总结记忆?¶
数百步的执行中,Agent 会产生大量中间步骤,记忆膨胀极快。此时,需要主动“遗忘”和“总结”。
策略一:分层摘要
-
将 Agent 的推理过程分成阶段。每完成一个子任务,就调用 LLM 生成该阶段的摘要,并丢弃该阶段的详细 scratchpad,只保留摘要。
-
最终 Prompt 由“全局目标 + 多个阶段摘要 + 当前阶段的详细步骤”组成。
策略二:结构化状态追踪
-
不让 LLM 记忆所有细节,而是让 Agent 维护一个结构化的“任务状态对象”(例如 JSON),记录已完成的任务、待办事项、关键数据等。
-
每次推理时,只将这个状态对象的最新版本注入 Prompt,而不是全部历史。
策略三:利用外部存储作为“外脑”
-
将重要的中间结果(如“已经处理了37个文件”)写入 Redis 或数据库。
-
Agent 可以调用一个
read_state工具来读取这些信息,避免将所有数据保留在内存中。
策略四:滑动窗口与遗忘
-
对于长对话,只保留最近的 K 轮完整对话,早期的对话直接丢弃或用一句摘要替代。
-
可以对记忆设置“遗忘曲线”,距离当前越远的事件,其权重越低,最终从 Prompt 中移除。
实践反思:真正的智能体不应依赖无限的上下文窗口,而应像人类一样,将信息编码到外部知识结构中(笔记本、数据库),并在需要时精准检索。LangChain 目前缺乏这方面的内置支持,需要自己造轮子。
🧐 谈谈你对“Agent 记忆”和“人类记忆”类比的理解,LangChain 在这方面的抽象是否合理?¶
类比理解:
-
人类记忆:分为感觉记忆、短期记忆(工作记忆)和长期记忆。短期记忆用于当前任务,容量有限;长期记忆需要巩固,且会衰减和变形。人类会主动遗忘、总结、将知识结构化。
-
Agent 记忆:在 LangChain 中,
scratchpad类似于工作记忆(当前任务的推理草稿),ConversationBufferMemory类似于未经加工的短期记忆,ConversationSummaryMemory类似于对经历进行总结后的长期记忆,向量库存储类似于语义记忆。
LangChain 抽象的合理性:
-
优势:LangChain 将记忆与链/Agent 解耦,提供了多种开箱即用的记忆类型,这是巨大的进步。它让开发者能够快速实现“记住对话”这一常见需求。
-
不足与过度简化:
- 缺乏主动记忆管理:人类记忆不是被动存储,而是主动筛选、巩固、遗忘。LangChain 的记忆是被动写入,没有内建的“重要性评估”和“遗忘”机制。开发者必须手动实现哪些该记、哪些该忘。
- 工作记忆过于简单:scratchpad 只是粗暴地堆叠文本。真正的智能工作记忆应该能提取关键实体、追踪子目标状态、支持多模态信息。目前依赖 LLM 自己从长文本中提取,效率低且不可靠。
- 长期记忆的检索粒度粗:
VectorStoreRetrieverMemory把记忆当成文档,检索时返回相似文本,缺乏对结构化事实的推理能力。人类记忆能进行逻辑推理(“我上次见他是在下雨天,所以带伞那次”),而 LangChain 只能做语义匹配。 - 记忆与推理的割裂:人类的记忆与推理密不可分。LangChain 的记忆组件主要作为上下文提供者,Agent 本身并不“拥有”记忆,只是被动接收。理想的 Agent 应能主动查询、更新自己的记忆。
改进方向:期望 LangChain(或未来的框架)能提供记忆管理器,它能够自动提取对话中的实体、关系、事件,构建动态的知识图谱;根据当前任务自动检索最相关的记忆;支持记忆的巩固和遗忘;并将记忆与 Agent 的推理循环深度集成,让 Agent 能够像人一样“回忆”、“联想”和“举一反三”。目前的 LangChain 更像是提供了一些记忆的“积木块”,但搭建出智能的记忆宫殿还需要大量的工程努力。