当前位置: 首页 > news >正文

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(i+1)/10, fmt.Sprintf("解析中 %d/10 页", i+1)) } } 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,数据库 5s,AI 生成 60s。
超时后优先使用兜底策略(缓存/降级),而非重试。
长耗时工具需要进度回调机制。
超时策略的目标是保障整体响应时间,单工具可牺牲。

最后强调一点:Agent 的超时策略不是在"防止慢工具",而是在"管理用户的等待预期"。
即使用了最好用的进度条和最完善的兜底策略,用户仍然会有不满——但他们不会责怪系统"卡死了",而是理解"这个任务确实需要时间"。
这就是超时策略的最终目标:把"不可接受的等待"变成"可接受的延迟"。

http://www.jsqmd.com/news/1170275/

相关文章:

  • PS去水印不模糊原图方法是什么?去除豆包水印实操教学
  • 2026年武汉汽车改装推荐指南 | 15年豪华车改装头部品牌,新能源升级性价比之王 - 前沿观察站
  • AssetStudio深度解析:Unity资源提取原理与实战指南
  • setState组件更新
  • 欧米茄发条教学与维修保养指南权威公示(2026年7月最新) - 欧米茄服务中心
  • MCP3551与PIC18F25K50高精度数据采集方案详解
  • App 内嵌 H5 秒开技术方案
  • 研究生 AI 率卡 15% 红线怎么降到达标?
  • Mininet 2.3.1 无控制器环境搭建:3步配置静态流表实现主机互通
  • Deepseek-R1-7B安卓端侧部署实战:从量化到HTTP服务
  • PB9 老HIS系统续命记:从10M崩溃到187个PBL的编译生存指南
  • Python 定时任务可靠性:Celery Beat + 数据库锁避免重复执行
  • 2026年7月最新青岛欧米茄官方售后客服电话及服务网点地址查询 - 欧米茄官方服务中心
  • Cursor Tab补全响应延迟>300ms?用这3行配置+2个插件重写补全管道,实测首字符触发提速2.8倍
  • meta-openeuler项目深度解析:9000+软件包YAML配置的终极管理方案
  • AI自我改进系统构建:从Harness Engineering到工程实践
  • EMQX 5.0 亿级连接实战:3步配置实现单节点百万MQTT设备接入
  • 当AI开始“思考”:我们该如何界定它的权利与责任?
  • 亲身到店探访海口亨得利官方名表服务中心|热线电话与门店地址(2026年7月更新) - 亨得利官方
  • Claude Fable 5延期发布:AI编程助手的技术突破与安全挑战
  • 设计品牌标语选实力强公司看案例
  • 大疆上云API 1.8.0 航线管理源码实战:5个核心接口与对象存储集成详解
  • uni-app 微信小程序开发:TypeScript 常用语法总结
  • 禅道 22.x 缺陷管理实战:5步闭环流程与7种解决方案详解
  • 从廊坊到全国:君启修缮“全国连锁+廊坊总部”双优势如何构建标准化防水服务体系? - 鲁顺
  • 弱模型在多次生成调用PaddlePaddle失败的代码后
  • syscontainer-tools 与 iSulad 集成指南:构建企业级容器平台
  • 告别出差:AR远程运维如何让专家“瞬移”到设备现场
  • TPA3128D2与PIC18F4550打造高性价比D类功放方案
  • 数字生命体作为智能观察员:从角色定义到平台集成实践