引言为什么 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_aftertime-travel 回放一个失败的 ReAct Agent 实战排障以及 Java / j-langchain 与 LangGraph 的调试差异如果你已经会写简单 Agent这篇文章会帮你从“能跑”进入“能排障”。一、为什么 ReAct Agent 特别需要“过程级调试”ReAct 的核心不是一次性回答而是边思考、边行动、边观察。这意味着一个 ReAct Agent 至少包含四类状态用户原始问题LLM 当前的推理结果Tool 调用结果下一步路由决策任何一个环节出问题最终答案都会偏。例如Tool 选错 → 后面全错Observation 没写回 messages → 模型下一轮相当于“没看到工具结果”State 字段名错了 → 节点之间信息断裂停止条件没触发 → 进入死循环所以调试 ReAct Agent 的关键不是问“为什么答案错了”而是问“它从哪一步开始偏了”一旦你习惯了这种思路Agent 调试就会比之前清晰很多。二、第一层调试先用最朴素的方法把每一步打出来很多人一上来就想找可视化平台、Tracing 平台、LangSmith。这些工具当然很好但真正排障时最先能救命的往往是最简单的三件事printverboselogging1.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那一定先打开verboseTrue它最大的价值不是“多打印一点日志”而是让你看到 Agent 的执行轨迹例如Thought: 我需要先查询天气 Action: get_weather Action Input: 北京 Observation: 北京今天晴天25度 Thought: 现在我可以计算昨天的温度 Action: calculator Action Input: 25-3 Observation: 22 Final Answer: 昨天是22度一旦这条链露出来你就能立刻判断模型有没有调用 Tool调的是不是正确的 ToolObservation 是否合理最终答案是不是基于 Observation 得出的3.logging从 demo 走向工程化的最低要求当节点越来越多以后只靠print很快会乱掉。更稳妥的方式是统一用loggingimportlogging logging.basicConfig(levellogging.INFO)loggerlogging.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_modeupdates看每一步改了什么这是最适合排查“节点返回值不对”的模式。forchunkingraph.stream({question:北京天气怎么样},stream_modeupdates):print(chunk)你看到的通常像这样{weather_node:{weather:北京今天晴天25度}}{answer_node:{answer:北京今天晴天25度。}}它回答的是每个节点刚刚往 State 里写了什么如果你预期tool_node应该写回messages结果这里根本没有messages问题就已经很清楚了。2.stream_modevalues看每一步之后完整 State 长什么样如果updates像看 diff那么values就像看快照。forchunkingraph.stream({question:北京天气怎么样},stream_modevalues):print(chunk)这时候你看到的是完整 State例如{question:北京天气怎么样,weather:北京今天晴天25度,answer:}下一步又变成{question:北京天气怎么样,weather:北京今天晴天25度,answer:北京今天晴天25度。}它特别适合排查某个字段是不是被覆盖掉了某一步是不是把旧数据清空了多个节点是不是在写同一个字段3.stream_modedebug看更细的执行轨迹当你已经知道“结果不对”但还不知道“到底从哪一步开始偏”就要上debugforchunkingraph.stream({question:北京天气怎么样},stream_modedebug):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 环境里这张图会直接显示出来。如果你想看得更细也可以打开 xraydisplay(Image(graph.get_graph(xrayTrue).draw_mermaid_png()))为什么流程图这么重要因为它直接帮你看清三件事节点有没有连对循环是不是合理条件路由是不是按预期存在例如你原本想做一个最基本的 ReActllm_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先打开verboseTrue或在 Graph 相关节点里加print。日志显示Thought: 我可以直接估算昨天温度。 Final Answer: 昨天可能是20到22度左右。这说明第一步问题已经非常明确模型根本没调 Tool。第二步检查 Tool 定义是否清楚你再去看工具代码tooldefweather(x:str)-str:获取信息问题马上暴露了工具名太模糊描述太模糊于是改成tooldefget_weather(city:str)-str:查询指定城市今天的天气和温度然后再跑。这次它开始调用 Tool 了但结果还是错。第三步用stream_modeupdates看每步更新forchunkingraph.stream(input_data,stream_modeupdates):print(chunk)结果你发现工具节点只写回了{weather:北京今天晴天25度}但你的 LLM 节点并没有读weather字段而是只读messages。问题就清楚了Tool 虽然执行了但 Observation 没进入下一轮上下文。第四步用stream_modevalues看完整 State继续看完整状态forchunkingraph.stream(input_data,stream_modevalues):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 后端熟悉的责任链FilterInterceptorAOP 日志也就是说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少量 Toolj-langchain 的事件流就已经很好用。但如果你做的是ReAct多工具循环中断与恢复多分支状态流可回放的 Agent那 LangGraph 的调试能力会明显更强。这不是谁更先进而是它们处理的问题层级不同。九、最实用的一套 LangGraph 调试顺序如果你只想记住一套最实用的方法那就记下面这套1. 先确认模型和 Tool 单独可用 2. 用 print / logging 看节点输入输出 3. 用 stream_modeupdates 看每步改了什么 4. 用 stream_modevalues 看完整 State 有没有坏 5. 用 stream_modedebug 看执行轨迹 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 全部绑在一起调。先单独运行 Toolprint(get_weather.invoke({city:北京}))如果 Tool 自己都不稳定后面调 Graph 只会更乱。3. 对 Agent 来说State 设计和日志一样重要很多人把全部精力放在 Prompt 上却忽略了 State 设计。实际上LangGraph 里最常见的问题之一就是State 定义不清导致节点之间信息断裂。所以每个字段最好都明确谁负责写谁负责读是否允许覆盖4. 调试时temperature 尽量设低如果你在调试阶段还把 temperature 设得很高轨迹会很不稳定。建议调试期尽量temperature0这样更容易复现问题。结语一句话总结调试 Agent最怕的不是报错而是“看起来能跑但你不知道它为什么这么跑”。LangGraph 真正强大的地方不只是能写 Agent而是它提供了一整套把 Agent 执行过程掰开看的能力print/logging看节点输入输出stream_modeupdates看每一步更新stream_modevalues看完整 State 快照stream_modedebug看执行细节draw_mermaid_png()看图结构interrupt_before/interrupt_after在关键点停下来time-travel回到失败现场回放当你把这些能力串起来以后ReAct Agent 就不再是一个黑盒。它会变成一个你可以看见暂停回放修改再验证的可调试系统。而这正是 Agent 从 Demo 走向生产级系统的关键一步。 相关资源j-langchain GitHubhttps://github.com/flower-trees/j-langchain