首字只要 800ms,用户为什么还是等了 7 秒?

举报
霍格沃兹测试学社 发表于 2026/09/07 14:03:05 2026/09/07
【摘要】 首Token延迟≠用户体验!用户真正需要的是可执行答案(如“可退款+原因+下一步”),而非空事件或心跳。性能评测须拆解queue_ms、first_text_ms、useful_ms、complete_ms四阶段,并采用开放到达模型压测,结合答案质量设门禁——让AI性能真正对齐业务决策。

“模型首 Token 延迟已经做到 800ms,为什么用户还说慢?”

因为很多系统把收到第一个 SSE 事件当成“首字”。那个事件可能只有 role、空白符,甚至只是心跳。用户真正需要的是“订单能否退款、要补什么材料、下一步点哪里”。如果答案到第 7 秒才出现,800ms 对体验没有解释力。

image.png

性能对象不是一个模型接口,而是一条业务链

以售后助手为例,一次请求可能经历:租户排队、意图路由、订单检索、权限校验、模型生成、退款规则工具、内容安全检查和前端渲染。任一环节的 P99 抬头,都会让用户觉得“AI 卡住了”。

因此,测试报告至少要拆出四个时间:

  • queue_ms:请求进入到真正开始执行;
  • first_text_ms:首个非空、可见字符出现;
  • useful_ms:首次出现可行动结论,例如“可以退款”以及原因;
  • complete_ms:结构化结果和全部文本完成。

image.png

下面的探针用 httpx 读取流式响应。关键点是过滤空事件,并由业务判定函数识别“有效答案”,而不是把 socket 收到字节当体验完成:

import json, time, httpx

ACTION_FIELDS = {"decision", "reason", "next_step"}

async def probe(payload: dict) -> dict:
    start = time.perf_counter()
    first_text = useful = None
    merged = ""

    async with httpx.AsyncClient(timeout=30) as client:
        async with client.stream("POST", "http://sut/chat", json=payload) as resp:
            resp.raise_for_status()
            async for line in resp.aiter_lines():
                if not line.startswith("data:"):
                    continue
                event = json.loads(line[5:])
                text = event.get("delta", "")
                if text.strip() and first_text is None:
                    first_text = time.perf_counter()
                merged += text
                state = event.get("business_state", {})
                if useful is None and ACTION_FIELDS <= state.keys():
                    useful = time.perf_counter()

    end = time.perf_counter()
    ms = lambda t: None if t is None else round((t - start) * 1000)
    return {"first_text_ms": ms(first_text),
            "useful_ms": ms(useful), "complete_ms": ms(end)}

为什么常规并发脚本会把结果测得太乐观

很多压测脚本是“一个虚拟用户收到完整响应后,再发下一次”。当服务变慢,发压端也跟着变慢,于是进入系统的新请求反而减少。最危险的排队时刻被脚本自动回避,这就是协调遗漏。

对客服高峰这类明确到达率的业务,应采用开放到达模型。下面的调度器每秒固定发起请求,不因上一个请求变慢而停下:

import asyncio, time

async def open_loop(rate_per_sec: int, seconds: int, payload: dict):
    tasks, loop = [], asyncio.get_running_loop()
    begin = loop.time()
    total = rate_per_sec * seconds

    for i in range(total):
        due = begin + i / rate_per_sec
        await asyncio.sleep(max(0, due - loop.time()))
        tasks.append(asyncio.create_task(probe(payload)))

    return await asyncio.gather(*tasks, return_exceptions=True)

def percentile(values, p):
    xs = sorted(v for v in values if v is not None)
    return xs[min(len(xs) - 1, int((len(xs) - 1) * p))]

image.png

性能门禁必须与答案质量一起看

单纯压低延迟,可能诱导模型更早输出未经工具确认的答案。建议将样本按纯问答、检索、单工具、多工具、超时恢复分桶,分别统计 P50/P95/P99,并同时检查:业务字段完整率、工具成功率、降级正确率、单请求 Token 与外部调用成本。

一个可执行的门禁可以是:纯问答 useful P95 小于 2.5 秒;单工具场景小于 4 秒;工具超时后 1 秒内明确告知用户并提供人工入口;任何场景不得为了抢首字而先承诺后核验。

所以,TTFT 没有过时,只是它只能回答“流什么时候动了”。用户关心的是“什么时候拿到能做决定的信息”。把这两个问题分开,性能测试才真正站到了业务一侧。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。