/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
LangGraph 把 Agent 从 Demo 变成服务:一条完整的落地路径
用 LangChain 写 Agent,Demo 阶段很爽,一上生产就露馅。
原因不是模型不行,是链是线性的,Agent 是循环的。
链只能 A→B→C 走到底;而 Agent 必须「想一想、调个工具、看看结果、再想一想」,这个「再」字,链表达不了。
LangGraph 就是为这个「再」字生的。

一、整个框架只有三样东西
别看文档长,核心就三个概念,理解了这三个,剩下的都是配置。
State —— 整个图共享的一份数据。用 TypedDict 定义,所有节点读它、改它。
from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # 追加,不是覆盖
steps: int
那个 Annotated[list, add_messages] 是新手最容易漏的地方。它的意思是:这个字段用 add_messages 这个 reducer 来合并——每个节点返回新消息时是追加,不是替换整个列表。写成普通的 list,你会看到历史消息莫名其妙消失。
Node —— 一个函数,输入 State,输出 State 的增量。
def agent_node(state: AgentState):
resp = llm.invoke(state["messages"])
return {"messages": [resp], "steps": state["steps"] + 1}
注意返回值:只返回要改的字段,不需要把整个 State 吐回来。
Edge —— 节点之间的跳转。普通边是「下一步」,条件边是「看情况」。
from langgraph.graph import StateGraph, END
g = StateGraph(AgentState)
g.add_node("agent", agent_node)
g.add_node("tools", tool_node)
g.set_entry_point("agent")
# 条件边:根据最后一条消息决定去哪
g.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
g.add_edge("tools", "agent") # 工具执行完,回到 agent
app = g.compile(checkpointer=MemorySaver())
add_edge("tools", "agent") 这一行,就是「循环」的来源——工具执行完必须回到思考节点,否则 Agent 只会调一次工具就结束。
二、循环必须有个刹车
上面那段代码有个隐患:如果模型一直觉得「还得再调一个工具」,图会无限转下去。
两个刹车,建议都加:
app = g.compile(checkpointer=MemorySaver())
result = app.invoke(
{"messages": [("user", "帮我查下订单 12345 的物流")]},
config={"recursion_limit": 25}, # 刹车一:最大步数
)
刹车二是节点里的显式判断:
def should_continue(state: AgentState):
if state["steps"] >= 10:
return END
last = state["messages"][–1]
return "tools" if getattr(last, "tool_calls", None) else END
recursion_limit 是兜底,节点判断是业务逻辑。只加一个都不够——前者会抛异常,后者可能因为模型输出格式怪而失效。
三、Checkpointer: Agent 最值钱的功能
这是 LangGraph 相比手写 while 循环最大的优势:它把每一步状态都存下来了。
from langgraph.checkpoint.memory import MemorySaver
app = g.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "user-42-session-1"}}
app.invoke({"messages": [("user", "帮我订明天去上海的机票")]}, config)
有了 thread_id,能力就出来了:
断点续跑。 用户第二天回来接着聊,从上次的状态继续,不用重放历史。
人工介入。 涉及付款、删数据这类操作,可以在图里插一个 interrupt,等人工确认后再继续:
from langgraph.types import interrupt
def confirm_node(state):
decision = interrupt({"ask": "确认下单?", "order": state["draft"]})
return {"approved": decision == "yes"}
这不是「锦上添花」的功能,这是 Agent 能不能上生产的分界线。 一个没法中断、没法恢复、出错只能从头再来的 Agent,业务方不敢用。
生产环境把 MemorySaver 换成 SqliteSaver 或 PostgresSaver,接口完全一样。
四、Tool 定义:docstring 是写给模型看的
from langchain_core.tools import tool
@tool
def query_logistics(order_id: str) –> str:
"""按订单号查询物流轨迹。
Args:
order_id: 订单号,纯数字,例如 1234567890
"""
return api.get(f"/logistics/{order_id}")
@tool 装饰器做的事,是把函数签名和 docstring 转成 JSON Schema,塞进模型的工具列表。
所以那句 docstring 不是注释,是 prompt。写「查询物流」和写「按订单号查询物流轨迹,order_id 必须是纯数字」,模型的调用准确率能差出一截。
三个写法建议:
五、从本地脚本到服务
app.invoke() 跑通,只完成了 30%。剩下 70% 是「怎么让别人调用」。
LangGraph 自带一条服务化路径:
pip install "langgraph-cli[inmem]"
langgraph dev
它会读 langgraph.json,把图注册成 HTTP 服务,同时起一个调试界面。前端直接用标准接口调,流式、状态查询、中断恢复都现成。

上面这张是这套课的第五章目录,从「LangGraph 是什么」到「Tool 的定义」,基本就是这条落地路径的完整顺序——先跑通本地服务,再调 API,最后才是接工具。这个顺序别颠倒,很多人一上来就写 Tool,结果连服务都起不来。
六、三个上线必踩的坑
坑一:State 里塞了不可序列化的对象。
数据库连接、模型客户端、锁对象,全都不能进 State。因为 Checkpointer 要把它序列化存库。做法是用 config 传,或者放进闭包。
坑二:工具报错直接把图打挂。
一个工具 500,整条链抛异常。正确做法是在 ToolNode 外层包一个 try,把错误信息作为工具结果返回给模型——让模型知道「这个工具挂了」,它才有机会换个方式。
坑三:流式输出接不上。
LangGraph 的流式有好几种模式(values / updates / messages),前端要的是 token 级,必须用 stream_mode="messages"。用错模式,你会看到整段结果一次性弹出来。
for chunk, meta in app.stream(inputs, config, stream_mode="messages"):
yield chunk.content
最后
LangGraph 的心智模型其实特别朴素:
把 Agent 当成一个状态机,State 是内存,Node 是动作,Edge 是规则。
想清楚这三样,代码量比手写 while 循环还少,但多了断点、中断、可观测这些「生产才需要」的东西。
这也是为什么现在做 Agent 的项目,基本都在往这个方向迁。
这套东西我是跟着一套 LangChain + LangGraph + MCP 的实战课从头跑了一遍,七章内容,LangGraph 这一章就有九个课时,从装本地服务到工具定义全走了一遍。笔记和工程代码我都存下来了。
想要这套 LangGraph 笔记 + 可跑工程的,扫码加我,备注「LangGraph」👇



