AI From U AI FROM U.COM

博客 / 指南

流式响应与 100 秒的边缘超时

为什么一个不用流式的长请求会在边缘等到约 100 秒就被切断,为什么把 stream 设成 true 就能避免,流式请求的用量是怎么在最后一帧里回来的,以及计量系统在流式请求前后做了什么:什么时候预留、按什么价格结算、中途断线时那笔预留会怎么处理。这些都能在控制台的三个计时上对上:上传、首 token、总耗时。

一个不走流式的请求,在整段回答生成完之前,一个字节都不会发给你。只回一行字的时候你察觉不到;生成很长的内容时,这就是问题的全部。从你的请求送到模型,到回答的第一个字节本该返回,中间可能隔着好几分钟,而站在本站前面的 CDN 边缘只允许大约 100 秒的沉默,超过就会放弃这个响应。过了那条线,请求死在边缘,而模型其实还在写。挡住这件事只需要一个字段,而且开了它,你被扣的钱一分不变。

为什么一个长的非流式请求会死在半路

非流式请求是一次往返,中间夹着一段很长的停顿。你的客户端把 body 发出去,模型把整段回答写完,直到最后一个 token 落定,第一批字节才顺着连接回来。在这两个时刻之间,连接上什么都没有传 — 而边缘盯的正是这个「什么都没有」:大约 100 秒的沉默,就是它在判定响应不会来了之前所允许的全部。你看到的是一个失败的请求。实际发生的是,回答还在写。

流式请求没有那段可以耗尽的停顿。第一个 token 一出现,字节就往你的客户端走,之后每一个 token 都是一边生成一边到达,所以连接从来不会沉默到让边缘放弃的程度。值得记住的是这一点:这不是把超时调长了,而是一个从不安静下来的响应。要花四分钟生成的内容,就流四分钟,然后完整到达。

要改的只有一个字段

这里的流式不是另一个 endpoint、另一把 key,也不是另一套价格。它就是你本来就在发的那个 body 里的一个字段,stream: true,在 /v1/chat/completions 和 /v1/responses 上的用法完全一样。

POST /v1/chat/completions

{
  "model": "astra",
  "stream": true,
  "messages": [
    {"role": "user", "content": "Write the report."}
  ]
}

官方 SDK 里是同一个字段,只是各自的写法:Python 写 stream=True,Node 写 stream: true。你这边真正变的只有回答的形态 — 变成一批一批到达、由你迭代的 chunk,而不是一个等着它出现的对象 — 而这个循环,每个客户端库都已经替你写好了。

计量系统在一次流式请求前后做了什么

走流式不会把你放到计量系统之外,也不会给你一张核不了的账单。用量照样会回来:流的最后一帧带着 token 数,而在 chat completions 上,网关会自己把 stream_options.include_usage 设上,所以这不是你可能忘记去要的东西。不管回答是流式还是一次性返回,计量系统结算用的都是同一组数字。

围绕这一点,顺序和这里每一个请求走的都一样。请求开始时会先取一笔预留 — 你控制台里的余额在那一刻就变了,而不是等最后一个 token 落地才变 — 结算则按流的最后一帧上报的用量来做,用的是取预留那一刻记下的价格。如果连接中途断了,这笔预留会进入对账并被释放,而不是被计入账单:没有走到结算那一步的 token,永远不会被扣钱。

三个计时,不是一个

当一个请求让你觉得慢,控制台会告诉你这是哪一种慢。每个请求都记三个计时 — 上传、到第一个 token 的时间、总耗时 — 因为这是三个不同的问题共用了一个字。上传很长,是你自己的上行链路在把 body 交给我们,这一侧再怎么改也缩不短它。到第一个 token 很快而总耗时很长,是一次长生成在做它该做的事,而这正是流式存在的理由。到第一个 token 本身就很长,才是值得写信来问我们的那一种。

还有一个数字,客户端应该读。当一个请求被 429 拒绝时,Retry-After 这个 header 会原封不动地透传出去 — 官方 SDK 正是照着它来安排重试节奏的,所以尊重它的客户端会按我们实际发出的间隔去等,而不是自己猜一个。

如果连接中途断了,这笔预留会进入对账并被释放,而不是被计入账单:没有走到结算那一步的 token,永远不会被扣钱。

本文为机器翻译,母语审校待完成。以英文版为准。