02 · LangChain:抽象最全,也最容易被误解
| 仓库 | langchain-ai/langchain |
| Star | 144.5k(2026-08-19,全类目第 5、纯框架第 1) |
| 版本 | langchain 1.3.15 / langchain-core 1.5.6 |
| 语言 | Python(LangChain.js 18.1k star) |
| 许可证 | MIT |
| 层级 | Framework(跑在 LangGraph 运行时之上) |
| 一句话 | 生态最大的 Agent 框架;1.x 之后把「重抽象」换成了「薄 Harness + 中间件」 |
一、必须先说的事:你听到的差评大多是 0.x 的
LangChain 的口碑两极分化,但绝大多数批评(「抽象套娃」「调试地狱」「LCEL 看不懂」)针对的是 0.x 时代的 Chain / Agent / AgentExecutor。1.0 是一次破坏性重写:
| LangChain 0.x | LangChain 1.x | |
|---|---|---|
| 核心抽象 | Chain、LLMChain、AgentExecutor、LCEL 管道 | create_agent + Middleware |
| 心智模型 | 「把组件串成链」 | 「Agent = Model + Harness,Harness 由中间件拼」 |
| 运行时 | 自己实现的 executor | 编译成 LangGraph 图 |
| 旧代码 | —— | 搬进独立包 langchain-classic(1.0.8) |
如果你在 2024 年放弃过 LangChain,1.x 值得重新看一眼 —— 它现在更像 OpenAI Agents SDK,而不是 2023 年的自己。
二、核心抽象:create_agent
官方对它的定位是「a minimal, highly configurable agent harness」—— 一个最小但高度可配置的 Agent 脚手架。
from langchain.agents import create_agent
agent = create_agent(
# 模型用「供应商:模型名」的字符串声明,换供应商只改这一行
model="openai:gpt-5.5",
# 工具列表:模型自己决定什么时候调、调哪个、传什么参数
tools=[get_weather],
# 系统提示词:告诉模型它的身份和行为准则
system_prompt="You are a helpful assistant",
)
# 注意入参不是一个字符串,而是一份「状态」:messages 是完整对话历史。
# 这么设计是为了让 agent 能直接当作一个节点塞进更大的图里(见本文第四节)。
result = agent.invoke(
{"messages": [{"role": "user", "content": "What's the weather in San Francisco?"}]}
)
# 返回值也是完整状态,模型的回答在 result["messages"][-1].content
三个要点:
- 模型用字符串声明:
"openai:gpt-5.5"、"anthropic:claude-sonnet-4-6"、"google:gemini-2.5-flash"。换供应商改一个字符串,这是 LangChain 最实在的价值之一。 - 返回的是一个 LangGraph 图(
CompiledStateGraph),所以你天然拿到流式、checkpoint、interrupt —— 这些能力 不是 LangChain 实现的,是它底下的 LangGraph 给的。 invoke的入参是 state 而不是字符串,出参也是完整 state。这个设计让 agent 能直接当作一个节点塞进更大的图里。
工具就是加了装饰器的函数
from langchain.tools import tool
@tool # 这个装饰器把普通函数变成模型可调用的工具
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
# ↑ docstring 会原样成为工具描述交给模型 —— 模型就是靠这句话判断「该不该调它」
# ↓ 函数签名 (city: str) 被自动转成 JSON Schema,告诉模型该传什么参数
return f"{city} is sunny, 22°C"
签名和 docstring 自动变成 JSON Schema。工具描述写得好不好,直接决定 Agent 好不好用 —— 这一点参见 Anthropic 长期运行代理工具设计精读。
结构化输出
from pydantic import BaseModel
# 用 Pydantic 声明你想要的输出结构
class Ticket(BaseModel):
title: str
priority: int
assignee: str | None # 允许为空
# 传了 response_format,agent 的最终输出就不再是自由文本,
# 而是一个已经校验过的 Ticket 对象 —— 不用再自己写正则从回答里抠 JSON
agent = create_agent(model="openai:gpt-5.5", tools=[...], response_format=Ticket)
三、Middleware:LangChain 1.x 真正的差异化
这是 1.x 最值得学的设计。中间件让你在 Agent Loop 的每个缝隙里插代码,而不用把整个循环拆开重写。

两类钩子
Node-style(顺序执行,适合日志、校验、改状态):
| Hook | 触发时机 |
|---|---|
before_agent | Agent 启动前(每次调用一次) |
before_model | 每次调模型之前 |
after_model | 每次模型返回之后 |
after_agent | Agent 结束后(每次调用一次) |
Wrap-style(包在调用外面,适合重试、缓存、改写):
| Hook | 触发时机 |
|---|---|
wrap_model_call | 包住每次模型调用 |
wrap_tool_call | 包住每次工具调用 |
两者的关键区别:wrap-style 的 handler 由你决定调几次 —— 调 0 次是短路,1 次是正常,多次就是重试。
写一个自定义中间件
from langchain.agents.middleware import before_model, AgentState
from langchain.messages import AIMessage
from langgraph.runtime import Runtime
from typing import Any
# @before_model:每次调模型「之前」都会跑一遍这个函数
# can_jump_to=["end"]:声明它有权提前结束整个 Agent Loop
@before_model(can_jump_to=["end"])
def check_message_limit(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
# state["messages"] 是当前完整对话历史,这里做一道「聊太久就打住」的闸门
if len(state["messages"]) >= 50:
return {
# 返回值会被合并进状态:追加一条消息告诉用户为什么停了
"messages": [AIMessage("Conversation limit reached.")],
"jump_to": "end", # 直接跳到结束,这一轮模型不会被调用
}
return None # 返回 None = 什么都不改,正常往下走
重试用 wrap 风格:
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable
# @wrap_model_call:这个函数「包在」每次模型调用外面。
# handler 就是「真正去请求模型」这个动作 —— 调几次由你决定
@wrap_model_call
def retry_model(
request: ModelRequest, # 即将发给模型的请求
handler: Callable[[ModelRequest], ModelResponse], # 调它才会真的请求模型
) -> ModelResponse:
for attempt in range(3): # 最多试 3 次
try:
return handler(request) # 调 1 次 = 正常流程,成功就直接返回
except Exception as e:
if attempt == 2: # 第 3 次还失败就放弃,把异常抛给上层
raise
print(f"Retry {attempt + 1}/3 after error: {e}")
# 补充:handler 调 0 次就是「短路」(比如命中缓存直接返回结果),
# 这正是 wrap 风格比 before / after 风格能做的事更多的原因
内置中间件(不用自己写的部分)
| 中间件 | 作用 |
|---|---|
SummarizationMiddleware | 上下文超限时自动摘要历史 |
HumanInTheLoopMiddleware | 指定工具执行前中断等审批 |
LLMToolSelectorMiddleware | 工具太多时先用小模型筛一遍 |
ToolRetryMiddleware | 工具失败自动重试 |
ModelFallbackMiddleware | 主模型挂了自动切备用模型 |
ModelCallLimitMiddleware | 限制模型调用次数,防跑飞 |
| PII detection | 敏感信息检测与脱敏 |
from langchain.agents.middleware import SummarizationMiddleware, HumanInTheLoopMiddleware
agent = create_agent(
model="claude-sonnet-4-6",
tools=[read_email, send_email],
middleware=[
# 上下文快满时自动把老对话摘要压缩,避免超出模型窗口
SummarizationMiddleware(...),
# 模型每次想调 send_email 都先停下等人批准;read_email 不拦(读操作无害)
HumanInTheLoopMiddleware(interrupt_on={"send_email": True}),
],
)
# 中间件按顺序生效,像洋葱一样一层层包住 Agent Loop
interrupt_on={"send_email": True} 这一行的含义是:模型每次想发邮件,都会先停下来,把这次调用的参数交给人看,人批准了才发。 这是本专题第 01 篇 D6 维度里的「第 3 档」HITL。
中间件是可组合、可复用、可单测的横切关注点。「PII 脱敏」「限流」「重试」这些逻辑,在图里写要污染节点,在子类里写会随着需求叠加变成一坨。中间件的设计几乎照搬了 Web 框架(Express / Django middleware)的成熟经验。
四、Agent 可以塞进更大的图
这是 LangChain 和其他框架最不同的一点:create_agent 的产物是一个 LangGraph 节点,可以直接被更大的确定性流程包住。
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.graph import START, StateGraph
# 先造一个普通 agent —— 和前面几段代码完全一样,没有任何特殊写法
email_agent = create_agent(
model="claude-sonnet-4-6",
tools=[read_email, send_email],
middleware=[HumanInTheLoopMiddleware(interrupt_on={"send_email": True})],
)
# 关键在这里:这个 agent 可以直接当成图里的「一个节点」
graph = (
StateGraph(AgentState) # 建图,状态类型沿用 AgentState
.add_node("classify", classify_node) # 节点 1 · 确定性代码:先给邮件分类
.add_node("email_agent", email_agent) # 节点 2 · 把整个 agent 塞进来当一步
.add_edge(START, "classify") # 入口固定走分类
.add_conditional_edges("classify", route) # route() 返回下个节点名,实现分支
.compile() # 编译成可执行的图
)
# 效果:外层流程完全由代码控制,只有「处理邮件」这一步交给模型自主发挥。
# agent 身上挂的中间件(审批、摘要、重试)会原样跟着一起进图。
对应到 01 的 D2 维度,这就是「混合控制」的标准写法 —— 外层流程写死,内层交给模型。所有中间件(HITL 中断、摘要、脱敏、重试)都跟着 agent 节点一起走进大图。
五、包结构:知道该装什么
LangChain 1.x 把包拆得很细,装错包是新手最常见的坑:
| 包 | 里面是什么 | 什么时候要 |
|---|---|---|
langchain-core (1.5.6) | 消息、工具、Runnable 等基础抽象 | 永远会被间接装上 |
langchain (1.3.15) | create_agent + 中间件 | 写 Agent 就要 |
langgraph (1.2.11) | 图运行时 | 要自定义拓扑时直接用 |
langchain-openai / langchain-anthropic / ... | 各家模型集成 | 按用到的供应商装 |
langchain-mcp-adapters (0.3.2) | 把 MCP server 变成 LangChain 工具 | 要接 MCP 时 |
langgraph-checkpoint-postgres (3.1.2) | Postgres 持久化 | 上生产时 |
langchain-classic (1.0.8) | 0.x 的 Chain / 旧 Agent | 只在迁移老代码时 |
langsmith (0.11.0) | 追踪与评测客户端 | 想看 trace 时 |
六、可观测性:绑在 LangSmith 上
LangChain 本身不自带 trace UI。设一个环境变量就能把全链路发到 LangSmith:
# 打开追踪开关,之后每次 agent 运行都会自动上报
export LANGSMITH_TRACING=true
# 你的 LangSmith API Key,在 smith.langchain.com 的设置页获取
export LANGSMITH_API_KEY=lsv2_...
这是 LangChain 全家桶里最「商业」的一环:开源框架免费,可观测性和部署平台收费。 如果公司不允许把 prompt 发到外部 SaaS,可以走 OpenTelemetry 导出自建,但体验和 LangSmith 差一截 —— 相比之下 OpenAI Agents SDK 和 Agno 的自带 tracing 门槛更低。
七、什么时候用 / 什么时候别用
用它,如果
- 你要接的模型 / 向量库 / 工具很杂 —— 集成数量是 LangChain 无可争议的护城河,没有第二家接近
- 团队要一套统一写法 ——
create_agent+ 中间件让不同人写出来的 Agent 长得一样 - 你的流程是「确定性外壳 + 自主内核」 —— agent 当节点塞进 StateGraph 这条路径没有对手
- 你想要渐进式下沉 —— 不够用了就掉到 LangGraph,需要全家桶就升到 DeepAgents,三层同一套概念