Qoni 上 Agent 频繁超时 / 先把排队、模型、工具三段耗时拆开

文章导读
超时错误本身只说明这次调用没有在预期时间内返回,说明不了时间花在哪一段。要判断 Qoni 上 Agent 频繁超时是平台排队、模型响应慢,还是自建工具回调拖住了整条链路,比较稳妥的做法是先把一次调用拆成三段:请求进入平台到开始推理的排队等待、模型侧从请求发出到输出结束的推理耗时、Agent 调用自建工具到回调返回的耗时。三段分别量化之后,再看哪一段超出了你的超时阈值,调整动作才有落点,否则很容易在
📋 目录
  1. A 在调用日志里标出三段耗时的对应字段
  2. B 用固定输入请求记录一次三段耗时基线
  3. C 在自建工具回调里加打点
  4. D 把重试放在正确层级,避免副作用重复执行
  5. E 按拆解结果给出对应的调整动作
A A

超时错误本身只说明这次调用没有在预期时间内返回,说明不了时间花在哪一段。要判断 Qoni 上 Agent 频繁超时是平台排队、模型响应慢,还是自建工具回调拖住了整条链路,比较稳妥的做法是先把一次调用拆成三段:请求进入平台到开始推理的排队等待、模型侧从请求发出到输出结束的推理耗时、Agent 调用自建工具到回调返回的耗时。三段分别量化之后,再看哪一段超出了你的超时阈值,调整动作才有落点,否则很容易在错误的层反复调参。

适用场景:Qoni 上 Agent 偶发或频繁超时,日志里只有超时结论、看不出瓶颈。操作动作:用调用日志字段或客户端分段计时,把排队、模型、工具三段耗时分别记下来,再用固定输入跑几次取基线。验证方式:重复请求看每段的波动范围,与超时阈值对比定位主要段。风险边界:字段命名随平台版本和自建服务不同,缺失时以客户端打点补齐,不要凭单次采样下结论。

在调用日志里标出三段耗时的对应字段

分段口径不统一时,最常见的错误是把排队等待算进模型耗时,或者把工具回调的等待算进模型耗时,最后得出“模型太慢”的错误结论。先在调用日志里找到能区分这三段的字段。不同平台和 SDK 的字段名不一致,下表是常见对应关系,需要结合实际返回结构确认,不要直接照抄字段名。

耗时段常见字段说明字段缺失时的替代观测
排队queue_wait_ms、waiting_ms、queue_time,或状态字段长时间停留在 queued请求已被接收但尚未进入模型推理客户端记录 request_start 到首个响应字节的间隔;平台状态回调的入队与开始时间差
模型model_latency_ms、first_token_ms、completion_time从开始推理到输出结束流式响应里首 token 时间戳与最后 token 时间戳之差
工具tool_call_ms、function_call_duration、callback_msAgent 发起工具调用到拿到回调结果在自建工具入口和出口各打一个时间戳

如果平台日志只给出总耗时,可以用 trace_id 或 request_id 把平台日志和自己服务的日志按时间对齐,用客户端计时差反推分段。注意各段时间戳可能来自不同机器,跨机器相减前先确认时钟源,能取同一进程内时间差的就不要跨机器算。

用固定输入请求记录一次三段耗时基线

基线要可对比,输入必须固定:同一段提示词长度、同一个模型、同一组工具、相近的并发度。用命令行跑几轮是最省事的记录方式,下面的骨架可按自己的接口地址和鉴权头替换。

for i in $(seq 1 5); do
  curl -s -o /dev/null \
    -w "round=%{time_starttransfer}s total=%{time_total}s\n" \
    -X POST "$AGENT_ENDPOINT" \
    -H "Content-Type: application/json" \
    -d '{"input":"固定的一段测试输入","tools":["tool_a"],"stream":false}'
done

把每一轮的结果填进下面这张表,至少重复三到五次,记录各段毫秒数和总耗时,并写出波动范围,例如“排队段在几十毫秒到几百毫秒之间浮动,模型段相对稳定”。单次采样不足以作为判断依据。

Qoni 上 Agent 频繁超时 / 先把排队、模型、工具三段耗时拆开
  • 排队段毫秒数:最小值 / 最大值
  • 模型段毫秒数:最小值 / 最大值
  • 工具段毫秒数:最小值 / 最大值
  • 端到端总耗时:最小值 / 最大值
  • 差额:总耗时减去三段之和,差额通常落在网络传输、网关转发或响应序列化上,需要单独确认

基线记录完再做任何改动,改动前后都用同一段固定输入复测,否则无法判断改动是否有效。

在自建工具回调里加打点

自建工具最容易背锅:工具内部查库、请求第三方接口、解析大文件都可能占用几十秒。给回调入口和出口加时间戳,是排除自建侧慢的必要动作。下面是通用计时骨架,语言和框架可按自己的服务替换。

import time, json, logging, uuid

def tool_handler(payload, trace_id=None):
    trace_id = trace_id or payload.get("trace_id") or str(uuid.uuid4())
    tool_name = payload.get("tool_name")
    t0 = time.monotonic()
    logging.info(json.dumps({"trace_id": trace_id, "tool_name": tool_name,
                             "phase": "tool_start", "ts_ms": int(t0 * 1000)}))
    try:
        return do_work(payload)
    finally:
        t1 = time.monotonic()
        logging.info(json.dumps({"trace_id": trace_id, "tool_name": tool_name,
                                 "phase": "tool_end", "ts_ms": int(t1 * 1000),
                                 "tool_duration_ms": round((t1 - t0) * 1000)}))

日志用 JSON 行格式,便于按 trace_id 检索和与平台日志拼接。字段名约定要与总耗时日志对齐:trace_id 与平台侧保持同一个值,tool_name 用于区分是哪个工具慢,tool_duration_ms 对应日志里的工具段耗时。计时用单调时钟而不是墙上时钟,避免机器校时造成负值。

Qoni 上 Agent 频繁超时 / 先把排队、模型、工具三段耗时拆开

对齐方式:把工具日志里的 tool_duration_ms 与调用日志里的工具段耗时做差,差值大通常说明打点位置没有覆盖到参数解析、结果序列化或回调框架本身的开销,需要在更外层的包装函数里再补一个点。

把重试放在正确层级,避免副作用重复执行

超时之后,如果在最上层无条件重试整条 Agent 调用,模型会重新推理一遍,工具会被重复调用,写类操作直接出问题。重试要放在能识别幂等性的那一层。

  • 幂等键构造:业务侧唯一 ID + 工具名 + 参数规范化后的哈希,参数规范化包括字典排序、剔除时间戳和随机数等易变字段,拼出的 idempotency_key 随调一起透传,工具侧落库或写缓存时先按这个键去重。
  • 重试上限:通常一到两次即可,并且要明确重试的是工具调用还是整条 Agent 链路,不要两层同时重试,否则重试次数会相乘。
  • 失败上抛位置:工具重试耗尽后在工具边界抛出,由 Agent 决定更换工具、减少工具数量还是降级回答;不要让工具内部无限重试,那只会把一次超时拉成更长的超时。
  • 不能重试的操作:下单、支付、发消息、写库、触发外部回调、涉及计费或额度扣减的调用。这类操作超时后应当先去查状态,而不是直接重发。

按拆解结果给出对应的调整动作

三段耗时拆开之后,调整动作应当只针对占比最大的那一段,下面这张映射表用于把定位结论转成具体改动,每条都指明看哪个指标验证。

定位结论主要指标调整动作验证方式
排队段占大头queue_wait_ms、排队超时次数调整客户端并发上限和限流参数,错峰调度,确认当前实例或租户的配额是否已接近上限用同一固定输入复测,看排队段的最小值和最大值是否收窄
模型段占大头first_token_ms、输入长度、一次注入的工具描述数量缩短提示词、裁剪历史上下文、减少单次可见工具数量,必要时改用流式输出以改善感知延迟对比裁剪前后的模型段耗时与总耗时,确认输出质量没有明显下降
工具段占大头tool_duration_ms、工具错误率调整工具超时值,同时确认不超过上游的总超时;对可复用的查询结果加缓存;长任务改成异步提交加轮询用同一 trace_id 对齐工具日志与调用日志,比较调整前后工具段耗时
总耗时明显大于三段之和网关侧耗时、响应体大小检查响应体大小、序列化开销和网关超时设置,必要时减少一次返回的数据量在客户端与服务端分别计时,比较差值

改完一项就复测一次,不要同时改多段,否则又回到无法归因的状态。如果三段都接近阈值而没有一段明显突出,通常是超时阈值本身设置得偏紧,需要结合端到端可接受延迟重新确认阈值。