跳转至

记忆与 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 中的中间步骤噪声过大,淹没了原始指令。

防御策略:

  1. 在 Prompt 中固化目标:在构建 Agent Prompt 时,将用户原始问题放在一个显眼的位置,并使用 SystemMessage 强调“你的最终目标是{original_question}”。每次循环,这个目标都会出现在上下文中,不会被后续的 observation 淹没。

  2. 利用 Memory 保持目标:将用户初始问题存入 Memory,并在每次 Agent 推理时,将 Memory 中的目标摘要作为额外上下文注入。例如,在 ConversationSummaryMemory 的摘要中,明确包含“用户希望完成X任务”。

  3. 显式追踪子目标完成状态:要求 Agent 在每一步思考时,先回顾最终目标,再列出已完成和未完成的子目标。这可以通过 Prompt 工程设计实现:

Thought: 我的最终目标是安排一次从北京到上海的旅行。目前我已经查了航班,还没有预订酒店。接下来我应该搜索上海的酒店。
  1. 限制推理步数并设置检查点:当步数超过阈值时,强制 Agent 暂停并总结已完成的工作,然后重新明确下一步计划。这相当于在 scratchpad 中插入一个人工检查点。

  2. 利用长期记忆存储任务状态:对于需要跨会话的复杂任务,可将任务状态(如 JSON 格式的已完成列表)存储在向量库或 Redis 中。每次 Agent 启动时,先检索当前任务状态,然后基于此继续工作。

实践反思:单纯依赖 LLM 的注意力来记住初始目标并不靠谱。应该在 Prompt 结构上下功夫,把目标做成“固定背景板”,每次推理都强制 LLM 先看目标,再看历史。这就像给人一份任务清单,每完成一项就划掉,始终知道终点在哪。


🗄️ 你如何设计一个 Agent 的记忆,使其能根据当前问题自动检索相关的长期记忆?

设计这种记忆的核心是 “检索增强记忆”。Agent 不应无差别地加载所有历史,而应根据当前问题,从长期记忆库中动态拉取最相关的片段,注入 Prompt。

设计步骤:

  1. 存储长期记忆:使用向量数据库(如 Chroma、Pinecone)存储对话片段、提取的实体、用户偏好等。每条记忆可以带元数据(时间戳、重要性评分等)。

  2. 自动检索:在 Agent 执行前,用当前用户输入(或改写后的查询)去向量库中检索最相关的 top-K 条记忆。

  3. 注入 Prompt:将检索到的记忆格式化为文本,放入 Prompt 的特定位置(例如 {long_term_memory})。

  4. 更新机制:在对话结束后,使用 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 或长期记忆库。

实现策略:

  1. 利用 Agent 的 Memory 自动保存 ConversationBufferMemory 等会保存整个对话历史,包括用户消息和最终的 AI 回复,但不会保存中间的 FunctionMessageToolMessage(这些只存在于 scratchpad)。要让 Memory 包含工具调用细节,需要将工具输出转化为 AI 能理解的内容,并通过自定义回调或修改 Agent 执行逻辑来写入 Memory。

  2. 在 Prompt 中要求 LLM 总结工具输出 最简单的方法:在 Agent 的 Prompt 中要求它获得工具结果后,必须用自然语言复述关键信息,这样这部分复述就会出现在最终的 AI 回复中,从而被 Memory 记录。例如:“查询结果显示上海明天25度。基于此,我推荐...”。这样,Memory 就间接记住了天气。

  3. 使用专用的“知识库记忆”存储结构化结果 对于重要的工具输出(如用户订单号、病历ID),在工具执行后,由应用层逻辑将其存入向量库或键值存储,并关联用户ID。下次对话时,通过检索取出。这是最可靠的方式。

  4. 通过回调函数拦截并存储 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_agentcreate_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,用于下一轮推理。

具体流程:

  1. LLM 输出 Thought: ... Action: ... Action Input: ...

  2. AgentExecutor 执行 Action,得到结果(即 Observation)。

  3. 将本次的 Thought, Action, Action Input, Observation 作为一个整体,追加到 scratchpad 字符串(或消息列表)中。

  4. 下一次调用 LLM 时,Prompt 中包含完整的 scratchpad,LLM 就能“看到”刚才发生了什么。

它算记忆的一部分吗? 不完全是。 Observation 是短期工作记忆,仅在当前任务闭环中存在。它不会被保存到 ConversationBufferMemory 等长期 Memory 中(除非 Agent 最终输出显式地复述了它)。如果把 Agent 比作一个人:

  • Memory 是人的长期记忆(记得昨天聊了什么)。

  • Scratchpad(包含 Observation)是人在解决当前问题时使用的草稿纸(写满了计算步骤,用完就扔)。

实际影响:

由于 Observation 不会进入 Memory,当下次对话再开启时,Agent 不记得之前调用工具的结果。如果需要在后续对话中引用这些结果,必须在应用层显式地将 Observation 写入 Memory 或长期知识库。


📏 如何限制 Agent 的记忆总长度,防止 Prompt 超限?

Agent 的记忆(包括对话历史和 scratchpad)随着运行不断膨胀,很容易超出 LLM 的上下文窗口。限制方法如下:

  1. 使用窗口或摘要记忆 用 ConversationBufferWindowMemory(k=5) 只保留最近 5 轮对话;或用 ConversationSummaryMemory 压缩早期对话。这是最直接的控制手段。

  2. 限制 scratchpad 长度

  3. 设置 max_iterations:Agent 最多执行 N 步,超过后强制停止,防止无限推理。

  4. 在 Prompt 中要求 LLM 精简 Thought:例如“请用一句话写出你的思考”。

  5. 对 scratchpad 进行后期处理:如果中间步骤过多,在注入前只保留最近的 M 步,或对早期步骤进行摘要。

  6. 动态 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 更像是提供了一些记忆的“积木块”,但搭建出智能的记忆宫殿还需要大量的工程努力。