回调函数调试Agent

2026年4月15日 · 576 字 · 3 分钟

如何使用回调函数调试Agent,无侵入式地监控和调试AI Agent的运行过程

什么是回调函数(Callback)

回调函数是Eino框架提供的一种切面能力,允许开发者在不修改原有代码的情况下,无侵入式地注入日志、追踪、指标等功能。这在调试AI Agent时特别有用,可以帮助我们:

  1. 监控Agent的执行流程 - 了解Agent每一步在做什么
  2. 调试输入输出 - 查看ChatModel接收到的消息和返回的结果
  3. 追踪工具调用 - 了解Agent调用了哪些工具,传入了什么参数
  4. 性能分析 - 分析每个步骤的耗时

回调函数的实现方式

callback_tool/callback.go中,我们实现了多种回调函数,满足不同的调试需求。

1. 全量日志回调 - LoggerCallbacks

这是最基础的回调实现,记录所有组件的所有事件:

type LoggerCallbacks struct{}

func (l *LoggerCallbacks) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
    log.Printf("[INPUT] name: %v, type: %v, component: %v, input: %v", info.Name, info.Type, info.Component, input)
    return ctx
}

func (l *LoggerCallbacks) OnEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
    log.Printf("[OUTPUT] name: %v, type: %v, component: %v, output: %v", info.Name, info.Type, info.Component, output)
    return ctx
}

func (l *LoggerCallbacks) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
    log.Printf("[ERROR] name: %v, type: %v, component: %v, error: %v", info.Name, info.Type, info.Component, err)
    return ctx
}

func (l *LoggerCallbacks) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
    return ctx
}

func (l *LoggerCallbacks) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
    return ctx
}

使用方式:

callbacks.AppendGlobalHandlers(new(LoggerCallbacks))

这个回调会打印所有组件(ChatModel、Tool等)的输入输出,适合初步调试时使用。

2. 选择性回调 - GetStartAndEndCallback

如果你只想关注开始和结束事件,可以使用Builder模式构建:

func GetStartAndEndCallback() callbacks.Handler {
    return callbacks.NewHandlerBuilder().
        OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
            log.Printf("[INPUT] name: %v, type: %v, component: %v, input: %v", info.Name, info.Type, info.Component, input)
            return ctx
        }).
        OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
            log.Printf("[OUTPUT] name: %v, type: %v, component: %v, output: %v", info.Name, info.Type, info.Component, output)
            return ctx
        }).
        Build()
}

使用方式:

callbacks.AppendGlobalHandlers(GetStartAndEndCallback())

使用Builder模式的好处是:你可以自由组合需要关注的事件类型(OnStart、OnEnd、OnError等)。

3. 特定组件回调 - ChatModel专用回调

如果你只想监控ChatModel组件,可以使用NewHandlerHelper来过滤:

func GetChatModelInputCallback() callbacks.Handler {
    return cbutils.NewHandlerHelper().ChatModel(&cbutils.ModelCallbackHandler{
        OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *model.CallbackInput) context.Context {
            fmt.Printf("\n[ChatModel Input] component: %s\n", info.Name)
            for i, msg := range input.Messages {
                fmt.Printf("  Message %d [%s]: %s\n", i+1, msg.Role, msg.Content)
                if len(msg.ToolCalls) > 0 {
                    fmt.Printf("    Tool Calls: %d\n", len(msg.ToolCalls))
                    for j, tc := range msg.ToolCalls {
                        fmt.Printf("      %d. %s: %s\n", j+1, tc.Function.Name, tc.Function.Arguments)
                    }
                }
            }
            return ctx
        },
    }).Handler()
}

输出示例:

[ChatModel Input] component: trip_plan
  Message 1 [system]: 请帮我完成旅行规划,包括什么时间去什么景点,并列出详细的高铁车次和出发时间
  Message 2 [user]: 我想去上海旅游3天(从明天开始),请帮我做一份旅游攻略
  Message 3 [assistant]: 我来帮您规划上海3天的旅游行程...
  Message 4 [tool]: ...

使用方式:

callbacks.AppendGlobalHandlers(callbacktool.GetChatModelInputCallback())

4. Tool专用回调

类似地,你可以只监控工具调用:

func GetToolInputCallback() callbacks.Handler {
    return cbutils.NewHandlerHelper().Tool(&cbutils.ToolCallbackHandler{
        OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *tool.CallbackInput) context.Context {
            fmt.Printf("\n[Tool Input] component: %s, args: %s\n", info.Name, input.ArgumentsInJSON)
            return ctx
        },
    }).Handler()
}

输出示例:

[Tool Input] component: search_tool, args: {"query":"上海旅游景点推荐"}
[Tool Input] component: crawl_tool, args: {"url":"https://..."}

使用方式:

callbacks.AppendGlobalHandlers(callbacktool.GetToolInputCallback())

实战案例:旅行规划Agent调试

下面我们来看如何在trip_agent.go中使用这些回调函数进行调试。

代码结构

package agent

import (
    "context"
    callbacktool "eino_study/callback_tool"
    "eino_study/config"
    "eino_study/model"
    "eino_study/tools"
    "fmt"
    "log"

    "github.com/cloudwego/eino/adk"
    "github.com/cloudwego/eino/callbacks"
    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/compose"
    "github.com/cloudwego/eino/schema"
)

func TripAgent() {
    ctx := context.Background()

    // ========== 注入回调函数 ==========
    // 根据调试需求,选择合适的回调方式:

    // 方式1:开启所有组件的所有回调(输出最详细)
    // callbacks.AppendGlobalHandlers(new(callbacktool.LoggerCallbacks))

    // 方式2:只关注开始和结束事件
    // callbacks.AppendGlobalHandlers(callbacktool.GetStartAndEndCallback())

    // 方式3:只关注ChatModel的输入(推荐)
    callbacks.AppendGlobalHandlers(callbacktool.GetChatModelInputCallback())

    // 方式4:只关注Tool的调用
    callbacks.AppendGlobalHandlers(callbacktool.GetToolInputCallback())

    // 可以组合使用多个回调
    // callbacks.AppendGlobalHandlers(callbacktool.GetChatModelInputCallback())
    // callbacks.AppendGlobalHandlers(callbacktool.GetToolInputCallback())

    // ========== 创建Agent ==========
    model := model.NewChatModel(config.NewConfig("../config/config.json"))

    agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Model: model.OpenaiChatModel,
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig: compose.ToolsNodeConfig{
                Tools: []tool.BaseTool{
                    tools.SearchTool(),
                    tools.CrawlTool(),
                },
            },
        },
        Name:        "trip_plan",
        Description: "出行规划",
        Instruction: "请帮我完成旅行规划,包括什么时间去什么景点,并列出详细的高铁车次和出发时间",
    })
    if err != nil {
        log.Fatal(err)
    }

    // ========== 运行Agent ==========
    runner := adk.NewRunner(ctx, adk.RunnerConfig{
        Agent: agent,
    })

    iter := runner.Query(ctx, "我想去上海旅游3天(从明天开始),请帮我做一份旅游攻略")

    var lastMsg adk.Message
    for {
        event, ok := iter.Next()
        if !ok {
            break
        }
        if event.Err != nil {
            log.Fatal(event.Err)
        }
        msg, err := event.Output.MessageOutput.GetMessage()
        if err != nil {
            log.Fatal(err)
        }
        // 调试时可以打印每一步的结果
        // fmt.Println(msg.Role, msg.Content)
        lastMsg = msg
    }

    fmt.Println("【最终答案】")
    if lastMsg.Role == schema.Assistant && len(lastMsg.Content) > 0 {
        fmt.Println(lastMsg.Content)
    }
}

调试输出分析

第一步:ChatModel接收初始消息

[ChatModel Input] component: trip_plan
  Message 1 [system]: 请帮我完成旅行规划,包括什么时间去什么景点...
  Message 2 [user]: 我想去上海旅游3天(从明天开始),请帮我做一份旅游攻略

第二步:Agent决定调用工具

[ChatModel Input] component: trip_plan
  Message 1 [system]: 请帮我完成旅行规划...
  Message 2 [user]: 我想去上海旅游3天...
  Message 3 [assistant]: 我需要先搜索上海的相关信息
    Tool Calls: 1
      1. search_tool: {"query":"上海旅游景点推荐 三日游攻略"}

第三步:工具执行

[Tool Input] component: search_tool, args: {"query":"上海旅游景点推荐 三日游攻略"}

第四步:ChatModel继续处理

[ChatModel Input] component: trip_plan
  Message 1 [system]: 请帮我完成旅行规划...
  Message 2 [user]: 我想去上海旅游3天...
  Message 3 [assistant]: [包含tool call]
  Message 4 [tool]: {"result": "搜索结果..."}

第五步:最终回答

通过观察回调输出,你可以清楚地了解Agent的整个思考过程。