【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 就能全局生效。这就是非侵入式设计的魅力。

相关推荐
她的男孩21 分钟前
分片上传的大文件人人可下载:文件模块 isPrivate 在合并时被抹成 false,另有 4 个静默坑
java·后端·架构
颜进强28 分钟前
18 · NestJS动态模块与 forRoot:`imports: [ConfigModule.forRoot({...})]` 到底在 import 什么
前端·后端·ai编程
xn713334 分钟前
Personal AI Agent 架构实战:Memory、权限、跨 App 与本地/云端设计
人工智能·后端·agent
谢亮_vipxieliang44 分钟前
Go defer、panic与recover核心知识点
开发语言·后端·golang
颜进强1 小时前
17 · 自定义 Provider 四形态:useValue / useClass / useFactory / useExisting 到底在选什么
前端·后端·ai编程
碎觉崽1 小时前
我的 QQ 机器人被腾讯反复踢下线,折腾了半个月才搞明白
后端
打工仔折腾 AI1 小时前
从零写一个CAD 05:以鼠标为中心的滚轮缩放矩阵顺序不能反
后端·python·线性代数·性能优化·矩阵·计算机外设·ai agent 实战
谢亮_vipxieliang2 小时前
Go基础数据类型全解
开发语言·后端·golang
专业程序开发源2 小时前
springboot古城景区管理系统88564-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
小蒜学长2 小时前
基于微信小程序的遇见咖啡店点餐系统设计与实现(代码+数据库+LW)
java·spring boot·后端·微信小程序·咖啡店点餐系统