You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.9 KiB

title
第二章ChatModelAgent、Runner、AgentEventConsole 多轮)

本章目标:引入 ADK 的执行抽象Agent + Runner并用一个 Console 程序实现多轮对话。

代码位置

前置条件

与第一章一致:需要配置一个可用的 ChatModelOpenAI 或 Ark

运行

examples/quickstart/chatwitheino 目录下执行:

go run ./cmd/ch02

看到提示后输入问题(空行退出):

you> 你好,解释一下 Eino 里的 Agent 是什么?
...
you> 再用一句话总结一下
...

关键概念

从 Component 到 Agent

第一章我们学习了 Component(组件),它是 Eino 中可替换、可组合的能力单元:

  • ChatModel:调用大语言模型
  • Tool:执行特定任务
  • Retriever:检索信息
  • Loader:加载数据

Component 和 Agent 的关系:

  • Component 不构成完整的 AI 应用:它只是能力单元,需要被组织、编排、执行
  • Agent 是完整的 AI 应用:它封装了完整的业务逻辑,可以直接运行
  • Agent 内部使用 Component:最核心的是 ChatModel(对话能力)和 Tool(执行能力)

为什么需要 Agent

如果只有 Component你需要自己

  • 管理对话历史
  • 编排调用流程(何时调用模型、何时调用工具)
  • 处理流式输出
  • 实现中断恢复
  • ...

Agent 提供了什么?

  • 完整的运行时框架:通过 Runner 统一管理执行过程
  • 标准的事件流输出Run() -> AsyncIterator[*AgentEvent],支持流式、中断、恢复
  • 可扩展能力:可以添加 tools、middleware、interrupt 等
  • 开箱即用:创建 Agent 后直接运行,无需关心内部细节

本章示例:

ChatModelAgent 是最简单的 Agent它内部只使用了 ChatModel,但已经具备了 Agent 的完整能力框架。后续章节会展示如何添加 Tool 等更多能力。

Agent 接口

Agent 是 ADK 中的核心接口,定义了智能体的基本行为:

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]
}

接口职责:

  • Name() / Description():标识 Agent 的名称和描述
  • Run():执行 Agent 的核心方法,接收输入消息,返回事件流

设计理念:

  • 统一抽象:所有 AgentChatModelAgent、WorkflowAgent、SupervisorAgent 等)都实现这个接口
  • 事件驱动:通过事件流(AsyncIterator[*AgentEvent])输出执行过程,支持流式响应
  • 可扩展性:后续加入 tools、middleware、interrupt 等能力时,接口保持不变

ChatModelAgent

ChatModelAgent 是 Agent 接口的一个实现,基于 ChatModel 构建:

agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
    Name:        "Ch02ChatModelAgent",
    Description: "A minimal ChatModelAgent with in-memory multi-turn history.",
    Instruction: instruction,
    Model:       cm,
})

ChatModel vs ChatModelAgent本质区别

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

为什么需要 ChatModelAgent

  1. 统一抽象ChatModel 只是 Component 的一种,而 Agent 是更高层的抽象,可以组合多种 Component
  2. 事件驱动Agent 输出事件流,支持流式响应、中断恢复、状态转移等复杂场景
  3. 可扩展性ChatModelAgent 可以添加 tools、middleware、interrupt 等能力,而 ChatModel 只能调用模型
  4. 编排友好Agent 可以被 Runner 统一管理,支持 checkpoint、恢复等运行时能力

简单来说:

  • ChatModel = "负责与大语言模型通信的组件屏蔽不同模型提供商的差异OpenAI、Ark、Claude 等)"
  • ChatModelAgent = "基于模型构建的智能体,可以调用模型,但还能做更多事"

类比理解:

  • ChatModel 就像"数据库驱动":负责与数据库通信,屏蔽 MySQL/PostgreSQL 的差异
  • ChatModelAgent 就像"业务逻辑层":基于数据库驱动构建,但还包含业务规则、事务管理等

特点:

  • 封装了 ChatModel 的调用逻辑
  • 提供统一的 Run() -> AgentEvent 输出形态
  • 后续可以添加 tools、middleware 等能力

Runner

Runner 是执行 Agent 的入口点,负责管理 Agent 的生命周期:

type Runner struct {
    a Agent  // 要执行的 Agent
    enableStreaming bool
    store CheckPointStore  // 用于中断恢复的状态存储
}

为什么需要 Runner

虽然 Agent 提供了 Run() 方法,但直接调用会缺少很多运行时能力:

  1. 生命周期管理Runner 管理 Agent 的启动、恢复、中断等状态
  2. Checkpoint 支持:配合 CheckPointStore 实现中断恢复(后续章节涉及)
  3. 统一入口:提供 Run()Query() 等便捷方法
  4. 事件流封装:将 Agent 的事件流转换为可消费的 AsyncIterator[*AgentEvent]

使用方式:

runner := adk.NewRunner(ctx, adk.RunnerConfig{
    Agent:           agent,
    EnableStreaming: true,
})

// 方式 1传入消息列表
events := runner.Run(ctx, history)

// 方式 2便捷方法传入单个查询字符串
events := runner.Query(ctx, "你好")

AgentEvent

AgentEvent 是 Runner 返回的事件单元:

type AgentEvent struct {
    AgentName string
    RunPath   []RunStep
    
    Output *AgentOutput  // 输出内容
    Action *AgentAction  // 控制动作
    Err    error         // 执行错误
}

主要字段:

  • event.Err:执行错误
  • event.Output.MessageOutputmessage 或 message stream流式
  • event.Action:中断/转移/退出等控制动作(后续章节用到)

消费方式:

for {
    event, ok := events.Next()
    if !ok {
        break
    }
    if event.Err != nil {
        // 处理错误
    }
    if event.Output != nil && event.Output.MessageOutput != nil {
        // 处理消息输出(可能是流式)
    }
}

多轮对话的实现

本章实现的是简单的多轮对话:用户输入 → 模型回复 → 用户继续输入 → ...

实现方式:

没有 tools 时,ChatModelAgent 在一次 Run() 里只会完成一轮模型调用。多轮对话是通过调用侧维护 history 实现的:

  1. history []*schema.Message 保存累计对话
  2. 每次用户输入:把 UserMessage 追加到 history
  3. 调用 runner.Run(ctx, history) 得到事件流,消费得到 assistant 文本
  4. 把本轮 assistant 文本追加回 history进入下一轮

**关键代码片段(注意:这是简化后的代码片段,不能直接运行,完整代码请参考 cmd/ch02/main.go

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)
    
    // 5. 追加 assistant 消息到 history
    history = append(history, schema.AssistantMessage(content, nil))
}

流程图:

┌─────────────────────────────────────────┐
│  初始化 history = []                     │
└─────────────────────────────────────────┘
                   ↓
        ┌──────────────────────┐
        │  用户输入 UserMessage  │
        └──────────────────────┘
                   ↓
        ┌──────────────────────┐
        │  追加到 history       │
        └──────────────────────┘
                   ↓
        ┌──────────────────────┐
        │  runner.Run(history) │
        └──────────────────────┘
                   ↓
        ┌──────────────────────┐
        │  消费事件流           │
        └──────────────────────┘
                   ↓
        ┌──────────────────────┐
        │  追加 AssistantMessage│
        └──────────────────────┘
                   ↓
              (循环继续)

本章小结

  • Agent 接口:定义智能体的基本行为,核心是 Run() -> AsyncIterator[*AgentEvent]
  • ChatModelAgent:基于 ChatModel 实现的 Agent提供统一的执行抽象
  • RunnerAgent 的执行入口管理生命周期、checkpoint、事件流等运行时能力
  • AgentEvent:事件驱动的输出单元,支持流式响应和控制动作
  • 多轮对话:通过调用侧维护 history 实现,每次 Run() 完成一轮对话