12v0.9 的 AgenticMessage 与泛型化
AgenticMessage/ContentBlock、AgenticModel、ADK 全面泛型化让 chat 与 agentic 复用同一体系。
schema/agentic_message.go:71schema/agentic_message.go:102components/model/interface.go:36adk/interface.go:43一个消息类型不够用了
第 8 章埋过一个伏笔:所有 ADK 类型都带着 [M MessageType],而 MessageType 只允许两种消息——*schema.Message 和 *schema.AgenticMessage。这一章补完 Part II 的最后一块拼图:v0.9 为什么要把整套 ADK 泛型化?
答案要从经典 schema.Message 的局限说起。它诞生于”纯文本对话”时代:一个 Content string 装文本,一个 ToolCalls 装工具调用。但当模型进入多模态 + 富内容块时代——一条助手消息里可能同时有推理过程、生成的图片、几个工具调用——把这些全塞进一个扁平结构就捉襟见肘了。
AgenticMessage:内容块的容器
新的 AgenticMessage(schema/agentic_message.go:71)换了一种建模思路:
type AgenticMessage struct { // Role is the message role. Role AgenticRoleType `json:"role"`
// ContentBlocks is the list of content blocks. ContentBlocks []*ContentBlock `json:"content_blocks,omitempty"`
// ResponseMeta is the response metadata. ResponseMeta *AgenticResponseMeta `json:"response_meta,omitempty"`
// Extra is the additional information. Extra map[string]any `json:"extra,omitempty"`}type AgenticMessage struct { Role AgenticRoleType // system / user / assistant ContentBlocks []*ContentBlock // 核心:一条消息 = 一串内容块 ResponseMeta *AgenticResponseMeta // token 用量、各家扩展 Extra map[string]any}对比经典 Message 你会发现两个刻意的删减:没有 Content string,也没有顶层 ToolCalls。一切内容——文本、图像、推理、工具调用、工具结果——都统一表达成 ContentBlocks 里的一颗颗块。角色也收敛成三个:system / user / assistant(schema/agentic_message.go:66),连”tool”角色都没有——工具结果作为内容块挂在 user 消息里。
const ( AgenticRoleTypeSystem AgenticRoleType = "system" AgenticRoleTypeUser AgenticRoleType = "user" AgenticRoleTypeAssistant AgenticRoleType = "assistant")flowchart TB MSG["AgenticMessage<br/>Role: assistant"] MSG --> CBS["ContentBlocks []*ContentBlock"] CBS --> B1["ContentBlock<br/>Type = reasoning"] CBS --> B2["ContentBlock<br/>Type = assistant_gen_text"] CBS --> B3["ContentBlock<br/>Type = assistant_gen_image"] CBS --> B4["ContentBlock<br/>Type = function_tool_call"] B1 -.填充.-> P1["Reasoning *Reasoning"] B2 -.填充.-> P2["AssistantGenText *…"] B3 -.填充.-> P3["AssistantGenImage *…"] B4 -.填充.-> P4["FunctionToolCall *…"]
AgenticMessage:一条消息 = 一串内容块
ContentBlock:一个”带标签的联合”
ContentBlock(schema/agentic_message.go:102)是这套设计的心脏。它不是接口,而是一个带判别字段的结构体(tagged union):
type ContentBlock struct { Type ContentBlockType `json:"type"`
// Reasoning contains the reasoning content generated by the model. Reasoning *Reasoning `json:"reasoning,omitempty"`
// UserInputText contains the text content provided by the user. UserInputText *UserInputText `json:"user_input_text,omitempty"`
// UserInputImage contains the image content provided by the user. UserInputImage *UserInputImage `json:"user_input_image,omitempty"`
// UserInputAudio contains the audio content provided by the user. UserInputAudio *UserInputAudio `json:"user_input_audio,omitempty"`
// UserInputVideo contains the video content provided by the user. UserInputVideo *UserInputVideo `json:"user_input_video,omitempty"`
// UserInputFile contains the file content provided by the user. UserInputFile *UserInputFile `json:"user_input_file,omitempty"`
// AssistantGenText contains the text content generated by the model. AssistantGenText *AssistantGenText `json:"assistant_gen_text,omitempty"`
// AssistantGenImage contains the image content generated by the model. AssistantGenImage *AssistantGenImage `json:"assistant_gen_image,omitempty"`
// AssistantGenAudio contains the audio content generated by the model. AssistantGenAudio *AssistantGenAudio `json:"assistant_gen_audio,omitempty"`
// AssistantGenVideo contains the video content generated by the model. AssistantGenVideo *AssistantGenVideo `json:"assistant_gen_video,omitempty"`
// FunctionToolCall contains the invocation details for a user-defined tool. FunctionToolCall *FunctionToolCall `json:"function_tool_call,omitempty"`
// FunctionToolResult contains the result returned from a user-defined tool call. FunctionToolResult *FunctionToolResult `json:"function_tool_result,omitempty"`
// ToolSearchFunctionToolResult contains the result of a client-side custom tool search tool call. // It carries the full definitions of newly discovered tools so that the model can // recognize which tools have been added and are now available for invocation. ToolSearchFunctionToolResult *ToolSearchFunctionToolResult `json:"tool_search_function_tool_result,omitempty"`
// ServerToolCall contains the invocation details for a provider built-in tool executed on the model server. ServerToolCall *ServerToolCall `json:"server_tool_call,omitempty"`
// ServerToolResult contains the result returned from a provider built-in tool executed on the model server.// … 省略 24 行;完整声明 L102–173,点击上方「浏览完整文件」type ContentBlock struct { Type ContentBlockType // 判别器:这是哪种块
Reasoning *Reasoning // 推理/思考 UserInputText *UserInputText // 用户文本 UserInputImage *UserInputImage // 用户图片 AssistantGenText *AssistantGenText // 助手生成文本 AssistantGenImage *AssistantGenImage // 助手生成图片 FunctionToolCall *FunctionToolCall // 工具调用 FunctionToolResult *FunctionToolResult // 工具结果 // …还有 audio/video/file、server tool、MCP 等一长串}规则很简单:Type 说明这是哪种块,对应的那个指针字段被填,其余为 nil。块的种类由 ContentBlockType 常量枚举(schema/agentic_message.go:40):reasoning、user_input_text/image/audio/video/file、assistant_gen_text/image/audio/video、function_tool_call/result,以及 server tool、MCP 一整套。
const ( ContentBlockTypeReasoning ContentBlockType = "reasoning" ContentBlockTypeUserInputText ContentBlockType = "user_input_text" ContentBlockTypeUserInputImage ContentBlockType = "user_input_image" ContentBlockTypeUserInputAudio ContentBlockType = "user_input_audio" ContentBlockTypeUserInputVideo ContentBlockType = "user_input_video" ContentBlockTypeUserInputFile ContentBlockType = "user_input_file" ContentBlockTypeToolSearchResult ContentBlockType = "tool_search_result" ContentBlockTypeAssistantGenText ContentBlockType = "assistant_gen_text" ContentBlockTypeAssistantGenImage ContentBlockType = "assistant_gen_image" ContentBlockTypeAssistantGenAudio ContentBlockType = "assistant_gen_audio" ContentBlockTypeAssistantGenVideo ContentBlockType = "assistant_gen_video" ContentBlockTypeFunctionToolCall ContentBlockType = "function_tool_call" ContentBlockTypeFunctionToolResult ContentBlockType = "function_tool_result" ContentBlockTypeServerToolCall ContentBlockType = "server_tool_call" ContentBlockTypeServerToolResult ContentBlockType = "server_tool_result" ContentBlockTypeMCPToolCall ContentBlockType = "mcp_tool_call" ContentBlockTypeMCPToolResult ContentBlockType = "mcp_tool_result" ContentBlockTypeMCPListToolsResult ContentBlockType = "mcp_list_tools_result" ContentBlockTypeMCPToolApprovalRequest ContentBlockType = "mcp_tool_approval_request" ContentBlockTypeMCPToolApprovalResponse ContentBlockType = "mcp_tool_approval_response")📝 为什么用结构体联合而不是 Go 接口
Go 没有原生的 sum type。用接口 + 类型断言也能表达”多态块”,但序列化(gob/JSON)、跨语言对齐、字段可发现性都会变差。用”判别字段 + 一组指针”的结构体联合,换来的是:字段一目了然、序列化直接、消费方
switch block.Type就能穷尽处理。这是在 Go 里表达 sum type 的务实惯用法。
泛型化:一套骨架,两种消息
有了两种消息类型,真正的问题来了:难道要把 Runner、Agent、中间件、模型接口全部写两遍吗?v0.9 的答案是泛型——用一个类型参数 M 把两者统一。
枢纽是 model.BaseModel[M](components/model/interface.go:36):
type BaseModel[M messageType] interface { Generate(ctx context.Context, input []M, opts ...Option) (M, error) Stream(ctx context.Context, input []M, opts ...Option) (*schema.StreamReader[M], error)}一个泛型接口,M 一填,两种模型就都有了。而且——关键——老代码一行不用改,因为熟悉的名字都成了别名:
type BaseChatModel = BaseModel[*schema.Message] // 经典 chat 模型type AgenticModel = BaseModel[*schema.AgenticMessage] // 新的 agentic 模型🔑 本章的设计钥匙
泛型化的本质是用一个类型参数消掉一整套重复。Runner、事件流、中间件、模型——只写一份带
[M]的实现,M=*schema.Message就是经典 chat 世界,M=*schema.AgenticMessage就是多模态 agentic 世界。两个世界共享同一套运行时骨架(第 8 章的事件流)、同一套中断续跑、同一套多智能体组合,却各自持有最贴合的消息模型。而BaseChatModel等别名保证了这场大重构对存量用户完全透明——这正是”用类型系统承载演进”的典范:能力翻倍,概念不增,旧代码不碎。
封闭联合:自由与克制的平衡
你可能会问:既然泛型了,为什么不允许用户自定义第三种消息类型?看 MessageType 的定义(adk/interface.go:43):
type MessageType interface { *schema.Message | *schema.AgenticMessage}这是一个封闭联合——外部包无法往里加类型。源码注释直言:正因为成员固定,框架内部的泛型函数才能用穷尽的类型 switch安全地处理所有情况。如果放开,任何一处 switch m.(type) 都会漏掉未知类型,整个类型安全的承诺就崩了。
这是一处深思熟虑的克制:泛型给了”一套代码两种消息”的复用,封闭联合给了”编译期穷尽”的安全。两者合起来,才是既灵活又不失控的演进。
两种消息如何互通
既然工具节点产出的是经典 *schema.Message,agentic 世界怎么消费?Eino 没有在 schema 包里塞一个双向转换器,而是在需要的接缝处按需转换——工具结果的 Message → AgenticMessage 桥接落在 compose/agentic_tools_node.go(如 toolMessageToAgenticMessage)。加上流式场景的合并原语 ConcatAgenticMessages(schema/agentic_message.go:901)把分片块按 Index 归并——两种消息在各自的地盘里自洽,只在边界做最小的、单向的适配。
// ConcatAgenticMessages concatenates a list of AgenticMessage chunks into a single AgenticMessage.func ConcatAgenticMessages(msgs []*AgenticMessage) (*AgenticMessage, error) { var ( role AgenticRoleType blocks []*ContentBlock metas []*AgenticResponseMeta extra map[string]any blockIndices []int indexToBlocks = map[int][]*ContentBlock{} extraList = make([]map[string]any, 0, len(msgs)) )
if len(msgs) == 1 { return msgs[0], nil }
for idx, msg := range msgs { if msg == nil { return nil, fmt.Errorf("message at index %d is nil", idx) }
if msg.Role != "" { if role == "" { role = msg.Role } else if role != msg.Role { return nil, fmt.Errorf("cannot concat messages with different roles: got '%s' and '%s'", role, msg.Role) } }
for _, block := range msg.ContentBlocks { if block == nil { continue } if block.StreamingMeta == nil { // Non-streaming block if len(blockIndices) > 0 { // Cannot mix streaming and non-streaming blocks return nil, fmt.Errorf("found non-streaming block after streaming blocks") } // Collect non-streaming block blocks = append(blocks, block) } else { // Streaming block if len(blocks) > 0 { // Cannot mix non-streaming and streaming blocks return nil, fmt.Errorf("found streaming block after non-streaming blocks") } // Collect streaming block by index// … 省略 57 行;完整声明 L900–1004,点击上方「浏览完整文件」本章小结(兼 Part II 复盘)
- 经典
Message(扁平文本+ToolCalls)撑不起多模态富内容;AgenticMessage(schema/agentic_message.go:71)用 ContentBlocks 统一承载一切内容。 ContentBlock(schema/agentic_message.go:102)是判别字段 + 一组指针的结构体联合,是 Go 里表达 sum type 的务实惯用法。- 泛型
BaseModel[M](components/model/interface.go:36)+ 别名(BaseChatModel/AgenticModel)= 一套骨架两种消息,且对旧代码透明。 MessageType(adk/interface.go:43)是封闭联合,换来编译期穷尽的类型安全。- 两种消息在边界按需单向转换,不引入全局双向转换器。
至此 Part II 收官。回望这一部分,Eino 的设计哲学其实只有一条主线:把”控制流""消息""能力”全都建模成可组合、可序列化的数据,再用类型系统兜底——于是流式、中断、多智能体、多模态,都成了同一套地基上长出的自然结果。
下一部分,我们打开引擎盖,看这套优雅的设计在 Runner、flowAgent、图引擎里究竟是怎么实现的。