分流线画在哪里,取决于你自己服务里请求的输入长度分布,而不是某个固定阈值。可以先按“短输入走整段返回、长输入或不稳定耗时的输入走流式”这条粗线切,然后用同一批真实输入跑两遍,看首段返回时机、超时与重试成本,再决定阈值往左还是往右挪。Ling-3.1-flash 的具体接口字段以官方文档为准,下面给的骨架只保留分流判断、超时重试和结果拼接三件事,字段名按文档替换即可。
分流边界建议从采样数据里读:字符量集中在几千以内的请求,整段返回更好收口;接近或超过上下文窗口的请求,流式能更早给出可读内容。先用一个统一入口包住两种返回,再按首段返回时机和超时记录调整阈值。阈值应是配置项,而不是散落在各处的 if,改动时只改一个地方才不容易漏。
记录一批真实请求的输入长度分布
先用网关日志、应用日志或采样埋点收一批真实请求,采样口径要固定,否则分桶之间不可比。通常需要记录这些字段:messages 各条 content 拼接后的字符数、段落数或消息条数、估算 token 数(用对应模型的分词器,或用字符数按经验比例粗估并标注为估算)、是否附带长引用或附件文本、来源业务标签。分桶建议按字符量切成 0-500、500-2k、2k-8k、8k-32k、32k+ 五档,同时保留一份段落数分桶,用来发现“字符少但段落极多”这种容易被 token 估算低估的情况。
# 每行一条 JSON 请求记录,取 messages 拼接后字符数并按桶计数
jq -r '.messages | map(.content) | join("\n") | length' requests.log \
| awk '{ if ($1 < 500) b="0-500";
else if ($1 < 2000) b="500-2k";
else if ($1 < 8000) b="2k-8k";
else if ($1 < 32000) b="8k-32k";
else b="32k+";
c[b]++ }
END { for (k in c) print k, c[k] }' | sort如果分布里 8k 以上只占很小一部分,把分流线压在 8k 附近是可以的;如果 2k-8k 是主体、且首屏等待对体验影响明显,可以先把线往左挪到 2k 附近。判断不要基于单次请求,至少覆盖一个完整业务周期,再看长尾那一档到底有多少。
为流式返回写最小调用骨架并观察首段返回时机
流式骨架只有两件事:把 stream 打开,按分片读并累积增量。接口字段以官方文档为准,下面是可替换的通用形。
const started = Date.now();
const resp = await fetch(`${API_BASE}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}` },
body: JSON.stringify({ model: 'ling-3.1-flash', messages, stream: true })
});
let firstChunkAt = null;
let acc = '';
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (firstChunkAt === null) firstChunkAt = Date.now() - started;
const text = decoder.decode(value, { stream: true });
for (const line of text.split('\n')) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (payload === '[DONE]') continue;
acc += extractDelta(payload); // 按文档字段取增量文本
}
}要观察的首段返回时间点,是从发出请求到第一个非空增量到达的毫秒数,和总耗时、分片间隔一起记进日志。如果在长输入下首段返回并不比整段返回早多少,且分片之间间隔很长、页面一直在等下一段,流式的体验收益就要打折;反之首段明显更早、增量稳定到达,长输入就值得走流式。这个比较必须用同一批输入的两条记录,单次观察说明不了问题。
为整段返回写超时与重试分支
短输入走整段返回,重点是别让偶发失败直接变成用户可见的错误。超时建议分两类:连接超时保持较短(几秒级别),整体响应超时按长度给不同值,比如短输入用一个较小固定值,靠近分流线的输入适当放宽。具体数值需要结合自己的网络与部署地域确认,不要照搬他人的配置。
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
async function callOnce(messages, timeoutMs) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
return await fetch(PATH, { method: 'POST', signal: ctrl.signal, /* 其余字段按文档填 */ });
} finally {
clearTimeout(timer);
}
}
async function callWithRetry(messages, { maxRetry = 2, baseDelay = 300 } = {}) {
for (let i = 0; i <= maxRetry; i++) {
try {
const resp = await callOnce(messages, timeoutFor(messages));
if (RETRYABLE.has(resp.status)) throw new RetryableError(resp.status);
return resp;
} catch (e) {
if (i === maxRetry || !isRetryable(e)) throw e;
await sleep(baseDelay * 2 ** i + Math.random() * 100);
}
}
}可重试的判断方式:网络层异常(超时、连接被重置)、429、5xx 归为可重试;参数错误、鉴权失败、内容被拒这类 4xx 不要重试,重试只会消耗额度和时间。重试次数建议 1-2 次,并且只对幂等请求重试;如果业务不允许重复计费或重复副作用,重试前先确认接口的幂等行为。整段返回的响应体是一次性拿全的,超时值必须覆盖该长度下的最坏情况,否则大输入会在超时上反复重试,比失败本身更耗时。
把两类返回的拼接逻辑收进同一个函数入口
分流判断散在多处最容易漏改。建议只保留一个入口,上游只传输入和场景标签,内部决定走流式还是整段,并把结果统一成同一种返回结构。入参约定:messages 数组必填;scene 业务标签,用于覆盖阈值;onDelta 可选回调,流式时逐段触发,不传时内部缓冲后一次返回;deadlineMs 可选,表示整体超时。出参约定:text 为拼接后的完整文本;mode 为 'stream' 或 'buffer',便于日志与对比;firstChunkAt 与 totalMs 记录耗时;truncated 为布尔,流式被中断或超时截断时置位。
async function callModel({ messages, scene, onDelta, deadlineMs }) {
const size = measure(messages); // 与采样口径保持一致
const threshold = thresholdFor(scene); // 阈值放配置,按场景可覆盖
const mode = size >= threshold ? 'stream' : 'buffer';
if (mode === 'stream') {
const r = await streamCall(messages, onDelta, deadlineMs);
return { text: r.text, mode, firstChunkAt: r.firstChunkAt, totalMs: r.totalMs, truncated: r.truncated };
}
const r = await bufferCallWithRetry(messages, deadlineMs);
return { text: r.text, mode, firstChunkAt: null, totalMs: r.totalMs, truncated: false };
}measure() 要和采样时的统计口径一致,否则线上分流与离线分析会对不上;thresholdFor() 从配置读取,改线时不改业务代码。上游调用方只认 text、mode 和耗时字段,不关心内部走哪条分支,这样替换返回方式时影响面最小。
用同一批输入对比两种返回方式的可读性与耗时
拿采样里的同一批输入,强制两种返回方式各跑一遍,记录这些字段做对照:输入字符量与估算 token、段落数、返回方式(stream 或 buffer)、首段返回耗时(整段返回记 null 或首字节时间)、总耗时、是否超时、是否触发重试及重试次数、输出字符数、是否被截断,再加一条人工备注,说明这段输出是否需要逐段渲染、首屏是否有明显等待感。
调整分流线的依据是这些对照记录:如果某个长度区间里,流式的首段返回明显早于整段返回的完整耗时,且截断和重试都很少,就可以把这段划给流式;如果两种方式总耗时接近,而流式带来的拼接、状态管理和连接开销更多,就留给整段返回。阈值改动后重跑同一批输入,把对比结果落表,每次只调一个变量,方便回溯是哪一步影响了判断。分桶边界、阈值、超时值都应写成配置并记录改动时间,方便后续复核。