--- tags: [eino, ai-development, go, quickstart, agent, adk] create time: 2026-04-29 15:00 title: 第二章:ChatModelAgent、Runner、AgentEvent(Console 多轮) weight: 2 --- ## 概述 在第一章掌握了 `ChatModel` 组件的基础用法后,本章引入 Eino ADK 中的执行抽象——**Agent + Runner**。通过创建一个 Console 程序实现多轮对话,你将理解 Agent 接口的设计意图、事件驱动的执行模型,以及 `AsyncIterator` 如何支持流式消费。 --- ## 代码位置 - 入口代码:[cmd/ch02/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch02/main.go) ## 前置条件 与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。 ## 运行 在 `examples/quickstart/chatwitheino` 目录下执行: ```bash go run ./cmd/ch02 ``` 看到提示后输入问题(空行退出): ``` you> 你好,解释一下 Eino 里的 Agent 是什么? ... you> 再用一句话总结一下 ... ``` ## 关键概念 ### 从 Component 到 Agent 第一章我们学习了 **Component**(组件),它是 Eino 中可替换、可组合的能力单元: | Component | 职责 | 示例 | |-----------|------|------| | `ChatModel` | 调用大语言模型 | OpenAI、Ark、Claude | | `Tool` | 执行特定任务 | 文件读取、代码搜索 | | `Retriever` | 检索信息 | 向量检索、关键词检索 | | `Loader` | 加载数据 | 文档解析器 | > [!question] 思考一下 > 假设你现在有一个 `ChatModel` 和一个 `Tool`,你能独立完成一个多轮对话的 AI 助手吗?如果能,你觉得会遇到哪些挑战? **Component 和 Agent 的关系:** - **Component 是积木**——单个 Component 只是能力单元,需要被组织、编排、执行 - **Agent 是整栋建筑**——它封装了完整的业务逻辑,可以直接运行 - **Agent 内部使用 Component**——最核心的是 `ChatModel`(对话能力)和 `Tool`(执行能力) **为什么需要 Agent?** 如果只有 Component,你需要自己管理: - 对话历史的多轮累积 - 调用流程编排(何时调模型、何时调工具) - 流式输出与中断处理 - 错误恢复和状态管理 - ... **Agent 提供了什么?** > [!tip] Agent 的核心价值 > Agent = 完整运行时 + 标准事件流 + 可扩展框架。你只需要创建 Agent,然后交给 Runner 执行,不需要关心内部细节。 - **完整的运行时框架**:通过 `Runner` 统一管理执行过程 - **标准的事件流输出**:`Run() -> AsyncIterator[*AgentEvent]`,支持流式、中断、恢复 - **可扩展能力**:可以添加 tools、middleware、interrupt 等 - **开箱即用**:创建 Agent 后直接运行,无需关心内部细节 **本章示例:** `ChatModelAgent` 是最简单的 Agent,它内部只使用了 `ChatModel`,但已经具备了 Agent 的完整能力框架。后续章节会逐步展示如何添加 `Tool`、middleware、interrupt 等能力。 ### Agent 接口 `Agent` 是 ADK 中的核心接口,定义了智能体的基本行为。所有类型的 Agent(ChatModelAgent、WorkflowAgent、SupervisorAgent 等)都实现这个统一接口: ```go type Agent interface { Name(ctx context.Context) string Description(ctx context.Context) string // Run 执行 Agent,返回事件流 Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] } ``` > [!tip] 设计精解 > `Run()` 的返回值是 `*AsyncIterator[*AgentEvent]`——这是一个**懒加载**的流式迭代器。调用 `Run()` 时不会立即执行,只有当你开始消费事件(调用 `events.Next()`)时,Agent 才开始运行。这让你可以在启动前先配置中间件或注入依赖。 > [!question] 接口签名疑问 > **为什么 `Name()` 和 `Description()` 也要传 ctx?** > -> 点击 [[chapter_02_chatmodelagent_runner_agentevent/why_ctx_in_agent_interface|深入探究]] 理解接口签名设计背后的哲学。 **接口职责拆解:** | 方法/字段 | 职责 | 类比 | |-----------|------|------| | `Name()` | 唯一标识 Agent | 函数名 | | `Description()` | 描述 Agent 功能 | 函数文档 | | `Run()` | 执行核心逻辑 | 函数调用 | **设计理念:** ```mermaid flowchart LR classDef noteStyle fill:#fff3e0,stroke:#ffb74d,stroke-width:2px A["Agent 接口"] --> B["ChatModelAgent"] A --> C["WorkflowAgent"] A --> D["SupervisorAgent"] A --> E["..."] B --> F["统一 Runner 执行"] C --> F D --> F F -.-> G["统一抽象
运行时多态"] class G noteStyle ``` 1. **统一抽象**:所有 Agent 类型都实现同一个接口,Runner 无需关心 Agent 内部实现 2. **事件驱动**:通过事件流输出,支持流式响应、中断恢复、状态转移 3. **开闭原则**:新增 Agent 类型时,Runner 和消费者代码无需修改 ### ChatModelAgent `ChatModelAgent` 是 Agent 接口的一个实现,基于 ChatModel 构建: ```go // 核心参数说明: // - Name / Description: Agent 的身份标识 // - Instruction: 系统指令,定义 Agent 的行为风格和目标 // - Model: 底层的 ChatModel 组件,负责实际的模型调用 agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "Ch02ChatModelAgent", Description: "A minimal ChatModelAgent with in-memory multi-turn history.", // 记忆体多轮对话的最小 Agent Instruction: instruction, Model: cm, }) ``` **ChatModel vs ChatModelAgent:本质区别** > [!question] 关键辨析 > ChatModel 和 ChatModelAgent 看起来都在"调用模型",它们的根本区别在哪里?为什么不能直接用 ChatModel 完成所有事情?
维度ChatModelChatModelAgent
定位Component(组件)Agent(智能体)
接口
Generate() / Stream()
Run() -> AsyncIterator[*AgentEvent]
输出直接返回消息内容返回事件流(含消息、控制动作等)
能力单纯的模型调用可扩展 tools、middleware、interrupt 等
适用场景简单的对话场景复杂的智能体应用
**为什么需要 ChatModelAgent?** 1. **统一抽象**:ChatModel 只是 Component 的一种,而 Agent 是更高层的抽象,可以组合多种 Component 2. **事件驱动**:Agent 输出事件流,支持流式响应、中断恢复、状态转移 3. **可扩展性**:ChatModelAgent 可以添加 tools、middleware、interrupt 等能力 4. **编排友好**:Agent 可以被 Runner 统一管理,支持 checkpoint、恢复等运行时能力 > [!tip] 类比理解 > | ChatModel | ChatModelAgent | 现实类比 | | --------- | -------------- | ---------- | | 数据库驱动 | 业务逻辑层 | 发动机 vs 整车 | | 单个乐器 | 交响乐团指挥 | 砖块 vs 建筑 | | API 端点 | 微服务 | 积木 vs 乐高模型 | **简单来说:** - **ChatModel** = "负责与大语言模型通信的组件,屏蔽不同模型提供商的差异(OpenAI、Ark、Claude 等)" - **ChatModelAgent** = "基于模型构建的智能体,可以调用模型,但还能做更多事" **特点:** - 封装了 ChatModel 的调用逻辑 - 提供统一的 `Run() -> AgentEvent` 输出形态 - 后续可以添加 tools、middleware 等能力 ### Runner `Runner` 是执行 Agent 的入口点,负责管理 Agent 的生命周期: ```go type Runner struct { a Agent // 要执行的 Agent enableStreaming bool // 是否启用流式输出 store CheckPointStore // 用于中断恢复的状态存储(后续章节) } ``` > [!question] 为什么需要 Runner? > Agent 已经有了 `Run()` 方法,为什么还要多一层 Runner?直接调用不就好了吗? 虽然 Agent 提供了 `Run()` 方法,但直接调用会缺少很多运行时能力: 1. **生命周期管理**:Runner 统一管理 Agent 的启动、恢复、中断等状态 2. **Checkpoint 支持**:配合 `CheckPointStore` 实现中断恢复(第七章详解) 3. **统一入口**:提供 `Run()` 和 `Query()` 等便捷方法 4. **事件流封装**:将 Agent 的事件流转换为可消费的 `AsyncIterator[*AgentEvent]` **使用方式:** ```go runner := adk.NewRunner(ctx, adk.RunnerConfig{ Agent: agent, EnableStreaming: true, // 流式模式:逐 token 消费;设为 false 则等待全部完成 }) // 方式 1:传入完整消息历史(支持多轮对话) events := runner.Run(ctx, history) // 方式 2:便捷方法,传入单个查询字符串 events := runner.Query(ctx, "你好") ``` > [!tip] EnableStreaming 的影响 > > | 模式 | 表现 | 适用场景 | > |------|------|----------| > | `true` | Runner 逐 token 转发事件,用户可实时看到回复 | 终端 Console、Chat UI | > | `false` | Runner 等待 Agent 全部执行完毕再返回结果 | API 后端、批处理任务 | **Runner 的执行流程:** ```mermaid flowchart TD A["runner.Run() / runner.Query()"] --> B["创建 AsyncIterator"] B --> C["开始消费事件"] C --> D{"下一个事件"} D -->|Err| E["处理错误并退出"] D -->|Output| F["展示给终端/客户端"] F --> D D -->|Action| G["控制动作(中断/转移/退出)"] G --> D D -->|结束| H["迭代器关闭,消费完成"] ``` ### AgentEvent `AgentEvent` 是 Runner 返回的事件单元,代表执行过程中的一个**离散步骤**: ```go type AgentEvent struct { AgentName string // 当前执行的是哪个 Agent RunPath []RunStep // 当前执行路径(支持嵌套 Agent) Output *AgentOutput // 输出内容 Action *AgentAction // 控制动作 Err error // 执行错误 } ``` > [!note] 事件驱动设计 > 与传统函数调用不同,Agent 的执行不是一次性的 `return result`,而是一系列事件的有序播放。这让你的应用可以实时感知每一个执行步骤——就像看直播而不是看录播。 **三大核心字段:** | 字段 | 含义 | 本章用途 | 后续章节 | |------|------|----------|----------| | `event.Err` | 执行过程中发生的错误 | 错误检测与退出 | 错误处理策略 | | `event.Output` | Agent 的输出结果 | 展示用户回复 | 流式消费、中间结果 | | `event.Action` | 控制动作(中断/转移/退出等) | —— | 第七章:Interrupt & Resume | --- ### AsyncIterator:事件流的消费方式 `Runner.Run()` 返回的是 `*AsyncIterator[*AgentEvent]`,这是一个非阻塞的流式迭代器。 > [!question] 为什么用 AsyncIterator? > 为什么不直接返回 `[]*AgentEvent` 或者单个结果? 因为 Agent 的执行是**流式**的:模型逐 token 生成回复,Tool 调用穿插其中。如果等全部完成再返回,用户需要等待更长时间。`AsyncIterator` 让你可以**实时消费**每一个事件。 **消费方式:** ```go // events 是 *AsyncIterator[*AgentEvent],由 runner.Run() 返回 events := runner.Run(ctx, history) for { event, ok := events.Next() // 获取下一个事件,阻塞直到有事件或结束 if !ok { break // 迭代器关闭,全部事件已消费 } // 三种处理方式互斥,根据具体场景判断 if event.Err != nil { // 1. 错误分支:执行出错,记录日志并决定是否继续 log.Printf("agent error: %v", event.Err) break } if event.Output != nil && event.Output.MessageOutput != nil { // 2. 输出分支:收到消息内容(可能是流式分片) msg := event.Output.MessageOutput.Message fmt.Print(msg.Content) } // 3. Action 分支:当前章用不到,后续章节(Interrupt/Resume)会深入 // if event.Action != nil { ... } } ``` > [!warning] 重要注意事项 > - **每次 `runner.Run()` 创建新的迭代器**,消费一次后不可重复使用 > - **不要忽略 `event.Err`**——Agent 内部可能静默失败(如工具执行超时) > - **注意 goroutine 安全**——多个消费者同时读取同一个 AsyncIterator 是不安全的 > [!question] 深入理解事件流消费模式? > 通过 Claude Code Agent 事件流消费的类比加深理解。 > -> 参考 [[chapter_02_chatmodelagent_runner_agentevent/async_iterator_consumption|AsyncIterator:事件流的消费方式]] ## 多轮对话的实现 本章实现的是简单的多轮对话:用户输入 → 模型回复 → 用户继续输入 → ... **核心思想:** 没有 tools 时,`ChatModelAgent` 在一次 `Run()` 里只会完成一轮模型调用。多轮对话是通过**调用侧维护 history** 实现的——每次调用都把完整的对话历史传进去,让模型知道之前聊了什么。 ```mermaid flowchart TD S["初始化 history = []"] --> L["进入循环"] L --> U["用户输入 UserMessage"] U --> H1["追加到 history"] H1 --> R["runner.Run(ctx, history)"] R --> E["消费事件流"] E --> C{"有 Output?"} C -->|是| A1["收集 assistant 文本"] A1 --> H2["追加 AssistantMessage 到 history"] H2 --> L C -->|否/结束| OUT["退出循环"] style S fill:#e1f5fe style OUT fill:#ffebee ``` **逐步拆解:** 1. **用 `history []*schema.Message` 保存累计对话**——所有已发生过的消息都存这里 2. **每次用户输入**:把 `UserMessage` 追加到 history 3. **调用 `runner.Run(ctx, history)`**:得到完整事件流,消费得到 assistant 回复 4. **把本轮 assistant 文本追加回 history**:进入下一轮时,模型能看到全部对话历史 **关键代码片段(注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [cmd/ch02/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch02/main.go)): ```go // history 维护完整的对话历史,容量预设 16 条消息 history := make([]*schema.Message, 0, 16) for { // 1. 读取用户输入,空行表示退出 line := readUserInput() if line == "" { break } // 2. 将用户消息追加到 history // 这样模型在下一轮能"记住"之前的对话 history = append(history, schema.UserMessage(line)) // 3. 调用 Runner 执行 Agent // 返回的事件流包含所有输出步骤(消息、工具调用等) events := runner.Run(ctx, history) // 4. 消费事件流,收集 assistant 的回复内容 content := collectAssistantFromEvents(events) fmt.Println("[assistant]", content) // 5. 将 assistant 回复也追加到 history // nil 表示本轮没有工具调用(后续章节会用到) history = append(history, schema.AssistantMessage(content, nil)) } ``` > [!note] 关于 history 的内存管理 > > 当前实现将所有消息保留在内存中。在实际应用中,你可能需要: > - 设置最大消息数量限制(如上面的 `16`) > - 使用摘要压缩(Summarization Middleware,第五章介绍) > - 使用外部存储(Memory 组件,第三章介绍) ## 本章小结 | 核心概念 | 说明 | 关键要点 | |----------|------|----------| | **Agent 接口** | 定义智能体的基本行为,`Run() -> AsyncIterator[*AgentEvent]` | 统一抽象,所有 Agent 类型共享同一接口 | | **ChatModelAgent** | 基于 ChatModel 实现的 Agent | 最简 Agent,是后续扩展的基础 | | **Runner** | Agent 的执行入口 | 管理生命周期、Checkpoint、事件流封装 | | **AgentEvent** | 事件驱动的输出单元 | 包含 Output(消息)、Action(控制)、Err(错误) | | **AsyncIterator** | 流式迭代器,逐事件消费 | 实时响应,不阻塞等待全部完成 | | **多轮对话** | 调用侧维护 history 实现 | 每次 `Run()` 传完整历史,每轮追加新消息 | > [!success] 学习成果 > 完成本章后,你应该能够: > - 理解 Component 和 Agent 的本质区别 > - 使用 `adk.NewChatModelAgent` 创建自己的 Agent > - 通过 Runner 执行 Agent 并消费事件流 > - 实现基于 history 的多轮对话 > > > [!tip] 动手练习 > > 试着修改 `Instruction` 参数,给你的 Agent 设定一个角色(如"你是一个编程导师"),观察不同指令对回复的影响。这就是 Prompt Engineering 的雏形! ## 下一章预告 [[Eino/quick_start/chapter_03_memory_and_session|第三章:Memory 与 Session]] 将引入持久化存储机制,让对话历史跨进程保留,不再因为程序重启而丢失记忆。 ## 关联笔记 - [[Eino/quick_start/chapter_01_chatmodel_and_message]] - [[Eino/quick_start/chapter_03_memory_and_session]] - [[agent_interface]] - [[chat_model]]