ADK 2.x-Go (05). LLM Flow 执行循环深度解析
系列第五篇,也是整个系列最核心的一篇 ⭐。
internal/llminternal/base_flow.go是 ADK 的「心脏」——所有 LLM Agent 的执行都汇聚到这里。本篇拆解Flow.Run的 step-loop 主循环、runOneStep的五阶段、12 个 Request Processor 各做什么、callLLM的回调三段式、handleFunctionCalls的并发执行与响应合并、以及transfer_to_agent的后处理。第 04 篇讲 Runner 把控制权交给 agent 后,agent 内部跑的就是这套 Flow。理解了它,ADK Go 就理解了 80%。
一、Flow 结构体:执行引擎的全貌
第 02 篇讲过,llmAgent.run 会实例化一个 llminternal.Flow 并把所有配置(回调、工具、处理器链)喂给它。Flow 就是真正驱动 step-loop 的执行引擎。先看它的结构体:
type Flow struct {
Model model.LLM // LLM 抽象(OpenAI/Gemini/Apigee)
Tools []tool.Tool // 工具列表(来自 agent 的 State.Tools)
RequestProcessors []func(...) iter.Seq2[...] // ⭐ 12 个请求处理器(有序链)
ResponseProcessors []func(...) error // 2 个响应处理器
// 6 组回调(来自 llmagent.Config,第 02 篇讲过)
BeforeModelCallbacks []BeforeModelCallback
AfterModelCallbacks []AfterModelCallback
OnModelErrorCallbacks []OnModelErrorCallback
BeforeToolCallbacks []BeforeToolCallback
AfterToolCallbacks []AfterToolCallback
OnToolErrorCallbacks []OnToolErrorCallback
}
关键认知:Flow 是一个纯执行器——它自己不持有 agent 配置,所有配置在构造时由 llmAgent.run 传入。这使得 Flow 可以专注于一件事:驱动 step-loop。两条处理器链(Request/Response Processors)是硬编码的默认值:
// DefaultRequestProcessors:有序的请求处理器链,依次转换 LLMRequest
DefaultRequestProcessors = []func(ctx agent.InvocationContext, req *model.LLMRequest, f *Flow) iter.Seq2[*session.Event, error]{
basicRequestProcessor, // 1. 基础配置
toolProcessor, // 2. 注入工具(含 HITL/认证工具)
authPreprocessor, // 3. 认证
RequestConfirmationRequestProcessor, // 4. HITL 确认
instructionsRequestProcessor, // 5. 指令模板插值
identityRequestProcessor, // 6. agent 身份/全局指令
ContentsRequestProcessor, // 7. ⭐ 按 Branch/IsolationScope 过滤历史
nlPlanningRequestProcessor, // 8. NL Planning(必须在 contents 之后)
codeExecutionRequestProcessor, // 9. 代码执行(必须 contents 之后)
outputSchemaRequestProcessor, // 10. 结构化输出 schema
AgentTransferRequestProcessor, // 11. 注入 transfer_to_agent 工具
removeDisplayNameIfExists, // 12. 清理 displayName
}
DefaultResponseProcessors = []func(...) error{
nlPlanningResponseProcessor, // 1. NL Planning 后处理
codeExecutionResponseProcessor, // 2. 代码执行后处理
}
⚠️ 顺序至关重要
源码注释多次强调顺序依赖:
nlPlanningRequestProcessor必须在ContentsRequestProcessor之后(因为 NL Planning 会把 planning content 标记为 thought,需要 contents 先就位);codeExecutionRequestProcessor也必须在 contents 之后(它会修改 contents 来优化数据文件)。不要随便调整这个顺序,会破坏历史隔离和 planning 机制。
二、Flow.Run:step-loop 主循环 ⭐
这是 ADK 最核心的循环。源码出奇地简洁——它的职责就是「反复跑 step,直到产生最终回复」:
func (f *Flow) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
for { // ⭐ 外层 step 循环
var lastEvent *session.Event
for ev, err := range f.runOneStep(ctx) {
if err != nil { yield(nil, err); return }
if !yield(ev, nil) { return } // 先把事件透传出去
lastEvent = ev
}
// 终止条件:最后事件是"最终回复"且不是纯 thinking turn
if lastEvent == nil ||
(lastEvent.IsFinalResponse() && !isThoughtOnlyTurn(lastEvent)) {
return
}
if lastEvent.LLMResponse.Partial {
yield(nil, fmt.Errorf("TODO: last event is not final")) // 流式未完,待处理
return
}
}
}
}
step-loop 的终止逻辑(最关键的判断)
runOneStep 产出一批事件,记下 lastEvent
│
▼
┌─────────────────────────────────────────────────────────┐
│ lastEvent == nil? │
│ 是 → return(空 step,结束) │
│ │
│ lastEvent.IsFinalResponse() && !isThoughtOnlyTurn? │
│ 是 → return(有最终回复,结束)⭐ 正常出口 │
│ │
│ lastEvent.LLMResponse.Partial? │
│ 是 → 报错(流式中途断,TODO 待处理) │
│ │
│ 否则 → 继续 for 循环,跑下一个 step │
└─────────────────────────────────────────────────────────┘
🔑 三个关键概念
① step = 1 次 LLM 调用 + 0~N 次工具调用。一个 step 里,LLM 被调用一次,如果它返回 function_call,则并发执行这些工具,把 function_response 拼回去,然后这个 step 结束。
② IsFinalResponse 决定循环是否结束。如果 LLM 没有调用工具、直接给了文本回复,这个事件就是「最终回复」,step-loop 结束。如果 LLM 调了工具,事件不是 final,循环继续——下一个 step 会带上工具结果再调一次 LLM。
③ isThoughtOnlyTurn 的特殊性。有些模型支持 thinking(Gemini 的 thought),一个 turn 可能全是思考没有答案——它报告 IsFinalResponse=true,但其实没有真正回答。
!isThoughtOnlyTurn确保不在纯思考 turn 上停下,而是再调一次模型要真正的答案。
三、runOneStep:一个 step 的五阶段 ⭐
runOneStep 是 step-loop 的内部细节。一个 step 分五个阶段:
runOneStep 的五阶段流水线
──────────────────────────────────────────────────────────────────────────
① preprocess(ctx, req)
跑 12 个 Request Processor + 工具预处理,可能产 HITL 事件
req 被逐步填充:工具声明、指令、过滤后的历史、输出 schema...
│
② callLLM(ctx, req, stateDelta, artifactDelta)
Plugin.BeforeModel → agent.BeforeModelCallbacks → generateContent →
OnModelError → AfterModelCallbacks(任何非 nil 回调可短路/替换)
流式产出 responseWithEventID
│
③ postprocess(ctx, req, resp)
跑 2 个 Response Processor(NL Planning / 代码执行)
│
④ finalizeModelResponseEvent(ctx, resp, tools, stateDelta)
把 resp 包成 Event,填 Author/Branch/StateDelta/LongRunningToolIDs
yield 模型回复事件
│
⑤ handleFunctionCalls(ctx, tools, resp)
如含 FunctionCall:并发执行工具,合并 FunctionResponse 成一个事件
若 ev.Actions.TransferToAgent 非空 → 处理转移(见第八节)
精简后的源码:
func (f *Flow) runOneStep(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield ...) {
req := &model.LLMRequest{Model: f.Model.Name()}
// ① 预处理:12 个 Request Processor + 工具/toolset 预处理
for ev, err := range f.preprocess(ctx, req) { ... }
if ctx.Ended() { return }
stateDelta := make(map[string]any)
// ②~⑤ 对每个 LLM response:
for resp, err := range f.callLLM(ctx, req, stateDelta, artifactDelta) {
if err != nil { yield(nil, err); return }
// ③ 后处理(NL Planning / 代码执行)
if err := f.postprocess(ctx, req, resp); err != nil { ... }
// 跳过空内容、无错误码、未中断的响应(代码执行器的特殊触发)
if resp.Content == nil && resp.ErrorCode == "" && !resp.Interrupted { continue }
// ④ 构造模型回复事件并 yield
modelResponseEvent := f.finalizeModelResponseEvent(ctx, resp, tools, stateDelta)
if !yield(modelResponseEvent, nil) { return }
if resp.Partial { continue } // 流式片段,继续等完整响应
// ⑤ 处理工具调用
ev, err := f.handleFunctionCalls(ctx, tools, resp.LLMResponse, nil, nil)
if ev == nil { continue }
// yield 工具响应事件 + 可能的确认请求事件
yield(ev, nil)
if toolConfirmationEvent != nil { yield(toolConfirmationEvent, nil) }
// 结构化输出:set_model_response 工具的结果作为最终回复
if outputSchemaResponse != "" { yield(createFinalModelResponseEvent(...)) }
// ⑤' transfer_to_agent 后处理(见第八节)
if ev.Actions.TransferToAgent == "" { return }
// ... 处理 transfer ...
}
}
}
四、12 个 Request Processor:请求预处理流水线
这是 preprocess 阶段的核心。12 个处理器依次转换 LLMRequest,每个都能修改 req 或产出事件(如 HITL 事件)。精简后的 preprocess:
func (f *Flow) preprocess(ctx agent.InvocationContext, req *model.LLMRequest) iter.Seq2[*session.Event, error] {
return func(yield ...) {
// 按配置顺序跑每个 Request Processor
for _, processor := range f.RequestProcessors {
for ev, err := range processor(ctx, req, f) {
if err != nil { yield(nil, err); return }
if ev != nil { yield(ev, nil) }
}
}
// 然后跑工具/toolset 预处理(把工具声明注入 req.Tools)
toolPreprocess(ctx, req, f.Tools)
toolsetPreprocess(ctx, req)
}
}
4.1 12 个处理器逐一解读
| # | 处理器 | 做什么 |
|---|---|---|
| 1 | basicRequestProcessor |
基础配置:把 agent 的 GenerateContentConfig(温度等)合并进 req。处理 OutputSchema 与 Mode 的兼容性 |
| 2 | toolProcessor |
注入工具:遍历 agent 的 Tools,把需要认证/HITL 的工具做标记。第 03 篇的工具装配最终在这里生效 |
| 3 | authPreprocessor |
认证:处理需要 OAuth/API key 的工具,可能产 HITL 事件让用户授权 |
| 4 | RequestConfirmationRequestProcessor |
HITL 确认:检查上一轮是否有 pending 的确认请求,决定本轮是否需要注入确认工具 |
| 5 | instructionsRequestProcessor |
指令插值:把 {var} 模板用 session state 填充(第 02 篇讲的 Instruction 模板机制) |
| 6 | identityRequestProcessor |
agent 身份:拼接 GlobalInstruction(仅 root 生效,第 02 篇讲过),注入 agent 名字等身份信息 |
| 7 | ContentsRequestProcessor ⭐ |
历史过滤:按 Branch/IsolationScope 过滤会话历史,是 multi-agent 历史隔离的根本机制。Task 模式还要处理结构化输入 |
| 8 | nlPlanningRequestProcessor |
NL Planning:自然语言规划,把 planning content 标记为 thought。必须在 contents 之后 |
| 9 | codeExecutionRequestProcessor |
代码执行:修改 contents 来优化数据文件(如内联大文件)。必须在 contents 之后 |
| 10 | outputSchemaRequestProcessor |
结构化输出:若设了 OutputSchema 且非 Task 模式,注入 set_model_response 工具(第 03 篇提过) |
| 11 | AgentTransferRequestProcessor |
注入 transfer_to_agent 工具:根据子 agent 列表生成可转移目标(第 08 篇详讲) |
| 12 | removeDisplayNameIfExists |
清理:移除 displayName 字段(兼容性处理) |
🔑 第 7 个处理器 ContentsRequestProcessor 是 multi-agent 的灵魂
这是 12 个处理器里最复杂、也最重要的一个。它决定「这个 agent 在这次 step 里能看到哪些历史事件」。过滤逻辑基于两个维度:
Branch:事件用 a1.a2.a3这样的分支路径标记。agent 只看到自己分支及祖先分支的事件——这就是 transfer 后历史隔离的原理(第 08 篇详讲)。IsolationScope:Task 模式的隔离作用域。同一 isolation scope 的事件才可见,保证 task 子 agent 的多轮收集不被其他 task 污染。 没有这个处理器,所有 agent 都会看到完整历史,multi-agent 系统就会乱套。
五、callLLM:回调三段式 ⭐
callLLM 是 step 内部最复杂的部分——它围绕真正的模型调用织入了 Before/OnError/After 三段回调。核心逻辑:
func (f *Flow) callLLM(ctx, req, stateDelta, artifactDelta) iter.Seq2[*responseWithEventID, error] {
return func(yield ...) {
pluginManager := pluginManagerFromContext(ctx)
// ① Plugin.BeforeModel(全局插件优先)
if pluginManager != nil {
callbackResponse, callbackErr := pluginManager.RunBeforeModelCallback(cctx, req)
if callbackResponse != nil || callbackErr != nil {
yield(..., callbackErr); return // ⭐ 短路:跳过真正的模型调用
}
}
// ② agent.BeforeModelCallbacks(agent 自己的回调)
for _, callback := range f.BeforeModelCallbacks {
callbackResponse, callbackErr := callback(cctx, req)
if callbackResponse != nil || callbackErr != nil {
yield(..., callbackErr); return // ⭐ 短路
}
}
// ③ 真正调用模型(流式或非流式)
useStream := runconfig.FromContext(ctx).StreamingMode == runconfig.StreamingModeSSE
for resp, err := range generateContent(ctx, f.Model, req, useStream) {
if err != nil {
// ③' OnModelError:模型出错时的兜底(重试/降级)
cbResp, cbErr := f.runOnModelErrorCallbacks(ctx, req, ..., err)
if cbResp == nil { yield(nil, err); return }
resp = ... // 用兜底响应替换
}
// 给 function_call 补 ID(有些模型不填)
utils.PopulateClientFunctionCallID(ctx, resp.Content)
// ④ AfterModelCallbacks:响应后处理(日志/metrics/改写)
callbackResp, callbackErr := f.runAfterModelCallbacks(ctx, resp.LLMResponse, ..., err)
if callbackResp != nil { yield(替换后的响应); continue }
yield(resp, nil)
}
}
}
callLLM 的回调三段式(短路语义)
Plugin.BeforeModel ─┐ 非nil → 短路,跳过模型调用,用回调返回值
│
agent.BeforeModelCallbacks ─┤ 非nil → 短路
│
▼ 全部返回 nil
generateContent(真正调模型)
│
┌──────────┴──────────┐
▼ 出错 ▼ 成功
OnModelErrorCallbacks AfterModelCallbacks
非nil → 替换错误 非nil → 替换响应
nil → 透传错误 nil → 透传响应
🔑 回调三段式的两个铁律
铁律一:plugin 先于 agent callback 执行。看源码顺序——先
pluginManager.RunBeforeModelCallback,后f.BeforeModelCallbacks。这意味着 plugin 有「全局拦截至高优先级」(第 11 篇详讲)。铁律二:任何 Before 回调返回非 nil 即短路。
callbackResponse != nil || callbackErr != nil一旦成立,立即 yield 并 return——真正的generateContent根本不会执行。这是实现「LLM 缓存」(BeforeModel 返回缓存响应)、「请求审计/改写」的标准钩子。
5.1 BeforeModel 短路的实际应用:LLM 缓存
func cacheCallback(ctx agent.Context, req *model.LLMRequest) (*model.LLMResponse, error) {
key := hash(req) // 根据请求内容算缓存 key
if cached := cache.Get(key); cached != nil {
return cached, nil // ⭐ 返回非 nil,跳过真正的模型调用(省钱!)
}
return nil, nil // 返回 nil,继续正常调模型
}
a, _ := llmagent.New(llmagent.Config{
Model: openaiModel,
BeforeModelCallbacks: []llmagent.BeforeModelCallback{cacheCallback},
})
六、finalizeModelResponseEvent:构造模型回复事件
拿到模型响应后,要把它包装成一个 session.Event。这个函数很简短但字段语义重要:
func (f *Flow) finalizeModelResponseEvent(ctx, resp, tools, stateDelta) *session.Event {
// 补全 function_call 的 ID(有些模型不返回 ID)
utils.PopulateClientFunctionCallID(ctx, resp.Content)
ev := session.NewEvent(ctx, ctx.InvocationID())
ev.ID = resp.eventID
ev.Author = ctx.Agent().Name() // 谁产的就标谁的名(findAgentToRun 用)
ev.Branch = ctx.Branch() // 分支标记(ContentsRequestProcessor 过滤用)
ev.LLMResponse = *resp.LLMResponse
ev.Actions.StateDelta = stateDelta // 回调期间累积的状态变更
// 识别 long-running 工具调用(第 03 篇讲过 IsLongRunning)
ev.LongRunningToolIDs = findLongRunningFunctionCallIDs(resp.Content, tools)
return ev
}
💡 每个字段都对应后续某个机制
这个事件结构的每个字段都不是摆设:
Author→ 第 04 篇findAgentToRun按它找该谁接管Branch→ 第 7 号ContentsRequestProcessor按它过滤历史StateDelta→ 回调里ctx.State().Set()的变更都累积在这里,最终由 Runner 持久化LongRunningToolIDs→ 第 04 篇 HITL 恢复的openLongRunningCallIDs按它找挂起的调用TransferToAgent(在 Actions 里,由工具调用阶段填)→ 第八节的转移逻辑一个事件结构串起了 Runner(04 篇)、Flow(本篇)、多 Agent(08 篇)、HITL(10 篇)的所有机制。
七、handleFunctionCalls:并发工具调用与响应合并 ⭐
当 LLM 返回的 Content 里含 FunctionCall 时,进入这个阶段。ADK 会并发执行所有 function call,再把结果合并成一个事件。核心逻辑:
func (f *Flow) handleFunctionCalls(ctx, toolsDict, resp, toolConfirmations, liveSess) (*session.Event, error) {
fnCalls := utils.FunctionCalls(resp.Content) // 取出所有 function_call
fnResponseEvents := make([]*session.Event, len(fnCalls))
// ① 为每个 function_call 构造一个 task
tasks := make([]func(context.Context), len(fnCalls))
for i, fnCall := range fnCalls {
tasks[i] = func(taskCtx context.Context) {
toolCtx := agent.NewToolContext(...) // 每个工具独立上下文
// 按工具类型分发(见 7.1)
if streamTool, ok := curTool.(StreamingFunctionTool); ok {
// 流式工具:边算边吐
} else if funcTool, ok := curTool.(FunctionTool); ok {
result = f.callTool(toolCtx, funcTool, fnCall.Args) // 普通工具
}
// 构造 function_response 事件
ev := session.NewEvent(ctx, ctx.InvocationID())
ev.LLMResponse.Content.Parts = [{FunctionResponse: {ID: fnCall.ID, Name: fnCall.Name, Response: result}}]
ev.Actions = *toolCtx.Actions() // 工具里的状态变更
fnResponseEvents[i] = ev
}
}
// ② ⭐ 并发执行所有 task(platform.RunTasks)
platform.RunTasks(ctx, tasks)
// ③ 合并所有 function_response 成一个事件
mergedEvent, err := mergeParallelFunctionResponseEvents(fnResponseEvents)
return mergedEvent, err
}
7.1 工具分发的三种情况
handleFunctionCalls 内部对每个 function_call,按工具实现的接口分三种情况:
| 工具类型 | 处理方式 |
|---|---|
StreamingFunctionTool |
Live 模式下异步执行,边算边通过 liveSess.Send 吐结果;非 Live 模式同步收集所有 chunk 拼成结果 |
FunctionTool(普通) |
调 f.callTool(内部走 BeforeTool 回调 → tool.Run → OnToolError → AfterTool 回调链,第 03 篇讲过) |
| 找不到工具 / 非可执行工具 | 构造 tool not found 错误,走 runOnToolErrorCallbacks 兜底 |
7.2 platform.RunTasks:并发执行的 seam
注意 platform.RunTasks(ctx, tasks)——这不是 go 关键字,而是一个可替换的 seam:
💡 为什么用 platform.RunTasks 而非 go?
platform包提供可注入的执行器。默认实现用 goroutine 并发跑所有 task。但测试时可以注入串行 task runner(platform.WithTaskRunner),让并发变成可预测的顺序执行——这是 ADK 能为并发场景写确定性测试的关键(第 06 篇详讲)。同理,
platform还提供WithTimeProvider(fake clock)和WithUUIDProvider(顺序 UUID),三者配合让无网络、无模型的离线测试成为可能。
7.3 mergeParallelFunctionResponseEvents:状态合并
并发执行的 N 个工具各自产出一个 event(含自己的 StateDelta、TransferToAgent 等),最后要合并成一个。合并规则:
FunctionResponse:简单拼接,所有工具的响应都放进一个 Content。 StateDelta: deepMergeMap递归合并。两个工具都写了{"user": {"name": "x"}}和{"user": {"age": 20}},合并成{"user": {"name":"x","age":20}}。TransferToAgent / Escalate:「最后一个赢」。这是「决策型」动作,不能合并——两个工具都要转移,只能听最后一个的。
⚠️ 并发写状态的注意事项
因为工具并发执行,每个工具在自己的
ToolContext里写Actions.StateDelta,互不干扰。合并时才汇总。但如果你在工具里用了共享的外部资源(如全局变量、外部数据库),仍然需要自己处理并发安全——ADK 只隔离了 state 写入,不隔离你的业务副作用。
八、transfer_to_agent 后处理:控制权移交
runOneStep 末尾有一段重要逻辑——如果工具调用阶段产生了 TransferToAgent 动作,Flow 会在这里处理控制权移交:
// Actually handle "transfer_to_agent" tool. The function call sets the
// ev.Actions.TransferToAgent field. We are following python's execution flow.
if ev.Actions.TransferToAgent == "" {
return // 没有转移,step 结束
}
// 找到目标 agent
nextAgent := f.agentToRun(ctx, ev.Actions.TransferToAgent)
if nextAgent == nil { yield(nil, fmt.Errorf("failed to find agent: %s", ...)); return }
// ⭐ 关键:根据目标 agent 是否实现 nodeRunner,走两条路径
type nodeRunner interface {
RunNode(ctx agent.Context, nodeInput any) iter.Seq2[*session.Event, error]
}
var nextStream iter.Seq2[*session.Event, error]
if nr, ok := nextAgent.(nodeRunner); ok {
nextStream = nr.RunNode(agent.Promote(ctx), nil) // Node 路径(LlmAgent)
} else {
nextStream = nextAgent.Run(ctx) // Agent 路径(其他)
}
// forward 目标 agent 的所有事件
for ev, err := range nextStream {
if !yield(ev, err) || err != nil { return }
}
transfer_to_agent 的执行流
当前 agent 的某次 step
LLM 返回 transfer_to_agent("weather_agent")
handleFunctionCalls 把它当工具调用,设置 ev.Actions.TransferToAgent
│
▼
f.agentToRun(ctx, "weather_agent") 找到目标 agent
│
▼ 判断目标 agent 类型
┌────────────────────┬─────────────────────┐
│ LlmAgent(nodeRunner)│ 其他 agent │
│ → nr.RunNode() │ → nextAgent.Run() │
│ 走 Node 路径 │ 走 Agent 路径 │
└────────────────────┴─────────────────────┘
│
▼
forward 目标 agent 的所有事件 给上层(Runner)
目标 agent 接管后续对话——下一次 findAgentToRun 会选中它
🔑 transfer 的两个关键设计
① RunNode vs Run 的选择:如果目标 agent 实现了
nodeRunner接口(LlmAgent 通过RunNode方法实现),走RunNode(Node 路径,获得 HITL/并行能力);否则走Run(Agent 路径)。这呼应第 04 篇 Runner 的两条路径——但这里是在 Flow 内部、跨 agent 的转移。② forward 所有事件:转移后,目标 agent 的事件流被原样 yield 给上层(Runner)。上层并不知道发生了转移,只看到一连串事件。下一次用户消息时,第 04 篇的
findAgentToRun会因为历史里有目标 agent 的事件,而让目标 agent 接管。这就是 multi-agent 协作的核心机制——第 08 篇会深入对比 transfer vs agent-as-tool。
九、小结与动手清单
核心要点回顾
🎯 本篇五个关键认知(系列最核心)
step-loop 是 ADK 的心脏: Flow.Run反复跑runOneStep,直到产生最终回复(IsFinalResponse 且非纯 thinking turn)。一个 step = 1 次 LLM 调用 + 0~N 次工具调用。runOneStep 五阶段:preprocess(12 个 Request Processor)→ callLLM(回调三段式)→ postprocess(2 个 Response Processor)→ finalizeModelResponseEvent(构造事件)→ handleFunctionCalls(并发工具 + 合并)。顺序固定,不可乱。 12 个 Request Processor 是有序流水线:尤其第 7 个 ContentsRequestProcessor按 Branch/IsolationScope 过滤历史,是 multi-agent 历史隔离的根本。顺序有强依赖(nlPlanning/codeExecution 必须在 contents 之后)。callLLM 回调三段式有两个铁律:plugin 先于 agent callback;任何 Before 回调返回非 nil 即短路(可实现 LLM 缓存)。OnError 是出错兜底,After 可改写响应。 handleFunctionCalls 并发执行 + 合并: platform.RunTasks(可替换 seam,测试可注入串行 runner)并发跑所有 function call,mergeParallelFunctionResponseEvents深度合并 StateDelta,决策型动作(TransferToAgent)「最后赢」。转移靠 forward 目标 agent 事件实现。
动手清单(建议 2-3 天,这是最烧脑的一篇)
追踪一次完整 step:在 quickstart 里,给 agent 挂一个 functiontool。问一个会触发工具调用的问题,用 BeforeModelCallbacks和BeforeToolCallbacks打印每一步,对照本篇的五阶段图,画出「preprocess → callLLM → finalize → handleFunctionCalls → 下一个 step(带工具结果)→ 最终回复」的完整链路。体验 IsFinalResponse 的差别:观察「LLM 调了工具」和「LLM 直接回答」两种情况下,step-loop 的行为差异——前者继续循环,后者结束。 实现一个 LLM 缓存 callback:用 BeforeModelCallback实现简单缓存(key 用请求内容的 hash),验证第二次相同请求会短路、不调模型。观察并发工具调用:写两个会 sleep 的工具,让 LLM 一次调用它们两个,打印执行时间——验证它们是并发的(总时间≈max 而非 sum)。 追踪 transfer 的事件流:构造 root + Chat 子 agent,问一个属于子 agent 的问题。观察事件流的 Author如何从 root 变成子 agent——这就是 transfer 的 forward 机制。(进阶)注入串行 task runner:用 platform.WithTaskRunner注入串行执行器,写一个并发工具的测试,验证它变得可预测。
下一篇预告
第 **06 篇「并发工具调用与 platform seam」**将深入本篇提到的 platform.RunTasks 与状态合并细节——deepMergeMap 的递归合并规则、TransferToAgent「last wins」的决策合并、WithTimeProvider/WithUUIDProvider/WithTaskRunner 三大可注入点如何让并发场景的确定性测试成为可能。本篇打开了 handleFunctionCalls 的门,下一篇就走进去细看。
ADK 2.x-Go · 源码深度解读 · 05 · LLM Flow 执行循环(系列最核心 ⭐)
核心源码:internal/llminternal/base_flow.go(1422 行,ADK 的心脏)
配套:12 个 Processor 实现分布在 internal/llminternal/*_processor.go
本系列代码示例统一使用 OpenAI 模型 · 基于 google/adk-go v2 源码整理
夜雨聆风