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

相关推荐
choumou_M5 分钟前
MySQL_1:数据库悲观锁
后端·spring·spring cloud
Lost of 程序猿32 分钟前
ASP.NET Core SignalR 实战:从实时推送到底层协议与高可用部署
后端·asp.net
JasmineWr38 分钟前
Spring BeanDefinition 与 Bean 生命周期、循环依赖解析
java·后端·spring
QQ_21696290961 小时前
【项目编号:project86564】SpringBoot供应链管理系统:采购、供应商、库存、销售、统计报表一体化实战
java·spring boot·后端
灯澜忆梦1 小时前
【基于GO的Web开发10】gin获取HTML-Form表单提交参数
前端·后端·golang·html·gin
SomeB1oody10 小时前
【RustyML入门】6.3. 并行归约
开发语言·后端·机器学习·rust·教程
Profile排查笔记10 小时前
指纹浏览器哪个好?从 Profile、代理、权限和自动化能力判断是否适合
前端·人工智能·后端·自动化
用户9385156350711 小时前
用 AI 结对编程从 0 搭一个"单词后台管理系统":Next.js + Supabase + Drizzle + shadcn/ui 全记录
后端·postgresql·next.js
爱学习的小邓同学11 小时前
Golang --- (1)第一个Golang程序
开发语言·后端·golang
小灰灰搞电子12 小时前
Rust+Slint 实现的“DNA双螺旋”加载动画源码分享
后端·rust·slint·加载动画