LangChain 初阶教程:内存与中间件
LangChain 官方教程,讲解如何给无状态 Agent 添加记忆和中间件,适合初学者但深度有限。
LangChain 官方教程,讲解如何给无状态 Agent 添加记忆和中间件,适合初学者但深度有限。
我们此前构建的每个 Agent,都会在 invoke() 返回的那一刻忘掉一切。向它追问一个问题,它完全不知道你们刚才聊了什么。本文将用 memory 解决这个问题,然后加入 middleware——这是 LangChain 用来约束 Agent 可以执行哪些操作的机制。
第 1 部分——什么是 LangChain,以及你的第一个脚本
第 2 部分——Prompt、模型与 LCEL chain
第 3 部分——工具与你的第一个 Agent
第 4 部分——RAG:读取你自己的文档
第 5 部分(本文)——Memory 与 middleware
第 6 部分——使用 LangSmith 调试和观察 Agent
每次调用 agent.invoke(...),Agent loop 都会从一张白纸开始运行——LangChain 默认没有提供一个内置位置,用于在多次调用之间保存之前的消息。为了解决这个问题,LangChain Agent 支持 checkpointer:它会在每个步骤结束后保存对话状态,并使用你指定的 thread_id 作为键。
from dotenv import load_dotenv
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
load_dotenv()
checkpointer = InMemorySaver()
agent = create_agent(
"gpt-5",
checkpointer=checkpointer,
system_prompt="You are a helpful, concise assistant.",
)
config = {"configurable": {"thread_id": "conversation-1"}}
agent.invoke(
{"messages": [{"role": "user", "content": "Hi! My name is Ramesh."}]},
config=config,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What's my name?"}]},
config=config,
)
print(result["messages"][-1].content)
第二次调用能够正确回答“Ramesh”。尽管这是一次独立的 invoke() 调用,但由于两次调用共享同一个 thread_id,Agent 仍然记得之前的信息。换一个 thread_id,你就会得到一段全新的对话。在实际应用中,你正是通过这种方式,为每个用户或每个聊天会话隔离 memory。
InMemorySaver 会将所有内容保存在当前进程的内存中,因此脚本一旦退出,这些内容就会消失。如果你希望数据在重启后仍然存在,可以把它替换为 PostgresSaver 或其他持久化 checkpointer。本系列一直遵循同样的模式:只改一行,其余代码保持不变。
长对话最终会超出模型的 context window,而且每一轮都会增加成本。LangChain 的 @before_model hook 允许你在消息列表发送给模型之前,对其进行裁剪:
from typing import Any
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langchain.agents import AgentState
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
@before_model
def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""Keep only the most recent messages to stay within context limits."""
messages = state["messages"]
if len(messages) <= 6:
return None # nothing to trim yet
first_msg = messages[0]
recent_messages = messages[-5:]
return {
"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), first_msg, *recent_messages]
}
agent = create_agent(
"gpt-5",
checkpointer=checkpointer,
middleware=[trim_messages],
)
这就是 middleware:它是一个在 Agent loop 的特定节点进行拦截的函数。这里的拦截点是每次调用模型之前,它可以借此修改接下来要发生的事情。
对于生产环境中的 Agent,最重要的 middleware 模式就是 human-in-the-loop。如果某个工具能够发送电子邮件、扣款或删除数据,通常不应该让模型在无人监督的情况下执行这些操作。
from langchain.agents.middleware import HumanInTheLoopMiddleware
agent = create_agent(
"gpt-5",
tools=[send_email, get_weather],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {"allowed_decisions": ["approve", "reject"]},
}
)
],
checkpointer=checkpointer,
)
完成这项配置后,每当 Agent 决定调用 send_email 时,执行过程都会暂停,并将控制权交还给你的应用程序,而不是立即运行该工具。随后,你的应用可以向人工审核者展示拟发送的邮件,只有在获得批准后才真正发送。Agent 无法绕过这道关卡,因为限制是在模型控制范围之外强制执行的,而不是仅仅通过 prompt 要求模型遵守。
注意,get_weather 并没有出现在 interrupt_on 中。只有你在这里指定的工具才需要经过审批,因此低风险工具仍然可以自动运行。
一个达到生产级水准的客服 Agent,可能会将本系列目前介绍的所有内容组合起来:
agent = create_agent(
"gpt-5",
tools=[search_company_docs, send_email], # Part 3 & 4
system_prompt="You are a support assistant...", # Part 2
checkpointer=checkpointer, # memory, this part
middleware=[trim_messages, HumanInTheLoopMiddleware(
interrupt_on={"send_email": {"allowed_decisions": ["approve", "reject"]}}
)], # guardrails, this part
)
仅用大约十行代码,就得到了一个真正具备生产级形态的 Agent:它能记住对话,基于你的文档生成有依据的回答,而且没有人工批准就无法发送电子邮件。
在本系列的最后一篇文章——第 6 部分中,我们介绍了如何真正看清 Agent 正在做什么:使用 LangSmith 追踪每一次模型调用和工具调用,并利用这种可观测性,调试那些不可避免的问题,例如 Agent 选择了错误的工具,或者陷入了循环。
本文是 LangChain 六部分入门系列的第 5 部分。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。