12 LangGraph 图式 Agent

QuibblerAgentQuibblerAgent 2026-11-13 约 18 分钟 13 次阅读

上一篇拿 Claude Agent SDK 与手写版逐项对完了账,系列的最后一位选手再用另一种世界观重写 Agent:不是循环,是图。本文是「从零手写 Agent」系列第 12 篇收官之作:LangGraph 的状态图范式、十行重写的写法、手动建图与手写循环的对应关系,最后给出三种实现的选型收束,并回放整个系列的来路。

本文按"图式世界观 → 十行重写 → 手动建图 → checkpointer 与选型 → 总结"组织。

1、图式世界观

LangGraph 的核心抽象是三个词:State(一个共享字典,对应手写版的历史列表)、Node(一个函数,收到状态返回更新——"调用模型"与"执行工具"各是一个节点)、Edge(节点间的转移,其中条件边按状态决定去向)。你手写的 while 循环,在 LangGraph 里被显式画成了一张图。

flowchart TD S[START] --> M[调用模型节点] M -->|tool_use| T[执行工具节点] T --> M M -->|end_turn| E[END]

解读:图里"模型到工具再回模型"的环,就是 while stop_reason == "tool_use" 的可视化;条件边上那两个标签,正是循环退出判断的两个分支。范式换了,控制流一模一样——这是走完前十篇后应有的直觉。相关资料见 LangGraph 文档。

1.1、State 的追加语义

三件套里最值得多看一眼的是 State。手写版直接往 history 列表 append;LangGraph 的节点不修改状态,只返回一份更新,由框架按每个字段的 reducer 合并进共享字典。MessagesState 内置的追加语义等价于自己声明一个带 reducer 的状态:

from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]   # 追加合并,不是整体覆盖

解读:Annotated 把"这个字段怎么合并"写进了类型声明——节点只管返回新消息,框架负责追加,这正是手写版两处 history.append 隐含的"追加而非覆盖"约定,在图式范式里成了显式、可更换的策略。把 reducer 换成自定义函数,同一张图就能改写记忆行为,比如追加前去重、超长时滚动截断——第 7 篇的压缩器在图范式下正是从这个接入口挂进去的。

2、十行重写

LangGraph 提供了现成的 ReAct 循环图,配合装饰器声明工具,重写工作量骤降:

from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

@tool
def read_file_tool(path: str) -> str:
    """读取项目内的文本文件。"""
    return read_file(path)          # 直接复用手写版的工具函数

llm = ChatAnthropic(model=MODEL, max_tokens=2048)
agent = create_react_agent(
    llm, [read_file_tool, search_files_tool],
    prompt="你是代码调研助手,回答要引用文件和行号。",
)
result = agent.invoke({"messages": [("user", task)]})

解读:@tool 装饰器把函数签名与文档字符串自动转成工具说明书——与手写 schema 是同一件事的两种工效学;result["messages"] 就是手写版的历史列表,LangGraph 管它叫状态。工具函数层完全复用,框架替换的只是循环与协议层。

2.1、被替换的只有循环层

分清哪一层被换掉、哪一层原样保留,是对照的关键。原样保留的是工具函数本体:read_file 这些第 6 篇写好的函数一个字不用改。被替换的有三样:工具说明书从手写 JSON schema 变成装饰器从签名与 docstring 生成;循环从 while 变成图引擎驱动;消息从裸字典变成 LangChain 的消息对象。连系统提示词都有对应物——create_react_agent 的 prompt 参数,就是第 5 篇 system 参数的别名。

顺带一个选型注脚:LangGraph 1.x 起官方文档力推的新入口是 langchain.agents 里的 create_agent,create_react_agent 仍是文档完整的经典入口,两者心智模型一致,本文沿用后者。

3、手动建图

预制的循环图不够用时,手动建图把控制流完全显式化。骨架如下:

from langgraph.graph import StateGraph, MessagesState, START, END

def call_model(state):              # 节点一:调用模型
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

def run_tools(state):               # 节点二:执行工具并回填结果
    ...

def should_continue(state):         # 条件边:手写循环的退出判断
    last = state["messages"][-1]
    return "tools" if getattr(last, "tool_calls", None) else END

g = StateGraph(MessagesState)
g.add_node("model", call_model)
g.add_node("tools", run_tools)
g.add_edge(START, "model")
g.add_conditional_edges("model", should_continue)
g.add_edge("tools", "model")
agent = g.compile()

解读:run_tools 节点里要做的事,正是系列第 4 篇的往返核心——取出调用块、按 id 配对回填结果消息,各家框架无一例外。手动建图的价值在复杂流程编排:固定的多阶段流水线、插入人工审批节点、失败走不同重试分支——这些画在图上一目了然,写在循环里就是面条。

3.1、补全 run_tools 与首次调用

骨架里省略的 run_tools 是唯一还需要动手写的部分,补全如下:

from langchain_core.messages import ToolMessage

TOOLS = {t.name: t for t in [read_file_tool, search_files_tool]}

def run_tools(state):
    last = state["messages"][-1]
    results = []
    for call in last.tool_calls:            # 取出本轮全部调用块
        fn = TOOLS[call["name"]]            # 第 4 篇的函数表,原样复用
        try:
            out = fn.invoke(call["args"])   # @tool 函数直接调用
            results.append(ToolMessage(content=str(out),
                                       tool_call_id=call["id"]))
        except Exception as e:              # 第 6 篇铁律:失败要大声
            results.append(ToolMessage(content=f"工具失灵:{e}",
                                       tool_call_id=call["id"]))
    return {"messages": results}

解读:这正是系列第 4 篇往返核心的逐条落地——遍历调用块、查函数表执行、按 tool_call_id 配对回填,连异常转文字结果都遵守第 6 篇"失败要大声"的原则。骨架里的 llm_with_tools 则来自 llm.bind_tools(tools),绑定工具说明书的那次调用,与手写版 tools=self.tools_spec 同位。建图收尾与首次调用如下:

agent = g.compile()                                  # 节点与边编译成可执行图
result = agent.invoke({"messages": [("user", task)]})

for m in result["messages"]:                         # 历史就在状态里
    print(getattr(m, "type", "?"))                   # 逐条打印,verbose 同款
answer = result["messages"][-1].content

解读:compile 把节点与边编译成可执行图,invoke 一次跑完;返回值里的 messages 就是手写版的 history——同一个数组,从循环的私有变量变成了图的公共状态。要过程可观测,把 invoke 换成 stream 即可逐事件输出,对应手写版的 verbose 开关。API 细节见 Graph API 参考。

3.2、与手写循环逐行对照

至此可以把第 5 篇的循环与这张图逐行对上,对照如下:

手写版部件 LangGraph 对应 说明
history 列表 State 的 messages 同一个数组,追加语义由 reducer 声明
messages.create 调用 call_model 节点 一次模型请求一个节点
stop_reason 判断 条件边 should_continue 退出分支从循环体搬到边上
_execute 与结果回填 run_tools 节点 取调用块、按 id 配对不变
max_turns 安全阀 recursion_limit 图引擎步数上限,默认 25
verbose 过程打印 stream 事件流 可观测性挂在图引擎上

解读:两列的差距比想象小——范式换了,五脏六腑全是从前那些零件。差异的方向更有启发:手写版的退出判断嵌在循环体内部,加一条分支就要动心脏;图式把分支升格为"边"这种一等公民,多一条路径只是多画一条线,不改任何现有节点。审批流就是一个现成例子:在模型与工具之间插一个人工节点,条件边改成"通过走工具、驳回回模型",生产里最常见的人机协同 Agent 就是这么一张图——下一节说的复杂编排,价值正源于此。

4、checkpointer 与三强选型

LangGraph 的 checkpointer 把状态每步自动落盘:同一会话连续提问它记得你,换成 SQLite 存储后跨进程也记得,还能回退到任意一步做时间旅行调试。它与系列第 7 篇持久化的差异是本质性的注脚:手写版存的是"结果",checkpointer 存的是"轨迹"。

4.1、checkpointer 上手

两个会话之间"记得你"的最小验证如下:

from langgraph.checkpoint.memory import InMemorySaver

agent = g.compile(checkpointer=InMemorySaver())     # 每步自动存档
cfg = {"configurable": {"thread_id": "user-42"}}    # 会话身份证

agent.invoke({"messages": [("user", "我叫小齐,喜欢蓝色")]}, cfg)
agent.invoke({"messages": [("user", "我叫什么、喜欢什么颜色?")]}, cfg)

解读:thread_id 是会话身份证:同一个 id 的多次 invoke 共享一份状态,第二问不带任何历史直接答对——第 2 篇要靠宿主程序自己持有 history 才能做到的事,这里一个参数就内置了。生产用法是把 InMemorySaver 换成 SqliteSaver(独立安装 langgraph-checkpoint-sqlite,用 SqliteSaver.from_conn_string 建存储),落盘后跨进程也记得;更妙的是每一步都是一帧存档,get_state_history 能拉出全部历史帧,任意一帧都可作回放起点——第 7 篇的"存结果"升级成"存轨迹",时间旅行调试从设想变成内置功能。持久化细节见 LangGraph 持久化文档。

三种实现的选型收束如下:

实现 核心抽象 杀手锏 适合场景
手写 while 循环 每行都归你管 原型、学习、特殊控制流
Agent SDK query 黑盒 开箱工程化 标准编码与文件任务
LangGraph 状态图 复杂流程编排 多阶段工作流与人审分支

解读:简单 Agent 三者皆可;要工程保障选 Agent SDK;流程复杂、需要审批与分支重试,图式才发力。模型与端点配置沿环境变量注入,与手写版共用同一套接入层。

5、总结

LangGraph 用 State、Node、Edge 三件套把 Agent 显式建模为图,条件边就是手写循环的退出判断,checkpointer 把第 7 篇的存档升级为可回溯轨迹;三种实现没有王者,只有场景匹配。回到系列第 0 篇的公式——Agent 等于大模型加工具加循环——走完十二篇后应有一层新的理解:公式里的每一项都平常无奇,真正的功夫在工具面怎么划、上下文放什么、如何证明它干得好。模型只是心脏,你才是设计师。后续方向建议从 MCP 接入与真实 embedding 开始,把手写版继续养大。LangChain 生态见 LangChain 官网。

连同第 0 篇的路线图,十三篇至此收官。把来路铺开看:

timeline title 从零手写 Agent 之旅 第1-3篇 : API 真相 : 对话记忆 : 流式输出 第4-5篇 : 工具调用 : 循环落地 第6-8篇 : 真实工具 : 上下文治理 : 子 Agent 第9-10篇 : RAG : 评估闭环 第11-12篇 : SDK 对账 : 图式重写

解读:三个阶段各有主旋律——前三篇打地基(协议、记忆、流式),中间五篇造机器(循环、工具、边界、压缩、分身),后四篇补工程化再对照框架(检索、评估、SDK、图式)。回头看,每一篇其实都在回答同两个问题:它怎么工作,以及它失控了怎么办——第二问的答案从 max_turns 一路长到权限链与 checkpointer,这条"防跑飞"的暗线比任何单个知识点都值得带走。

给准备走这条路的读者三条建议。其一,第 1 到 5 篇必须亲手敲,一步别跳——裸 HTTP、往返、循环是所有框架的解码器,抄近道的人永远只能在文档里捡咒语。其二,第 6 到 8 篇的边界意识决定你的 Agent 能不能见生产,权限、沙箱、压缩永远先于炫技。其三,第 9、10 篇的评估闭环至少完整跑一次——没有标尺,后面所有调优都是玄学;两篇框架对照放在最后,正是因为对账需要资本。学完不是终点:挑一个你自己的真实小项目,把 miniagent 养成日常工具,这门手艺才算真正毕业。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.3k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Multi-Agent 详解:多智能体协作架构与实践
精选Agent

Multi-Agent 详解:多智能体协作架构与实践

一个智能体配上一套工具,已经能完成相当多任务。但让它同时“当产品经理、写后端、跑测试、做代码评审”,很快就会露馅:上下文里塞满了互相干扰的指令,十几件工具的清单来回切换,任务做到一半忘了开头。解法看起来很自然——像人类团队一样分工:让多个智能体各司其职,由一个协调者调度,这就是 Multi-Agent(多智能体)系统。它也是 2026 年智能体工程里讨论度最高、误用率也最高的架构。本文按“架构模式…

144