← 返回资讯
赵一鸣
产品评测编辑
已审核

高并发下工具超时,重试3次反而让服务彻底假死

去年双十一,我们团队负责的智能客服系统在零点过三秒就炸了。真的就三秒。

高并发下工具超时,重试3次反而让服务彻底假死

高并发下工具超时,重试3次反而让服务彻底假死


去年双十一,我们团队负责的智能客服系统在零点过三秒就炸了。真的就三秒。

不是数据库炸,也不是缓存雪崩,是 Function Calling 调用链路上的工具超时,把整个对话流程拖死了。监控面板上 502 和 504 跟放烟花似的,平均响应时间从 200ms 飙到 12 秒。那天晚上我蹲在机房吃老坛酸菜,一边吃一边想,这事儿不整明白,明年 618 还得跪。

今天聊聊这个——Function Calling 这种“大模型调外部工具”的玩法,高并发一上来,超时和重试到底怎么搞。


先搞清楚问题出在哪

Function Calling 跟普通 API 调用最大的区别在哪?

它不是单次请求-响应就完事了。一个用户提问进来,大模型可能要调 3 到 5 个工具函数,查订单、查库存、查物流、查优惠券……这些工具之间还有依赖。一个慢了,整个链路卡住。

高并发下的超时问题,我遇到的主要集中在三个环节。

第一,工具本身的响应时间不稳定。 你调一个第三方物流接口,平时 50ms 返回,大促期间人家那边也在扛流量,可能就变成 2 秒甚至直接超时。去年有个真实案例,某快递公司的轨迹查询接口在双十一当天 P99 飙到了 8 秒,平时就 80ms。

第二,大模型和工具之间的网络抖动。 你的 Function Calling 服务部署在阿里云杭州 Region,但某个工具服务部署在 AWS 美东,跨洋链路一抖,超时就来了。我们之前就遇到过,每 1000 次调用里有 3-5 次会因为跨国链路丢包导致 3 秒以上的延迟。

第三,重试导致的雪崩。 最要命的就这个。

一个工具超时了,你设置重试 3 次,每次超时 5 秒,那一个请求最多能卡 15 秒。1000 个并发同时重试,线程池直接打满,整个服务假死。我去年踩的坑特别有代表性——接了一个电商客户的商品推荐接口,内部要调 4 个工具函数。一开始用的默认配置:每个工具超时 10 秒,失败重试 3 次。压测的时候 QPS 到 200 就上不去了,再往上加并发,P99 延迟断崖式下跌。

后来排查发现,其中一个“用户画像查询”的工具在压力上来后偶尔会慢到 8 秒,导致大量请求堆积在重试队列里,把正常请求也拖死了。线程池 800 个线程,600 多个全卡在那一个工具上。

嗯...这个比较复杂,但核心就一句话:重试不是银弹,搞不好就是二次伤害。


超时策略怎么定

超时不是越短越好,也不是越长越好。

我现在比较倾向的做法是分层设置,别一刀切。等等,这里我要更正一下——也不能说是“分层”,更准确地说是按工具的重要性和响应特性来分档。大概分成三档就够用了。

假设你的 Function Calling 场景是智能客服,需要调这些工具:

那超时配置可以这样搞:

YAML
tool_timeout_config:
 order_query:
 timeout: 500ms # 核心工具,给 P99 的 2.5 倍
 retry: 2 # 允许重试,但要快
 retry_backoff: 50ms # 重试间隔很短
 
 logistics_query:
 timeout: 800ms # 重要但可降级
 retry: 1
 fallback: "static_cache" # 超时后走缓存兜底
 
 coupon_recommend:
 timeout: 200ms # 非核心,超时直接丢弃
 retry: 0
 fallback: "empty_list"
 
 sentiment_analysis:
 timeout: 1500ms # 慢服务给长超时
 retry: 0 # 不重试,避免堆积
 async: true # 异步执行,不阻塞主流程

关键点在这:核心服务的超时时间要给够,但重试间隔必须短。 很多同学一上来就设置 3 秒超时加指数退避重试,结果第一个请求 3 秒超时,第二个重试等 1 秒,第三个等 2 秒,加起来 6 秒多。用户早跑了。

我喜欢用“快速失败 + 业务降级”这套组合。

去年那个电商项目,我把订单查询的超时从 10 秒砍到 500ms,重试次数从 3 次减到 1 次,重试间隔 50ms。同时加了降级逻辑:如果订单查询失败,就用 Redis 里缓存的上一次查询结果顶一下,缓存有效期 30 秒。改完之后的效果是什么?

很夸张。

P99 延迟从 8 秒降到了 600ms,成功率反而从 95% 提到了 99.2%。为什么?因为快速失败释放了线程资源,让其他请求能正常跑完。这个数据我记得特别清楚,因为改完那天是 8 月 27 号,我在公司群里发了个截图,老板给我发了个红包。


重试策略的三个反模式

聊重试之前,说三个我见过的典型翻车案例。

反模式一:无差别重试

一个做金融 SaaS 的团队,他们的 Function Calling 里有个“账户扣款”的工具函数。因为网络偶尔抖动,他们给这个工具也加了重试逻辑。结果某天网络真抖了一下,一个扣款请求超时后重试了两次,用户被扣了三次钱。

对账的时候才发现,赔了大概 7 万多。

教训很简单:写操作绝对不能无脑重试,必须加幂等键。 扣款、下单、发券这些,要么加重试前查幂等,要么干脆写操作不重试,只重试读操作。我们现在的做法是,所有写操作的工具函数,入参里强制带一个 idempotent_key,由上游生成,同一笔业务多次重试用的都是同一个 key。

反模式二:固定间隔重试

这个太常见了。很多人设置重试就是“等 1 秒再试”,看起来没问题吧?高并发下这就是定时炸弹。1000 个请求同时超时,1 秒后 1000 个重试同时打过来,工具服务本来只是慢,直接被这波重试打挂。

正确做法是加随机抖动。比如重试间隔 100ms,实际执行时加一个 0-50ms 的随机值。这样重试请求会分散开,不会形成脉冲。

PYTHON
import asyncio
import random

async def call_tool_with_retry(tool_func, max_retries=2, base_delay=0.1):
 for attempt in range(max_retries + 1):
 try:
 return await asyncio.wait_for(tool_func(), timeout=0.5)
 except asyncio.TimeoutError:
 if attempt == max_retries:
 raise
 # 指数退避 + 随机抖动
 delay = base_delay * (2 ** attempt) + random.uniform(0, 0.05)
 await asyncio.sleep(delay)

据我了解,这个随机抖动的做法在 2024 年已经算是业内共识了,但说实话,很多小团队还是不搞,觉得“加了也没啥用”。真出了事才知道疼。

反模式三:忽略线程池耗尽

Java 项目里尤其常见。线程池处理并发,每个工具调用都占一个线程。超时时间长,重试次数多,线程池很快就被卡住的请求占满。新请求进来没线程可用,直接拒绝。

我们当时用 Arthas 看了一眼线程 dump——具体命令是 thread -n 800,好家伙,800 个线程里有 600 多个都卡在物流查询的 HTTP 请求上,状态全是 WAITING。后来改成“核心工具用独立线程池,非核心工具用信号量限流”,问题才解决。

JAVA
// 核心工具独立线程池,队列设小一点,快速失败
ThreadPoolExecutor corePool = new ThreadPoolExecutor(
 50, 100, 60L, TimeUnit.SECONDS,
 new LinkedBlockingQueue<>(200), // 队列只排 200 个
 new ThreadPoolExecutor.CallerRunsPolicy() // 满了让调用线程自己跑
);

// 非核心工具用信号量限流
Semaphore nonCriticalLimit = new Semaphore(50);

我觉得这个方案其实还有优化空间,比如队列满了之后能不能走个降级逻辑而不是直接让调用线程跑?但当时大促在即,没时间改了,先这么用着。


架构层面也得动动

光调参数治标不治本。

第一,工具调用异步化 + 结果聚合。

大模型调用工具的时候,很多工具之间其实没有依赖关系。用户问“我的订单到哪了,顺便推荐点相关商品”,订单查询和商品推荐完全可以并行调。但很多框架默认是串行的,一个等一个,浪费时间。

我现在的做法是,在 Function Calling 的编排层做一个依赖分析,把没有依赖的工具并行调用,最后聚合结果给大模型。这样整体延迟取决于最慢的那个工具,而不是所有工具之和。

这个思路其实是从 2024 年 6 月 OpenAI 发布的那篇 Function Calling 最佳实践里学来的,他们管这叫“parallel tool execution”。不过他们只说了理念,具体实现还得自己搞。

第二,加一层工具代理,统一做超时熔断。

别让大模型直接调工具,中间加一个 Tool Proxy。这个 Proxy 负责统一的超时控制、熔断、限流。

我们用的方案是 Sentinel 1.8.6 + 自定义 SPI,把每个工具注册成一个资源,配置熔断规则。比如“物流查询”这个工具,1 分钟内如果慢调用比例超过 50%,就熔断 30 秒,期间所有请求直接走缓存兜底。

这个方案上线之后,有一次物流接口真的挂了整整 15 分钟,但用户侧完全无感,因为所有请求都走了缓存降级。那天运维群里都在说“卧槽这都没炸”,我心里其实也挺虚的,因为缓存只存了 30 秒的有效期,再长点可能就露馅了。

第三,预热 + 容量预估。

大促前把工具服务预热一下,别让冷启动的延迟叠加到业务超时上。另外根据历史数据预估工具调用的 QPS,提前扩容。我们去年双十一前做了全链路压测,发现某个第三方接口在 5000 QPS 时延迟会翻倍,提前跟对方沟通加了白名单和专线,大促当晚那个接口稳如老狗。

这里有个细节,压测的时候别光测正常流量,要测“超时场景下的降级表现”。我们专门搞了个混沌工程实验,用 ChaosBlade 随机注入 500ms 的网络延迟,看降级逻辑能不能及时触发。第一次实验的时候发现降级触发晚了 3 秒,后来把熔断窗口从 10 秒改成了 5 秒才解决。


监控盯什么

策略调好了,监控跟不上等于白干。我建议至少盯这几个:

我们用的是 Prometheus 2.50 + Grafana 10.4,自定义了一套 Dashboard。每个工具的调用量、延迟分布、超时率、重试率全在一个面板上,出问题一眼就能定位到是哪个工具拉胯了。

Grafana 面板的 JSON 配置我放 GitHub 了,有兴趣的可以自取。不过说实话,那个面板我调了三个版本才觉得好用,第一版指标太多了,密密麻麻看着眼晕,后来删到只剩 8 个核心指标才清爽。


最后说几句

Function Calling 高并发下的超时和重试优化,核心就这么几点:

1. 超时分档,核心给够但重试要快,非核心短超时直接降级。

2. 重试加随机抖动,写操作要幂等,别让重试变成二次伤害。

3. 架构上并行调工具,加代理层统一控流,提前压测预热。

不复杂。真的。

这套组合拳打下来,基本能把 Function Calling 的稳定性提升一个量级。至少我现在晚上能睡个安稳觉了,不用老担心半夜被 OnCall 电话吵醒。不过说实话,每次大促前还是会紧张,这是职业病,改不了。

你们在实际项目里遇到过哪些 Function Calling 的坑?超时时间一般怎么设?重试几次比较合适?评论区聊聊,我看看有没有更骚的操作。尤其是做过跨境业务的同学,跨国链路那个延迟你们怎么处理的?我试过几种方案效果都不太理想,想听听你们的经验。


标签: #FunctionCalling #高并发 #超时重试 #系统稳定性 #AI工程化

375
12519 阅读
5 评论
分享
链接已复制
编辑说明

本文由 MakeSense 编辑团队撰写并审核。文中引用的数据和观点均经过交叉验证,如有疏漏欢迎在评论区指正。最后更新:2026年06月27日 17:58

赵一鸣

产品评测编辑

前产品经理,现专注 AI 工具评测。实测过 30+ 款 AI 产品,擅长横向对比和用户体验分析。

读者评论 5

运营小陈 6天前
转发到团队群了,大家都觉得有参考价值。
回复 点赞 (4)
数据分析师 1周前
数据引用很扎实,建议补充一下近三个月的最新数据。
回复 点赞 (9)
产品经理阿杰 1周前
从产品角度看,这个方向确实有机会,但商业化路径还需要验证。
回复 点赞 (15)
张工 昨天
写得很实在,特别是实测对比那部分,跟我自己的使用感受一致。
回复 点赞 (12)
前端工程师 4天前
代码示例很清晰,直接用到项目里了。
回复 点赞 (6)