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 必须是纯数字」,模型的调用准确率能差出一截。
三个写法建议:
- 参数说明里给例子(模型会照着例子格式化参数)
- 明确写「什么时候不该用」,减少误调
- 返回结构化字符串(JSON 或 markdown 表格),别返回
str(dict)
五、从本地脚本到服务
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」👇

322



被折叠的 条评论
为什么被折叠?



