【Eino 框架入门】Callback 可观测性:给 Agent 装个"监控摄像头"

3 阅读3分钟

【Eino 框架入门】Callback 可观测性:给 Agent 装个"监控摄像头"

Agent 跑起来后,你肯定想知道:模型调了几次?Token 花了多少?哪个 Tool 最慢?

这些都是"可观测性"要解决的问题。Eino 用 Callback 机制来实现。

Callback 是什么?

打个比方:Agent 是主角(业务逻辑),Callback 是旁边的记录员。

  • 主角干活时,记录员在旁边记笔记
  • 主角开始干活 → 记录员写下"XX 开始执行"
  • 主角干完了 → 记录员写下"XX 完成,耗时 23ms"
  • 主角出错了 → 记录员写下"XX 失败:xxx"

核心特点:记录员不干扰主角干活,只是在关键节点记一笔。

五个钩子时机

Callback 在组件生命周期的 5 个固定点位触发:

时机方法什么时候触发
开始OnStart组件开始处理前
结束OnEnd组件成功返回后
出错OnError组件返回错误时
流式输入OnStartWithStreamInput组件接收流式输入时
流式输出OnEndWithStreamOutput组件返回流式输出时

一个 ChatModel 调用的流程:

ChatModel.Generate(ctx, messages)
            ↓
    ┌───────────────┐
    │   OnStart     │  ← 记录:开始调用,输入是啥
    └───────────────┘
            ↓
    ┌───────────────┐
    │   模型处理     │
    └───────────────┘
            ↓
    ┌───────────────┐
    │    OnEnd      │  ← 记录:返回结果,Token 消耗
    └───────────────┘

快速实现一个 Callback

HandlerHelper 只注册你关心的钩子:

handler := callbacks.NewHandlerHelper().
    OnStart(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
        log.Printf("[trace] %s/%s 开始", info.Component, info.Name)
        return ctx
    }).
    OnEnd(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
        log.Printf("[trace] %s/%s 完成", info.Component, info.Name)
        return ctx
    }).
    OnError(func(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
        log.Printf("[trace] %s/%s 出错: %v", info.Component, info.Name, err)
        return ctx
    }).
    Handler()

// 全局注册,所有组件都会触发
callbacks.AppendGlobalHandlers(handler)

RunInfo 告诉你是谁触发的:

type RunInfo struct {
    Name      string  // 业务名称,如 "my_agent"
    Type      string  // 实现类型,如 "OpenAI"
    Component string  // 组件类型,如 "ChatModel"
}

集成 CozeLoop

CozeLoop 是字节跳动的可观测性平台,能可视化整个调用链:

import clc "github.com/cloudwego/eino-ext/callbacks/cozeloop"

client, _ := cozeloop.NewClient(
    cozeloop.WithAPIToken("your_token"),
    cozeloop.WithWorkspaceID("your_workspace"),
)

// 一行注册
callbacks.AppendGlobalHandlers(clc.NewLoopHandler(client))

然后在 CozeLoop 的 Web UI 就能看到漂亮的调用链路图,包括:

  • 每次模型调用的耗时、Token 消耗
  • 每个 Tool 的执行时间
  • 完整的调用层级关系

完整配置示例

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

    // 配置 CozeLoop(可选)
    apiToken := os.Getenv("COZELOOP_API_TOKEN")
    workspaceID := os.Getenv("COZELOOP_WORKSPACE_ID")

    if apiToken != "" && workspaceID != "" {
        client, err := cozeloop.NewClient(
            cozeloop.WithAPIToken(apiToken),
            cozeloop.WithWorkspaceID(workspaceID),
        )
        if err != nil {
            log.Fatalf("cozeloop.NewClient failed: %v", err)
        }
        defer func() {
            time.Sleep(5 * time.Second) // 等待数据上报
            client.Close(ctx)
        }()
        callbacks.AppendGlobalHandlers(clc.NewLoopHandler(client))
        log.Println("CozeLoop tracing enabled")
    }

    // 创建 Agent 并运行...
}

运行效果:

[trace] starting session: 083d16da-6b13-4fe6-afb0-c45d8f490ce1
you> 你好
[trace] chat_model_generate: model=gpt-4.1-mini tokens=150
[trace] tool_call: name=list_files duration=23ms
[assistant] 你好!有什么我可以帮助你的吗?

注意事项

流式回调要关流OnEndWithStreamOutput 拿到的 StreamReader 必须读完并关闭,否则会 goroutine 泄漏。

不要修改 Input/Output:它们被所有下游 Handler 共享,改了会污染别人。

RunInfo 可能为 nil:顶层调用可能没有 RunInfo,用前要检查:

if info != nil {
    log.Printf("[trace] %s/%s", info.Component, info.Name)
}

可观测性的三大价值

场景能做什么
性能分析哪个 Tool 最慢?模型调用延迟分布?
错误追踪哪个环节崩了?完整调用链长啥样?
成本优化Token 消耗排行?哪个对话最烧钱?

小结

Callback 是 Eino 的"旁路机制":不干扰业务,只在关键节点抽取信息。它从底层 component 到上层 adk 一以贯之,所有组件都支持。

业务代码完全不用改,注册一个 Handler 就能全局生效。这就是非侵入式设计的魅力。