← 回到知识图谱
实践持续完善

智能体编排框架横评(1):LangChain 1.x

本文总结LangChain 入门:装好包写出第一个 Agent,再逐个讲模型、工具、消息、结构化输出、记忆和中间件,最后一步步写出建单前要员工确认的报销 Agent。

创建 2026-11-05更新 2026-11-06阅读约 30 分钟
Agent框架LangChain

智能体编排框架横评 第 1 站 · 上一站:总览 · 下一站:LangGraph · 全程:总览

LangChain 是什么

LangChain 是一个用来开发大模型应用的 Python 库(也有 JS 版)。它做两件事:

  1. 统一各家模型的接口。 OpenAI、Anthropic、Google 等厂商的 API 格式各不相同,LangChain 把它们包成同一套调用方式,换模型时业务代码基本不用改。
  2. 提供一个现成的 Agent。 用 create_agent 传入模型和工具,就得到一个会自己决定调哪个工具、调完再看结果、直到能回答为止的 Agent。

create_agent 内部跑的是一个固定的循环:

  1. 把对话发给模型;
  2. 模型如果要求调用工具,就执行工具,把结果加进对话,回到第 1 步;
  3. 模型不再要求调用工具,就把它的回答返回给你。

这个循环你不能改形状,但可以通过中间件在循环的固定位置插入自己的逻辑,比如调用工具前先让人确认、对话太长时自动摘要。

LangChain 和下一站 LangGraph 的关系:create_agent 编译出来就是一张 LangGraph 图,记忆、暂停、恢复这些能力都来自 LangGraph。只需要"模型加工具"时用 LangChain;需要自己规定流程的分支和并行时,到 智能体编排框架横评(2):LangGraph 直接写图。

安装

pip install -U langchain langchain-openai

langchain 是主包,langchain-openai 是 OpenAI 模型的集成包。用别家模型就装对应的包,比如 langchain-anthropic、langchain-google-genai。

然后把 API key 设成环境变量:

export OPENAI_API_KEY="sk-..."

本文的代码按 LangChain 1.x 的 Python 官方文档写,是示意代码,没有运行过。

第一个 Agent

写一个能查报销制度的 Agent。

first_agent.py 文件代码:

from langchain.agents import create_agent

def search_policy(query: str) -> str:
    """查报销制度条款"""
    return "市内打车单次标准 80 元"          # 示意,真实情况查数据库或知识库

agent = create_agent(
    model="openai:gpt-5.5",                # "厂商:模型名",可以换成别的模型
    tools=[search_policy],
    system_prompt="你是公司的报销助手。",
)

result = agent.invoke({"messages": [{"role": "user", "content": "打车报销标准是多少?"}]})
print(result["messages"][-1].content)

输出结果类似:

根据公司制度,市内打车单次报销标准是 80 元。

这次运行里,模型先发现需要查制度,调用了 search_policy,拿到"80 元"后再组织成回答。result["messages"] 里保存了完整过程:用户的问题、模型的工具调用、工具结果、模型的最终回答,最后一条就是回答。

基本概念

模型

model 参数可以直接写字符串 "厂商:模型名",也可以先创建一个模型对象再传进去,方便设置温度等参数:

from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-5.5", temperature=0)
agent = create_agent(model=model, tools=[search_policy])

模型对象也能单独使用,不经过 Agent:

print(model.invoke("用一句话介绍你自己").content)

工具

工具就是 Python 函数。模型靠两样东西决定什么时候调用、怎么传参数:函数的 docstring(当作工具说明)和参数的类型提示(当作参数格式)。所以这两样都要写清楚。

可以直接传函数,也可以用 @tool 装饰器,后者能自定义工具名和说明:

from langchain.tools import tool

@tool
def get_invoice(invoice_id: str) -> dict:
    """按发票号查金额和报销标准"""
    return {"id": invoice_id, "amount": 118.5, "limit": 80}

print(get_invoice.name)          # get_invoice
print(get_invoice.description)   # 按发票号查金额和报销标准

消息和状态

Agent 的输入输出都是一个字典,核心字段是 messages,也就是对话记录。消息有几种角色:

角色 类 内容
用户 HumanMessage 用户说的话
模型 AIMessage 模型的回答,或者它要调用的工具(在 .tool_calls 里)
工具 ToolMessage 工具的执行结果,用 tool_call_id 对应到是哪一次调用
系统 SystemMessage 系统提示词

传入时可以写成 {"role": "user", "content": "..."} 这样的字典,LangChain 会自动转换。想看 Agent 一路做了什么,把消息挨个打印出来:

for m in result["messages"]:
    m.pretty_print()

结构化输出

有时你要的不是一段文字,而是一个能被程序直接使用的结果。用 response_format 传一个 Pydantic 模型:

from pydantic import BaseModel

class Verdict(BaseModel):
    reimbursable: bool     # 能不能报
    over_amount: float     # 超标多少元
    reason: str

agent = create_agent("openai:gpt-5.5", tools=[search_policy, get_invoice], response_format=Verdict)
r = agent.invoke({"messages": [{"role": "user", "content": "INV-102 能报吗?"}]})
print(r["structured_response"])

输出结果类似:

reimbursable=True over_amount=38.5 reason='超出标准 38.5 元,需要说明原因后提交'

模型支持原生结构化输出时,LangChain 直接用模型的功能;不支持时,它把格式包装成一个特殊工具让模型调用。两种情况你的写法一样。

记忆:checkpointer 和 thread_id

默认情况下每次 invoke 都是全新的对话,Agent 不记得上一次说了什么。要让它记住,编译时传一个 checkpointer,调用时用 thread_id 区分不同的对话:

from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent("openai:gpt-5.5", tools=[search_policy], checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": "emp-001"}}

agent.invoke({"messages": [{"role": "user", "content": "我叫小王"}]}, cfg)
r = agent.invoke({"messages": [{"role": "user", "content": "我叫什么?"}]}, cfg)
print(r["messages"][-1].content)   # 你叫小王

InMemorySaver 存在内存里,进程退出就没了。上线时换成存数据库的版本,比如 PostgresSaver。checkpointer 也是后面"暂停等人确认"的前提。

跨对话的长期数据(比如员工的偏好)用 store= 参数,见 智能体编排框架横评(2):LangGraph 的 Store 一节。

中间件

中间件是挂在 Agent 循环上的钩子。LangChain 内置了一批常用的:

中间件 作用
HumanInTheLoopMiddleware 指定的工具执行前暂停,等人批准
SummarizationMiddleware 对话太长时把旧消息压缩成摘要
ModelCallLimitMiddleware 限制调用模型的次数,防止死循环
ToolCallLimitMiddleware 限制调用工具的次数
ModelRetryMiddleware / ToolRetryMiddleware 模型或工具出错时自动重试
PIIMiddleware 检测并处理手机号、邮箱这类个人信息

用法是把它们放进 middleware 列表:

from langchain.agents.middleware import ModelCallLimitMiddleware

agent = create_agent(
    "openai:gpt-5.5",
    tools=[search_policy],
    middleware=[ModelCallLimitMiddleware(run_limit=10)],   # 一次运行最多调 10 次模型
)

也可以自己写。最简单的是用装饰器,下面这个在每次调用模型前打印消息条数:

from langchain.agents.middleware import before_model

@before_model
def log_size(state, runtime):
    print(f"调用模型前,共 {len(state['messages'])} 条消息")
    return None            # 返回字典会合并进状态,返回 None 表示不改

agent = create_agent("openai:gpt-5.5", tools=[search_policy], middleware=[log_size])

钩子一共有六个位置:

  • before_agent / after_agent:一次运行开始前、结束后;
  • before_model / after_model:每次调用模型前、后;
  • wrap_model_call / wrap_tool_call:把一次模型调用或工具调用包起来,可以在前后加逻辑、重试,或者干脆不调用。

动手:一步步写报销 Agent

场景见 主线:一个报销请求走完整个 Agent 系统:员工问"上周打车超标还能报吗,能就帮我提单"。三个工具里只有 create_ticket 会真正改数据,所以只有它需要员工确认。

第 1 步:准备三个工具

from langchain.tools import tool

@tool
def search_policy(query: str) -> str:
    """查报销制度条款"""
    return "市内打车单次标准 80 元"

@tool
def get_invoice(invoice_id: str) -> dict:
    """按发票号查金额和报销标准"""
    return {"id": invoice_id, "amount": 118.5, "limit": 80}

@tool
def create_ticket(invoice_id: str, reason: str) -> str:
    """提交报销单,会真正建单"""
    return "R-55821"

第 2 步:创建 Agent,给建单加确认

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    "openai:gpt-5.5",
    tools=[search_policy, get_invoice, create_ticket],
    system_prompt="先查制度和发票,再判断能不能报。",
    middleware=[HumanInTheLoopMiddleware(interrupt_on={
        "create_ticket": {"allowed_decisions": ["approve", "reject"]},
    })],
    checkpointer=InMemorySaver(),      # 暂停要靠它保存状态,必须有
)

interrupt_on 列出要拦的工具,没列出的工具照常执行。allowed_decisions 规定员工能做哪些选择,这里只给"批准"和"拒绝",不允许员工改金额。

为什么不在提示词里写"建单前先问员工"?提示词只是建议,模型可能不照做。中间件是在代码里拦截的:模型提出调用 create_ticket 之后、工具真正执行之前,Agent 一定会停下来。

第 3 步:运行,停在建单前

cfg = {"configurable": {"thread_id": "emp-001"}}
r = agent.invoke(
    {"messages": [{"role": "user", "content": "上周打车超标还能报吗,能就帮我提单"}]},
    config=cfg, version="v2",
)
print(r.interrupts)

version="v2" 让返回值带一个 .interrupts 属性。模型查完制度和发票、决定建单时,Agent 暂停,.interrupts 里是这次待批准的工具调用和参数,把它展示给员工。

第 4 步:员工确认后恢复

from langgraph.types import Command

agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=cfg, version="v2")

用同一个 thread_id 恢复,Agent 执行 create_ticket,再让模型组织回复。恢复可以发生在几分钟之后,也可以在另一个进程里,只要 checkpointer 用的是持久化的存储。

第 5 步:员工拒绝

agent.invoke(
    Command(resume={"decisions": [{"type": "reject", "message": "这张发票我自己再核对一下"}]}),
    config=cfg, version="v2",
)

拒绝时工具不执行,模型会收到拒绝原因,再决定怎么回复员工。

下面这张图画的是 create_agent 的一圈循环和各个钩子的位置。点开 HumanInTheLoopMiddleware 可以看到建单调用怎么被拦下、员工的决定怎么送回循环。

create_agent 里一圈循环,中间件挂在哪可缩放的动画图,启用 JavaScript 后可以播放和放大。
  1. 员工提问,进入循环前先跑一次 before_agent
  2. 每次调模型前跑 before_model,可以在这里裁剪消息
  3. 模型返回后跑 after_model,建单前的确认就挂在这里
  4. 放行的工具调用交给工具节点执行
  5. 工具结果追加进 messages,回到 before_model 再问一次
  6. 模型不再调工具时,跑 after_agent,然后回复员工

after_model的细节:

  1. 模型返回后,after_model 先看它提出了哪些工具调用
  2. 两个只读工具不在名单里,直接放行
  3. create_ticket 在 interrupt_on 里,调用 interrupt() 暂停
  4. 暂停前的状态写进检查点,invoke 带着 interrupts 返回
  5. 员工看过真实参数,前端用同一个 thread_id 恢复
  6. approve:这次调用原样交给工具节点执行
  7. reject:不执行,拒绝原因作为工具结果交给模型

进阶

人在回路的更多选项

allowed_decisions 一共有四种:

决定 效果
approve 按原参数执行
edit 改参数后执行
reject 不执行,把原因告诉模型
respond 不执行,把员工写的一段文字当成工具结果交给模型

respond 要小心:模型会把那段文字当作工具成功执行的结果。所以不能用 respond 来拒绝建单,否则模型会以为单子已经建好了。

还有两个常用参数:

  • when:一个判断函数,按参数决定要不要拦,比如金额超过 500 元才要审批。恢复时它会重新执行一次,所以结果必须固定,不能依赖时间、随机数这类会变的东西。
  • interrupt_mode="per_call":模型一次提出多个待审批的调用时,每个调用单独暂停、单独批复。默认是 batched,一次批复全部。

中间件的执行顺序

middleware 列表里的顺序有意义:

  • before_* 按列表顺序执行;
  • after_* 按列表倒序执行;
  • wrap_* 层层嵌套,列表第一个在最外层,包住后面所有的。

官方建议 HumanInTheLoopMiddleware 排在 ToolRetryMiddleware 之类的工具中间件前面,否则会出现先重试、后审批的错位。

中间件是跑在 LangGraph 图里的,所以暂停恢复时,排在人在回路前面的中间件会再执行一遍。这些中间件在调用下一层之前不要有副作用。

运行时上下文

用户 ID、数据库连接这类不属于对话、但工具要用的东西,不要塞进消息里,用 context 传:

from dataclasses import dataclass

@dataclass
class Ctx:
    user_id: str

agent = create_agent("openai:gpt-5.5", tools=[...], context_schema=Ctx)
agent.invoke({"messages": [...]}, context=Ctx(user_id="emp-001"))

工具和中间件可以通过 runtime 读到它。

多个 Agent 怎么组合

官方文档列了五种模式:

  • Subagents:主 Agent 把子 Agent 包成工具来调用,最常用;
  • Handoffs:通过工具调用切换当前由哪个 Agent 回答;
  • Skills:按需加载额外的提示词和知识;
  • Router:先分类,再分发给不同的 Agent;
  • Custom workflow:用 LangGraph 自己写流程。

官方也提醒,很多场景其实一个 Agent 就够了。和其他框架的比较见 智能体编排框架横评(4):多智能体框架。

观察 Agent 在做什么

接 LangSmith 只需设两个环境变量:

export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="..."

之后每次运行的模型调用、工具调用、耗时都能在 LangSmith 网页上看到。不想用 LangSmith,可以装 langsmith[otel] 走 OpenTelemetry,送到自己的追踪系统;Langfuse 这类开源工具通过 callbacks 接入,写法是 config={"callbacks": [CallbackHandler()]}。该看哪些信号见 可观测性:追踪一条任务的代价。

接 MCP 工具

MCP 是一种让工具和 Agent 解耦的协议,见 技能、MCP 与集成。LangChain 1.4 起内置了 MCP 支持,装 pip install "langchain[mcp]" 后用 from langchain.mcp import MCPAdapter,它把 MCP 服务器上的工具转成普通的 LangChain 工具,再传给 create_agent。这部分目前还是 beta,导入时会有警告。

常见问题

模型不调用工具,或者参数传错。 先检查 docstring 和类型提示。工具说明写得含糊,模型就不知道什么时候该用。

Agent 停不下来,一直调工具。 加 ModelCallLimitMiddleware 或 ToolCallLimitMiddleware 设上限,它们默认都不限制次数。

工具抛异常,整个运行就中断了。 想让模型看到错误信息自己换个办法,加 ToolErrorMiddleware,它把异常转成一条错误 ToolMessage 交给模型。

人在回路不生效。 检查有没有传 checkpointer,恢复时 thread_id 是否和第一次一样。

审批之后单子建了两张。 审批只管"要不要执行",不管"执行了几次"。重试、恢复都可能让工具再跑一次,create_ticket 自己要带幂等键,见 可靠执行:状态、幂等与恢复。

照着旧教程写,报 AgentExecutor 找不到。 见下面的版本说明。

版本说明

LangChain 1.0 在 2025-10-22 发布,本文按 2026-11 时的 1.4 系列文档写。1.0 是一次大调整,网上很多教程还是 0.x 的写法,对照时注意:

  • 旧的各种 chain 和 AgentExecutor、initialize_agent 都移到了 langchain-classic 包,旧代码装这个包、改 import 还能跑。新代码统一用 create_agent。
  • LangGraph 里原来的 create_react_agent 就是现在的 create_agent,参数 prompt= 改名 system_prompt=,pre_model_hook 这类钩子改成了中间件。
  • 自定义状态只支持 TypedDict,Python 最低 3.10。
  • MCP 以前要装独立的 langchain-mcp-adapters,1.4 起并进了主包。

JS 版的用法基本一致,名字改成驼峰(createAgent、humanInTheLoopMiddleware),schema 用 zod。

和手写循环的对照

跟读 learn-claude-code(1):循环内核 里 s01 的循环只有一个出口:模型这一轮没有要求调工具就停。create_agent 的循环骨架和它一样,差别在骨架外面。

手写 harness(s01 到 s04) LangChain 1.x
while 循环,没有 tool_use 就停 create_agent 的 model 节点和 tools 节点
tool_use 和 tool_result 按 id 配对 AIMessage.tool_calls 和 ToolMessage 按 tool_call_id 配对,框架代做
工具出错返回字符串给模型 需要加 ToolErrorMiddleware
UserPromptSubmit 钩子 before_agent
PreToolUse 加三道闸门 wrap_tool_call 前半段,或 HumanInTheLoopMiddleware
PostToolUse 只能看 wrap_tool_call 后半段,可以改写结果
Stop 钩子否决结束 after_model 返回 jump_to
压缩 SummarizationMiddleware
没有检查点 checkpointer 加 thread_id,来自 LangGraph

几个值得对照的地方:

钩子的组合方式不同。 s04 的钩子是平铺列表,遇到第一个有返回值的就停。LangChain 的 before_*、after_* 返回字典合并进状态,不会提前停;wrap_* 是一层包一层,调用下一层之前相当于 PreToolUse,之后相当于 PostToolUse,不调用就等于拦截。

Stop 的方向相反。 s04 的 Stop 钩子在模型想结束时触发,可以否决结束,让它继续干活。LangChain 的 after_model 加 jump_to 常用来提前跳到结束,也可以跳回 model 重来,用后者能做出类似 s04 否决结束的效果。目标闸门见 固定编排与目标闸门:Workflow 运行时和 Goal Loop。

权限只覆盖一部分。 s03 有三道闸门:硬拒绝、规则、问用户。人在回路中间件只对应第三道,前两道要自己写成 wrap_tool_call 中间件,或者用 when 表达规则。

业务判断仍然在你手上。 工具说明写得好不好,哪个工具算有副作用,审批界面展示什么,超标判断写在工具里还是交给模型,框架都不管。工具调用的标准形态见 工具调用。

自测

create_agent 内部的循环是怎么走的?什么时候结束?

参考:把对话发给模型;模型要求调工具就执行工具、把结果加进对话再发给模型;模型不再要求调工具时,把它的回答返回。

模型靠什么决定调用哪个工具、传什么参数?

参考:工具名、docstring(工具说明)和参数的类型提示。这几样写不清楚,模型就容易不调或者调错。

想让 Agent 记住上一轮对话,要做哪两件事?

参考:创建时传 checkpointer(比如 InMemorySaver),调用时在 config 里给同一个 thread_id。

报销例子里,为什么建单前的确认要交给 HumanInTheLoopMiddleware,而不是写在 system_prompt 里?为什么不开 respond?

参考:提示词只是建议,模型可能直接调用 create_ticket;中间件在模型提出调用之后、工具执行之前暂停,拦截发生在代码里。respond 的文字会被当成工具成功的结果交给模型,拿它拒绝建单,模型会以为单子已经建好了。

中间件列表是 [ToolRetryMiddleware(...), HumanInTheLoopMiddleware(...)],会出什么问题?

参考:wrap_* 按列表嵌套,第一个在最外层,于是重试包在审批外面,和官方建议的顺序相反。应该把人在回路排在前面。另外恢复时排在人在回路前面的中间件会再执行一遍,它们在调用下一层之前不能有副作用。

照着 0.x 的教程写了 AgentExecutor,在 1.x 里怎么办?

参考:装 langchain-classic 并改 import 可以继续用;新代码改用 langchain.agents.create_agent,中间件等新功能只在它上面。

参考资料

以下链接均查阅于 2026-11。

连接

这是一篇可以继续生长的笔记。沿着关系继续阅读 ↗