Skip to main content

02 · LangChain:抽象最全,也最容易被误解

仓库langchain-ai/langchain
Star144.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.xLangChain 1.x
核心抽象ChainLLMChainAgentExecutor、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

三个要点:

  1. 模型用字符串声明"openai:gpt-5.5""anthropic:claude-sonnet-4-6""google:gemini-2.5-flash"。换供应商改一个字符串,这是 LangChain 最实在的价值之一。
  2. 返回的是一个 LangGraph 图CompiledStateGraph),所以你天然拿到流式、checkpoint、interrupt —— 这些能力不是 LangChain 实现的,是它底下的 LangGraph 给的。
  3. 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 的每个缝隙里插代码,而不用把整个循环拆开重写。

Middleware 在 Agent Loop 中的位置

图片来源:LangChain Docs — Middleware

两类钩子

Node-style(顺序执行,适合日志、校验、改状态):

Hook触发时机
before_agentAgent 启动前(每次调用一次)
before_model每次调模型之前
after_model每次模型返回之后
after_agentAgent 结束后(每次调用一次)

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。

为什么中间件比继承一个 Agent 类好

中间件是可组合、可复用、可单测的横切关注点。「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,三层同一套概念

别用它,如果

  • 你只需要一个会调工具的循环 —— 那 OpenAI Agents SDKPydantic AI 更薄更直接
  • 你受不了依赖树 —— LangChain 的传递依赖一直是抱怨重灾区
  • 可观测性必须自建且不能外发 —— LangSmith 是它体验最好的路径,绕开会难受
  • 团队是 TypeScript 主导 —— LangChain.js(18.1k)功能滞后于 Python 版,Vercel AI SDK / Mastra 在 TS 世界更主流

八、和它的兄弟们

你的需求用哪个
快速搭一个会调工具的 Agent,团队统一写法LangChain create_agent
拓扑不是「循环到结束」,要自定义图 / 长时任务 / 断点续跑LangGraph
想要开箱即用的规划、文件系统、子智能体、上下文压缩DeepAgents

三者是同一套技术栈的三层,不是三个竞品。这一点在 01 分层坐标系 里有完整展开。


参考资料