在这里插入图片描述

从“原理懵圈”到“代码封神”:一篇让你彻底告别Agent智障期的LangGraph ReAct实战全攻略,手把手教你把状态、工具、循环、编译一次跑通!

🔥 手把手构建ReAct Agent:从定义状态到运行图

1️⃣ ReAct Agent核心机制解密

2️⃣ State定义:别让Agent失忆

3️⃣ Tool集成:给Agent装手脚

4️⃣ ReAct循环节点:思考与行动

5️⃣ Graph编译与运行:组装工作流

6️⃣ 运行调试:Agent抽风怎么办

文字目录:

  1. ReAct Agent核心机制解密:为什么你的Agent总像没头苍蝇?
  2. State定义:别让Agent沦为“金鱼记忆”患者
  3. Tool集成:给Agent装上真手脚,而不是画饼充饥
  4. ReAct循环节点:思考与行动的编排艺术
  5. Graph编译与运行:从散落节点到严密工作流
  6. 运行调试:当Agent开始“抽风”,你怎么接招?

嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》

学编程就像打怪升级,总会遇到卡关的时候。但最怕的不是卡关,是你连BOSS的机制都没搞明白,就提着木剑往上冲。你是不是也这样?刷了几篇Agent的科普文,看了几个“五分钟上手LangGraph”的视频,感觉自己行了,结果一打开IDE,面对State、Node、Edge、ToolNode、Conditional Edge这些概念,瞬间从“我悟了”变成“我废了”。别慌,今天这篇就是专门给你这把“木剑”附魔的。咱们不整虚的,就从零开始,手把手在LangGraph里搭建一个真正能跑通的ReAct Agent。看完这篇,你不仅能跑通代码,还能明白每一步为什么要这么干。


1. ReAct Agent核心机制解密:为什么你的Agent总像没头苍蝇?

很多人一上来就急着抄代码,结果抄完发现Agent要么不调用工具瞎编答案,要么调了工具之后直接输出原始JSON,完全不给用户一个像样的回复。这就是典型的“机制没搞懂,代码全白写”。

ReAct,说白了就是Reasoning + Acting。它不是LangGraph独有的概念,但LangGraph把它做成了一个显式的状态机。核心逻辑是一个“思考-行动-观察”的循环。LLM负责在脑子里盘算(Thought),决定要不要调用工具;如果要,就发出工具调用指令(Action);工具跑完把结果吐回来(Observation);LLM拿到结果再接着想,直到觉得可以回答用户了,才输出最终答案。

开始

🧠 Thought
LLM推理

需要工具?

🛠️ Action
调用Tool

👀 Observation
获取结果

输出答案

痛点分析

新手在这个环节有三个致命的思维误区。

误区一:以为有了Function Calling就等于有了ReAct。 错。Function Calling只是LLM具备“发出工具调用指令”的能力,但它不负责“循环”。没有循环,模型最多调一次工具,根本不会根据工具返回的结果进行二次推理。你让Agent查天气,它调了API拿到数据,直接就把原始JSON甩给用户,这就是缺少ReAct循环的表现。

误区二:把Thought和Observation混为一谈。 有些同学喜欢在prompt里写:“你先想一想,然后调用工具,然后把工具结果告诉我”。模型确实照做了,但它的“想一想”和“工具结果”全塞在一个回复里,流程黑盒,你根本分不清它现在处于哪个阶段。一旦出错,你都不知道是模型想错了,还是工具返回错了。

误区三:总觉得“一步直达”比“循环迭代”更高级。 很多新手写Agent时,潜意识里希望LLM一次就给出完美答案,好像循环多了就显得Agent很笨。实际上,真实世界的复杂问题必须拆解。ReAct的魅力就在于让LLM像人一样“走一步看一步”,而不是开局一张嘴,剩下全靠蒙。

举个错误的代码思路,你是不是也写过类似的东西?

# 错误示范:试图用一个prompt解决所有问题
def naive_agent(query):
    llm = ChatOpenAI()
    tools_description = "你可以使用搜索工具..."
    prompt = f"{tools_description}\n用户问题:{query}\n请思考并直接给出答案。"
    return llm.invoke(prompt).content

这段代码的问题在于,LLM根本不知道它该在什么时候停下来去调工具。它要么瞎编,要么把工具描述当知识库来用,输出一堆似是而非的东西。

解决方案/正确做法

在LangGraph里,ReAct循环不是藏在prompt里的“潜规则”,而是被画在图上的“显式拓扑”。你必须用节点(Node)和边(Edge)把“思考→行动→观察→再思考”这个环给搭出来。

这样做的好处是,流程完全白盒化。每一个Thought都是一次LLM调用节点,每一个Action都是ToolNode的执行,Observation就是ToolNode返回的ToolMessage。你可以精确地知道Agent现在卡在哪个环节,状态里存了什么。

正确的认知应该是:把LangGraph当成一个状态机框架,而不是一个prompt工程框架。prompt只是节点内部的一小部分,图的骨架才是灵魂。当你显式地定义了循环,Agent才能像下图这样“转起来”:

需要工具

Observation
ToolMessage

无需工具

🤖 Agent节点
LLM思考

🧰 Tools节点
执行动作

✅ 最终答案

小结: 搞懂ReAct循环是你不被Agent带偏的第一道防火墙。别再把希望全寄托在prompt的“聪明才智”上,显式的图结构才是可控性的来源。


2. State定义:别让Agent沦为“金鱼记忆”患者

如果你把LangGraph的图比作一个工厂,那State就是工厂里的传送带。每个节点是工位,State负责把物料从一个工位运到下一个工位。ReAct Agent的State,最核心的职责就是保存对话历史,让LLM在每次思考时都能拿到完整的上下文,包括它自己之前的Thought、发出去的Action,以及收回来的Observation。

痛点分析

新手在定义State时,简直是“百花齐放”地踩坑。

坑位一:抱着原生dict不放。 很多人图省事,直接定义state = {"messages": [], "query": ""},然后在各个节点里靠Key硬编码去取。这样做在节点少的时候似乎能跑,但一旦节点多了,你根本记不住哪个节点往state里塞了什么字段,类型对不对,是不是已经被某个节点偷偷改掉了。调试的时候,state就像个黑盒,print出来一长串,你看着都头大。

坑位二:消息被“覆盖”而不是“追加”。 有些同学知道要用TypedDict,于是写了个:

# 错误示范:没有使用reducer,消息会被覆盖
class BadState(TypedDict):
    messages: list

结果agent节点返回{"messages": [new_message]},直接把上一轮的对话历史给顶掉了。Agent瞬间失忆,问它“刚才我说了什么”,它一脸茫然。这根本不是金鱼记忆,这是被格式化硬盘了。

坑位三:字段命名和类型与LangChain生态脱节。 LangGraph和LangChain是一家人,它们约定俗成地把对话历史放在一个叫做messages的字段里,类型是Sequence[BaseMessage],并且配合一个叫做add_messages的reducer。如果你非要自己起个名叫chat_history,类型写成list[dict],那很多预置组件(比如ToolNode、检查点机制)就和你对不上了,后续集成会越来越痛苦。

看看这个典型的错误示范:

from typing import TypedDict

# 错误:类型太松散,没有reducer,字段也不规范
class MyState(TypedDict):
    input: str
    history: list  # 用list而不是Sequence[BaseMessage]
    result: str

解决方案/正确做法

在LangGraph里,定义State的黄金法则是:用TypedDict做骨架,用Annotated做Reducer,尽量对齐MessagesState的约定。

最推荐的做法是直接继承MessagesState,它内部已经帮你定义好了带add_messages reducer的messages字段。但如果你需要加一些自定义字段(比如循环计数器、用户ID),那就自己定义TypedDict:

from typing import Annotated, Sequence, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    # 核心:messages字段必须使用add_messages这个reducer
    messages: Annotated[Sequence[BaseMessage], add_messages]
    # 自定义字段,用于后续防死循环等场景
    iteration_count: int

这里Annotated[Sequence[BaseMessage], add_messages]是灵魂。add_messages是一个reducer函数,它告诉LangGraph:当多个节点都返回messages字段时,不要覆盖,而是把它们追加合并成一个列表。这样agent节点返回的AIMessage和tools节点返回的ToolMessage就能自动拼接在一起,形成完整的ReAct轨迹。

add_messages
自动追加

add_messages
自动追加

读取完整上下文

agent节点
返回AIMessage

🗂️ AgentState
messages列表

tools节点
返回ToolMessage

这样做的好处是什么?首先,类型安全。你的IDE能自动提示state里有哪些字段,是什么类型。其次,状态流转清晰,每个节点只负责生产自己的那部分数据,剩下的交给reducer去合并。最后,与LangChain生态无缝兼容,ToolNode、Memory、Checkpointer都能直接识别messages字段。

小结: State定义得像样,Agent才不会在节点间“失忆”或者“胡言乱语”。记住,Annotatedadd_messages就是你在LangGraph里的“记忆保险”。


3. Tool集成:给Agent装上真手脚,而不是画饼充饥

Agent光会思考没用,它得能干实事。Tool就是Agent的手脚,让它能查天气、搜资料、算数学、调数据库。但在LangGraph里,Tool不是随便写个Python函数就完事的,它必须让LLM看得懂、调得动、收得回。

痛点分析

新手在Tool环节犯的错,堪称“花样作死大赛”。

作死第一式:普通函数直接上岗。 你以为写了个def search(query): ...就能给LLM用了?太天真。LLM又不是你的同事,你叫它一声它不知道这个函数需要什么参数、返回什么格式。没有@tool装饰器,就没有JSON Schema,LLM的Function Calling机制根本不知道该往哪插参数。

作死第二式:Tool的描述写得像谜语。 来看这个经典反面教材:

from langchain_core.tools import tool

@tool
def foo(query: str) -> str:
    """这是一个搜索工具"""
    ...

LLM看到“这是一个搜索工具”,内心是崩溃的:搜什么?参数query是关键词还是URL?返回的是摘要还是全文?描述不清楚,LLM要么不敢调,要么瞎调。Tool的docstring就是LLM的“使用说明书”,你写得含糊,它就用得拉胯。

作死第三式:返回的数据格式对LLM极不友好。 比如这样:

@tool
def search(query: str):
    result = api_call(query)
    return result  # 返回一个复杂的对象或裸JSON

LLM看到的是一个Python对象或者一团没格式化的JSON,它根本不知道怎么从中提取信息给人类用户。ToolNode内部不会帮你做“让LLM看得舒服”的格式化。

作死第四式:绑定节点时直接用裸函数。

# 错误:直接把函数当节点加进去
builder.add_node("tools", search_weather)

这样做绕过了ToolNode的管理,工具调用的并行处理、错误封装、消息格式转换全都没了。LangGraph的ToolNode是专门用来执行工具并生成标准ToolMessage的,你必须用它。

解决方案/正确做法

定义Tool的正确姿势,我总结为“三件套”:@tool装饰器写清楚、返回字符串要干净、ToolNode来托管。

第一步,写好你的Tool函数,docstring必须包含“这个工具是干嘛的”、“每个参数是什么意思”:

from langchain_core.tools import tool

@tool
def search_weather(city: str) -> str:
    """查询指定城市的实时天气情况。
    
    Args:
        city: 城市名称,例如'北京'、'Shanghai'。
    
    Returns:
        该城市当前天气的简洁文字描述。
    """
    # 这里假装调用了天气API
    return f"{city}当前气温26℃,晴,空气质量优。"

第二步,把Tool装进ToolNodeToolNode会自动处理LLM发来的tool_calls,并行执行对应的工具函数,然后把结果包装成标准的ToolMessage塞回State里:

from langgraph.prebuilt import ToolNode

tools = [search_weather]
tool_node = ToolNode(tools)

# 在StateGraph中注册节点
builder.add_node("tools", tool_node)

第三步,也是最关键的一步,让你的LLM“知道”它有这些工具可用。通过bind_tools方法:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")
# 绑定!绑定!绑定!重要的事情说三遍
llm_with_tools = llm.bind_tools(tools)

只有bind之后,LLM在推理时才会输出带有tool_calls字段的AIMessage。LangGraph的agent节点拿到这个消息,才能通过条件边把它路由到tools节点去执行。

生成tool_calls

产出ToolMessage

🧠 LLM
bind_tools后

🧰 ToolNode

这样做的好处是,整个Tool调用链路标准化了。你不再需要手写正则去解析模型想调什么工具,也不用自己拼消息格式。ToolNode帮你把脏活累活全干了,你只需要关心业务逻辑。

小结: Tool定义得清爽,Agent才知道“有事可做,有路可走”。记住,你的docstring不是写给人看的,是写给LLM看的“需求文档”。


4. ReAct循环节点:思考与行动的编排艺术

好了,现在咱们有了State当记忆,有了Tool当手脚,接下来该把ReAct循环真正落地成LangGraph的节点和边了。这是整个Agent的“心脏起搏器”,跳得好不好,全看这一块的编排。

痛点分析

这一块的坑,深不见底。

深坑一:LLM没bind_tools,模型永远“懒得动手”。 有些同学定义了Tool,加进了ToolNode,结果在agent节点里直接调用裸LLM:

# 错误:LLM没有绑定工具,它根本不知道能调工具
def agent_node(state: AgentState):
    llm = ChatOpenAI(model="gpt-4o-mini")
    return {"messages": [llm.invoke(state["messages"])]}

这样模型永远只输出文本,永远不会产生tool_calls,你的ToolNode就成了摆设,Agent退化成了一个纯聊天机器人。

深坑二:条件边逻辑写反,Agent陷入“鬼打墙”。 来看一个经典死循环:

# 错误:永远返回tools,停不下来
def should_continue(state: AgentState):
    last_msg = state["messages"][-1]
    # 忘了检查tool_calls是否存在,或者逻辑写反了
    return "tools"

# 然后这样连边
builder.add_conditional_edges("agent", should_continue, {"tools": "tools"})

结果Agent调完工具,回到LLM,LLM输出最终答案,但条件边一看,还是把它踢回tools节点。于是Agent就在那疯狂自转,直到你的API余额烧光。

深坑三:节点返回的数据结构不遵守State契约。 LangGraph要求节点返回一个dict,key对应State的字段名。有些同学直接返回一个字符串或者一个Message对象:

# 错误:返回的不是dict
def bad_agent_node(state: AgentState):
    response = llm_with_tools.invoke(state["messages"])
    return response  # 这是一个AIMessage对象,不是dict

LangGraph拿到这个对象,发现它没法和State合并,直接报错或者行为异常。

深坑四:忘记把tools节点连回agent节点。 有些同学知道tools执行完要回到LLM继续思考,但忘了加这条边:

# 漏了这条关键的回头路
# builder.add_edge("tools", "agent")

于是Tool执行完,图就不知道往哪走了,直接卡住或者跑到奇怪的地方去。

解决方案/正确做法

正确的节点编排,要拆成“思考节点”和“行动节点”,再用一条“条件边”做路由。

思考节点(agent节点): 负责调用绑定了工具的LLM,产出Thought和潜在的Action请求。

def agent_node(state: AgentState):
    """Agent思考节点:调用LLM决定下一步"""
    messages = state["messages"]
    # 必须是绑定了tools的LLM
    response = llm_with_tools.invoke(messages)
    # 返回dict,key对齐State字段,value是list(因为add_messages要求追加)
    return {"messages": [response]}

行动节点(tools节点): 前面已经用ToolNode定义好了,它会消费tool_calls,产出ToolMessage

条件边(should_continue): 负责检查LLM的最后一句话,判断是要继续调工具,还是已经可以收官了。

from langgraph.graph import END

def should_continue(state: AgentState) -> str:
    """路由决策:Agent是继续干活,还是直接交卷?"""
    last_message = state["messages"][-1]
    # 关键判断:最后一条消息是不是AIMessage,且它有没有要求调用工具
    if last_message.tool_calls:
        return "tools"  # 有工具要调,去tools节点
    return END  # 没了,结束

这里有几个细节必须注意。首先,last_message.tool_calls是LangChain的AIMessage对象上的标准属性,如果LLM决定调工具,这个列表里会有字典,包含namearguments。其次,返回值"tools"END必须与add_conditional_edges里的映射字典对上号。

然后,把图的流转关系画清楚:

tool_calls存在

无工具调用

ToolMessage

agent节点

should_continue

tools节点

END

对应的代码是:

from langgraph.graph import StateGraph, END

builder = StateGraph(AgentState)

# 注册节点
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)

# 关键:tools执行完,必须带着Observation回到agent重新思考
builder.add_edge("tools", "agent")

# 条件边:agent之后,是继续调工具,还是结束?
builder.add_conditional_edges(
    "agent",
    should_continue,
    {
        "tools": "tools",  # 如果should_continue返回"tools",就去tools节点
        END: END           # 如果返回END,就结束
    }
)

这样做的好处是,职责分离到了极致。agent节点只负责“想”,tools节点只负责“干”,should_continue只负责“判”。三者互不干涉,通过State传递消息。你想改LLM模型?只改agent节点。你想加新工具?只改ToolNode的tools列表。这种解耦,就是LangGraph相比纯prompt方案的最大优势。

小结: 节点职责清晰,边逻辑靠谱,ReAct循环才能转起来而不是“鬼打墙”。记住,bind_tools是起点,条件边是闸门,回指边是生命线。


5. Graph编译与运行:从散落节点到严密工作流

零件都造好了,现在要把它们组装成一辆能跑的车。在LangGraph里,这个组装过程就是构建StateGraph,添加节点与边,设置入口,最后compile。编译通过的那一刻,你的Agent才算真正“活”过来。

痛点分析

到了组装环节,新手往往会暴露“工程能力不足”的短板。

短板一:入口点没设,图成了“无头苍蝇”。 有些同学辛辛苦苦把节点和边都加好了,一运行报错:Missing entry point。因为他忘了告诉LangGraph:“嘿,用户消息来了,先交给谁处理?”没有set_entry_point,图根本不知道从哪个节点开始跑。

短板二:条件边的映射字典写反。 add_conditional_edges接收三个参数:源节点、路由函数、映射字典。新手常常搞混映射字典的key和value。比如:

# 错误:映射写反了,或者key对不上should_continue的返回值
builder.add_conditional_edges(
    "agent",
    should_continue,
    {"tools": END, END: "tools"}  # 完全反了!
)

结果Agent该结束时去调工具,该调工具时直接结束,行为完全混乱。

短板三:输入格式错误,invoke直接崩。 LangGraph的图期望输入是一个符合State结构的字典。有些同学直接传字符串:

# 错误:直接传字符串,State需要的是dict
result = graph.invoke("上海天气怎么样?")

正确的方式应该是传入一个dict,且messages字段是Message对象的列表。

短板四:compile之前没检查,运行时才暴雷。 有些图的结构问题(比如节点不存在却被边引用了)在compile阶段就能发现,但新手常常builder.compile()graph.invoke()写在一起,报错信息一出来,分不清是构建问题还是运行问题。

解决方案/正确做法

组装Graph要遵循“四步走”原则:加节点、设入口、连边、再编译。

第一步,创建StateGraph,把定义好的节点加进去:

from langgraph.graph import StateGraph

builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)

第二步,设置入口点。用户的输入消息,应该先交给agent节点去理解和规划:

builder.set_entry_point("agent")

第三步,把边连好。包括普通的顺序边和条件边:

# tools干完活,必须回到agent复盘
builder.add_edge("tools", "agent")

# agent干完活,由should_continue决定下一步
builder.add_conditional_edges(
    "agent",
    should_continue,
    {
        "tools": "tools",
        END: END
    }
)

这里要注意,add_conditional_edges的第三个参数是一个“路由返回值→目标节点”的映射。你的should_continue函数返回什么字符串,这里就必须有对应的key。

第四步,编译:

react_graph = builder.compile()

compile()会把你的图结构编译成一个CompiledGraph对象。这个对象不仅支持同步的invoke,还支持异步的ainvoke、流式输出的stream,以及后面会讲到的断点调试能力。

编译完成后,运行它:

from langchain_core.messages import HumanMessage

final_state = react_graph.invoke(
    {
        "messages": [HumanMessage(content="上海天气怎么样,适合穿什么衣服?")]
    }
)

# 取最后一条AI消息作为答案
print(final_state["messages"][-1].content)

当你看到Agent正确地调用了天气工具,拿到气温数据,然后结合穿衣建议给出最终回答时,那种成就感,真的比你单纯调一个聊天接口爽多了。因为你知道,这一整个决策链条,是你亲手用代码搭出来的,每一步都可控、可观测、可修改。

小结: Graph编译通过的那一刻,你才真正拥有了一个“活”的ReAct Agent。记住,入口、边映射、输入格式,这三样东西错了,编译器救不了你,只能靠自己细心。


6. 运行调试:当Agent开始“抽风”,你怎么接招

代码写完了,一跑就崩?或者Agent陷入了“我觉得我需要搜索→搜索完了→我觉得我还需要搜索”的死循环?别慌,Agent开发里,写代码只占三成,调试占七成。会写Agent是入门,会调Agent才是本事。

痛点分析

调试Agent的痛,和调试普通应用完全不是一个量级。因为Agent的行为带有“不确定性”,同样的输入,LLM可能这次想调工具,下次就直接回答了。

痛点一:黑盒运行,中间过程全靠猜。 很多新手只用一个graph.invoke(),然后直接拿最后一条消息。中间LLM想了什么、调了什么工具、工具返回了什么,完全看不到。一旦结果不对,你只能对着最终结果干瞪眼,根本不知道问题出在哪个节点。

# 错误:黑盒运行,放弃治疗
result = graph.invoke({"messages": [HumanMessage(content="...")]})
print(result["messages"][-1].content)  # 错了?那再跑一次试试

痛点二:Tool异常直接炸穿整个Graph。 你的Agent去调一个外部API,结果网络超时了,或者API返回500。如果你的Tool函数里没有try-catch,异常会直接抛到LangGraph的执行引擎里,整个图直接挂掉,用户看到的就是一堆难看的报错堆栈。

@tool
def fragile_tool(query: str) -> str:
    # 一旦api_call抛异常,整个Agent崩掉
    return api_call(query)

痛点三:Token无限燃烧,钱包在哭泣。 ReAct循环如果没有终止条件,或者条件边写错了,Agent就会陷入无限循环。调一次工具,回来再想想,想想又调一次……你的API调用次数和Token消耗直线上升,等发现的时候,余额已经蒸发了一半。

痛点四:状态黑盒,不知道图跑到哪了。 特别是当图比较复杂的时候,你甚至不确定某个节点到底有没有被执行,或者它拿到的State是不是你预期的那样。

解决方案/正确做法

调试Agent,我送你四把利器:stream观测、异常兜底、循环熔断、状态审计

第一把利器:用stream代替invoke,让过程白盒化。

LangGraph的stream方法可以让你看到图执行的每一个步骤,每个节点输出什么State变化,一目了然:

for event in react_graph.stream(
    {"messages": [HumanMessage(content="北京明天天气如何?")]}
):
    for node_name, node_state in event.items():
        print(f"--- 节点 [{node_name}] 输出 ---")
        # 通常我们关心最后一条消息的内容
        last_msg = node_state["messages"][-1]
        print(last_msg)
        print()

运行之后,你会看到类似这样的输出顺序:agent节点先输出一个带tool_calls的AIMessage,然后tools节点输出一个ToolMessage,然后agent节点再输出一个最终的AIMessage。哪个环节掉了链子,一眼就能定位。

第二把利器:Tool内部做异常处理,把错误变成“人话”返回给LLM。

不要让Tool抛异常,而是把异常包装成字符串,让LLM有机会在下一轮Thought里处理或者告知用户:

@tool
def robust_search(query: str) -> str:
    """尝试搜索,失败时返回友好提示"""
    try:
        return api_call(query)
    except Exception as e:
        # 把错误信息作为Observation返回给LLM
        return f"搜索工具调用失败,错误信息:{str(e)}。请尝试换一种问法。"

这样即使工具挂了,Agent也不会崩,它可能会告诉用户:“刚才搜索出了点问题,但我可以直接根据已有知识回答……”

第三把利器:在State里加计数器,实现循环熔断。

这是生产环境必备的安全阀。修改你的State和节点逻辑:

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
    iteration_count: int  # 增加循环计数器

在agent节点里,每次调用都把计数器加一:

def agent_node(state: AgentState):
    messages = state["messages"]
    response = llm_with_tools.invoke(messages)
    
    current_count = state.get("iteration_count", 0)
    return {
        "messages": [response],
        "iteration_count": current_count + 1
    }

should_continue里加上熔断判断:

def should_continue(state: AgentState) -> str:
    # 如果循环超过5轮,强制结束,防止死循环
    if state.get("iteration_count", 0) > 5:
        return END
    
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

这个小小的改动,能让你在深夜跑测试的时候睡个安稳觉。Agent再抽风,最多转5圈就得给我停下。

第四把利器:善用状态打印做审计。

在关键节点里,你可以临时加上print,把当前的messages长度、最后一条消息的类型打印出来。虽然土,但极其有效。LangGraph的状态是纯粹的Python对象,没有黑魔法,print是最好用的调试手段之一。

def agent_node(state: AgentState):
    print(f"[DEBUG] 当前消息数: {len(state['messages'])}")
    # ... 后续逻辑

这四招结合起来,你的Agent就从“不可控的玄学玩具”变成了“可观测的工程组件”。

小结: 会写Agent是入门,会调Agent才是本事。调试能力决定了你的Agent是玩具还是生产工具。记住,stream是你的眼睛,熔断是你的保险,异常处理是你的底线。


写在最后

咱们这一路走下来,从ReAct循环的原理,到State的记忆设计,到Tool的手脚安装,再到节点的编排、图的编译,最后到运行的调试,算是把LangGraph里构建一个ReAct Agent的完整链路给盘透了。

我知道,看完这篇文章,你可能还是会遇到报错,还是会发现Agent偶尔不按套路出牌。但这很正常。LangGraph不是那种“ import 一下就能毁天灭地”的魔法库,它是一个让你“把不确定的LLM行为框定在确定的状态机里”的工程框架。它的价值不在于让你少写几行代码,而在于让你能清楚地知道:此刻Agent在想什么,下一步它会做什么,如果错了该去哪个节点修。

编程之路不易,但每一步成长都算数。你今天搞明白了State和Reducer的区别,明天就能少踩一个坑;你今天亲手编译通过了一个Graph,明天就能在此基础上搭出更复杂的智能工作流。保持好奇,持续动手,别怕报错,报错才是你真正在学习的证明。你已经在路上了,继续往前走,你也能成为把Agent玩得飞起的大仙。


关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》

更多推荐