主流 Agent 框架
LangChain / LangGraph 深度剖析
LangChain 是 LLM 应用框架里最被"恨爱交织"的——抽象层太厚被骂,但生态大到没有替代品。2024 年它把 Agent 能力单独拆成 LangGraph,从"链式编排"转向"状态机编排",这是 Agent 工程化的重要转折。本文讲清两者的边界,以及什么场景该选哪个。
本文边界:聚焦 LangChain 和 LangGraph 的架构演进与核心抽象。RAG 实战见 RAG 章节;工具调用规范见 Function Calling;多 Agent 编排见 LangGraph 多 Agent。
面试官想考什么
读完这篇你要能正面回答下面这些题。每题后面括号里是面试官真正想看你答出什么。
为什么需要 LangChain
2022 年底 ChatGPT 火了之后,每个想接 LLM 的开发者都遇到同样的事:
# 原始的 LLM 调用:每个 provider 一套 API
import openai
response = openai.chat.completions.create(model="gpt-4o", messages=[...])
import anthropic
response = anthropic.messages.create(model="claude-3-5-sonnet", messages=[...])
# 想做个 RAG?要自己实现:
# - 文档切分(怎么切?按段落?按 token?滑动窗口?)
# - embedding(哪家?OpenAI/Cohere/BGE?维度多少?)
# - 向量库(Pinecone/Chroma/Weaviate/FAISS?)
# - 检索(top-k?相似度阈值?混合检索?)
# - prompt 拼接(怎么塞 context?格式?)
# - 输出解析(JSON?Markdown?要不要 retry?)每个步骤都有十几种选择,每种选择又有几个 provider。一个 RAG 应用要写 500 行胶水代码。LangChain 的价值是把这些步骤抽象成标准化的组件——你换 vector store 不用改业务逻辑,换 LLM provider 不用重写 prompt 管线。
但 LangChain 早期(2023 上半年)的抽象方向有问题——它发明了太多自己的概念(Chain、Tool、AgentExecutor、CallbackHandler),开发者要学一套全新的"LangChain DSL"才能调通最简单的功能。2023 年下半年 LangChain 做了一次重大重构,引入 LCEL(LangChain Expression Language),用 | 操作符替代了大部分嵌套类调用。
# 重构前(被骂的 LangChain)
chain = LLMChain(
llm=OpenAI(),
prompt=PromptTemplate(template="..."),
output_parser=PydanticOutputParser(...),
memory=ConversationBufferMemory(),
callbacks=[StreamingStdOutCallbackHandler()]
)
result = chain.run(input_text="hello")
# 重构后(LCEL)
chain = prompt | model | output_parser
result = chain.invoke({"input_text": "hello"})LCEL 看起来只是语法糖,但它带来了真正的工程价值——任何 Runnable 自动获得 invoke / batch / stream / ainvoke / abatch / astream 六种调用方式,pipe 上下游自动并行执行,整条 chain 可以通过 LangSmith 追踪每一步。
LCEL 的核心:Runnable 协议
LCEL 的底层是 Runnable 协议——所有可以"运行"的东西(LLM、Prompt、Parser、Retriever、自定义函数)都实现这个接口:
class Runnable:
def invoke(self, input): ... # 单次同步调用
def batch(self, inputs): ... # 批量并行
def stream(self, input): ... # 流式输出
async def ainvoke(self, input): ... # 异步版本
async def abatch(self, inputs): ...
async def astream(self, input): ...实现了 Runnable 协议就能用 | 串联。但 LCEL 不只是串联——它还提供了几个组合原语:
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
# 并行执行两条分支
parallel = RunnableParallel({
"context": retriever,
"question": RunnablePassthrough(),
})
# 完整的 RAG chain
rag_chain = (
parallel # 同时跑:检索 + 透传问题
| prompt # 拼接 prompt
| llm # 调用 LLM
| StrOutputParser() # 解析输出
)
# 一句话获得:
# - invoke:单次 RAG 查询
# - batch:批量 RAG(100 个问题并发)
# - stream:流式输出 token
# - 全程 LangSmith 追踪
result = rag_chain.invoke("LangChain 是什么?")这是声明式 vs 命令式的差距——你描述"数据流是什么样",框架决定"怎么执行最快"。batch 模式会自动把 100 个独立查询并发执行,stream 模式会让最后一个 LLM 节点直接流式 yield token——你不需要关心这些细节。
从 AgentExecutor 到 LangGraph:为什么必须重写
2023 年的 LangChain 有个组件叫 AgentExecutor,封装了 ReAct 风格的 Agent 循环:
# 老的 AgentExecutor(已弃用)
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
result = executor.invoke({"input": "查一下今天的天气"})看起来很简洁,但生产环境用起来全是坑:
问题 1:Agent 调用工具失败时,AgentExecutor 自己重试,
重试 5 次都失败才报错,你想中途介入改 prompt 做不到。
问题 2:Agent 想做一件需要 30 步的复杂任务,
在第 15 步崩了。重启后从第 1 步开始——
因为 AgentExecutor 没有持久化中间状态。
问题 3:你想加 human-in-the-loop,
让用户审批某些操作再继续。AgentExecutor 是黑盒 while 循环,
你只能让它一口气跑完。
问题 4:你想让两个 Agent 协作(一个写代码、一个 review),
发现 AgentExecutor 是为单 Agent 设计的,
多 Agent 要自己实现消息传递。本质问题:AgentExecutor 把 Agent 状态机藏在了一个 while 循环里,外部无法观测、无法中断、无法持久化。这种黑盒在 demo 阶段没问题,到生产就是地狱。
LangChain 团队 2024 年初推出 LangGraph 来解决这些问题。核心思路:把 Agent 的执行流程显式建模为状态机(图),每个节点是一次 LLM 调用或工具执行,边描述状态转移。
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
from operator import add
# 1. 定义 State——Agent 的所有状态
class AgentState(TypedDict):
messages: Annotated[list, add] # Reducer:新消息追加到列表
iteration: int
# 2. 定义节点——每个节点是一次状态转移
def call_model(state: AgentState) -> AgentState:
response = llm.invoke(state["messages"])
return {"messages": [response], "iteration": state["iteration"] + 1}
def call_tool(state: AgentState) -> AgentState:
last_message = state["messages"][-1]
tool_result = execute_tool(last_message.tool_calls[0])
return {"messages": [tool_result]}
# 3. 定义路由——根据 state 决定下一步
def should_continue(state: AgentState) -> str:
if state["messages"][-1].tool_calls:
return "tool"
return END
# 4. 组装图
graph = StateGraph(AgentState)
graph.add_node("model", call_model)
graph.add_node("tool", call_tool)
graph.set_entry_point("model")
graph.add_conditional_edges("model", should_continue, {"tool": "tool", END: END})
graph.add_edge("tool", "model")
app = graph.compile()
result = app.invoke({"messages": [HumanMessage("查询天气")], "iteration": 0})代码量比 AgentExecutor 多,但每一步都是显式可见的。这种"啰嗦"换来了:
- 可观测:每个节点的输入输出都能记录、回放
- 可中断:在任意节点暂停,等待外部输入再继续
- 可持久化:State 是普通 Python 对象,序列化保存到数据库
- 可组合:图可以嵌套——一个节点本身也可以是一个子图(多 Agent 协作的基础)
State 和 Reducer:并发更新的核心
LangGraph 的 State 设计是面试常考点。State 不是简单的 dict——它的每个字段都关联一个 Reducer,决定多次更新如何合并:
from typing import TypedDict, Annotated
from operator import add
class State(TypedDict):
# add reducer:列表追加(不是覆盖)
messages: Annotated[list, add]
# 默认(无 reducer):直接覆盖
current_step: str
# 自定义 reducer:取最大值
max_score: Annotated[float, max]为什么需要 Reducer?因为 LangGraph 支持节点并发执行:
A 和 B 并发返回更新,如果没有 Reducer,框架不知道是 doc1 覆盖 doc2 还是合并。Annotated[list, add] 明确说"把所有更新追加到列表"。
这种设计借鉴了 Erlang/Akka 的 actor 模型——节点是纯函数,输入 State 输出 State 变更,框架负责合并。纯函数 + 显式合并策略 = 可并行 + 可重放。
Checkpointer:暂停、恢复、时间旅行
LangGraph 最强大的特性是 Checkpointer——每个"超级步骤"(一轮所有并发节点执行完)结束时,自动把 State 快照保存到数据库:
from langgraph.checkpoint.postgres import PostgresSaver
# 用 Postgres 做 checkpoint 存储
checkpointer = PostgresSaver.from_conn_string("postgresql://...")
app = graph.compile(checkpointer=checkpointer)
# thread_id 区分不同会话
config = {"configurable": {"thread_id": "user-123"}}
# 第一次调用——Agent 跑到一半被中断
try:
result = app.invoke({"messages": [...]}, config=config)
except KeyboardInterrupt:
pass
# 一个月后,从中断点继续
result = app.invoke(None, config=config) # input=None 表示从 checkpoint 恢复Checkpointer 解锁了三个能力:
1. 暂停 / 恢复
Agent 在执行长任务时崩溃或被中断,重启后从最近 checkpoint 继续,不需要从头开始。这对长任务(数据迁移、批量处理)是必备能力。
2. 时间旅行(Time Travel)
可以列出某个 thread 的所有历史 checkpoint,跳回任意一个继续:
# 列出所有历史 checkpoint
history = list(app.get_state_history(config))
# 跳回 3 步前,从那个状态分叉新的执行路径
old_state = history[3]
new_config = {"configurable": {"thread_id": "user-123-branch"}, "checkpoint_id": old_state.config["configurable"]["checkpoint_id"]}
app.invoke(None, config=new_config)这相当于给 Agent 加了"撤销"和"分支"能力——非常适合调试和 A/B 测试不同策略。
3. Human-in-the-loop
某些节点需要人工审批才能继续,传统做法是阻塞等待 input()。LangGraph 的做法是用 interrupt() 把节点挂起,持久化状态,立刻返回:
from langgraph.types import interrupt, Command
def approval_node(state: State):
# 挂起执行,等待人工输入
user_decision = interrupt({"question": "确认执行此操作?", "preview": state["plan"]})
if user_decision == "approve":
return {"approved": True}
return {"approved": False}
# 第一次调用——会在 approval_node 挂起
result = app.invoke({...}, config=config)
print(result["__interrupt__"]) # 显示需要用户决策的内容
# ……过了几小时,用户审批后……
result = app.invoke(Command(resume="approve"), config=config)注意:interrupt() 不是 time.sleep()——它把整个 Agent 状态持久化到 checkpoint,进程可以完全退出,用户隔天上线后用同样的 thread_id 调用,从挂起点继续。这是真正的"异步审批"。
实战:一个带 human-in-the-loop 的代码审查 Agent
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command
from langchain_anthropic import ChatAnthropic
from typing import TypedDict, Annotated
from operator import add
class ReviewState(TypedDict):
code: str
messages: Annotated[list, add]
issues: list
approved_fixes: list
llm = ChatAnthropic(model="claude-sonnet-4-6")
# 节点 1:分析代码找问题
def analyze(state: ReviewState):
response = llm.invoke(f"找出代码中的 bug:\n{state['code']}")
issues = parse_issues(response.content)
return {"messages": [response], "issues": issues}
# 节点 2:等待人工审批
def request_approval(state: ReviewState):
# 挂起,让用户挑选要修哪些 issue
selected = interrupt({
"issues": state["issues"],
"prompt": "选择要修复的问题(逗号分隔索引)"
})
approved = [state["issues"][int(i)] for i in selected.split(",")]
return {"approved_fixes": approved}
# 节点 3:生成修复
def fix(state: ReviewState):
response = llm.invoke(f"修复以下问题:\n{state['approved_fixes']}\n原代码:{state['code']}")
return {"messages": [response]}
# 组装
graph = StateGraph(ReviewState)
graph.add_node("analyze", analyze)
graph.add_node("approval", request_approval)
graph.add_node("fix", fix)
graph.set_entry_point("analyze")
graph.add_edge("analyze", "approval")
graph.add_edge("approval", "fix")
graph.add_edge("fix", END)
checkpointer = SqliteSaver.from_conn_string("checkpoints.db")
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "review-pr-123"}}
# 第一次调用:跑到 approval 节点挂起
result = app.invoke({"code": open("buggy.py").read()}, config=config)
print(result["__interrupt__"][0].value) # 显示问题列表,等用户输入
# 用户输入后继续
result = app.invoke(Command(resume="0,2"), config=config)
print(result["messages"][-1].content) # 修复后的代码整个流程跨进程、跨时间——Agent 在 approval 节点挂起后,Python 进程可以退出。用户半天后审批,从挂起点继续,前面的所有上下文(代码、找到的 issue)都从 checkpoint 自动恢复。
容易踩的坑
坑 1:State 字段忘了加 Reducer 导致并发更新丢失
- 现象:两个并发节点都返回
{"results": [...]},最终 State 里只剩一个节点的结果 - 根因:没加 reducer 时默认是"覆盖"语义,后写的覆盖先写的
- 修法:列表类字段用
Annotated[list, add],dict 类字段用Annotated[dict, merge_dict],自定义合并逻辑就写个函数
坑 2:LCEL 链很长时调试地狱
- 现象:
chain = a | b | c | d | e | f中间某步出错,traceback 看不出哪里 - 根因:LCEL 是声明式,错误堆栈在框架内部
- 修法:必须用 LangSmith 做追踪。或者手动拆分 chain:
step1 = a | b; step2 = c | d; ...,分段调用看哪步出问题
坑 3:Checkpointer 用 InMemory 上生产,重启数据全丢
- 现象:开发时用
MemorySaver()一切正常,部署后重启就丢 checkpoint - 根因:InMemory checkpointer 只存在内存里,进程结束就消失
- 修法:生产必须用 SqliteSaver / PostgresSaver / RedisSaver。注意 Postgres 的连接池配置,高并发时容易耗尽连接
坑 4:把 LangChain 当唯一选择,简单场景过度工程化
- 现象:写一个"问 LLM 一个问题"的接口都要
PromptTemplate | LLM | StrOutputParser - 根因:LangChain 不是万金油——它的价值在抽象切换和复杂编排,简单场景反而是负担
- 修法:单次 LLM 调用直接用 Anthropic/OpenAI SDK;只有当你需要"多 provider 切换 / 多步骤编排 / 工具调用循环 / RAG 管线"时才上 LangChain
LangChain vs LangGraph:边界划分
很多人混淆两者,其实分工很清楚:
| 维度 | LangChain | LangGraph |
|---|---|---|
| 抽象层次 | 组件层(LLM、Prompt、Tool、Retriever) | 编排层(Agent 状态机) |
| 核心范式 | 声明式 pipeline(LCEL) | 命令式状态机(Graph) |
| 适用场景 | RAG、单次查询、简单链式逻辑 | 多 Agent、长任务、需要持久化和 HITL |
| 学习曲线 | 中(LCEL 语法 + 大量组件) | 较陡(要理解 State/Reducer/Checkpointer) |
| 何时不用 | 单次 LLM 调用、超简单脚本 | 没有复杂分支和状态的简单 chain |
实际项目里两者常一起用——LangGraph 的节点内部用 LCEL 写具体逻辑:
# LangGraph 节点内部用 LangChain 的 LCEL
def rag_node(state):
chain = retriever | prompt | llm | StrOutputParser()
answer = chain.invoke(state["question"])
return {"answer": answer}面试题深度解析
Q1: LCEL 解决了什么问题?
- 30 秒版本:LCEL 用 pipe 操作符替代了嵌套类调用,把"链式调用"从命令式变成声明式。任何 Runnable 自动获得 6 种调用方式(invoke/batch/stream + async 版本),LangSmith 自动追踪每一步,并行节点自动并发执行。
- 追问:声明式比命令式真的更好吗? 不一定——简单场景命令式更直观。但当你需要把同一个 chain 跑 batch、跑 stream、跑 async 时,命令式要写 3 套代码,LCEL 写一套就行。这就是声明式的杠杆。
- 追问:LCEL 的性能开销大吗? 对单次调用,
|操作符的开销可以忽略(就是函数组合)。但 LCEL 的元数据追踪(每步的输入输出 ID、callback hooks)会有 5-10% 开销,高 QPS 场景需要关掉 callbacks。
Q2: 为什么 LangGraph 用图而不用 DAG 或纯 FSM?
- 30 秒版本:DAG(有向无环图)不支持循环,但 Agent 本质就是循环(思考-行动-观察的 ReAct loop)。纯 FSM(有限状态机)状态空间爆炸——Agent 可能有无数中间状态。LangGraph 选了"有环有向图 + 显式 State"——节点是状态转移函数,边是控制流,State 是可变数据。
- 追问:LangGraph 的图能表达所有 Agent 模式吗? 大部分能。ReAct、Plan-and-Execute、Reflexion、多 Agent 协作都能映射成图。但极端动态的场景(运行时生成新节点)做不到——图编译后结构固定。这种场景要用 LangChain Tools + 自定义循环。
- 追问:LangGraph 和 Airflow/Prefect 这类 workflow engine 有什么区别? 维度差异——Airflow 是 batch DAG(一次性跑完)、Prefect 类似。LangGraph 是 stateful、长期运行(可以挂起几天)、面向 LLM 任务(每个节点是一次 LLM 调用)。共同点都是"显式工作流"。
Q3: Checkpointer 怎么实现的?性能瓶颈在哪?
- 30 秒版本:每个"超级步骤"(一轮并发节点全部完成)后,框架把整个 State 对象序列化(pickle/JSON)写入存储后端(SQLite/Postgres/Redis)。读取时反序列化重建 State。性能瓶颈主要在 State 大小——如果你把整个 RAG 文档塞进 State,每步都序列化几 MB,会拖慢整体执行。
- 追问:如何优化大 State? 三种思路:(1) State 里只存索引/引用,实际大对象(文档、图片)存对象存储(S3/Redis),State 里存 URL。(2) 用增量 checkpoint,只写 diff。(3) 减少节点数——把多个轻量操作合并成一个节点,减少 checkpoint 频率。
- 追问:高并发下 Postgres 会成为瓶颈吗? 会。每个 thread 的每个 checkpoint 都是一次写。高并发时(比如 1000 个用户同时跑 Agent),Postgres 的写并发会成瓶颈。生产方案:(1) 用 RedisSaver 做热点 checkpoint,定期批量同步到 Postgres。(2) 按 thread_id 分库分表。(3) 接受 checkpoint 延迟(异步写)。
延伸阅读
- 官方文档 python.langchain.com 和 langchain-ai.github.io/langgraph — LangGraph 文档质量明显高于 LangChain,必读 Concepts 章节
- LangChain 源码 github.com/langchain-ai/langchain —
libs/core/langchain_core/runnables/是 LCEL 实现的核心,理解 Runnable 协议必看 - LangGraph 源码 github.com/langchain-ai/langgraph —
langgraph/graph/和langgraph/checkpoint/是两个关键模块,前者是图执行引擎、后者是持久化 - LangSmith smith.langchain.com — 配套的可观测平台。在生产用 LangChain/LangGraph 但不用 LangSmith 等于盲飞
- Pregel 论文 Pregel: A System for Large-Scale Graph Processing(Google, 2010)— LangGraph 的 superstep 概念源自 Pregel,理解原始设计能更好地理解 LangGraph 的并发模型