Ch02 main.go 逐段详解 整体架构总览 先用一张图看清整个程序的结构:
1 2 3 4 5 6 7 8 9 10 11 12 graph TD A[main 函数] --> B[解析命令行参数] B --> C[创建 ChatModel] C --> D[创建 ChatModelAgent] D --> E[创建 Runner] E --> F[进入多轮对话循环] F --> G[读取用户输入] G --> H[追加到 history] H --> I[runner.Run 执行] I --> J[printAndCollectAssistantFromEvents<br/>消费事件流并打印] J --> K[追加 assistant 回复到 history] K --> G
第一部分:导入包(第 18~32 行) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import ( "bufio" "context" "errors" "flag" "fmt" "io" "os" "strings" "github.com/cloudwego/eino/adk" "github.com/cloudwego/eino/schema" examplemodel "github.com/cloudwego/eino-examples/adk/common/model" )
包
作用
bufio
提供带缓冲的 I/O,这里用 bufio.NewScanner 逐行读取用户输入
context
Go 的上下文机制,用于控制超时、取消等
errors
错误处理,这里用 errors.Is(err, io.EOF) 判断流是否结束
flag
命令行参数解析(第一章已学过)
fmt
格式化输出
io
提供 io.EOF 常量,表示”流结束”
os
操作系统交互,这里用 os.Stdin(标准输入)和 os.Stdout(标准输出)
strings
字符串操作,这里用 strings.TrimSpace 去除空白、strings.Builder 拼接字符串
adk
Eino 的 ADK 包 ,提供 Agent、Runner、AgentEvent 等核心抽象
schema
Eino 的数据模型包 ,提供 Message、Role 等基础类型
examplemodel
示例项目的公共包 ,封装了 ChatModel 的创建逻辑
💡 Go 语法知识:包别名 1 examplemodel "github.com/cloudwego/eino-examples/adk/common/model"
这里 examplemodel 是给这个包起的别名 。因为这个包的名字是 model,但 Eino 框架里也有一个 model 包(github.com/cloudwego/eino/components/model),为了避免冲突,给它起了个别名 examplemodel。后面用 examplemodel.NewChatModel() 来调用。
第二部分:main 函数 —— 初始化阶段(第 34~56 行) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { var instruction string flag.StringVar(&instruction, "instruction" , "You are a helpful assistant." , "" ) flag.Parse() ctx := context.Background() cm := examplemodel.NewChatModel() agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "Ch02ChatModelAgent" , Description: "A minimal ChatModelAgent with in-memory multi-turn history." , Instruction: instruction, Model: cm, }) if err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1 ) } runner := adk.NewRunner(ctx, adk.RunnerConfig{ Agent: agent, EnableStreaming: true , })
逐行分析: ① 解析命令行参数 1 2 3 var instruction string flag.StringVar(&instruction, "instruction" , "You are a helpful assistant." , "" ) flag.Parse()
和第一章一样,用 flag 包解析命令行参数。instruction 是系统提示词,默认值是 "You are a helpful assistant."。
② 创建 ChatModel 1 cm := examplemodel.NewChatModel()
调用公共包 chat_model.go 中的 NewChatModel() 函数。这个函数的逻辑和第一章的 newChatModel 几乎一样:
读取环境变量 MODEL_TYPE
如果是 "ark",创建 Ark(火山引擎/豆包)的 ChatModel
否则,创建 OpenAI 的 ChatModel
返回类型是 model.ToolCallingChatModel(接口类型)
💡 与第一章的区别 :第一章是在 main.go 里直接写的 newChatModel 函数,第二章把它提取到了公共包里,方便后续章节复用。
③ 创建 ChatModelAgent 1 2 3 4 5 6 agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "Ch02ChatModelAgent" , Description: "A minimal ChatModelAgent with in-memory multi-turn history." , Instruction: instruction, Model: cm, })
这是本章的核心新概念 。ChatModelAgent 是对 ChatModel 的一层封装,把它从一个”组件”升级为一个”智能体”。
配置字段
类型
说明
Name
string
Agent 的名字,用于日志、调试、事件标识
Description
string
Agent 的描述,用于多 Agent 场景下的路由决策
Instruction
string
系统提示词,会作为 SystemMessage 注入到对话中
Model
model.ToolCallingChatModel
底层的 ChatModel 组件
💡 为什么要包一层? 文档中的类比很好:ChatModel 就像”数据库驱动”,ChatModelAgent 就像”业务逻辑层”。Agent 提供了统一的 Run() 接口、事件流输出、后续可扩展 tools/middleware 等能力。
④ 创建 Runner 1 2 3 4 runner := adk.NewRunner(ctx, adk.RunnerConfig{ Agent: agent, EnableStreaming: true , })
Runner 是 Agent 的”执行器”。你可以把它理解为一个”遥控器”:
Agent 是”电视机”(有能力,但需要被操控)
Runner 是”遥控器”(负责启动、管理、控制 Agent 的执行)
配置字段
说明
Agent
要执行的 Agent
EnableStreaming
设为 true 表示启用流式输出(模型边生成边返回)
第三部分:main 函数 —— 多轮对话循环(第 58~82 行) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 history := make ([]*schema.Message, 0 , 16 ) scanner := bufio.NewScanner(os.Stdin) for { _, _ = fmt.Fprint(os.Stdout, "you> " ) if !scanner.Scan() { break } line := strings.TrimSpace(scanner.Text()) if line == "" { break } history = append (history, schema.UserMessage(line)) events := runner.Run(ctx, history) content, err := printAndCollectAssistantFromEvents(events) if err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1 ) } history = append (history, schema.AssistantMessage(content, nil )) } if err := scanner.Err(); err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1 ) }
逐行分析: ① 初始化 history 和 scanner 1 2 history := make ([]*schema.Message, 0 , 16 ) scanner := bufio.NewScanner(os.Stdin)
make([]*schema.Message, 0, 16):创建一个空的 Message 切片,初始长度为 0,预分配容量为 16。这个 history 就是对话历史 ,每一轮对话的用户消息和 AI 回复都会追加到这里。
bufio.NewScanner(os.Stdin):创建一个从标准输入(键盘)逐行读取的扫描器。
💡 Go 语法知识:make([]T, length, capacity)
length:切片当前的元素个数(这里是 0,表示空切片)
capacity:切片底层数组的预分配大小(这里是 16,表示预留 16 个位置,避免频繁扩容)
当 append 超过 capacity 时,Go 会自动扩容(通常翻倍)
② 对话循环 1 2 3 4 5 6 7 8 9 for { _, _ = fmt.Fprint(os.Stdout, "you> " ) if !scanner.Scan() { break } line := strings.TrimSpace(scanner.Text()) if line == "" { break }
这是一个无限循环 (for {}),每次循环就是一轮对话。退出条件有两个:
scanner.Scan() 返回 false(用户按了 Ctrl+D,表示输入结束)
用户输入了空行
③ 追加用户消息 → 执行 Agent → 收集回复 → 追加 AI 消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 history = append (history, schema.UserMessage(line)) events := runner.Run(ctx, history) content, err := printAndCollectAssistantFromEvents(events) if err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1 ) } history = append (history, schema.AssistantMessage(content, nil ))
这就是文档中描述的多轮对话核心流程 :
1 2 3 4 5 6 7 第1轮:history = [User("你好")] → runner.Run → AI回复"你好!有什么可以帮你的?" → history = [User("你好"), Assistant("你好!有什么可以帮你的?")] 第2轮:history = [User("你好"), Assistant("你好!有什么可以帮你的?"), User("解释一下Go的goroutine")] → runner.Run → AI回复"goroutine是Go语言的轻量级线程..." → history = [User("你好"), Assistant("你好!..."), User("解释一下..."), Assistant("goroutine是...")]
每次调用 runner.Run(ctx, history) 时,完整的对话历史 都会传给模型,这样模型就能”记住”之前的对话内容。
⚠️ 注意 :runner.Run() 返回的不是直接的文本,而是 *adk.AsyncIterator[*adk.AgentEvent](事件流迭代器)。需要用 printAndCollectAssistantFromEvents 函数来消费它。
④ 与第一章的对比
维度
第一章 (ch01)
第二章 (ch02)
对话轮次
单轮(命令行参数传入问题)
多轮(循环读取用户输入)
调用方式
直接调用 cm.Stream()
通过 runner.Run()
返回类型
*schema.StreamReader[*schema.Message]
*adk.AsyncIterator[*adk.AgentEvent]
历史管理
不需要(单轮)
手动维护 history 切片
抽象层级
Component 层
Agent + Runner 层
第四部分:printAndCollectAssistantFromEvents 函数(第 86~135 行) 这是本章最复杂的函数,负责消费事件流、打印 AI 回复、收集回复文本 。
1 2 func printAndCollectAssistantFromEvents (events *adk.AsyncIterator[*adk.AgentEvent]) (string , error ) { var sb strings.Builder
参数 :events *adk.AsyncIterator[*adk.AgentEvent] —— 事件流迭代器,由 runner.Run() 返回
返回值 :(string, error) —— 收集到的 AI 回复文本 + 可能的错误
strings.Builder:高效的字符串拼接器,用于收集所有流式片段拼成完整回复
💡 Go 语法知识:strings.Builder 在 Go 中,字符串是不可变的。如果用 s += "abc" 反复拼接,每次都会创建新字符串,效率很低。strings.Builder 内部用 []byte 缓冲区,最后一次性转成字符串,效率高得多。
① 外层循环:消费事件流 1 2 3 4 5 for { event, ok := events.Next() if !ok { break }
events.Next() 是 AsyncIterator 的核心方法:
阻塞等待 直到有新事件到来
返回 (event, true) 表示拿到了一个事件
返回 (_, false) 表示迭代器已关闭(所有事件都消费完了)
② 错误检查 1 2 3 if event.Err != nil { return "" , event.Err }
如果事件中包含错误(比如模型 API 调用失败),直接返回错误。
③ 过滤非 Assistant 消息 1 2 3 4 5 6 7 8 if event.Output == nil || event.Output.MessageOutput == nil { continue } mv := event.Output.MessageOutput if mv.Role != schema.Assistant { continue }
事件流中可能包含各种类型的事件(控制动作、工具调用结果等),这里只关心 Assistant 角色的消息输出 。mv 是 MessageVariant 类型(消息变体),它可能是流式的,也可能是非流式的。
④ 处理流式消息(IsStreaming == true) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if mv.IsStreaming { mv.MessageStream.SetAutomaticClose() for { frame, err := mv.MessageStream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { return "" , err } if frame != nil && frame.Content != "" { sb.WriteString(frame.Content) _, _ = fmt.Fprint(os.Stdout, frame.Content) } } _, _ = fmt.Fprintln(os.Stdout) continue }
当 EnableStreaming: true 时,模型的回复会以流式 方式到达。这段代码的逻辑是:
mv.MessageStream.SetAutomaticClose() :设置自动关闭。当流读完(遇到 EOF)后,自动释放资源。这样就不需要手动调用 Close() 了。
内层循环 :不断调用 mv.MessageStream.Recv() 读取每一个”帧”(chunk):
io.EOF:流结束,退出内层循环
其他错误:返回错误
正常帧:把内容追加到 sb(用于收集完整文本),同时打印到屏幕
fmt.Fprintln(os.Stdout) :流式输出完毕后打印一个换行符
💡 与第一章的对比 :第一章直接调用 cm.Stream() 得到 StreamReader,然后循环 Recv()。第二章的流式处理逻辑本质上是一样的,只是被包在了 AgentEvent 里面。
⑤ 处理非流式消息(IsStreaming == false) 1 2 3 4 5 6 if mv.Message != nil { sb.WriteString(mv.Message.Content) _, _ = fmt.Fprintln(os.Stdout, mv.Message.Content) } else { _, _ = fmt.Fprintln(os.Stdout) }
如果不是流式的(比如 EnableStreaming 设为 false),模型会一次性返回完整消息。直接取 mv.Message.Content 打印即可。
⑥ 返回收集到的完整文本
把 strings.Builder 中收集的所有内容转成字符串返回。这个字符串会被 main 函数用来构造 AssistantMessage 追加到 history。
完整执行流程图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 sequenceDiagram participant User as 用户 (键盘) participant Main as main 函数 participant Runner as Runner participant Agent as ChatModelAgent participant Model as ChatModel (OpenAI/Ark) Main->>Main: 创建 ChatModel, Agent, Runner Main->>Main: history = [] loop 多轮对话 Main->>User: 打印 "you> " User->>Main: 输入 "你好" Main->>Main: history.append(UserMessage("你好")) Main->>Runner: runner.Run(ctx, history) Runner->>Agent: agent.Run(ctx, input) Agent->>Model: cm.Stream(ctx, messages) Model-->>Agent: StreamReader (流式) Agent-->>Runner: AsyncIterator[AgentEvent] Runner-->>Main: AsyncIterator[AgentEvent] loop 消费事件流 Main->>Main: events.Next() Note over Main: 检查 event.Output.MessageOutput Main->>Main: mv.MessageStream.Recv() Main->>User: 打印 frame.Content (逐块) Main->>Main: sb.WriteString(frame.Content) end Main->>Main: history.append(AssistantMessage(content)) end
关键概念总结 本章引入的三层抽象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌─────────────────────────────────────────────┐ │ Runner(执行器) │ │ - 管理 Agent 的生命周期 │ │ - 提供 Run() / Query() 便捷方法 │ │ - 支持 Checkpoint(后续章节) │ ├─────────────────────────────────────────────┤ │ ChatModelAgent(智能体) │ │ - 封装 ChatModel + Instruction │ │ - 统一输出为 AgentEvent 事件流 │ │ - 后续可扩展 Tools / Middleware │ ├─────────────────────────────────────────────┤ │ ChatModel(组件) │ │ - 底层模型调用(OpenAI / Ark) │ │ - Generate() / Stream() │ └─────────────────────────────────────────────┘
与第一章的核心差异
维度
第一章
第二章
抽象层级
直接用 Component
Component → Agent → Runner
对话模式
单轮(命令行参数)
多轮(交互式循环)
输出方式
StreamReader
AsyncIterator[AgentEvent]
历史管理
无
手动维护 history 切片
可扩展性
低(需要自己编排)
高(后续可加 Tools、Middleware 等)