vault backup: 2026-05-05 21:21:58

This commit is contained in:
2026-05-05 21:21:58 +08:00
parent d094f15d7b
commit b6b7133591
4 changed files with 383 additions and 7 deletions
+53
View File
@@ -125,6 +125,59 @@ flowchart LR
>
> 将 `safeToolMiddleware`(错误捕获)放在最内层(数组末尾),确保其他 Middleware 抛出的中断错误能正确向外传播,不被吞掉。
### 深入:为什么安全中间件要放在最内层?
很多开发者会问:**为什么不在最外层放一个全局的 `RecoverMiddleware` 来兜底?** 要理解这一点,需要区分三种不同的错误处理方式:
| 方式 | 捕获目标 | 处理策略 | 放置位置 |
|------|---------|---------|---------|
| **安全转换** (`safeToolMiddleware`) | 业务错误(文件不存在、参数错误) | 转为字符串喂给模型,让 Agent 自愈 | **最内层**(工具调用前) |
| **中断传播** | `InterruptRerunError` 等控制信号 | 不做任何转换,原样向上抛出 | 贯穿所有层 |
| **系统兜底** (`defer recover()`) | `panic`(nil pointer、数组越界) | 记录日志,保护进程不崩溃 | **入口层**(如 `main()` / `http.Server`) |
```go
// ❶ 最内层:业务错误转换 —— 模型可以继续执行
func (m *safeToolMiddleware) WrapInvokableToolCall(...) (...) {
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
}
}
// ❷ 入口处:panic 兜底 —— 保护进程
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered from panic: %v", r)
}
}()
// ... 启动 Agent 服务
}
```
> [!question] 进阶思考
>
> 如果把 `safeToolMiddleware` 移到最外层(数组首位),会发生什么?
>
> <details>
> <summary>🔍 点击查看推导</summary>
>
> 考虑这个场景:用户在 Agent 多轮对话中点击了"停止"按钮,底层产生了一个 `InterruptRerunError`。
>
> 1. Tool 执行器检测到中断信号,抛出 `InterruptRerunError`
> 2. 如果 `safeToolMiddleware` 在外层——此时请求还在外层尚未进入内层,**中断信号在内层往外冒泡时会首先经过内层中间件**
> 3. 因为中断信号是在最内层的 Tool 处产生的,无论 `safeToolMiddleware` 在哪一层,只要它检查了 `IsInterruptRerunError` 就不会吞掉它
> 4. 但真正的问题是:如果内层的其他逻辑(非中断)也出错,外层中间件还没来得及处理就被内层的错误"跳过"了
>
> 更准确地说,顺序的关键在于:**中间件是按装饰器模式嵌套的,外层包裹内层,内层最先执行也最先返回**。内层先做错误分类,外层再做全局处理,这样既保证了中断信号的畅通,又保证了业务错误的收敛。
> </details>
### ModelRetryConfig:内置重试配置
`ModelRetryConfig` 提供了 ChatModel 级别的自动重试能力: