---
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` 如何支持流式消费。

---

<!-- @block-anchor:overview:start -->
<!-- @block-anchor:overview:end -->

## 代码位置

- 入口代码：[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["统一抽象<br/>运行时多态"]
    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 完成所有事情？

<table>
<tr><td>维度</td><td>ChatModel</td><td>ChatModelAgent</td></tr>
<tr><td><strong>定位</strong></td><td>Component（组件）</td><td>Agent（智能体）</td></tr>
<tr><td><strong>接口</strong></td><td><pre>Generate() / Stream()</pre></td><td><pre>Run() -> AsyncIterator[*AgentEvent]</pre></td></tr>
<tr><td><strong>输出</strong></td><td>直接返回消息内容</td><td>返回事件流（含消息、控制动作等）</td></tr>
<tr><td><strong>能力</strong></td><td>单纯的模型调用</td><td>可扩展 tools、middleware、interrupt 等</td></tr>
<tr><td><strong>适用场景</strong></td><td>简单的对话场景</td><td>复杂的智能体应用</td></tr>
</table>

**为什么需要 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]]
