第二章:ChatModelAgent、Runner、AgentEvent(Console 多轮)

本章目标:引入 Runner 实现多轮对话,理解 Agent 事件流和对话历史管理。

代码位置

前置条件

与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)。

运行

examples/quickstart/chatwitheino 目录下执行:

go run ./cmd/ch02

输出示例:

you> 你好
[assistant] 你好!有什么我可以帮助你的吗?
you> 我刚才说了什么?
[assistant] 你刚才说了"你好"。

从单轮到多轮:为什么需要 Runner

第一章我们实现了单轮对话,但有两个问题:

  1. 没有历史记忆:每次调用都是独立的,Agent 不知道之前说了什么
  2. 手动管理流式输出:需要自己处理 stream.Recv() 循环

Runner 的定位:

  • Runner 是 Agent 的运行时容器:管理 Agent 的调用和事件流
  • Runner 不管理对话历史:历史由外部维护和传入
  • Runner 提供统一的事件流:把 Agent 的执行过程抽象为一系列事件

简单类比:

  • Agent = “演员”(知道怎么演)
  • Runner = “导演”(管理演出流程)
  • 对话历史 = “剧本”(由外部编剧维护)

关键概念

Agent 接口

type Agent interface {
    Generate(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.Message, error)
    Stream(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.StreamReader[*schema.Message], error)
}

Runner

Runner 是 Agent 的运行时容器:

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

Runner 的核心方法 Run 接收对话历史,返回事件迭代器:

events := runner.Run(ctx, history)

AgentEvent

Runner 返回的 AsyncIterator[*AgentEvent] 包含以下事件类型:

type AgentEvent struct {
    Output     *AgentOutput     // Agent 的输出
    ToolCall   *ToolCallEvent   // Tool 调用事件
    ToolResult *ToolResultEvent // Tool 执行结果
    Interrupt  *InterruptEvent  // 中断事件(后续章节)
}

事件消费模式

events := runner.Run(ctx, history)
for {
    event, ok := events.Next()
    if !ok {
        break
    }
    if event.Output != nil && event.Output.MessageOutput != nil {
        // 处理消息输出(完整或流式)
    }
}

对话历史管理

简单内存管理

第二章使用最简单的方式管理对话历史:一个 []*schema.Message 切片。

对话流程:
1. history = []
2. 用户输入 → history = append(history, userMsg)
3. Agent 回复 → history = append(history, assistantMsg)
4. 下一轮用户输入 → 重复 2-3

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

history := make([]*schema.Message, 0, 16)

for {
    // 读取用户输入
    line := readUserInput()
    
    // 追加到历史
    history = append(history, schema.UserMessage(line))
    
    // 运行 Agent
    events := runner.Run(ctx, history)
    
    // 收集 Assistant 回复
    content := printAndCollectAssistantFromEvents(events)
    
    // 追加到历史
    history = append(history, schema.AssistantMessage(content, nil))
}

本章小结

  • Runner:Agent 的运行时容器,管理调用流程和事件流
  • AgentEvent:统一的事件类型,包含输出、Tool 调用、中断等
  • 对话历史:由外部维护,每轮追加 user 和 assistant 消息
  • 多轮对话:通过传入完整历史实现上下文连续

扩展思考

内存管理的问题:

  • 历史无限增长 → 需要裁剪策略
  • 进程重启丢失 → 需要持久化
  • 多会话混乱 → 需要 Session 管理

这些问题将在第三章通过引入 Session/Store 机制解决。