前言
在 2025 年年初就给自己的计划是接触并参与开源,那时候对开源一无所知,不知道什么是 Issue,不知道如何提一个易于被开源社区接受的 PR。
在三月份偶然的机会给 cloudwego/eino-ext 提交并成功合入自己人生第一个 featrue pr,虽然很简单,但成为了我参与开源的第一步。
随后了解到了像 GSoC,OSPP 等国内外的开源活动,于是在五月份开始准备并申请 OSPP 开源课题,也很幸运在 6 月底成功中选,并在 7 月份的实习之余着手开发。
项目介绍
Loongsuite-go-agent 是阿里云团队推动的 Go 项目自动插桩方案,通过混合编译技术,结合 Go AST(抽象语法树)分析,对目标应用进行零代码改动的自动化插桩。在传统的基于 OpenTelemetry SDK,需要开发人员手动进行埋点,但手动埋点存在以下问题:
- Trace 埋点繁琐:每个调用点都需埋点,并需注意 Trace 上下文的传递,防止链路串联错误。
- Metrics 统计复杂:每次调用都需要统计,还需注意指标发散问题。
- SDK 版本更新频繁:Golang 官方仅维持最新两个版本,业务应用升级时需同时升级SDK,工作量很大
这种方式显著降低了接入门槛,并提升了 APM 工具在 Go 项目中的可扩展性和易用性。同时积极探索** LLM 模型在推理和服务过程中的全链路可观测性解决方案**,支持相关AI应用开发框架,以提升 LLM 应用的可维护性和稳定性
开发需求
在 alibaba/loongsuite-go-agent 项目中,通过插件的方式提供对 cloudwego/eino 框架的大模型可观测能力,并支持 OpenTelemetry GenAI 的规范。相关代码合入主干分支。
方案调研
在课题申请阶段需要进行充分的框架代码熟悉和方案调研,主要包括 Eino、Loongsuite-go-agent集成、OTel GenAI 规范、测试和 Example代码。
Eino
Eino 提供了一个强调简洁性、可扩展性、可靠性与有效性,且更符合 Go 语言编程惯例的 LLM 应用开发框架。
而 Eino 框架由以下几个部分组成,其中Eino为核心框架逻辑,包含类型定义、流处理机制、组件抽象、编排功能、切面机制等,Eino-Ext为一些具体组件的实现,例如DeepSeek ChatModel、Langfuse Callbacks等。其中切面机制非常适合做 Graph 的 trace 指标收集。
Callback实现了**“横切面功能注入"和"中间状态透出”**两大核心功能。其工作原理是:用户提供并注册自定义的Callback Handler函数,Component和Graph在执行过程中的固定时机(如组件开始执行、执行完成、发生错误等关键切面)主动回调这些函数,并传递对应的执行信息。这种设计实现了核心业务逻辑控制面与可观测组件的完全解耦。
const (
TimingOnStart CallbackTiming = iota // 进入并开始执行
TimingOnEnd // 成功完成即将 return
TimingOnError // 失败并即将 return err
TimingOnStartWithStreamInput // OnStart,但是输入是 StreamReader
TimingOnEndWithStreamOutput // OnEnd,但是输出是 StreamReader
)
type Handler interface {
OnStart(ctx context.Context, info *RunInfo, input CallbackInput) context.Context
OnEnd(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context
OnError(ctx context.Context, info *RunInfo, err error) context.Context
OnStartWithStreamInput(ctx context.Context, info *RunInfo,
input *schema.StreamReader[CallbackInput]) context.Context
OnEndWithStreamOutput(ctx context.Context, info *RunInfo,
output *schema.StreamReader[CallbackOutput]) context.Context
}
Eino通过数据复制的方式,保证了在Callbacks的处理过程中数据流的并发安全性,每个Handler都能消费到一份独立的数据流。
因此我们只需要实现 Handler 接口,对不同 Component 输入返回中提取收集感兴趣的指标即可,这也是 Eino 官方更推荐的对 Graph 横切面功能注入和中间状态透出的最佳实践。同时需要考虑一下几个点:
- 通过该方式注册的 Handler 只会在 Graph(Chain等)Compile 的时候注册,因此只会在用户构建 Graph 的时候生效。关于不在 Graph 中的拦截在方案二中详细讨论。
- 这里面的 Handler 的 Hook 函数中的 input 和 output 均为 interface{} 类型,因此在处理不同 Component 类型时,需要通过 info 中的 Type 等信息进行相应的类型转化。可以考虑通过使用callbacks.NewHandlerHelper()来分别实现对应的已经做好类型转换的 Handler 接口来简化这一步骤。
- 对与流式的 input 和 output,Eino 本身会主动 Copy 这个流,我们只需确保注册 defer streamReader.close() 来及时 Close 这个流以避免原始 Stream 无法 Close 和释放资源。
- 对于在 Graph 外的 Component,官方提供的解决方案为
ctx = callbacks.InitCallbacks(ctx, runInfo, handlers...),即通过 context 的方式注入 runInfo 和 handlers,在 ChatModel 的 handler 注册中使用到了这个特性
Loongsuite-go-agent集成
在正常情况下,go build命令通过以下主要步骤来编译一个Golang应用程序:
- 源代码解析:Golang编译器首先解析源代码文件,并将它们转换为抽象语法树(AST)。
- 类型检查:解析后,类型检查确保代码遵守Golang的类型系统。
- 语义分析:这涉及分析程序的语义,包括变量定义和使用,以及包导入。
- 编译优化:语法树被转换为中间表示,并执行各种优化以提高代码执行效率。
- 代码生成:生成目标平台的机器码。
- 链接:将不同的包和库链接在一起,形成一个单一的可执行文件。
通过阅读 Loongsuite-go-agent 文档 ,Agent 自动埋点工具会在上述过程中新增两个步骤:预处理和埋点。
预处理:分析依赖关系并选择稍后应使用的规则。埋点:根据规则生成代码,并将新代码注入源代码。
对于需求的实现,其实只需要按照文档 的指引在 /pkg/rules 下添加相应的 Hook 规则即可。所以重点在于对 Hook 函数的选择,ChatModel 作为我们重点关注的 Component,许多配置是我们指标收集的重点,同时不同厂商的 LLM 存在区别,这里主要 Hook OpenAI、Claude、Gemini、QianWen、Ark 的** Generate 和 Stream 方法,通过反射获取到 Client 的一些元配置信息。**
// Hook OpenAI Generate 方法
//go:linkname openaiGenerateOnEnter github.com/cloudwego/eino-ext/components/model/openai.openaiGenerateOnEnter
func openaiGenerateOnEnter(call api.CallContext, cm *openai.ChatModel, ctx context.Context, in []*schema.Message, opts ...model.Option) {
if !einoEnabler.Enable() {
return
}
config := ChatModelConfig{}
cli := reflect.ValueOf(*cm).FieldByName("cli")
if cli.IsValid() && !cli.IsNil() {
conf := cli.Elem().FieldByName("config")
if conf.IsValid() && !conf.IsNil() {
if conf.Elem().FieldByName("BaseURL").IsValid() {
config.BaseURL = conf.Elem().FieldByName("BaseURL").String()
}
if conf.Elem().FieldByName("PresencePenalty").IsValid() && !conf.Elem().FieldByName("PresencePenalty").IsNil() {
config.PresencePenalty = conf.Elem().FieldByName("PresencePenalty").Elem().Float()
}
if conf.Elem().FieldByName("Seed").IsValid() && !conf.Elem().FieldByName("Seed").IsNil() {
config.Seed = conf.Elem().FieldByName("Seed").Elem().Int()
}
if conf.Elem().FieldByName("FrequencyPenalty").IsValid() && !conf.Elem().FieldByName("FrequencyPenalty").IsNil() {
config.FrequencyPenalty = conf.Elem().FieldByName("FrequencyPenalty").Elem().Float()
}
}
}
handler := utilscallbacks.NewHandlerHelper().ChatModel(einoModelCallHandler(config)).Handler()
info := &callbacks.RunInfo{
Name: "OpenAI Generate",
Type: "OpenAI",
Component: components.ComponentOfChatModel,
}
ctx = callbacks.InitCallbacks(ctx, info, handler)
call.SetParam(1, ctx)
}
而对于 Prompt、Embedding、Tool 等 Component 目前均统一在 newGraph 的 OnEnter Hook 函数中注册。
//go:linkname newGraphOnEnter github.com/cloudwego/eino/compose.newGraphOnEnter
func newGraphOnEnter(call api.CallContext, cfg interface{}) {
if !einoEnabler.Enable() {
return
}
handler := utilscallbacks.NewHandlerHelper().
Graph(NewComposeHandler("graph")).Chain(NewComposeHandler("chain")).
Prompt(einoPromptCallbackHandler()).Transformer(einoTransformCallbackHandler()).
Embedding(einoEmbeddingCallbackHandler()).Indexer(einoIndexerCallbackHandler()).
Retriever(einoRetrieverCallbackHandler()).Loader(einoLoaderCallbackHandler()).
Tool(einoToolCallbackHandler()).ToolsNode(einoToolsNodeCallbackHandler()).
Lambda(NewComposeHandler("lambda")).
Handler()
callbacks.AppendGlobalHandlers(handler)
}
OTel GenAI 规范
https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/
OTel 官方给出了 GenAI 规范,统一 Trace Attribute 和 Metrics 的收集,但绝大部分仍处于 Development 阶段,因此我们也需要在此基础上做一部分扩展来满足更加全面的指标收集。
测试与 Example
根据项目测试规范,需要完成基础测试、Muzzle测试和最新深度检查,以保证项目的可维护性和稳定性。
const eino_dependency_name = "github.com/cloudwego/eino"
const eino_module_name = "eino"
func init() {
TestCases = append(TestCases,
NewGeneralTestCase("eino-0.3.36-invoke-agent-test", eino_module_name, "v0.3.36", "", "1.18", "", TestAgentInvokeEino),
NewGeneralTestCase("eino-0.3.36-stream-agent-test", eino_module_name, "v0.3.36", "", "1.18", "", TestAgentStreamEino),
...
NewMuzzleTestCase("eino-muzzle", eino_dependency_name, eino_module_name, "v0.3.36", "", "1.18", "", []string{"go", "build", "test_invoke_agent.go", "eino_common.go"}),
NewLatestDepthTestCase("eino-latest-depth", eino_dependency_name, eino_module_name, "v0.3.36", "v0.3.37", "1.18", "", TestAgentInvokeEino),
)
}
func TestAgentInvokeEino(t *testing.T, env ...string) {
UseApp("eino/v0.3.36")
RunGoBuild(t, "go", "build", "test_invoke_agent.go", "eino_common.go")
RunApp(t, "test_invoke_agent", env...)
}
func TestAgentStreamEino(t *testing.T, env ...string) {
UseApp("eino/v0.3.36")
RunGoBuild(t, "go", "build", "test_stream_agent.go", "eino_common.go")
RunApp(t, "test_stream_agent", env...)
}
...
同时需要使用 testhttp 来创建 MockChatModel,断言预期 Trace 和 Metrics
func main() {
ctx := context.Background()
g := compose.NewGraph[[]*schema.Message, *schema.Message]()
reactAgentKeyOfLambda, err := NewMockReActAgentLambda(ctx)
if err != nil {
panic(err)
}
err = g.AddLambdaNode("model", reactAgentKeyOfLambda)
if err != nil {
panic(err)
}
_ = g.AddEdge(compose.START, "model")
_ = g.AddEdge("model", compose.END)
graph, err := g.Compile(ctx)
if err != nil {
panic(err)
}
_, err = graph.Invoke(ctx, []*schema.Message{schema.UserMessage("hello")})
if err != nil {
panic(err)
}
verifier.WaitAndAssertTraces(func(stubs []tracetest.SpanStubs) {
verifier.VerifyLLMAttributes(stubs[0][3], "chat", "eino", "mock-chat")
verifier.VerifyLLMCommonAttributes(stubs[0][9], "tool_node", "eino", trace.SpanKindClient)
verifier.VerifyLLMCommonAttributes(stubs[0][10], "execute_tool", "eino", trace.SpanKindClient)
}, 1)
}
开发过程与产出
课题开发
总体上充分按照申请书计划推进开发,提前完成开发任务并持续参与到社区
完成 Go Agent Eino 插件的集成主要代码编写,核心 Tarce Attribute 收集与测试代码编写。
完善 GenAI Metrics 指标收集与测试代码编写
修复 ChatModel Handler 重复注册失效问题,优化指标收集
添加 Eino Example
课题产出
Trace
通过 Jeager Trace 的可视化结果,下图中展示了从 graph 节点开始,到 retriever、embeddings、ft.search、chat、tool_node 等关键组件的完整调用链路,共 22 个 spans,最大深度达 6 层。
这表明本项目成功将 Eino 框架内的大模型调用、检索、工具调用等环节全部纳入可观测范围,开发者可以一眼定位请求流经的所有关键步骤
Metrics
主要支持下列 metrics,并抽象统一的 api 方便接入,后续将支持更多的 metrics
- gen_ai.client.token.usage
- gen_ai.client.operation.duration
- gen_ai.server.time_to_first_token
以上产出共同完善了 Go Agent 在 Eino 框架下的可观测能力扩展,既保证了功能的完整性与稳定性,也提升了代码的可维护性和社区可用性,为后续在 LLM 场景中的持续迭代和生态建设打下了基础。使用体验可前往 /example/eino 。
社区参与
在完成课题开发的同时,持续参与社区贡献。
支持 K8s client-go 插件支持,监控 informer 事件分发类型、数量、处理时长等核心指标
支持 Dependencies 规则匹配
在累积了一定的贡献之后也很幸运的被提名为项目的 Reviewer,作为项目的 collaborator 继续深入参与社区。
总结
在与导师的沟通中,我学到了很多关于开源项目的知识,其中包括一些代码规范、代码风格等等,也学到了很多关于开源社区的知识,比如如何提交一个易被接受 PR,如何与导师交换建议,如何更加深入参与社区等。
在本项目中,学到了不少关于 Go 构建变异过程的细节,如特殊注解 golinkname、ast解析、go build 流程等,同时理解在 AI Agent 火热的时代,可观测是尤其的重要!
这段开源经历对我的职业生涯来说是一个良好的开始,也是一段有价值、值得铭记的经历。我会继续保持对开源的热情,努力为开源社区作出自己的贡献。最后十分感谢OSPP平台带我走进开源的世界并如此近距离接触到优秀的前辈们!