系列「企业级 AI Agent 实现拆解」E35 篇,Part 9 起步篇第五章。你的 Agent 不按预期走,或者干脆卡住了------这篇讲怎么看清楚它在哪里、在干什么。
读完这篇你会知道
- 姿势一:加日志------Agent 执行链里该记什么
- 姿势二:
devops.Init------HTTP 调试服务器看每个节点的输入输出和耗时- 姿势三:Mermaid 可视化------把 Agent 的 Graph 画出来
- 三种姿势什么时候用哪个
为什么调试 Agent 比调试普通程序难
普通程序:函数入参固定,输出确定,断点打下去就能看到状态。
Agent:
- 输入是自然语言,不同表述走不同路径
- LLM 的中间推理不可见,你不知道它"想"了什么
- 多节点编排,某个节点慢或者输出错了,要追几层才能找到
- 流式输出边出边处理,出错时已经输出了一半
三个姿势分别对应这三类问题。
姿势一:加日志
最笨但最直接的方法,适合刚起步、还没用编排框架的场景。
记什么
至少记这三件事:
go
// 1. 每次 LLM 调用的输入和输出
log.Printf("[llm] input: %v", messages)
resp, err := chatModel.Generate(ctx, messages)
log.Printf("[llm] output: %s, finish_reason: %s", resp.Content, resp.ResponseMeta.FinishReason)
// 2. 工具调用
log.Printf("[tool] call: %s, args: %s", toolName, argsJSON)
result, err := tool.Invoke(ctx, args)
log.Printf("[tool] result: %s", result)
// 3. 循环轮次(ReAct 循环最容易死循环)
log.Printf("[react] turn=%d, action=%s", turn, action)
Token 用量放到 INFO 级:
go
usage := resp.ResponseMeta.Usage
log.Printf("[token] prompt=%d, completion=%d, total=%d",
usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens)
不需要记的:
- 中间的字符串拼接过程(你的代码逻辑,不是 Agent 的决策)
- 每个 HTTP 请求的 headers(太啰嗦,掩盖关键信息)
用 Callback 统一加日志(不污染主流程)
E31 的 Agent 是直接调 agent.Stream(ctx, messages)。如果想加日志但不改主流程,用 callback:
go
import "github.com/cloudwego/eino/callbacks"
type DebugCallback struct{}
func (d *DebugCallback) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
log.Printf("[%s] start: %T", info.Name, input)
return ctx
}
func (d *DebugCallback) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
log.Printf("[%s] end: %T", info.Name, output)
return ctx
}
func (d *DebugCallback) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
log.Printf("[%s] error: %v", info.Name, err)
return ctx
}
然后把 callback 注册到 context:
go
ctx = callbacks.CtxWithHandlers(ctx, []callbacks.Handler{&DebugCallback{}})
sr, err := agent.Stream(ctx, messages)
这样不改 Agent 主流程,所有节点的生命周期事件都会触发你的日志。
姿势二:eino-ext devops 交互调试器
自己写日志只能看你加的点,而且看不到节点之间的数据流转。eino-ext/devops 提供一个本地 HTTP 调试服务器,每个节点执行后推送一条 SSE 事件,包含输入、输出、耗时和 Token 数。
启动调试服务器:
go
import "github.com/cloudwego/eino-ext/devops"
func main() {
ctx := context.Background()
// 启动调试 HTTP 服务器(默认 127.0.0.1:52538)
if err := devops.Init(ctx); err != nil {
log.Fatal(err)
}
// 正常编译和运行你的 Agent
// ...
}
devops.Init 在全局注册一个 GraphCompileCallback,所有后续编译的 Graph/Chain 都会自动被捕获。
在浏览器里调试:
访问 http://127.0.0.1:52538(或 Eino 官方 IDE 插件),能看到:
- 所有已编译的 Graph 列表
- 每个 Graph 的节点-边拓扑图(Canvas)
- 从任意节点开始执行(不用从头跑)
- 每个节点完成后推送
NodeDebugState:输入、输出、Token 数、耗时
用命令行看 SSE 流(不用浏览器):
bash
# 创建调试线程
curl -X POST http://127.0.0.1:52538/eino/devops/debug/v1/graphs/{graph_id}/threads
# 从指定节点开始,SSE 流式返回每节点状态
curl -N -X POST http://127.0.0.1:52538/eino/devops/debug/v1/graphs/{graph_id}/threads/{tid}/stream \
-H 'Content-Type: application/json' \
-d '{"from_node": "ChatModel", "input": {...}}'
什么时候用:
- 工具没被调用------看 ChatModel 节点输出有没有
tool_calls - 某个节点耗时异常------每帧
NodeDebugState带毫秒级耗时 - 想从中间节点开始重试,不用重跑前置节点
注意: devops 服务器仅用于开发环境,不要在生产部署里启动它。
姿势三:Mermaid 可视化
你的 Agent 走了什么路径?用可视化直接把 Graph 画出来。
go
import "github.com/cloudwego/eino/devops"
// graph 编译前,生成 Mermaid 图
diagram, err := devops.Visualize(graph)
if err != nil {
log.Fatal(err)
}
fmt.Println(diagram)
输出是标准的 Mermaid 文本:
lua
graph TD
__start__ --> ChatTemplate
ChatTemplate --> ChatModel
ChatModel --> |tool_calls != nil| ToolsNode
ChatModel --> |finish_reason == stop| __end__
ToolsNode --> ChatModel
把这段文本粘到 mermaid.live,就能看到图:
scss
[start] → [ChatTemplate] → [ChatModel] ⟶(有工具调用) [ToolsNode] → [ChatModel]
⟶(stop) [end]
什么时候用:
- 新接手一个别人写的 Agent,不知道它的结构
- 怀疑分支路由配错了(比如工具调用应该回到 ChatModel 但实际走到了 end)
- 给团队 review 时说明 Agent 的执行流程
在 HTTP handler 里暴露调试端点(开发环境):
go
// 开发环境加一个 /debug/graph 端点
http.HandleFunc("/debug/graph", func(w http.ResponseWriter, r *http.Request) {
diagram, _ := devops.Visualize(graph)
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, diagram)
})
三种姿势怎么选
| 问题 | 用哪个姿势 |
|---|---|
| Agent 没有按预期调工具 | 姿势二(devops),看 ChatModel 节点输出是否有 tool_call |
| 工具返回了什么 | 姿势二(devops),看 ToolsNode 节点输出 |
| 某次请求 Token 用了多少 | 姿势一(日志),在 OnEnd callback 里记 usage |
| 新接手 Agent,不懂它的结构 | 姿势三(Mermaid),先画图再读代码 |
| 某个节点报错但看不到上下文 | 姿势二(devops)+ 姿势一(OnError callback) |
| 分支路由配错,走了错的路径 | 姿势三(Mermaid)看图 + 姿势二确认实际路径 |
实战:一次调试全流程
假设你的 Agent 问了一个天气问题,却没有调用 get_weather 工具,直接给了一个"我不知道":
第一步:启动 devops,重现问题
go
devops.Init(ctx) // 加这一行,启动本地调试服务器
访问 http://127.0.0.1:52538,选择你的 Graph,创建调试线程,发送问题。
看 ChatModel 节点的输出:
json
{"role": "assistant", "content": "我不知道北京今天的天气"}
没有 tool_calls 字段------LLM 没有调工具。
第二步:看 ChatModel 的输入
json
[
{"role": "system", "content": "你是一个助手"},
{"role": "user", "content": "北京今天天气怎么样?"}
]
Prompt 里没有提到工具,LLM 不知道有工具可用。
根因: 工具没有绑定到 ChatModel。修复:
go
// 错误:直接用 chatModel
runner := chain.AppendChatModel(chatModel)
// 正确:绑定工具
chatModelWithTools, _ := chatModel.WithTools([]*schema.ToolInfo{weatherToolInfo})
runner := chain.AppendChatModel(chatModelWithTools)
小结
三种调试姿势:
- 日志 + Callback:精确记录你关心的点,适合生产环境监控
- compose.WithDebug:自动记录所有节点 I/O,适合开发调试(生产关掉)
- Mermaid 可视化:把 Graph 画出来,适合理解结构和排查路由问题
Agent 调试的思路跟传统程序一样:缩小范围,找到第一个行为偏离预期的节点 。WithDebug 是缩小范围最快的工具。
下一篇 E36 会拆 compose.WithDebug 的源码,看这个一行代码背后怎么在每个节点插入记录逻辑。
代码参考:eino-examples/quickstart · Eino devops/visualize