Function Calling 超时策略:别让一个慢工具拖垮整个 Agent 响应 Function Calling 超时策略别让一个慢工具拖垮整个 Agent 响应一、Agent 调用了 PDF 解析工具用户等了 60 秒后关闭了页面不同的工具有不同的延迟特性。搜索 API 通常 300-800ms。数据库查询 50-200ms。PDF 解析可能 10-60 秒。如果 Agent 的顺序调用模式无差别地等待每个工具返回。一个慢工具会把整个对话卡住。用户看到正在思考…15 秒后怀疑出 bug 了。30 秒后刷新页面60 秒后放弃。而模型 API 实际上还在等工具返回。超时策略不是给所有工具设一个统一值。而是根据工具类型和时间预算做分层控制。这是 Agent 开发中一个典型的乐观设计陷阱。开发者在本地测试时工具调用通常很快本地数据库、小文件上线后遇到生产环境的慢工具就手足无措。Agent 的端到端延迟是一个累加值——T LLM推理时间 Σ(工具调用时间)。如果 5 个工具中有一个耗时 30 秒整个对话体验就被拖垮——哪怕其他 4 个工具都在 500ms 内返回。对用户而言15 秒是一个心理阈值。15 秒内用户处于等待模式15 秒后进入怀疑模式——是不是卡了要不要刷新如果 Agent 能在 15 秒内给出部分结果或告知正在处理较长任务用户的焦虑感显著降低。这就是进度反馈的价值——不是加速任务而是管理期望。二、分层超时控制模型flowchart TB A[工具调用开始] -- B{工具类型} B --|快速工具| C[超时: 5s] B --|中速工具| D[超时: 15s] B --|慢速工具| E[超时: 60s] C -- F{超时?} F --|是| G[返回部分结果 or 缓存兜底] F --|否| H[正常返回] D -- I{超时?} I --|是| G I --|否| H E -- J{进度回调?} J --|支持| K[推送中间状态给用户] K -- L{超时?} L --|是| M[返回已完成的部分] L --|否| H三层超时设计的核心在于差异化的失败模式。快速工具超时 → 大概率是下游故障直接降级兜底不给用户看错误详情。中速工具超时 → 可能只是瞬时繁忙兜底的同时记录日志以便排查。慢速工具超时 → 预期内的可能进度反馈已经给了用户心理预期超时后提供部分结果而非空结果。这种差异化处理避免了一刀切的粗暴——所有超时都返回统一的错误信息。用户对搜索超时和PDF解析超时的容忍度完全不同Agent 的响应策略也应该不同。三、带超时的工具调度实现下面的 Go 代码实现了一个按工具类型分层超时的执行器支持兜底策略和进度回调。package toolbox import ( context fmt sync time ) // ToolTimeout 工具超时配置 type ToolTimeout struct { // 按工具名配置超时时间 Timeouts map[string]time.Duration Default time.Duration } // DefaultTimeouts 推荐的超时配置 var DefaultTimeouts ToolTimeout{ Default: 15 * time.Second, Timeouts: map[string]time.Duration{ web_search: 8 * time.Second, db_query: 5 * time.Second, api_call: 10 * time.Second, pdf_parse: 30 * time.Second, image_generate: 60 * time.Second, code_execute: 20 * time.Second, cache_get: 1 * time.Second, }, } // TimeoutError 超时错误 type TimeoutError struct { ToolName string Timeout time.Duration Elapsed time.Duration } func (e *TimeoutError) Error() string { return fmt.Sprintf( 工具 %s 执行超时 (限制: %v, 实际: %v), e.ToolName, e.Timeout, e.Elapsed, ) } // ProgressCallback 长耗时工具的进度回调 type ProgressCallback func(percent float64, message string) // ToolResult 工具执行结果 type ToolResult struct { Value interface{} Error error Duration time.Duration TimedOut bool } // TimeoutExecutor 带超时的工具执行器 type TimeoutExecutor struct { timeouts ToolTimeout maxTotal time.Duration // 单次 Agent 调用的总超时 } // NewTimeoutExecutor 创建超时执行器 func NewTimeoutExecutor(timeouts ToolTimeout, maxTotal time.Duration) *TimeoutExecutor { return TimeoutExecutor{ timeouts: timeouts, maxTotal: maxTotal, } } // Execute 执行工具调用带超时 func (e *TimeoutExecutor) Execute( ctx context.Context, toolName string, fn func(ctx context.Context) (interface{}, error), onProgress ProgressCallback, // 可选进度回调 ) *ToolResult { // 获取该工具的超时配置 timeout : e.timeouts.Default if t, ok : e.timeouts.Timeouts[toolName]; ok { timeout t } // 创建带超时的 context toolCtx, cancel : context.WithTimeout(ctx, timeout) defer cancel() start : time.Now() // 使用 channel 接收执行结果 type result struct { value interface{} err error } resultCh : make(chan result, 1) go func() { val, err : fn(toolCtx) resultCh - result{val, err} }() // 等待结果或超时 select { case res : -resultCh: return ToolResult{ Value: res.value, Error: res.err, Duration: time.Since(start), TimedOut: false, } case -toolCtx.Done(): return ToolResult{ Value: nil, Error: TimeoutError{ ToolName: toolName, Timeout: timeout, Elapsed: time.Since(start), }, Duration: time.Since(start), TimedOut: true, } } } // ExecuteWithFallback 执行工具调用超时后使用兜底策略 func (e *TimeoutExecutor) ExecuteWithFallback( ctx context.Context, toolName string, fn func(ctx context.Context) (interface{}, error), fallback func() (interface{}, error), ) *ToolResult { result : e.Execute(ctx, toolName, fn, nil) if result.TimedOut fallback ! nil { // 超时了使用兜底 fallbackVal, fallbackErr : fallback() if fallbackErr ! nil { result.Error fmt.Errorf( 工具超时且兜底策略失败: %w, fallbackErr, ) } else { result.Value fallbackVal result.Error nil // 兜底成功 } } return result } // ---- 使用示例 ---- func demo() { executor : NewTimeoutExecutor(DefaultTimeouts, 60*time.Second) ctx : context.Background() // 场景1正常超时 slowSearch : func(ctx context.Context) (interface{}, error) { select { case -ctx.Done(): return nil, ctx.Err() case -time.After(10 * time.Second): return search results, nil } } result : executor.Execute(ctx, web_search, slowSearch, nil) if result.TimedOut { fmt.Printf(搜索超时: %v\n, result.Error) } // 场景2超时后兜底返回缓存结果 cacheFallback : func() (interface{}, error) { return cached_results_for_search, nil } result2 : executor.ExecuteWithFallback( ctx, web_search, slowSearch, cacheFallback, ) fmt.Printf(结果: %v\n, result2.Value) // 场景3长耗时工具的进度反馈 pdfParse : func(ctx context.Context) (interface{}, error) { // 这里应该通过 onProgress 实时推送进度 for i : 0; i 10; i { select { case -ctx.Done(): return nil, ctx.Err() case -time.After(2 * time.Second): // onProgress(float64(i1)/10, fmt.Sprintf(解析中 %d/10 页, i1)) } } return pdf_parsed, nil } progressFn : func(pct float64, msg string) { fmt.Printf(进度: %.0f%% - %s\n, pct*100, msg) } result3 : executor.Execute(ctx, pdf_parse, pdfParse, progressFn) fmt.Printf(PDF 解析: %v (耗时 %v)\n, result3.Value, result3.Duration) }ExecuteWithFallback的兜底策略设计是这段代码的核心价值。超时不等于失败——很多场景下过时的数据比没有数据好。缓存兜底让搜索超时时返回上次缓存的结果标记数据可能不是最新的这样用户至少能看到东西而不是一个空白页面。这是一种优雅降级的思维——不是追求 100% 完美而是在任何情况下都给用户最小可用的结果。四、超时与重试的协调超时不应该立即触发重试。超时意味着工具还在执行中只是慢重试意味着同时有两个慢请求。推荐策略超时 → 降级兜底 → 不重试。如果是网络瞬时故障的超时重试才有意义。兜底策略的选择搜索超时 → 返回空结果并告知用户。数据库超时 → 返回缓存数据标记陈旧。生成超时 → 返回部分结果或让用户稍后查看。超时和重试的区分点在于根因。网络超时连接被拒绝、TLS 握手失败→ 瞬时故障可以重试。执行超时服务端处理太慢→ 不是瞬时故障不应该重试——否则等于用 2 个慢请求叠加。一个实用的区分方法是检查context.DeadlineExceeded和具体的net.Error。如果是前者 底层错误是connection refused→ 快速重试。如果是前者 没有底层网络错误 → 说明服务端在缓慢处理直接兜底。另外超时配置不要硬编码。超时值应该从配置中心读取支持热更新。一个工具的 P99 延迟可能在业务高峰时翻倍你需要在不发版的情况下调整超时阈值。建议把超时配置放在 Redis 或配置中心中Agent 启动时加载运行时通过 Admin API 动态调整。五、总结工具调用超时策略应按工具类型分层设置。搜索引擎 8s数据库 5sAI 生成 60s。超时后优先使用兜底策略缓存/降级而非重试。长耗时工具需要进度回调机制。超时策略的目标是保障整体响应时间单工具可牺牲。最后强调一点Agent 的超时策略不是在防止慢工具而是在管理用户的等待预期。即使用了最好用的进度条和最完善的兜底策略用户仍然会有不满——但他们不会责怪系统卡死了而是理解这个任务确实需要时间。这就是超时策略的最终目标把不可接受的等待变成可接受的延迟。