)
1. 从 Host MultiAgent 的 Graph 说起状态到底怎么流转MultiAgent Host 这套东西第一次看源码的人十有八九会卡在同一个地方图跑起来了specialist 也注册了但 host 到底怎么把消息喂进去、specialist 的结果又怎么回流、多意图的时候谁来汇总这三个问题不搞清楚后面选 prebuilt 模式基本靠猜。我先把结论摆前面Host MultiAgent 的核心不是多个 Agent 互相聊天而是一个 host LLM 通过 tool call 把任务分发给 specialist再用一个共享的 state 结构体把所有人的输出串起来。理解了 state就理解了整个图。1.1 state 结构体三个字段撑起整个图源码里compose.go:30-47定义了图的内部状态就三个字段type state struct { Messages []*schema.Message IsMultipleIntents bool SpecialistResults map[string]string }Messages是当前消息列表host LLM 和 specialist 都往里写。IsMultipleIntents是多意图标记当 host 一次产出多个 tool call 时置为 true。SpecialistResults存各 specialist 的原始结果专门给 summarizer 汇总用。这三个字段的分工很清晰Messages 负责对话上下文IsMultipleIntents 负责分流判断SpecialistResults 负责结果归集。状态通过ProcessState在节点间流转每个节点host / specialist / summarizer都能读写。这里有个容易踩的坑很多人以为 specialist 的结果会直接 append 到 Messages 里其实不是。specialist 的原始结果先进SpecialistResults要不要进 Messages、以什么形式进由 summarizer 决定。这个设计是为了避免多意图场景下 Messages 被塞爆。1.2 输入注入map2list 把图状态转成消息列表compose.go:49-83的入口适配器干的事是把 Eino 的图状态map[string]any转成 LLM 能理解的消息列表。所有 specialist 的输出也通过同样的方式回流到消息列表。为什么需要这一层因为 Eino 的图状态是通用的 map而 LLM 要的是结构化的[]*schema.Message。map2list 就是这两者之间的翻译层。你如果自己写节点往 state 里塞数据的时候要注意类型塞错了 map2list 转不出来图会静默失败——这个坑我踩过报错信息很不明显。1.3 流式判断只看第一个 chunk 的 tool calltypes.go:182-204定义了一个很关键的辅助函数type firstChunkStreamToolCallChecker struct { done bool hasCall bool } func (c *firstChunkStreamToolCallChecker) Check(chunk *schema.Message) bool { if c.done { return c.hasCall } c.done true c.hasCall len(chunk.ToolCalls) 0 return c.hasCall }它只检查第一个 chunk 有没有 tool call。为什么只看第一个因为流式场景下host LLM 要么在第一个 chunk 就决定调 tool要么就是不调。不需要等全部 chunk 到了再判断——这是性能优化也避免了等全部流式结果再决定的延迟。multiSpecialistsBranch:222-244用这个 checker 来分流有 tool call 就走 specialist 分支没有就直接回答。这个判断逻辑简单到有点反直觉但实测下来确实够用因为模型在流式输出时tool call 的决策通常出现在最前面。1.4 多意图汇总Summarizer 的两层逻辑multiIntentSummarizeNode:286-335处理多意图汇总逻辑分两层有 Summarizer 配置时:303-318用 LLM 汇总各 specialist 的结果。把 specialist 结果拼成消息列表喂给 summarizer ChatModel产出最终回答。无 Summarizer 配置时:319-333纯拼接——[weather]: 结果1\n[flight]: 结果2——不做 LLM 汇总。这个设计很实用不是所有场景都需要 LLM 汇总。简单场景下纯拼接就够了省一次 LLM 调用。你在配置的时候如果发现响应慢先看看是不是给一个纯拼接就够用的场景配了 Summarizer。1.5 回调体系HandOff 的三层设计callback.go定义了三层回调。MultiAgentCallback是用户注册的回调接口OnHandOff(HandOffInfo)在每次 host 把任务交给 specialist 时触发。ConvertCallbackHandlers把MultiAgentCallback转成 Eino 通用回调OnStart/OnEnd注入到 Graph 的节点回调中。HandOffInfo记录ToAgentNameArgument也就是谁交给了谁、带着什么理由。三层设计的好处是用户只需关心 HandOff 事件不需要理解 Eino 内部的回调机制。你要做可观测性盯着 OnHandOff 就够了。2. TaoToken 前置统一 Key 与 API 通道在拆 prebuilt 模式之前得先把调用链路打通。MultiAgent 场景下最烦的一件事是host LLM、specialist、summarizer 可能用的是不同模型每个模型一套 Key、一套 Base URL配置散落在各处出问题的时候根本不知道是哪一段挂了。TaoToken 在这里的作用是把 Key 和 API 通道统一。你只需要一个 Key、一个 Base URL就能在 host、specialist、summarizer 之间切换不同模型调用链路自检的时候也只需要看一个出口。2.1 为什么 MultiAgent 场景特别需要统一通道单 Agent 场景下一个模型一个 Key 还能忍。但 MultiAgent 一旦跑起来host 调一次、specialist 调 N 次、summarizer 再调一次如果每层都是不同的供应商排障成本是指数级上升的。我试过在一个三 specialist 的图里混用两套 Key结果一次 401 排查了快半小时——最后发现是某个 specialist 的 Key 环境变量名写错了。统一通道之后这类问题基本消失因为只有一个地方可能配错。2.2 获取 Key 与配置入口访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进控制台创建 API Key。API 端点是 https://taotoken.net/api这个不加 UTM。Key 拿到后建议用环境变量管理不要硬编码进代码export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api2.3 模型 ID 的选择MultiAgent 场景下host 和 specialist 对模型的要求不一样。host 需要强 tool call 能力它要决定调哪个 specialistspecialist 可能只需要某个垂直能力summarizer 需要强总结能力。你可以通过模型对话页面先验证各模型在 tool call 上的表现再决定怎么分配。模型对话入口https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite如果你打算长期跑编码类 AgentCoding Plan 会更划算https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite3. 可复制配置三种 prebuilt 模式怎么切这一节是重点。ADK 的三种 prebuilt 模式——supervisor、plan-execute、deep——在源码层面的差异最终都体现在配置上。我把三种模式的配置片段都给你你直接改参数就能切。3.1 Supervisor 模式配置supervisor/supervisor.go只有 121 行核心就两个动作。第一个是限制子 agent 只能回 supervisor// supervisor.go:101-108 for _, subAgent : range conf.SubAgents { subAgents append(subAgents, adk.AgentWithDeterministicTransferTo(ctx, adk.DeterministicTransferConfig{ Agent: subAgent, ToAgentNames: []string{supervisorName}, })) }AgentWithDeterministicTransferTo在每个子 agent 外面包一层限制它只能 transfer 到ToAgentNames列表中的 agent。这里只有 supervisor 的名字。效果是子 agent 之间不能直接通信所有通信必须经过 supervisor。第二个动作是统一追踪。supervisorContainer把整个 supervisor 结构supervisor 所有子 agent包装成一个 agent。当 callback 注册时OnStart/OnEnd 只触发一次创建单一 trace root所有 agent 共享同一个 trace 上下文。配置片段JSON 形式路径按你的项目结构调整{ mode: supervisor, supervisor: { name: main_supervisor, model: claude-sonnet-4-20250514, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY }, sub_agents: [ { name: weather_agent, model: claude-sonnet-4-20250514, transfer_to: [main_supervisor] }, { name: flight_agent, model: claude-sonnet-4-20250514, transfer_to: [main_supervisor] } ] }注意transfer_to只写了 supervisor。这就是 supervisor 模式的核心约束。3.2 Plan-Execute 模式配置plan_execute.go有 881 行是三个 prebuilt 中代码量最大的。核心是New函数:862-880func New(ctx context.Context, cfg *Config) (adk.ResumableAgent, error) { loop, _ : adk.NewLoopAgent(ctx, adk.LoopAgentConfig{ Name: execute_replan, SubAgents: []adk.Agent{cfg.Executor, cfg.Replanner}, MaxIterations: maxIterations, }) return adk.NewSequentialAgent(ctx, adk.SequentialAgentConfig{ Name: plan_execute_replan, SubAgents: []adk.Agent{cfg.Planner, loop}, }) }结构是SequentialAgent(Planner, LoopAgent(Executor, Replanner))。Planner 生成计划LoopAgent 循环执行直到完成Executor 执行第一步Replanner 决定继续还是完成。状态传递靠四个 Session KeyKey写入者读取者内容UserInputSessionKeyPlanner:329Executor、Replanner用户原始输入PlanSessionKeyPlanner:403、Replanner:762Executor、Replanner当前计划ExecutedStepSessionKeyExecutor通过 OutputKeyReplanner最新执行结果ExecutedStepsSessionKeyReplanner:671Executor、Replanner所有已执行步骤关键细节ExecutedStepsSessionKey的累积发生在 Replanner:667-671不是在 Executor。Replanner 把当前步骤的结果追加到历史列表然后决定下一步。Replanner 有两个工具:110-142PlanTool生成/更新计划参数steps[]RespondTool生成最终响应参数response。Replanner 的 prompt:192-238告诉 LLM 二选一要么调 respond_tool 结束要么调 plan_tool 更新计划继续。循环终止靠BreakLoopAction:748if msg.ToolCalls[0].Function.Name r.respondTool.Name { action : adk.NewBreakLoopAction(r.Name(ctx)) generator.Send(adk.AgentEvent{Action: action}) return msg, nil }配置片段{ mode: plan_execute, planner: { model: claude-sonnet-4-20250514, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY }, executor: { model: claude-sonnet-4-20250514, output_key: executed_step }, replanner: { model: claude-sonnet-4-20250514, tools: [plan_tool, respond_tool] }, max_iterations: 10 }max_iterations一定要设不然 Replanner 一直不调 respond_tool 就会死循环。3.3 Deep 模式配置deep/deep.go只有 268 行但构造了一个完整的 agent 脚手架。核心是NewTyped:116-166func NewTyped[M adk.MessageType](ctx context.Context, cfg *TypedConfig[M]) (adk.TypedResumableAgent[M], error) { // 1. 构建内置中间件 handlers, _ : buildTypedBuiltinAgentMiddlewares(ctx, cfg) // 2. 构建 task tool子 agent 调度 tt, _ : typedTaskToolMiddleware(ctx, ...) handlers append(handlers, tt) // 3. 创建 ChatModelAgent return adk.NewTypedChatModelAgent(ctx, adk.TypedChatModelAgentConfig[M]{...}) }Deep agent 启动时自动装配三个中间件buildTypedBuiltinAgentMiddlewares:209-232write_todos:244-267是任务管理工具参数todos[]每个 todo 含 content/activeForm/status状态三态 pending → in_progress → completed。这是 Claude Code 的 TaskCreate 模式的复刻。filesystem 工具:219-229在配置了 Backend/Shell/StreamingShell 时自动注册文件读写、glob、grep、shell 执行等工具通过filesystem2.NewTyped中间件注入。task tooltask_tool.go:35-58是子 agent 调度工具把每个子 agent 包装为 AgentTool通过subagent_type参数路由。task_tool.go:61-124的typedNewTaskTool是 Deep agent 的核心func typedNewTaskTool[M](...) (tool.InvokableTool, error) { t : typedTaskTool[M]{ subAgents: map[string]tool.InvokableTool{}, } // 1. 如果未禁用通用子 agent创建一个 if !withoutGeneralSubAgent { generalAgent, _ : adk.NewTypedChatModelAgent(ctx, ...) t.subAgents[generalAgent.Name(ctx)] adk.NewTypedAgentTool(ctx, generalAgent) } // 2. 把用户提供的子 agent 也包装成 AgentTool for _, a : range subAgents { t.subAgents[a.Name(ctx)] adk.NewTypedAgentTool(ctx, a) } return t, nil }InvokableRun:156-175的路由逻辑很简单按subagent_type找对应的 AgentTool把description作为参数传给子 agent。配置片段{ mode: deep, main_agent: { model: claude-sonnet-4-20250514, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, builtin_tools: [write_todos, filesystem, task] }, sub_agents: [ { name: code_agent, model: claude-sonnet-4-20250514, subagent_type: code }, { name: search_agent, model: claude-sonnet-4-20250514, subagent_type: search } ], without_general_sub_agent: false }without_general_sub_agent设为 false 时会自动创建一个 general-purpose agent它共享主 agent 的 instruction、tools、middlewares、handlers但有自己的独立 session。3.4 三种模式一张表维度SupervisorPlan-ExecuteDeep核心机制transfer 交接计划→执行→重规划循环task tool 子 agent 调度子 agent 通信只能和 supervisor通过 session state通过 task tool 参数上下文共享完整共享问题所在通过 session value 选择性传递task tool 启动独立 session流程控制supervisor LLM 决定Replanner 二选一工具主 agent LLM 决定调哪个 task内置工具无无write_todos filesystem task推荐程度NOT RECOMMENDED推荐推荐代码量121 行881 行268 行685 行 prompt4. 本地运行验证从配置到成功结果配置写完了得跑起来验证。这一节给你完整的验证步骤包括怎么确认调用链路是通的。4.1 环境准备先确认 Go 版本和依赖go version # 需要 go 1.21设置环境变量export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api4.2 最小验证先跑通单次调用在跑 MultiAgent 之前先用一个最小脚本确认通道是通的package main import ( context fmt os github.com/cloudwego/eino-ext/components/model/openai ) func main() { ctx : context.Background() cm, err : openai.NewChatModel(ctx, openai.ChatModelConfig{ BaseURL: os.Getenv(TAOTOKEN_BASE_URL), APIKey: os.Getenv(TAOTOKEN_API_KEY), Model: claude-sonnet-4-20250514, }) if err ! nil { panic(err) } resp, err : cm.Generate(ctx, []*schema.Message{ schema.UserMessage(回复 OK 两个字母), }) if err ! nil { panic(err) } fmt.Println(resp.Content) }跑通会输出OK。这一步不通后面都不用试。4.3 验证 Supervisor 模式用 3.1 的配置启动观察日志里有没有OnHandOff事件。正常的话你会看到类似[HandOff] main_supervisor - weather_agent, argument: 查询北京天气 [HandOff] weather_agent - main_supervisor, argument: 返回天气结果如果只看到第一次 HandOff 没有回程说明transfer_to配错了子 agent 回不来。4.4 验证 Plan-Execute 模式用 3.2 的配置启动观察 Session Key 的流转。正常的话你会看到[Planner] 生成计划: [查天气, 查航班, 汇总] [Executor] 执行步骤 1: 查天气 - 结果: 晴 [Replanner] 追加步骤 1 到历史决定继续 [Executor] 执行步骤 2: 查航班 - 结果: CA1234 [Replanner] 追加步骤 2 到历史决定继续 [Executor] 执行步骤 3: 汇总 - 结果: ... [Replanner] 调用 respond_tool触发 BreakLoopAction如果 Replanner 一直不调 respond_tool检查max_iterations是不是设太大了或者 prompt 里二选一的指令不够明确。4.5 验证 Deep 模式用 3.3 的配置启动观察 task tool 的调用。正常的话你会看到[Main] 调用 write_todos: 创建 3 个任务 [Main] 调用 task tool: subagent_typecode, description实现登录接口 [code_agent] 独立 session 启动返回结果 [Main] 更新 todo 状态为 completedDeep 模式的关键验证点是子 agent 是不是独立 session。你可以在子 agent 里打印 session ID和主 agent 对比不一样就对了。4.6 调用链路自检三种模式都跑通后做一次完整的链路自检。在 TaoToken 控制台看请求日志确认host 的请求、specialist 的请求、summarizer 的请求都从同一个出口出去Key 都是同一个。如果有请求没出现在日志里说明那段代码没走统一通道得回去改配置。控制台入口https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite5. 本篇常见错排查这一节列几个真实会遇到的报错以及怎么定位。5.1 401 Unauthorized最常见。先确认环境变量有没有生效echo $TAOTOKEN_API_KEY如果输出是空的说明 export 没生效或者你在新的 shell 里跑。Key 本身的问题去控制台重新生成一个。还有一种情况Key 配对了但 Base URL 写成了https://taotoken.net/api/多了个斜杠有些客户端会拼成//v1/chat/completions也会 401。去掉末尾斜杠。5.2 local proxy failed这个报错通常出现在你本地配了代理但代理没起来。检查env | grep -i proxy如果有HTTP_PROXY之类的变量先 unset 掉再跑。MultiAgent 场景下host 和 specialist 可能在不同的 goroutine 里代理配置不一致会导致部分请求走代理部分不走表现就是时好时坏。5.3 reading choices: unexpected end of JSON input这个报错说明响应体不是合法的 JSON。常见原因有两个一是 Base URL 配错了请求打到了非 API 端点返回了 HTML二是模型 ID 写错了服务端返回了错误页。先确认 Base URL 是https://taotoken.net/api再确认模型 ID 在模型对话页面能正常调用。5.4 OAuth 相关报错如果你用的是 Claude Code 类的客户端可能会遇到 OAuth 报错。这类客户端有些走 OAuth 流程有些走 API Key。确认你的配置里用的是 API Key 模式不是 OAuth 模式。Claude Code 接入文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite5.5 子 agent 不返回结果Supervisor 模式下如果子 agent 不返回先检查transfer_to列表。Plan-Execute 模式下检查ExecutedStepsSessionKey有没有正确累积。Deep 模式下检查subagent_type和注册的子 agent 名字对不对得上。5.6 配置三件套对照不管你用 CC Switch、Cline MCP 还是 Codex 的 auth.json配置三件套都是一样的配置项值Base URLhttps://taotoken.net/apiAPI Key你的 Key环境变量注入Model ID按场景选host 用强 tool call 模型Codex 的 auth.json 示例{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: claude-sonnet-4-20250514 }Cline MCP 配置示例{ mcpServers: { taotoken: { url: https://taotoken.net/api, apiKey: sk-你的key, model: claude-sonnet-4-20250514 } } }6. 选型思路与下一步三种模式拆完选型其实不复杂。Supervisor 的 NOT RECOMMENDED 标注不是功能不好用而是经验证明这个方向不如另一个方向。源码注释写得很直白transfer 共享完整上下文上下文膨胀快缺乏隔离子 agent 能看到不该它关心的信息。AgentTool 给每个子 agent 独立 session 和 checkpoint隔离性更好。Plan-Execute 的本质是工具驱动流程控制。Replanner 不是通过代码分支决定继续还是完成而是给 LLM 两个工具plan_tool / respond_tool让 LLM 自己选。这是一种把控制流交给模型的设计。适合任务步骤明确、需要多轮迭代的场景。Deep agent 是 Claude Code 的复刻。从 write_todos 的任务管理到 task tool 的子进程调度再到 filesystem 工具链Deep agent 把 Claude Code 在编码场景中验证过的模式搬到了 Eino ADK。prompt 就是代码——685 行 prompt 不是文档而是 agent 行为的核心逻辑。主 agent 什么时候用 task tool、什么时候并行、什么时候写 todos这些行为不是硬编码的而是软编码在 prompt 里。如果你要长期跑编码类 AgentCoding Plan 会比按量付费划算https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite接入文档在这里https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keys 管理https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite下一篇讲 Agent 间 Transfer 交接——用户在多个 Agent 间无缝切换以及 Eino ADK 的 transfer 机制源码。