目录 · 第 2 / 28 章
EinoPart I · ADK 的使用

02ChatModelAgent 与 Runner

Agent 配置项、Runner.Run vs Query、EnableStreaming 与事件流消费。

adk/chatmodel.goadk/runner.goadk/utils.go

最小可跑的第一个 Agent

ChatModelAgent 是 ADK 里最常用的 Agent 类型:给它一个模型、一段指令,它就能对话;再给它工具,它就能自动进入「思考—调用工具—再思考」的 ReAct 循环(第 14 章会拆解这个循环)。先看最小形态——只有模型和指令,没有工具:

agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "assistant",
Description: "一个通用助手",
Instruction: "你是一个乐于助人的中文助手。",
Model: chatModel, // 来自 eino-ext,如 openai.NewChatModel(...)
})
if err != nil {
return err
}
runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent})
iter := runner.Query(ctx, "用一句话解释什么是事件溯源")
for {
event, ok := iter.Next()
if !ok {
break
}
msg, _, err := adk.GetMessage(event)
if err != nil || msg == nil {
continue
}
fmt.Print(msg.Content)
}

跑起来就这么多。下面把每个关键点对着源码讲清楚。

sequenceDiagram
  participant U as 调用方
  participant R as Runner
  participant A as ChatModelAgent
  participant M as ChatModel
  U->>R: Query(ctx, "问题")
  R->>A: Run(ctx, input)
  A->>M: Generate / Stream(messages)
  M-->>A: 消息(可能带 ToolCalls)
  A-->>R: AsyncIterator[*AgentEvent]
  loop iter.Next()
    R-->>U: event(Output / Action / Err)
  end

Runner.Query 到事件流的一次调用

Config:一个 Agent 的全部配置

ChatModelAgentConfig(adk/chatmodel.go:260)是 TypedChatModelAgentConfig[*schema.Message] 的别名。它的字段决定了这个 Agent 的一切能力,先记住最核心的几个:

// TypedChatModelAgentConfig is the generic configuration for ChatModelAgent.
type TypedChatModelAgentConfig[M MessageType] struct {
// Name of the agent. Better be unique across all agents.
// Optional. If empty, the agent can still run standalone but cannot be used as
// a sub-agent tool via NewAgentTool (which requires a non-empty Name).
Name string
// Description of the agent's capabilities.
// Helps other agents determine whether to transfer tasks to this agent.
// Optional. If empty, the agent can still run standalone but cannot be used as
// a sub-agent tool via NewAgentTool (which requires a non-empty Description).
Description string
// Instruction used as the system prompt for this agent.
// Optional. If empty, no system prompt will be used.
// Supports f-string placeholders for session values in default GenModelInput, for example:
// "You are a helpful assistant. The current time is {Time}. The current user is {User}."
// These placeholders will be replaced with session values for "Time" and "User".
Instruction string
// Model is the chat model used by the agent.
// If your ChatModelAgent uses any tools, this model must support the model.WithTools
// call option, as that's how ChatModelAgent configures the model with tool information.
Model model.BaseModel[M]
ToolsConfig ToolsConfig
// GenModelInput transforms instructions and input messages into the model's input format.
// Optional. Defaults to defaultGenModelInput which combines instruction and messages.
GenModelInput TypedGenModelInput[M]
// Exit defines the tool used to terminate the agent process.
// Optional. If nil, no Exit Action will be generated.
// You can use the provided 'ExitTool' implementation directly.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
Exit tool.BaseTool
// OutputKey stores the agent's response in the session.
// Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content).
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
OutputKey string
// MaxIterations defines the upper limit of ChatModel generation cycles.
// The agent will terminate with an error if this limit is exceeded.
// … 省略 109 行;完整声明 L259–415,点击上方「浏览完整文件」
字段类型作用
NamestringAgent 名字,会出现在事件的 AgentName
Descriptionstring描述;被当作工具/子 Agent 时,这就是它的「工具说明」
Instructionstring系统提示,支持 {占位符} 注入 session 值(第 4 章)
Modelmodel.BaseModel[M]大模型;若要用工具,模型必须支持 model.WithTools
ToolsConfigToolsConfig工具配置(第 3 章)
MaxIterationsintReAct 循环上限,默认 20(adk/chatmodel.go:308)

| Exit | tool.BaseTool | 可选的退出工具;为 nil 时不生成 Exit 动作 | | OutputKey | string | 把最终输出写回 session 的键名 |

📝 Description 不是给人看的注释

Description 看起来像文档字符串,但在多智能体场景里它有实打实的功能:当这个 Agent 被 NewAgentTool 包装成工具时(第 7、16 章),Description 就是模型看到的「这个工具能干什么」。写得含糊,上层模型就不知道该不该调用它。

构造函数 NewChatModelAgent(adk/chatmodel.go:484)会做基本校验:Model 为 nil 直接报错;GenModelInput 为 nil 时填入默认实现(负责把 Instruction + 历史消息拼成模型输入,第 4 章详述)。

// NewChatModelAgent creates a new ChatModelAgent with the given config.
func NewChatModelAgent(ctx context.Context, config *ChatModelAgentConfig) (*ChatModelAgent, error) {
return NewTypedChatModelAgent[*schema.Message](ctx, config)
}

Run vs Query:两个入口

Runner 有两个跑法,区别只在「输入形态」:

Query(adk/runner.go:108)接受一个字符串,内部包成一条 user 消息:

func (r *TypedRunner[M]) Query(ctx context.Context, query string,
opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
msgs, err := newUserMessage[M](query)
if err != nil {
return errorIterator[M](err)
}
return r.Run(ctx, []M{msgs}, opts...)
}

Run(adk/runner.go:102)接受一个消息切片 []M,适合你已经有多轮历史、或要塞入 system/assistant 消息的场景:

func (r *TypedRunner[M]) Run(ctx context.Context, messages []M,
opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
return typedRunnerRunImpl(r.a, r.enableStreaming, r.store, ctx, messages, opts...)
}
iter := runner.Run(ctx, []*schema.Message{
schema.SystemMessage("只用 JSON 回答"),
schema.UserMessage("列出三种一致性模型"),
})

两者返回的都是同一个 *AsyncIterator[*TypedAgentEvent[M]]。记住:Query 只是 Run 的单字符串语法糖,没有任何行为差异。

消费事件流:Next 循环与 GetMessage

事件流的消费永远是同一个模式——Next() 循环直到 ok == false:

for {
event, ok := iter.Next()
if !ok {
break
}
// ...
}

AsyncIterator 底层是一个无界 channel(adk/utils.go:31),Next() 从里面收事件,第二个返回值告诉你流是否已耗尽(adk/utils.go:35)。

type AsyncIterator[T any] struct {
ch *internal.UnboundedChan[T]
}
func (ai *AsyncIterator[T]) Next() (T, bool) {
return ai.ch.Receive()
}

但事件里的输出可能是流式的(Output.MessageOutput.IsStreaming == true),这时 Message 字段是空的,内容藏在一个 MessageStream 里。手动处理这两种情况很啰嗦,所以 ADK 给了 GetMessage(adk/utils.go:300):

// GetMessage extracts the Message from an AgentEvent. For streaming output,
// it duplicates the stream and concatenates it into a single Message.
func GetMessage(e *AgentEvent) (Message, *AgentEvent, error) {
return TypedGetMessage(e)
}
msg, newEvent, err := adk.GetMessage(event)

它会:非流式时直接取 Message;流式时复制一份流(Copy(2))、拼接成完整消息返回,同时把原事件里的流替换成一个仍可消费的副本(所以返回了一个 newEvent)。这样你既拿到了完整文本,又没有「消费掉别人还要用的流」。

🔑 本章的设计钥匙

「流」在 Eino 里是一等公民,但流有个天然陷阱:一份流只能被读一次。ADK 的做法是——凡是需要「窥视」流内容的地方,都先 Copy 再读,读完把原位置换成一个新的、未被消费的流。GetMessage 就是这个原则的门面。你在第 21 章会看到底层 StreamReader 的 copy-on-read 实现。

EnableStreaming 到底开关了什么

新手常以为 EnableStreaming 是「要不要流式打字机效果」的 UI 开关。其实它更底层:它决定 ChatModelAgent 内部对编译好的图调用的是 Stream 还是 Invoke(adk/chatmodel.go:1202):

if mp.input.EnableStreaming {
msgStream, err_ = runnable.Stream(ctx, in, runOpts...)
} else {
msg, err_ = runnable.Invoke(ctx, in, runOpts...)
}

它是 RunnerConfig 上的字段(adk/runner.go:68),Runner 会把它盖进每一个 AgentInput。开了它,事件里的 Output 就是流式的,你需要用 GetMessage 或手动读 MessageStream;关了它,Message 直接就是完整消息。

type TypedRunnerConfig[M MessageType] struct {
Agent TypedAgent[M]
EnableStreaming bool
CheckPointStore CheckPointStore
}

一个容易忽略的细节:续跑(Resume)时的流式模式来自 checkpoint,而不是调用方(adk/runner.go:124)。也就是说,你中断时是流式的,恢复时也会是流式的——状态里记着呢。这是「中断/续跑要忠实还原当时上下文」的一个具体体现,第 5 章会正面讲中断。

// Resume continues an interrupted execution from a checkpoint, using an "Implicit Resume All" strategy.
// This method is best for simpler use cases where the act of resuming implies that all previously
// interrupted points should proceed without specific data.
//
// When using this method, all interrupted agents will receive `isResumeFlow = false` when they
// call `GetResumeContext`, as no specific agent was targeted. This is suitable for the "Simple Confirmation"
// pattern where an agent only needs to know `wasInterrupted` is true to continue.
func (r *TypedRunner[M]) Resume(ctx context.Context, checkPointID string, opts ...AgentRunOption) (
*AsyncIterator[*TypedAgentEvent[M]], error) {
return r.resumeInternal(ctx, checkPointID, nil, opts...)
}

本章小结

  • ChatModelAgent 是最常用的 Agent:模型 + 指令即可对话,加工具即进入 ReAct 循环。
  • ConfigName / Description / Instruction / Model 是四个必知字段;Description 在多智能体里是「工具说明」。
  • QueryRun 的单字符串语法糖,二者返回同一种事件流。
  • 消费事件永远用 for { Next() };用 GetMessage 安全地处理流式/非流式两种输出。
  • EnableStreaming 决定内部走 Stream 还是 Invoke;续跑时的流式模式由 checkpoint 决定。

下一章,我们给这个 Agent 装上工具,看清 ToolInfo / InvokableTool 的契约,以及「工具绑定为什么是不可变的」。

源码

正在读取完整文件…