【Eino 框架入门】Middleware 中间件:给 Agent 加一层"异常保护罩"

4 阅读3分钟

【Eino 框架入门】Middleware 中间件:给 Agent 加一层"异常保护罩"

写 Agent 最头疼的是什么?Tool 报错直接崩掉整个对话。用户说"读取 xxx.txt",文件不存在,程序炸了,用户得重新开始。

Middleware 就是来解决这个问题的。

Middleware 是什么?

打个比方:Agent 是干活的员工,Middleware 是旁边的保安。

  • 员工干活时,保安在旁边看着
  • 员工出错了,保安拦住错误,不让它炸掉整个系统
  • 错误信息转成文字,告诉员工"出啥事了,你自己看着办"

核心效果:错误不再"终止程序",而是变成"提示信息",让模型自己决定下一步怎么办。

没有 Middleware 的痛点

// Tool 执行失败
result, err := read_file("nonexistent.txt")
// err != nil → 错误向上传播 → Agent 崩溃 → 用户重新开始

有 Middleware 之后

// Middleware 拦截 Tool 调用
result, err := endpoint(ctx, args, opts...)
if err != nil {
    // 不返回 error,而是返回字符串
    return fmt.Sprintf("[tool error] %v", err), nil
}

模型收到的不是程序崩溃,而是一段文字:[tool error] open nonexistent.txt: no such file or directory。模型看到后会说"抱歉文件不存在,让我看看目录里有哪些文件...",然后自己调整策略。

核心实现

type safeToolMiddleware struct {
    *adk.BaseChatModelAgentMiddleware
}

func (m *safeToolMiddleware) WrapInvokableToolCall(
    _ context.Context,
    endpoint adk.InvokableToolCallEndpoint,
    _ *adk.ToolContext,
) (adk.InvokableToolCallEndpoint, error) {
    return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
        result, err := endpoint(ctx, args, opts...)
        if err != nil {
            // 特殊错误要继续传播(比如中断恢复)
            if _, ok := compose.IsInterruptRerunError(err); ok {
                return "", err
            }
            // 普通错误转成字符串,不炸程序
            return fmt.Sprintf("[tool error] %v", err), nil
        }
        return result, nil
    }, nil
}

关键点endpoint 是原始的 Tool 调用,我们用装饰器模式包了一层,在出错时把 error 转成 string。

流式 Tool 怎么处理?

流式 Tool 更麻烦,因为错误可能发生在流的中途:

func (m *safeToolMiddleware) WrapStreamableToolCall(
    _ context.Context,
    endpoint adk.StreamableToolCallEndpoint,
    _ *adk.ToolContext,
) (adk.StreamableToolCallEndpoint, error) {
    return func(ctx context.Context, args string, opts ...tool.Option) (*schema.StreamReader[string], error) {
        sr, err := endpoint(ctx, args, opts...)
        if err != nil {
            // 同步错误:返回一个只有错误信息的流
            return singleChunkReader(fmt.Sprintf("[tool error] %v", err)), nil
        }
        // 包装流,捕获流中途的错误
        return safeWrapReader(sr), nil
    }, nil
}

safeWrapReader 用 goroutine 监听原始流,一旦出错就转成错误信息发出去:

func safeWrapReader(sr *schema.StreamReader[string]) *schema.StreamReader[string] {
    r, w := schema.Pipe[string](64)
    go func() {
        defer w.Close()
        for {
            chunk, err := sr.Recv()
            if errors.Is(err, io.EOF) {
                return
            }
            if err != nil {
                _ = w.Send(fmt.Sprintf("\n[tool error] %v", err), nil)
                return
            }
            _ = w.Send(chunk, nil)
        }
    }()
    return r
}

模型限流怎么办?

除了 Tool 报错,模型 API 也可能限流(429)。这个用 ModelRetryConfig 解决:

ModelRetryConfig: &adk.ModelRetryConfig{
    MaxRetries: 5,
    IsRetryAble: func(_ context.Context, err error) bool {
        return strings.Contains(err.Error(), "429") ||
            strings.Contains(err.Error(), "Too Many Requests")
    },
}

遇到 429 就自动重试,最多 5 次,带指数退避。

洋葱模型

多个 Middleware 会形成洋葱结构:

请求 → A.WrapB.Wrap → C.Wrap → 实际执行 → C返回 → B返回 → A返回 → 响应

代码里数组顺序决定谁在外层:

Handlers: []adk.ChatModelAgentMiddleware{
    &middlewareA{},           // 最外层
    &reductionMiddleware{},   // 中间层
    &safeToolMiddleware{},    // 最内层(建议放错误处理)
}

实践建议:错误处理的 Middleware 放数组末尾(最内层),确保其他 Middleware 抛出的中断错误能正确传播。

完整配置示例

agent, err := deep.New(ctx, &deep.Config{
    Name:           "MyAgent",
    ChatModel:      cm,
    Instruction:    instruction,
    Backend:        backend,
    StreamingShell: backend,
    MaxIteration:   50,
    Handlers: []adk.ChatModelAgentMiddleware{
        &safeToolMiddleware{},  // Tool 错误处理
    },
    ModelRetryConfig: &adk.ModelRetryConfig{
        MaxRetries: 5,
        IsRetryAble: func(_ context.Context, err error) bool {
            return strings.Contains(err.Error(), "429")
        },
    },
})

小结

Middleware 本质上就是装饰器模式:包一层,改行为。核心作用是把"程序崩溃"变成"错误提示",让模型有机会自己纠错。

Eino 内置了几个实用的 Middleware:

  • reduction:Tool 输出太长时自动截断
  • summarization:对话历史太长时自动摘要
  • skill:动态加载技能