LangGraph 与 ReAct Agent 调试技巧:从日志到可视化全解析

引言:为什么 Agent 最难的不是"写出来",而是"知道它为什么错"?

很多人第一次做 ReAct Agent,都会有一种挫败感:

  • 代码能跑,但结果不对
  • Tool 明明定义了,Agent 却不调用
  • Graph 明明连上了,却在某一步开始死循环
  • 最终答案错了,但你根本不知道是 Prompt、Tool、State,还是模型出了问题

这和普通 CRUD 系统很不一样。

传统后端开发里,你通常面对的是:

text 复制代码
输入 → 业务逻辑 → 输出

而 Agent 的真实执行轨迹更像:

text 复制代码
用户问题
→ LLM 思考
→ 选择 Tool
→ Tool 执行
→ Observation 写回 State
→ LLM 再思考
→ 决定继续还是结束

也就是说,Agent 出错时,问题往往不在"最终答案"本身,而在它的执行路径。

它可能在第 1 步做对了,第 2 步偏了,第 3 步又把偏差放大,最后你只看到一个错误答案。

所以,真正的 Agent 调试,不是只看最终输出,而是要同时看:

  • 每一步输入了什么
  • 每一步走到了哪个节点
  • 每一步 State 如何变化
  • Tool 返回了什么
  • Agent 为什么没有停下来

这也是 LangGraph 特别适合做复杂 Agent 的一个重要原因:

它不只是让你"能写 Agent",还让你"能看见 Agent 是怎么跑的"。

这篇文章就专门解决这个问题。我们会从最基础的 print / logging 开始,一直讲到:

  • stream_mode=["debug", "updates", "values"]
  • graph.get_graph().draw_mermaid_png()
  • interrupt_before / interrupt_after
  • time-travel 回放
  • 一个失败的 ReAct Agent 实战排障
  • 以及 Java / j-langchain 与 LangGraph 的调试差异

如果你已经会写简单 Agent,这篇文章会帮你从"能跑"进入"能排障"。


一、为什么 ReAct Agent 特别需要"过程级调试"?

ReAct 的核心不是一次性回答,而是:

边思考、边行动、边观察。

这意味着,一个 ReAct Agent 至少包含四类状态:

  1. 用户原始问题
  2. LLM 当前的推理结果
  3. Tool 调用结果
  4. 下一步路由决策

任何一个环节出问题,最终答案都会偏。

例如:

  • Tool 选错 → 后面全错
  • Observation 没写回 messages → 模型下一轮相当于"没看到工具结果"
  • State 字段名错了 → 节点之间信息断裂
  • 停止条件没触发 → 进入死循环

所以,调试 ReAct Agent 的关键不是问:

"为什么答案错了?"

而是问:

"它从哪一步开始偏了?"

一旦你习惯了这种思路,Agent 调试就会比之前清晰很多。


二、第一层调试:先用最朴素的方法把每一步打出来

很多人一上来就想找可视化平台、Tracing 平台、LangSmith。

这些工具当然很好,但真正排障时,最先能救命的,往往是最简单的三件事:

  • print
  • verbose
  • logging

1. print:先确认节点到底有没有执行

例如你写了一个 LangGraph 节点:

python 复制代码
from typing import TypedDict

class AgentState(TypedDict):
    question: str
    weather: str
    answer: str


def weather_node(state: AgentState):
    print("[weather_node] input:", state)
    result = "北京今天晴天,25度"
    print("[weather_node] output:", result)
    return {"weather": result}

这种写法虽然原始,但它特别适合回答最基础的问题:

  • 节点有没有真正执行
  • 传进来的 State 到底长什么样
  • 节点返回的字段是不是你以为的那个字段

很多问题在这一步就能发现。

例如你原本以为 state["question"] 一定存在,结果打印出来根本没有;或者你以为自己返回的是 {"weather": ...},结果实际上返回了 {"result": ...},后面节点当然读不到。

2. verbose:先把 ReAct 的轨迹暴露出来

如果你还在用 LangChain 的 agent 封装,而不是完全手写 Graph,那一定先打开:

python 复制代码
verbose=True

它最大的价值不是"多打印一点日志",而是让你看到 Agent 的执行轨迹,例如:

text 复制代码
Thought: 我需要先查询天气
Action: get_weather
Action Input: 北京
Observation: 北京今天晴天,25度
Thought: 现在我可以计算昨天的温度
Action: calculator
Action Input: 25-3
Observation: 22
Final Answer: 昨天是22度

一旦这条链露出来,你就能立刻判断:

  • 模型有没有调用 Tool
  • 调的是不是正确的 Tool
  • Observation 是否合理
  • 最终答案是不是基于 Observation 得出的

3. logging:从 demo 走向工程化的最低要求

当节点越来越多以后,只靠 print 很快会乱掉。

更稳妥的方式是统一用 logging

python 复制代码
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def calculator_node(state):
    logger.info("calculator_node input=%s", state)
    result = {"answer": "22"}
    logger.info("calculator_node output=%s", result)
    return result

这样做的好处是:

  • 后面接日志采集平台更方便
  • 可以按级别区分 info / warning / error
  • 方便定位是哪一个 node 出的问题

一句话说:

print 适合第一时间确认问题,logging 适合长期维护。


三、LangGraph 内置调试:stream_mode=["debug", "updates", "values"]

当你的 Agent 进入 LangGraph 这一层后,只靠 print 已经不够了。

因为你真正想知道的是:

Graph 在每一步到底改了什么?

当前完整 State 长什么样?

它到底是怎么沿着边走的?

这时候,LangGraph 的 stream_mode 就非常关键。

1. stream_mode="updates":看每一步改了什么

这是最适合排查"节点返回值不对"的模式。

python 复制代码
for chunk in graph.stream(
    {"question": "北京天气怎么样?"},
    stream_mode="updates"
):
    print(chunk)

你看到的通常像这样:

python 复制代码
{"weather_node": {"weather": "北京今天晴天,25度"}}
{"answer_node": {"answer": "北京今天晴天,25度。"}}

它回答的是:

每个节点,刚刚往 State 里写了什么?

如果你预期 tool_node 应该写回 messages,结果这里根本没有 messages,问题就已经很清楚了。

2. stream_mode="values":看每一步之后完整 State 长什么样

如果 updates 像看 diff,那么 values 就像看快照。

python 复制代码
for chunk in graph.stream(
    {"question": "北京天气怎么样?"},
    stream_mode="values"
):
    print(chunk)

这时候你看到的是完整 State,例如:

python 复制代码
{
  "question": "北京天气怎么样?",
  "weather": "北京今天晴天,25度",
  "answer": ""
}

下一步又变成:

python 复制代码
{
  "question": "北京天气怎么样?",
  "weather": "北京今天晴天,25度",
  "answer": "北京今天晴天,25度。"
}

它特别适合排查:

  • 某个字段是不是被覆盖掉了
  • 某一步是不是把旧数据清空了
  • 多个节点是不是在写同一个字段

3. stream_mode="debug":看更细的执行轨迹

当你已经知道"结果不对",但还不知道"到底从哪一步开始偏",就要上 debug

python 复制代码
for chunk in graph.stream(
    {"question": "北京天气怎么样?"},
    stream_mode="debug"
):
    print(chunk)

debug 模式的价值在于:

  • 能更细粒度地看到当前运行的是哪个节点
  • 更接近真实的执行轨迹
  • 更容易理解 Graph 的控制流

尤其在 ReAct Agent 里,debug 模式非常适合观察:

text 复制代码
LLM 节点 → Tool 节点 → 再回 LLM 节点

这条循环到底有没有按预期发生。

4. 三种模式怎么选?

最实用的顺序是:

text 复制代码
先用 updates 看"写了什么"
再用 values 看"全局状态还好吗"
最后用 debug 看"图到底怎么走的"

这样排查起来最快。


四、可视化工具:graph.get_graph().draw_mermaid_png() 把图画出来

很多 LangGraph 问题,根本不是节点代码问题,而是图连错了。

例如:

  • 明明应该 llm_call → tool_node
  • 结果却直接 llm_call → END
  • 或者 tool_node → tool_node
  • 或者条件边永远走同一个分支

这时候,最有效的方法不是继续读代码,而是先把图画出来。

最常见写法

python 复制代码
from IPython.display import Image, display

display(Image(graph.get_graph().draw_mermaid_png()))

如果你在 Notebook 环境里,这张图会直接显示出来。

如果你想看得更细,也可以打开 xray:

python 复制代码
display(Image(graph.get_graph(xray=True).draw_mermaid_png()))

为什么流程图这么重要?

因为它直接帮你看清三件事:

  1. 节点有没有连对
  2. 循环是不是合理
  3. 条件路由是不是按预期存在

例如你原本想做一个最基本的 ReAct:

text 复制代码
llm_call → tool_node → llm_call → END

结果画出来一看居然是:

text 复制代码
llm_call → END

那问题根本就不是 Prompt,而是边没有连上。

对于复杂 Graph 来说,流程图几乎就是最便宜、最快速的排错方法。


五、中断调试:interrupt_before / interrupt_after

当你已经能看到日志、能看到 State、能看到图,下一步最有价值的能力就是:

在关键节点前后"停下来"。

这就是中断调试。

1. 为什么中断很重要?

因为很多问题不是"最后错了",而是:

在某一步,参数就已经错了。

例如:

  • Tool 调用前,参数提取错了
  • Tool 返回后,Observation 没写回 State
  • 路由决策前,某个标志位不对

如果你只能看最终日志,你往往只能猜。

而如果你能在节点前后停下来,就能直接看现场。

2. interrupt_before

顾名思义,就是在某个节点执行前停住。

特别适合:

  • 想确认进入这个节点前的 State 是否正确
  • 想检查 Tool 调用参数是否已经准备好
  • 想看分支路由之前的判断依据

3. interrupt_after

就是在某个节点执行后停住。

特别适合:

  • 想确认节点执行结果写回了什么
  • 想看 Tool 的 Observation 有没有落到正确字段
  • 想看某个节点是不是覆盖了旧 State

一句话说:

interrupt_before 看输入,interrupt_after 看输出。


六、时间旅行(time-travel)与回放:不是炫技,而是最强复盘工具

当你已经有 checkpoint / persistence 后,LangGraph 一个非常强大的能力就是:

time-travel

你可以把它理解成:

  • 回到过去某个执行点
  • 从那里重新跑
  • 或者改一点 State,再从那里分叉重跑

这对 ReAct Agent 来说特别有价值。

因为很多 Agent 失败不是偶发,而是:

某一步开始偏了,后面一路错到底。

如果你只能从头重跑,每次都要把整个输入重新构造一遍,非常低效。

而有了 time-travel 以后,你可以:

  • 回到出问题前的 checkpoint
  • 重新执行后续节点
  • 验证修复有没有生效

它最适合什么场景?

特别适合:

  • 线上失败复盘
  • 修完 bug 后验证是否真的解决
  • 比较两个 Prompt 改动是否影响轨迹
  • 尝试不同分支,而不是每次从头跑

对 Agent 来说,这比普通日志强很多,因为它不是"回忆发生了什么",而是:

把失败现场真正还原出来。


七、实战:一步步调试一个失败的 ReAct Agent

下面做一个最典型的失败案例。

目标问题是:

text 复制代码
北京今天 25 度,比昨天高 3 度,那昨天多少度?

理论上,正确轨迹应该是:

text 复制代码
1. 调 get_weather
2. 得到 25 度
3. 调 calculator(25-3)
4. 输出 22 度

但现在 Agent 给出的结果却是:

text 复制代码
昨天可能是 20 到 22 度左右。

明显在猜。

第一步:先看它有没有调用 Tool

先打开 verbose=True 或在 Graph 相关节点里加 print

日志显示:

text 复制代码
Thought: 我可以直接估算昨天温度。
Final Answer: 昨天可能是20到22度左右。

这说明第一步问题已经非常明确:

模型根本没调 Tool。

第二步:检查 Tool 定义是否清楚

你再去看工具代码:

python 复制代码
@tool
def weather(x: str) -> str:
    """获取信息"""

问题马上暴露了:

  • 工具名太模糊
  • 描述太模糊

于是改成:

python 复制代码
@tool
def get_weather(city: str) -> str:
    """查询指定城市今天的天气和温度"""

然后再跑。

这次它开始调用 Tool 了,但结果还是错。

第三步:用 stream_mode="updates" 看每步更新

python 复制代码
for chunk in graph.stream(input_data, stream_mode="updates"):
    print(chunk)

结果你发现工具节点只写回了:

python 复制代码
{"weather": "北京今天晴天,25度"}

但你的 LLM 节点并没有读 weather 字段,而是只读 messages

问题就清楚了:

Tool 虽然执行了,但 Observation 没进入下一轮上下文。

第四步:用 stream_mode="values" 看完整 State

继续看完整状态:

python 复制代码
for chunk in graph.stream(input_data, stream_mode="values"):
    print(chunk)

你会看到:

  • weather 字段有值
  • messages 没追加 ToolMessage

这说明错不在模型,而在 Tool 节点返回值设计。

第五步:把图画出来,确认边没连错

这时你再画一下流程图:

python 复制代码
display(Image(graph.get_graph().draw_mermaid_png()))

图显示:

text 复制代码
llm_call → tool_node → llm_call → END

说明边本身没问题。

所以问题进一步收敛为:

node 逻辑没把 Observation 正确写回 State。

第六步:在 tool_node 后中断

这时候最有效的做法,就是 interrupt_after=["tool_node"]

停下来一看,当前 State 是:

python 复制代码
{
  "messages": [...原始对话...],
  "weather": "北京今天晴天,25度"
}

但没有 ToolMessage。

于是你修改工具节点:

python 复制代码
return {
    "messages": state["messages"] + [tool_message]
}

第七步:用 time-travel 回到失败现场再跑一次

现在不需要从头重新构造整个输入。

直接回到工具节点前的 checkpoint,再 replay。

这次日志变成:

text 复制代码
Action: get_weather
Observation: 北京今天晴天,25度
Action: calculator
Observation: 22
Final Answer: 昨天是22度。

到这里,问题才真正修复。

这个案例最重要的不是代码,而是顺序

这套顺序非常值得记住:

text 复制代码
先看有没有调 Tool
→ 再看 Tool 定义
→ 再看 updates
→ 再看 values
→ 再看 graph
→ 再 interrupt
→ 最后 replay 验证

这就是调试 ReAct Agent 最有效的一条路径。


八、Python / LangGraph 与 Java / j-langchain 的调试差异

这一部分非常值得写,因为它能让 Java 工程师马上理解:

为什么 LangGraph 的调试体验和 j-langchain 不一样。

1. j-langchain 更像"链路事件调试"

在 j-langchain 里,你更常见的调试方式是:

  • streamEvent() 返回的事件流
  • on_chain_start
  • on_chain_end
  • 看 Prompt、LLM、Parser 的输入输出

这种方式非常像 Java 后端熟悉的:

  • 责任链
  • Filter
  • Interceptor
  • AOP 日志

也就是说,j-langchain 更适合回答:

这一条 AI 链上,每个组件做了什么?

对于:

  • Prompt → LLM → Parser
  • 简单 Tool 调用
  • 线性工作流

它的调试体验非常顺手。

2. LangGraph 更像"状态机 / 图执行调试"

而 LangGraph 不只是看"经过了哪些节点",它更强调:

  • 当前 State 是什么
  • 走的是哪条边
  • 为什么进入这个节点
  • checkpoint 存在哪里
  • 能不能回到中间再跑一次

也就是说,LangGraph 更适合回答:

这个 Agent 为什么会沿着这条路径走?

这对于:

  • ReAct
  • 多 Tool 循环
  • 条件路由
  • 中断恢复
  • time-travel 回放

特别重要。

3. 两者最本质的差别

一句话概括:

j-langchain 更像"调用链调试";

LangGraph 更像"状态机调试"。

所以如果你做的是:

  • Java 后端里的简单 AI 链
  • Prompt → 模型 → Parser
  • 少量 Tool

j-langchain 的事件流就已经很好用。

但如果你做的是:

  • ReAct
  • 多工具循环
  • 中断与恢复
  • 多分支状态流
  • 可回放的 Agent

那 LangGraph 的调试能力会明显更强。

这不是谁更先进,而是它们处理的问题层级不同。


九、最实用的一套 LangGraph 调试顺序

如果你只想记住一套最实用的方法,那就记下面这套:

text 复制代码
1. 先确认模型和 Tool 单独可用
2. 用 print / logging 看节点输入输出
3. 用 stream_mode="updates" 看每步改了什么
4. 用 stream_mode="values" 看完整 State 有没有坏
5. 用 stream_mode="debug" 看执行轨迹
6. 用 draw_mermaid_png() 看图是不是连错了
7. 用 interrupt_before / interrupt_after 卡住关键节点
8. 用 checkpoint + time-travel 回放失败现场

这套顺序的好处是:

  • 从最简单的方法开始
  • 每一步都回答更具体的问题
  • 不会一开始就陷入复杂工具
  • 真遇到复杂 bug,也有更强手段接上

很多 Agent bug,用不到 time-travel 就能解决;

但真正复杂的 bug,没有 time-travel 又很难彻底定位。

所以它们不是替代关系,而是层层递进的关系。


十、给工程师的调试建议

最后给你几条非常实用的经验。

1. 不要一上来就改 Prompt

很多人结果一错,第一反应就是改 Prompt。

但真正的问题,可能根本不在 Prompt,而在:

  • Tool 名字写错
  • Observation 没写回 messages
  • 边连错了
  • State 字段丢了

所以排查一定要先看轨迹,再改 Prompt。

2. 先确认 Tool 能单独运行

不要把模型、Graph、Tool 全部绑在一起调。

先单独运行 Tool:

python 复制代码
print(get_weather.invoke({"city": "北京"}))

如果 Tool 自己都不稳定,后面调 Graph 只会更乱。

3. 对 Agent 来说,State 设计和日志一样重要

很多人把全部精力放在 Prompt 上,却忽略了 State 设计。

实际上,LangGraph 里最常见的问题之一就是:

State 定义不清,导致节点之间信息断裂。

所以每个字段最好都明确:

  • 谁负责写
  • 谁负责读
  • 是否允许覆盖

4. 调试时,temperature 尽量设低

如果你在调试阶段还把 temperature 设得很高,轨迹会很不稳定。

建议调试期尽量:

python 复制代码
temperature=0

这样更容易复现问题。


结语

一句话总结:

调试 Agent,最怕的不是报错,而是"看起来能跑,但你不知道它为什么这么跑"。

LangGraph 真正强大的地方,不只是能写 Agent,而是它提供了一整套把 Agent 执行过程掰开看的能力:

  • print / logging:看节点输入输出
  • stream_mode="updates":看每一步更新
  • stream_mode="values":看完整 State 快照
  • stream_mode="debug":看执行细节
  • draw_mermaid_png():看图结构
  • interrupt_before / interrupt_after:在关键点停下来
  • time-travel:回到失败现场回放

当你把这些能力串起来以后,ReAct Agent 就不再是一个黑盒。

它会变成一个你可以:

  • 看见
  • 暂停
  • 回放
  • 修改
  • 再验证

的可调试系统。

而这,正是 Agent 从 Demo 走向生产级系统的关键一步。

📎 相关资源

相关推荐
怕浪猫3 小时前
第16章 、LangChain错误处理与鲁棒性设计
langchain·openai·ai编程
庚昀◟3 小时前
基于 LangChain、RAG、LoRA 、Streamlit 的知识库问答客服系统从零到一(附项目源码)
人工智能·langchain·milvus
JaydenAI3 小时前
[FastMCP设计、原理与应用-14]FastMCP——架构之魂,构建MCP应用的统一入口与调度中枢
python·ai编程·ai agent·mcp·fastmcp
weisian1513 小时前
进阶篇-LangChain篇-15--高级Agent架构—复杂任务拆解(Plan-and-Execute架构)和多智能体协作(LangGraph)
java·架构·langchain·langgraph·planexecute架构
大模型真好玩17 小时前
LangChain DeepAgents 速通指南(七)—— DeepAgents使用Agent Skill
人工智能·langchain·deepseek
逾明17 小时前
Claude Code及Codex的MCP安装和Mastergo MCP的使用
前端·mcp
王飞飞不会飞1 天前
服务器LLama Factory Lora 微调模型过程记录
langchain·aigc
阿巴阿巴2361 天前
MCP Function Tools
mcp
Csvn1 天前
🌟 LangChain 30 天保姆级教程 · Day 27|RAG 安全加固实战!防注入、防泄露、权限控制,守护企业知识资产!
langchain