ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Langchain Agent 记忆机制剖析:短期 scratchpad 与长期 history 的协同流程

Langchain Agent 记忆机制剖析:短期 scratchpad 与长期 history 的协同流程 1. 为什么 AgentExecutor 的记忆总像黑盒很多人第一次用 Langchain 的 AgentExecutor都会有一种“它好像记住了又好像没记住”的错觉。你调用agent_executor.invoke({input: ...})Agent 能自动决定要不要调工具、调几次、什么时候停看起来挺聪明可一旦你问“刚才那步工具算出来的结果是多少”它又经常答不上来。问题不在模型而在于你没分清 AgentExecutor 内部其实有两套完全不同的记忆通道一套是单次 invoke 内、随工具调用不断膨胀的短期 scratchpad另一套是跨多次 invoke、靠外部存储维持的长期 history。这两套东西名字都带“记忆”但生命周期、存放位置、注入 prompt 的位置全都不一样。scratchpad 是 AgentExecutor 自己在 while 循环里维护的临时消息列表一次 invoke 结束就丢history 是 RunnableWithMessageHistory 在 invoke 前后帮你读写的外部存储靠 session_id 区分会话。搞混了它们就会出现“工具输出下一轮消失”“历史消息没进 prompt”“多轮对话串了会话”这些典型症状。这篇就围绕 Langchain Agent 的 scratchpad 与 history 协同流程展开面向多轮对话加工具调用的场景。我会先给一套可复制的记忆配置骨架再把 AgentExecutor 内部一次 invoke 的时序拆开最后给出验证上下文是否按预期注入的调试动作以及几个我实际踩过的坑。适合已经能跑通基础 Agent、但想搞清楚记忆到底怎么流动的人。2. 前置准备模型接入与依赖安装在动记忆机制之前得先有一个能稳定调用的 LLM 端点。我这边习惯用 TaoToken 做模型接入层它兼容 OpenAI 风格的接口Langchain 里直接配base_url和api_key就能用省去在代码里硬编码各家差异的麻烦。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这条不带 UTM 参数。先去控制台把 Key 建出来地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面生成密钥 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。Key 拿到后建议放环境变量别写进代码提交。依赖这块Langchain 拆包比较碎装的时候注意版本对齐pip install langchain langchain-core langchain-community langchain-openai如果你要用 Redis 做长期存储再加一个pip install redis环境变量这样设export TAOTOKEN_API_KEY你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api模型初始化时把 base_url 指过去import os from langchain_openai import ChatOpenAI llm ChatOpenAI( modelgpt-4o-mini, api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], temperature0, )这一步跑通的标准很简单单独llm.invoke(你好)能返回内容就说明接入层没问题后面记忆机制的调试才不会被网络问题干扰。3. 可复制的 Agent 记忆配置骨架先给完整骨架再逐段解释。核心就两个占位符history和agent_scratchpad它们必须按固定顺序出现在 prompt 里。from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_community.chat_message_histories import ChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.tools import tool tool def python_safe_run(code: str) - str: 执行一段安全的 Python 表达式并返回结果。 try: result eval(code, {__builtins__: {}}, {}) return f运行成功结果{result} except Exception as e: return f运行失败{e} tools [python_safe_run] systext 你是一个会调用工具的助手需要计算时请调用 python_safe_run。 prompt ChatPromptTemplate.from_messages([ (system, systext), MessagesPlaceholder(variable_namehistory), (user, {input}), MessagesPlaceholder(variable_nameagent_scratchpad), ]) agent create_openai_tools_agent(llm, tools, prompt) agent_executor AgentExecutor( agentagent, toolstools, verboseTrue, max_iterations20, ) memory ChatMessageHistory() agent_with_memory RunnableWithMessageHistory( agent_executor, lambda session_id: memory, input_messages_keyinput, history_messages_keyhistory, )几个关键点必须说清楚。第一history占位符放在 system 之后、当前 user 之前这样历史对话会作为上下文出现在当前问题前面符合对话模型的阅读顺序。第二agent_scratchpad必须放在最后因为它是本轮工具调用的临时记录逻辑上属于“当前这轮正在发生的事”放在 user 之后才合理。第三RunnableWithMessageHistory的工厂函数lambda session_id: memory决定了 session_id 到存储对象的映射生产环境里这里应该换成按 session_id 查 Redis 或数据库。调用方式resp agent_with_memory.invoke( {input: 帮我算一下 11}, config{configurable: {session_id: user_1}}, ) print(resp[output])第二次调用时只要 session_id 还是user_1上一轮的 Human 和 AI 消息就会被自动塞进history占位符。这就是长期记忆的全部魔法没有更玄的东西。4. 一次 invoke 内部scratchpad 与 history 的协同时序现在把 AgentExecutor 内部拆开看。假设用户输入“用 Python 分别算 11 到 55”一次 invoke 里发生了什么。进入RunnableWithMessageHistory.invoke后它先做前置动作拿 session_id 去存储里取历史消息塞进input[history]。然后调用内层AgentExecutor.invoke此时传入的字典大致是{ history: [HumanMessage(...), AIMessage(...)], input: 用 Python 分别算 11 到 55, agent_scratchpad: [] }AgentExecutor 内部维护一个scratchpad_messages []然后进入 while 循环最多跑max_iterations次。每一轮它把当前 scratchpad 塞进 inputs渲染 prompt 给 LLM。LLM 如果返回 tool_calls就执行工具把工具返回的 ToolMessage 追加进 scratchpad进入下一轮如果 LLM 不再要求调工具循环结束返回最终 AI 消息。把 5 次计算展开成回合看scratchpad 是这样累积的回合LLM 看到的 scratchpad 内容工具返回后新增1空ToolMessage(2)2[ToolMessage(2)]ToolMessage(4)3[ToolMessage(2), ToolMessage(4)]ToolMessage(6)4[ToolMessage(2), ToolMessage(4), ToolMessage(6)]ToolMessage(8)5[..., ToolMessage(8)]ToolMessage(10)循环结束后AgentExecutor 返回最终 AI 消息。此时RunnableWithMessageHistory的后置钩子触发把本轮的(HumanMessage, AIMessage)打包调用memory.add_messages(new_round)写进 ChatMessageHistory。注意写进去的只有这一对scratchpad 里那些 ToolMessage 默认不会进 memory。所以协同关系可以概括成history 负责跨 invoke 的对话连续性scratchpad 负责单次 invoke 内的工具链连续性。两者在 prompt 里各占一个位置互不覆盖。下一次 invoke 时scratchpad 重新从空列表开始history 则从存储里重新加载。5. 验证上下文是否按预期注入光看代码不够得实际验证。我常用的办法是开verboseTrue然后观察每次 LLM 调用时 prompt 的实际内容。AgentExecutor 在 verbose 模式下会打印出每轮发给模型的完整消息列表你能直接看到 history 和 scratchpad 被替换成了什么。更精确的做法是加一个自定义回调在 chain 开始时打印输入from langchain_core.callbacks import BaseCallbackHandler class PromptDebugHandler(BaseCallbackHandler): def on_llm_start(self, serialized, prompts, **kwargs): for i, p in enumerate(prompts): print(f LLM 第 {i1} 次调用prompt 长度 {len(p)} ) print(p[:800]) print( 截断 ) debug PromptDebugHandler()调用时挂上agent_with_memory.invoke( {input: 把上一步结果再乘以 3}, config{ configurable: {session_id: user_1}, callbacks: [debug], }, )验证要点有三个。第一第二次 invoke 时prompt 里应该能看到上一轮的 Human 和 AI 消息说明 history 注入成功。第二如果本轮触发了工具prompt 里应该能看到 ToolMessage说明 scratchpad 在单轮内正常累积。第三跨 invoke 时上一轮的工具输出不应该出现在新 prompt 里如果出现了说明你把 scratchpad 误写进了长期存储。还可以直接检查 memory 内容print(memory.messages)正常情况下这里只有成对的 Human 和 AI没有 ToolMessage。如果发现 ToolMessage 混进来了那就是记忆钩子被改过或者哪里手动 add 了。6. 本篇常见错排查报错一Input to ChatPromptTemplate is missing variables {history}原因通常是 prompt 里写了MessagesPlaceholder(history)但调用时没通过RunnableWithMessageHistory包装或者包装了但没传history_messages_keyhistory。检查包装器的参数名和占位符名是否完全一致大小写敏感。报错二多轮对话串会话典型表现是 user_1 的历史跑到了 user_2 的对话里。根因在工厂函数lambda session_id: memory返回了同一个全局 memory 对象所有 session 共用一份。正确做法是按 session_id 建字典或查 Redisstore {} def get_history(session_id: str): if session_id not in store: store[session_id] ChatMessageHistory() return store[session_id] agent_with_memory RunnableWithMessageHistory( agent_executor, get_history, input_messages_keyinput, history_messages_keyhistory, )报错三工具输出下一轮消失这不是 bug是默认行为。scratchpad 本来就不进长期存储。如果你确实需要跨轮保留工具细节得自定义记忆包装器重载_exit方法把本轮 scratchpad 一起写进 memory。但要注意这样会让 history 越来越长token 消耗上升很快建议配合截断策略。报错四max_iterations触顶后返回空Agent 陷入工具循环比如工具一直返回错误、模型反复重试。把max_iterations调大只是拖延真正要做的是让工具返回明确的失败信息并在 system prompt 里告诉模型“工具失败时直接告知用户不要重试”。报错五history 占位符位置放错有人把MessagesPlaceholder(history)放在agent_scratchpad之后结果历史消息被工具链消息隔开模型理解错乱。记住顺序system → history → user → agent_scratchpad这个顺序不要动。7. 下一步把记忆接进真实工程骨架跑通之后接下来就是工程化。长期存储从内存换成 Redis用RedisChatMessageHistory替换ChatMessageHistorysession_id 用真实用户 ID 或对话 ID。如果要做长期编码或 Agent 类应用可以考虑用 Coding Plan 把模型调用和额度管理统一起来 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。想先在线验证模型对话效果可以直接用模型对话页面试 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接入细节和参数说明在文档里 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你在用 Claude Code 这类工具Anthropic 兼容接入的说明在这里 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。最后留一个我实际调优时的小技巧在 system prompt 里显式告诉模型“history 是历史对话agent_scratchpad 是本轮工具记录”模型对两个占位符的利用会更准确尤其在工具调用密集的场景下能明显减少“忘记上一步结果”的情况。
返回列表