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

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

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

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

Callback 是什么?

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

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

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

五个钩子时机

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

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

一个 ChatModel 调用的流程:

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

快速实现一个 Callback

HandlerHelper 只注册你关心的钩子:

go 复制代码
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 告诉你是谁触发的:

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

集成 CozeLoop

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

go 复制代码
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 的执行时间
  • 完整的调用层级关系

完整配置示例

go 复制代码
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 并运行...
}

运行效果:

ini 复制代码
[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,用前要检查:

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

可观测性的三大价值

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

小结

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

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

相关推荐
我叫黑大帅2 小时前
如何使用PHP创建图像验证码
后端·面试·php
何中应2 小时前
Wiki搭建
后端·开源软件·wiki
AI茶水间管理员2 小时前
手动执行 Claude 压缩上下文后,到底保留了什么?(附Vibe Coding 案例)
人工智能·后端
神奇小汤圆2 小时前
AQS 同步器——Java 并发框架的核心底座全解析
后端
fy121632 小时前
Spring Boot项目中解决跨域问题(四种方式)
spring boot·后端·dubbo
gelald2 小时前
Spring - IoC 容器原理
java·后端·spring
神奇小汤圆2 小时前
突发,Claude Code 源码意外泄漏!
后端
掘金者阿豪2 小时前
2026全球视觉理解大模型盘点:国内外TOP20排行榜与技术格局
人工智能·后端
稻草猫.2 小时前
Spring AOP
java·后端·spring·java-ee·idea