当前位置: 首页 > news >正文

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

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

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

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

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

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

输入 → 业务逻辑 → 输出

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

用户问题 → 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 节点:

fromtypingimportTypedDictclassAgentState(TypedDict):question:strweather:stranswer:strdefweather_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,那一定先打开:

verbose=True

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

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

importlogging logging.basicConfig(level=logging.INFO)logger=logging.getLogger(__name__)defcalculator_node(state):logger.info("calculator_node input=%s",state)result={"answer":"22"}logger.info("calculator_node output=%s",result)returnresult

这样做的好处是:

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

一句话说:

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


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

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

因为你真正想知道的是:

Graph 在每一步到底改了什么?
当前完整 State 长什么样?
它到底是怎么沿着边走的?

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

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

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

forchunkingraph.stream({"question":"北京天气怎么样?"},stream_mode="updates"):print(chunk)

你看到的通常像这样:

{"weather_node":{"weather":"北京今天晴天,25度"}}{"answer_node":{"answer":"北京今天晴天,25度。"}}

它回答的是:

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

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

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

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

forchunkingraph.stream({"question":"北京天气怎么样?"},stream_mode="values"):print(chunk)

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

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

下一步又变成:

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

它特别适合排查:

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

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

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

forchunkingraph.stream({"question":"北京天气怎么样?"},stream_mode="debug"):print(chunk)

debug模式的价值在于:

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

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

LLM 节点 → Tool 节点 → 再回 LLM 节点

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

4. 三种模式怎么选?

最实用的顺序是:

先用 updates 看“写了什么” 再用 values 看“全局状态还好吗” 最后用 debug 看“图到底怎么走的”

这样排查起来最快。


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

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

例如:

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

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

最常见写法

fromIPython.displayimportImage,display display(Image(graph.get_graph().draw_mermaid_png()))

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

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

display(Image(graph.get_graph(xray=True).draw_mermaid_png()))

为什么流程图这么重要?

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

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

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

llm_call → tool_node → llm_call → END

结果画出来一看居然是:

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

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

目标问题是:

北京今天 25 度,比昨天高 3 度,那昨天多少度?

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

1. 调 get_weather 2. 得到 25 度 3. 调 calculator(25-3) 4. 输出 22 度

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

昨天可能是 20 到 22 度左右。

明显在猜。

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

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

日志显示:

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

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

模型根本没调 Tool。

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

你再去看工具代码:

@tooldefweather(x:str)->str:"""获取信息"""

问题马上暴露了:

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

于是改成:

@tooldefget_weather(city:str)->str:"""查询指定城市今天的天气和温度"""

然后再跑。

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

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

forchunkingraph.stream(input_data,stream_mode="updates"):print(chunk)

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

{"weather":"北京今天晴天,25度"}

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

问题就清楚了:

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

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

继续看完整状态:

forchunkingraph.stream(input_data,stream_mode="values"):print(chunk)

你会看到:

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

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

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

这时你再画一下流程图:

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

图显示:

llm_call → tool_node → llm_call → END

说明边本身没问题。

所以问题进一步收敛为:

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

第六步:在tool_node后中断

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

停下来一看,当前 State 是:

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

但没有 ToolMessage。

于是你修改工具节点:

return{"messages":state["messages"]+[tool_message]}

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

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

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

这次日志变成:

Action: get_weather Observation: 北京今天晴天,25度 Action: calculator Observation: 22 Final Answer: 昨天是22度。

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

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

这套顺序非常值得记住:

先看有没有调 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 调试顺序

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

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:

print(get_weather.invoke({"city":"北京"}))

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

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

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

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

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

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

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

4. 调试时,temperature 尽量设低

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

建议调试期尽量:

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 走向生产级系统的关键一步。

📎 相关资源

  • j-langchain GitHub:https://github.com/flower-trees/j-langchain
http://www.jsqmd.com/news/685431/

相关文章:

  • Java Loom响应式改造失败率高达67%?资深专家复盘17个真实故障场景及可复用修复模板
  • Ubuntu 24.04下MT7922蓝牙驱动问题解决方案
  • 2026年4月北京本地收车权威机构推荐榜:北京无套路收车/北京正规收车/北京淘汰车回收/北京私家车回收/北京诚信收车/选择指南 - 优质品牌商家
  • 17-4Ph不锈钢厂商那家好?2026年17-4Ph不锈钢厂商推荐 - 品牌2026
  • Wasserstein GAN:原理、实现与实战调优
  • 从采集到冻存:如何确保血清血浆样本在多因子检测中的可靠性?
  • 番外篇第10集:大结局!AIOps 统一可视化大屏与年度运维报告自动生成
  • 汽车智能制造效率困局怎么破?深度解析APS+AI如何赋能排程计划
  • Verilog参数化设计:从模块定义到灵活例化的实战指南
  • 使用 LangSmith 专业调试 AI Agent:追踪、评估与问题定位
  • 机器人声学验证技术:非侵入式行为监测方案
  • nli-MiniLM2-L6-H768效果展示:中英文混合标签(technology, 情感积极)精准识别
  • 别再只会用printf了!STM32串口发送字符串的3种实用方法对比(含源码)
  • VxWorks核心内核模块:任务管理模块深度解读(第一部分)
  • Python 容器类型判断与类型转换
  • 2026年西南地区铁马围挡厂家TOP5推荐一站式服务优选:装配式围挡租赁/铁马围挡/围挡租赁施工/地铁围挡/大门围挡/选择指南 - 优质品牌商家
  • 校招生怎么在面试中证明自己AI Coding能力
  • Rails 7.1 新特性深度解析:从Dockerfile生成到异步查询的全面升级
  • Raspberry Pi Pico 2 RISC-V开发实战指南
  • 程序员别再死磕CRUD!拥抱大模型才是破局出路
  • GLM-Image提示词实战手册:高质量生成必备结构+负向词避坑清单
  • Blazor Server + SignalR Edge边缘渲染架构实录(2026超低延迟方案):单节点支撑23,000并发UI流,吞吐提升410%的配置密钥
  • 工程师转型创业者的技术优势与商业思维融合
  • 智能整合员中的接口对接与流程优化
  • Gitee Repo:构筑国产软件供应链安全的数字长城
  • 【AI开源雷达】GitHub最热AI项目:多模态RAG、热点雷达与YouTube增强
  • Hypnos-i1-8B代码生成效果秀:根据注释自动生成Python/JavaScript函数
  • 程序员不内卷,深耕大模型赛道越走越稳
  • THIRDREALITY MK1智能机械键盘:Matter协议与家居控制实践
  • AI Agent Harness Engineering 如何应用于电商并提升 GMV 与转化率