目录
背景
- 最近要在一个线上项目引入Agent技术,因为我们这个项目是golang的,同时python解释性语言在性能、安全性稳定性等方面都比go差太远,所以考虑使用开源的golang Agent框架来进行搭建
简介
https://www.cloudwego.io/zh/docs/eino/overview/eino_open_source/
- eino是字节推出的LLM应用开发框架,它使用golang作为开发语言,提供了众多的支持LLM应用开发的工具,下面介绍下这个框架的逻辑,以及如何开发一个ai agent,并集成到现有的项目内,以及调用大模型的过程中遇到的一些问题
- 官网已经给出了一些使用的示例,可以参考https://www.cloudwego.io/zh/docs/eino/quick_start/simple_llm_application/搭建一个简单的LLM聊天功能
- 框架支持的内容比较丰富,除了支持Agent,还支持Chain、Graph等组件,但我们这次主要说Agent
Agent
- eino框架提供了两种Agent管理的方案,分别位于
flow和adk包下
React Agent
- 下面是官方文档提供的React Agent整体设计。React,指的是Reasoning and Acting,推理和执行分离,Agent核心逻辑其实就两个核心步骤,大模型分析 → \rightarrow →执行工具 → \rightarrow →大模型分析 → \rightarrow →执行工具,周而复始

- 可以参考https://www.cloudwego.io/zh/docs/eino/core_modules/flow_integration_components/react_agent_manual/,里面写的比较详细。下面是比较常用的配置
go
agt, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: openaiCli, // 可调用的模型
ToolsConfig: compose.ToolsNodeConfig{
Tools: todoTools, // 可调用的工具
},
MaxStep: 20, // 总交互次数,一次agent + 工具调用 相当于两个step
MessageRewriter: func(ctx context.Context, input []*schema.Message) []*schema.Message {
res := make([]*schema.Message, len(input)+1)
res = append(res, schema.SystemMessage("你是一个xxx")) // 系统提示词
return res
},
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
// 可进行上下文压缩, 或者存储历史记录
return input
},
})
- 这种在单agent场景还可以。但是如果要使用多Agent协同的话,最好使用下面的ADK
ADK
Agent Development Kit,是eino开发团队,基于Google-ADK的设计,提供的 Go 语言 的 Agents 开发的灵活组合框架,即 Agent、Multi-Agent 开发框架,并为多 Agent 交互场景沉淀了通用的上下文传递、事件流分发和转换、任务控制权转让、中断与恢复、通用切面等能力。- 下面是一个简单创建Agent的初始化方法
go
agt, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: openaiCli,
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: todoTools,
},
},
OutputKey: "main-agent",
MaxIterations: 50,
Name: "main-agent",
Description: "master agent",
Instruction: `角色:xxx助手
核心职责:专注于xxx。
输出规范:xxx`,
})
if err != nil {
return nil, err
}
- 你可能需要多个Agent协同工作,这可以这样实现
go
mAgt, err := adk.SetSubAgents(ctx, agt, subAgts) // 把subAgts加入到agt里面,作为agt的子agent
if err != nil {
return nil, fmt.Errorf("set sub agents failed: %w", err)
}
- 因为普通的http交互比较简单,下面讲解下如何实现流式输出。首先,我们可以提供一个对外的sse服务,类似下面这样(依赖了
"github.com/tmaxmax/go-sse"包,这个包可以实现POST请求建立SSE连接,之前简单看了下https://github.com/r3labs/sse.git这个sse的框架,好像不如上面那个框架好用)
go
sess, err := sse.Upgrade(resp.ResponseWriter, req.Request)
if err != nil {
WriteError(req, resp, http.StatusBadRequest, fmt.Errorf("sse upgrade failed: %w", err))
return
}
- 重点是下面的创建stream通信部分,对于adk的runner对象的Stream方法,它是返回了一个
AsyncIterator对象,这个对象内部有一个数组对象,且使用了一个Cond对象来保护这个数组(有兴趣可以看代码实现),每次有新的LLM/tool事件生成的时候,数组里面会写入一个AgentEvent事件,所以需要持续监听这个对象,直到任务结束。
go
// 主agent的流式消息返回
func (m *MainAgent) Stream(ctx context.Context, input []*schema.Message) *adk.AsyncIterator[*adk.AgentEvent] {
return adk.NewRunner(ctx, adk.RunnerConfig{
EnableStreaming: true,
Agent: m.agent,
}).Run(ctx, input)
}
func (a *AgentService) Stream(ctx context.Context, session *sse.Session, cDto *dto.ChatDto) error {
thisSession := &schema.Message{
Role: schema.User,
Content: cDto.Message,
}
next := a.MainAgent.Stream(ctx, []*schema.Message{
thisSession,
})
var cs bytes.Buffer
for {
event, ok := next.Next()
if !ok {
break
}
if event.Err != nil {
return fmt.Errorf("failed to generate stream: %w", event.Err)
}
if event.Output == nil {
log.Logger.Info("failed to generate stream:", "reason", "no output")
continue
}
if event.Output.MessageOutput == nil {
log.Logger.Info("failed to generate stream:", "reason", "no message output")
continue
}
if event.Output.MessageOutput.MessageStream == nil {
log.Logger.Info("failed to generate stream:", "reason", "no message stream")
continue
}
var msg sse.Message
for {
ms, msErr := event.Output.MessageOutput.MessageStream.Recv()
if errors.Is(msErr, io.EOF) {
break
}
if msErr != nil {
return fmt.Errorf("failed to generate stream: %w", msErr)
}
cs.WriteString(ms.Content)
}
msg.AppendData(cs.String())
cs.Reset()
err := session.Send(&msg) // 注意每次发送这个,会自动换行
if err != nil {
log.Logger.Error(err, "agent service send message failed")
return err
}
if err = session.Flush(); err != nil { // 刷新缓冲区,发送数据
log.Logger.Error(err, "agent service flush message failed")
return err
}
}
return nil
}
- 上面是一个完整的流消息事件的交互过程。这里每次交互都是独立的请求,不会保留上下文,需要自行处理
问题
流式输出tool call的参数拼接问题
- llm会根据tool_calls的index字段区分不同的tool,如果没有这个字段的话,流传输的工具调用参数会无法拼接。参考https://github.com/cloudwego/eino/issues/631
相关文档和issue如下
https://platform.openai.com/docs/guides/function-calling?api-mode=chat#handling-function-calls
https://github.com/ollama/ollama/issues/7881
- 观察模型是否有返回index字段。需要有这个,否则无法识别是否是同一个tool的调用
本文介绍了基本使用,由于涉及到细节点非常多,会在后面的文章逐步完善