10中间件与 Handler 的设计
六个钩子、Middlewares 先于 Handlers 的执行序、两类职责与内置件分类学。
adk/handler.go从”会用”到”看懂设计”
第 6 章你已经会叠中间件了。这一章我们回到设计层面,回答三个”为什么”:为什么 ADK 有两套中间件类型?九个钩子为什么这样切?十几个内置件如何归类?看懂这些,你就能判断”该用哪套、该实现哪个钩子、该往哪层塞逻辑”。
两套中间件:struct vs interface 的世代之争
翻开源码你会发现 ADK 里并存两种中间件:老的 AgentMiddleware(adk/chatmodel.go:239)是一个结构体,新的 TypedChatModelAgentMiddleware[M](adk/handler.go:139)是一个接口。这不是重复造轮子,而是一次有意的世代升级。
// Deprecated: Use ChatModelAgentMiddleware (interface-based Handlers) instead.// AgentMiddleware will be removed in a future release.//// AgentMiddleware provides hooks to customize agent behavior at various stages of execution.type AgentMiddleware struct { // AdditionalInstruction adds supplementary text to the agent's system instruction. // This instruction is concatenated with the base instruction before each chat model call. AdditionalInstruction string
// AdditionalTools adds supplementary tools to the agent's available toolset. // These tools are combined with the tools configured for the agent. AdditionalTools []tool.BaseTool
// BeforeChatModel is called before each ChatModel invocation, allowing modification of the agent state. BeforeChatModel func(context.Context, *ChatModelAgentState) error
// AfterChatModel is called after each ChatModel invocation, allowing modification of the agent state. AfterChatModel func(context.Context, *ChatModelAgentState) error
// WrapToolCall wraps tool calls with custom middleware logic. // Each middleware contains Invokable and/or Streamable functions for tool calls. WrapToolCall compose.ToolMiddleware}// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.//// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built// on top of it (e.g., DeepAgent).//// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?//// AgentMiddleware is a struct type, which has inherent limitations:// - Struct types are closed: users cannot add new methods to extend functionality// - The framework only recognizes AgentMiddleware's fixed fields, so even if users// embed AgentMiddleware in a custom struct and add methods, the framework cannot// call those methods (config.Middlewares is []AgentMiddleware, not a user type)// - Callbacks in AgentMiddleware only return error, cannot return modified context//// TypedChatModelAgentMiddleware is an interface type, which is open for extension:// - Users can implement custom handlers with arbitrary internal state and methods// - Hook methods return (context.Context, ..., error) for direct context propagation// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the// wrapped endpoint chain: wrappers can pass modified context to the next wrapper// - Configuration is centralized in struct fields rather than scattered in closures//// TypedChatModelAgentMiddleware vs AgentMiddleware:// - Use AgentMiddleware for simple, static additions (extra instruction/tools)// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping// - AgentMiddleware is kept for backward compatibility with existing users// - Both can be used together; see AgentMiddleware documentation for execution order//// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op// implementations for all methods.type TypedChatModelAgentMiddleware[M MessageType] interface { // BeforeAgent is called before each agent run, allowing modification of // the agent's instruction and tools configuration. BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
// AfterAgent is called after the agent run reaches a successful terminal state. // Successful terminal states are: final answer (model response with no tool calls), // and return-directly tool result. // // AfterAgent is NOT called when the agent terminates with an error (e.g., // ErrExceedMaxIterations, context cancellation, model errors). // // The state parameter contains the final conversation state, including all messages // from the completed run. // // AfterAgent handlers are called in the same order as BeforeAgent handlers // (first registered = first called). Consistent with all other middleware hooks, // if any handler returns an error, subsequent handlers are NOT called (fail-fast) // and the error is sent to the event stream.// … 省略 98 行;完整声明 L110–255,点击上方「浏览完整文件」老的结构体版长这样:
type AgentMiddleware struct { AdditionalInstruction string AdditionalTools []tool.BaseTool BeforeChatModel func(context.Context, *ChatModelAgentState) error AfterChatModel func(context.Context, *ChatModelAgentState) error WrapToolCall compose.ToolMiddleware}它简单,但源码在 adk/handler.go:117 直白地列出了它的天花板:
// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.//// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built// on top of it (e.g., DeepAgent).//// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?//// AgentMiddleware is a struct type, which has inherent limitations:// - Struct types are closed: users cannot add new methods to extend functionality// - The framework only recognizes AgentMiddleware's fixed fields, so even if users// embed AgentMiddleware in a custom struct and add methods, the framework cannot// call those methods (config.Middlewares is []AgentMiddleware, not a user type)// - Callbacks in AgentMiddleware only return error, cannot return modified context//// TypedChatModelAgentMiddleware is an interface type, which is open for extension:// - Users can implement custom handlers with arbitrary internal state and methods// - Hook methods return (context.Context, ..., error) for direct context propagation// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the// wrapped endpoint chain: wrappers can pass modified context to the next wrapper// - Configuration is centralized in struct fields rather than scattered in closures//// TypedChatModelAgentMiddleware vs AgentMiddleware:// - Use AgentMiddleware for simple, static additions (extra instruction/tools)// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping// - AgentMiddleware is kept for backward compatibility with existing users// - Both can be used together; see AgentMiddleware documentation for execution order//// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op// … 这是 L110–137 的片段(该行不在任何顶层声明内);点击上方「浏览完整文件」- 结构体是封闭的:用户无法给它加新方法来扩展能力。
- 框架只认它的固定字段:哪怕你内嵌它、加了自己的方法,框架也调不到——因为
config.Middlewares的类型是[]AgentMiddleware,不是你的类型。 - 回调只能返回
error:改不了context,没法把修改后的上下文往下传。
🔑 本章的设计钥匙
从 struct 升级到 interface,本质是把中间件从”填空题”变成”开放实现”。结构体版让你填几个预定义的字段(封闭、有限);接口版让你实现一个类型——可以带任意内部状态和方法,钩子还能返回修改后的
context往下传播。这正是”面向接口而非实现”在框架扩展点上的体现:框架只依赖接口,把”能扩展成什么样”的权力完全交给你。结构体版被保留只为向后兼容,新代码一律用接口版。
为什么钩子返回 context 如此重要
接口版每个钩子都返回 (context.Context, ..., error),而不是只返回 error。这个细节是整套设计的枢纽。
想象一个链路追踪中间件:它在 BeforeModelRewriteState 里往 ctx 里塞一个 span,后续的模型调用、工具执行都该看到这个 span。如果钩子不能返回 ctx,你塞进去的东西就断在原地,传不下去。接口版通过”钩子返回新 ctx、Wrap 包裹链传递 ctx”(adk/handler.go:124),让上下文能在整条调用链上连续流动。这是结构体版的 func(...) error 永远做不到的。
// TypedChatModelAgentMiddleware defines the interface for customizing TypedChatModelAgent behavior.//// IMPORTANT: This interface is specifically designed for TypedChatModelAgent and agents built// on top of it (e.g., DeepAgent).//// Why TypedChatModelAgentMiddleware instead of AgentMiddleware?//// AgentMiddleware is a struct type, which has inherent limitations:// - Struct types are closed: users cannot add new methods to extend functionality// - The framework only recognizes AgentMiddleware's fixed fields, so even if users// embed AgentMiddleware in a custom struct and add methods, the framework cannot// call those methods (config.Middlewares is []AgentMiddleware, not a user type)// - Callbacks in AgentMiddleware only return error, cannot return modified context//// TypedChatModelAgentMiddleware is an interface type, which is open for extension:// - Users can implement custom handlers with arbitrary internal state and methods// - Hook methods return (context.Context, ..., error) for direct context propagation// - Wrapper methods (WrapToolCall, WrapModel) enable context propagation through the// wrapped endpoint chain: wrappers can pass modified context to the next wrapper// - Configuration is centralized in struct fields rather than scattered in closures//// TypedChatModelAgentMiddleware vs AgentMiddleware:// - Use AgentMiddleware for simple, static additions (extra instruction/tools)// - Use TypedChatModelAgentMiddleware for dynamic behavior, context modification, or call wrapping// - AgentMiddleware is kept for backward compatibility with existing users// - Both can be used together; see AgentMiddleware documentation for execution order//// Use *TypedBaseChatModelAgentMiddleware as an embedded struct to provide default no-op// implementations for all methods.type TypedChatModelAgentMiddleware[M MessageType] interface { // BeforeAgent is called before each agent run, allowing modification of // the agent's instruction and tools configuration. BeforeAgent(ctx context.Context, runCtx *ChatModelAgentContext) (context.Context, *ChatModelAgentContext, error)
// AfterAgent is called after the agent run reaches a successful terminal state.// … 这是 L110–144 的片段(该行不在任何顶层声明内);点击上方「浏览完整文件」九个钩子的分类学
第 6 章列过九个钩子,这里我们按”作用对象 × 时机”给它们建一张坐标图,理解切法背后的意图:
| 层级 | 钩子 | 类型 | 用途 |
|---|---|---|---|
| Agent | BeforeAgent / AfterAgent | 观测·改写 | 进入前改指令/工具集,成功收尾时观测 |
| 模型 | BeforeModelRewriteState / AfterModelRewriteState | 观测·改写(持久化) | 每次调模型前后改消息/工具,改动进 state |
| 工具 | WrapInvokableToolCall 等四个 | 包裹 | 围绕一次工具执行插逻辑 |
| 模型调用 | WrapModel | 包裹 | 围绕模型调用本身:重试/降级/发事件 |
切法的意图有三条线索:
Before/After(钩子)vsWrap(包裹)。前者是”在某个时刻被叫一下”,适合观测和改状态;后者给你原始端点、你返回一个新端点,适合”在调用前后都插手”的洋葱式逻辑。- 持久化边界。
BeforeModelRewriteState的改动会写进 state、影响后续迭代;而WrapModel的改动只作用于单次调用。这条边界决定了”改消息去哪改”——源码在adk/handler.go:254明确劝退在WrapModel里改输入,因为它不持久化、还打断 prompt 缓存。
3. 工具钩子为何有四个。标准/增强 × 同步/流式,共四个 Wrap*ToolCall。因为工具有两代接口(裸字符串 vs 结构化 ToolArgument)和两种执行模式(同步 vs 流式),框架为每种组合都留了精确的包裹点,而不是用一个笼统的钩子糊过去。
执行序:一张必须记住的时序图
多个中间件 + 框架内部逻辑叠在一起,顺序不能靠猜。源码在 adk/chatmodel.go:323 给出了权威的模型调用生命周期(从最外层到最内层):
// 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,点击上方「浏览完整文件」1. AgentMiddleware.BeforeChatModel (老·钩子)2. ChatModelAgentMiddleware.BeforeModelRewriteState (新·钩子,可改 state)3. failoverModelWrapper (内部·多模型故障切换)4. retryModelWrapper (内部·重试)5. eventSenderModelWrapper (内部·发送模型事件)6. ChatModelAgentMiddleware.WrapModel (新·包裹,先注册=最外层)7. callbackInjectionModelWrapper (内部·注入回调)8. Model.Generate/Stream ← 真正的模型调用9. ChatModelAgentMiddleware.AfterModelRewriteState (新·钩子)10. AgentMiddleware.AfterChatModel (老·钩子)flowchart TB
subgraph OLD["老 AgentMiddleware(最外层)"]
B1["1 BeforeChatModel"]
subgraph NEW["新 Handlers"]
B2["2 BeforeModelRewriteState(改 state)"]
subgraph INT["框架内部 wrapper(洋葱)"]
W1["3 failover → 4 retry → 5 eventSender"]
W2["6 WrapModel(先注册=最外)"]
W3["7 callbackInjection"]
CALL["8 Model.Generate/Stream ← 真正调用"]
W1 --> W2 --> W3 --> CALL
end
A2["9 AfterModelRewriteState"]
end
A1["10 AfterChatModel"]
end
B1 --> B2 --> INT --> A2 --> A1
模型调用生命周期:老 Middlewares 包着新 Handlers
三条纪律从这张图里跳出来:
- 老
Middlewares包在新Handlers外面。BeforeChatModel(第 1 步)先于BeforeModelRewriteState(第 2 步);对称地,AfterChatModel(第 10 步)最后收尾。文档在adk/chatmodel.go:320明确:Handlers 在 Middlewares 之后处理。
- 先注册 = 最外层。多个
WrapModel按注册顺序[A, B, C]组成A(B(C(model)))(adk/chatmodel.go:349)。想让某件事最后看到原始输出,就把它注册在最内层——EventSenderModelWrapper就是这么用的。
- 框架内部件也在链上。重试、故障切换、事件发送不是散落的 if,而是作为一层层 wrapper 精确排在你的中间件之间。工具调用有一条对称的生命周期(
adk/chatmodel.go:356)。
📝 跨件与跨中断传值
中间件之间、乃至跨中断续跑传数据,用
SetRunLocalValue(adk/handler.go:338)/GetRunLocalValue(adk/handler.go:366)。值绑定到”当前这次 Run”,并会被序列化进 checkpoint——所以它天然兼容第 5 章的 HITL。要发自定义事件到事件流,用SendEvent(adk/handler.go:425)。// SetRunLocalValue sets a key-value pair that persists for the duration of the current agent Run() invocation.// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.//// Values stored here are compatible with interrupt/resume cycles - they will be serialized and restored// when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]()// in an init() function to ensure proper serialization.//// This function can only be called from within a ChatModelAgentMiddleware during agent execution.// Returns an error if called outside of an agent execution context.func SetRunLocalValue(ctx context.Context, key string, value any) error {if err := checkGobEncodability(key, value); err != nil {return err}err := processTypedState(ctx, func(extra map[string]any) map[string]any {if extra == nil {extra = make(map[string]any)}extra[key] = valuereturn extra})if err != nil {return fmt.Errorf("SetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)}return nil}// GetRunLocalValue retrieves a value that was set during the current agent Run() invocation.// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.//// Values stored via SetRunLocalValue are compatible with interrupt/resume cycles - they will be serialized// and restored when the agent is resumed. For custom types, you must register them using schema.RegisterName[T]()// in an init() function to ensure proper serialization.//// This function can only be called from within a ChatModelAgentMiddleware during agent execution.// Returns the value and true if found, or nil and false if not found or if called outside of an agent execution context.func GetRunLocalValue(ctx context.Context, key string) (any, bool, error) {var val anyvar found boolerr := processTypedState(ctx, func(extra map[string]any) map[string]any {if extra != nil {val, found = extra[key]}return extra})if err != nil {return nil, false, fmt.Errorf("GetRunLocalValue failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)}return val, found, nil}// SendEvent sends a custom AgentEvent to the event stream during agent execution.// This allows ChatModelAgentMiddleware implementations to emit custom events that will be// received by the caller iterating over the agent's event stream.//// This function can only be called from within a ChatModelAgentMiddleware during agent execution.// Returns an error if called outside of an agent execution context.func SendEvent(ctx context.Context, event *AgentEvent) error {return TypedSendEvent(ctx, event)}
内置件的两类职责
adk/middlewares/ 下的内置件不是随意堆砌,按职责能清晰分成两类:
- 给能力(注入工具):
filesystem(adk/middlewares/filesystem/filesystem.go:395,文件读写/grep/glob)、plantask(任务清单)、skill(加载 SKILL.md)、toolsearch(动态工具检索)。它们的共性是往 Agent 里加工具,扩展”能做什么”。
// New constructs and returns the filesystem middleware as a ChatModelAgentMiddleware.//// This is the recommended constructor for new code. It returns a ChatModelAgentMiddleware which provides:// - Better context propagation through WrapInvokableToolCall and WrapStreamableToolCall methods// - BeforeAgent hook for modifying agent instruction and tools at runtime// - More flexible extension points compared to the struct-based AgentMiddleware//// The middleware provides filesystem tools (ls, read_file, write_file, edit_file, glob, grep)// and optionally an execute tool if the Backend implements ShellBackend or StreamingShellBackend.//// Example usage://// middleware, err := filesystem.New(ctx, &filesystem.Config{// Backend: myBackend,// })// agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{// // ...// Handlers: []adk.ChatModelAgentMiddleware{middleware},// })func New(ctx context.Context, config *MiddlewareConfig) (adk.ChatModelAgentMiddleware, error) { return NewTyped[*schema.Message](ctx, config)}- 管上下文(改写状态):
summarization(超长历史摘要压缩)、reduction(两阶段截断/清理工具输出)、agentsmd(临时注入 Agents.md)、patchtoolcalls(修补悬空的工具调用)。它们的共性是在Before/AfterModelRewriteState里改消息,管理”喂给模型什么”。
这个二分不是我们硬套的,而是钩子设计逼出来的结果:BeforeAgent 改工具集 → 天然承载第一类;BeforeModelRewriteState 改消息 → 天然承载第二类。能力从工具进,上下文从状态改——记住这条,你一眼就能判断一个新需求该做成哪类中间件、实现哪个钩子。
本章小结
- ADK 有两套中间件:老
AgentMiddleware(struct,封闭)与新TypedChatModelAgentMiddleware[M](interface,开放)。新代码用接口版,结构体版仅为兼容。 - 升级的本质是”填空 → 开放实现”;接口版钩子返回
context,让上下文能沿整条调用链传播。 - 九个钩子按”层级 × 时机”分:Agent / 模型(持久化)/ 工具 / 模型调用;
Before/After是钩子,Wrap*是洋葱包裹。 - 执行序有权威时序图:老 Middlewares 包在新 Handlers 外,先注册=最外层,框架内部件(重试/切换/事件)也在链上。
- 内置件分两类:给能力(注入工具)与管上下文(改写状态),对应两组钩子。
理解了单个 Agent 的扩展模型,下一章我们上升到多智能体的组合范式:supervisor、plan-execute、DeepAgent 各代表一种把 Agent 拼起来的哲学。