流式中断不是免费重试:用游标断点续写省下重复token成本

LLM流式请求被抢占后应只续写剩余部分,而非重发整个prompt重复付费。
当LLM流式请求被抢占时,直觉性的"重发整个prompt"会导致为已生成的token重复付费。本文提出一套轻量运维习惯:调用前写下包含id、prompt hash、token预算和游标的job记录,流式输出时按短间隔持续flush前缀;中断后构造续写请求,将输出上限设为预算减去已花费,而非重新发起完整补全。文章同时指出两个关键陷阱:一是输入重放——若供应商对每次续写都重新计费prompt,巨大的system prompt会吞掉节省的成本;二是副作用调用(支付、发信等)不适用此法,需配合幂等键并在流完成后再触发。该方法在无法持久化前缀、需要字节级采样一致性或任务存在改变世界状态的副作用时应跳过。
当一个LLM流式请求被抢占(preempted)时,很多人的第一反应是重发整个prompt、祈祷下一个空闲槽位更友好。这个看似无害的本能,会把一次短暂的中断变成为同一个答案付两次钱的灾难。本文梳理一种轻量级的运维习惯——在流式输出过程中持续记录前缀游标,让中断后只续写剩余部分,而不是从头再来。
需要说明的是,原文作者明确指出:这是一个提议而非实测结果,示例代码未经真实执行,也不假设任何具体的模型名称、配额、机器规格,更不承诺某条免费通道下周还在。
为什么重发整个Prompt是在重复付费
核心认知是:被抢占的流不是免费重试。如果通道已经吐出了token,这些token就已经被消耗掉了。重新发送同样的消息,等于再买一遍你手上已经持有的前缀。
原文用了一个很贴切的比喻:快递员已经把包裹送到你家门口,然后被调离路线。你不会再雇一个快递员从仓库重走一遍——你保留已经送达的部分,只为最后一段楼梯付费。模型流式输出就是这个形状,但多了一道皱褶。
这道皱褶在于输入与输出是两种不同的成本:
- 已经收到的输出绝不该重新生成;
- 但输入上下文可能必须重新提交,因为供应商通常不会为缓存的prompt token给你开收据。
把这两种成本混为一谈,正是账单疯涨而答案却原地踏步的根源。

流式抢占(Preemption)的技术背景
在LLM推理服务中,抢占是指调度器因资源紧张(如GPU显存不足、队列优先级调整)而强行中断某个正在运行的推理任务。与传统HTTP请求不同,流式推理(Streaming Inference)采用Server-Sent Events(SSE)或类似机制,逐token向客户端推送输出。这意味着在被抢占的瞬间,模型可能已经生成并传输了数百乃至数千个token,而这些算力消耗在供应商侧已被计量——无论客户端是否完整收到响应。
主流LLM API(如OpenAI、Anthropic等)通常按实际生成的output token计费,而非按客户端成功接收的token计费。这是"重发整个prompt即重复付费"的根本原因:第一次请求的output token已经产生了费用,第二次重发又从零生成一遍,两次费用叠加,而你只需要第一次已生成部分之后的内容。
调用前先写下一行Job记录
这套方法的起点在调用之前,而不是中断发生时的慌乱之中。在发起请求前,先写一行job记录,给它四样东西:
- 一个 id
- prompt 的 hash
- 一个 token预算(budget)
- 一个归零的 游标(cursor)
其中 hash 是关键的拒绝机制。它让你能拒绝对一个已经被编辑过的prompt做续写——如果hash对不上,说明游标属于另一个问题,强行拼接只会答非所问。
当token陆续到达时,不要只在结尾flush一次,而要按短间隔持续flush。原文一句话点破了差别:结尾才flush是一本成功的日记,而每几十个token就flush一次,才是你真正会遭遇的失败的日记。每次flush存下助手已生成的文本、你视为已消耗的token计数,以及一个单调递增的revision。进程若在两次flush之间崩溃,你只损失这段间隙,而间隙之前的前缀安然无恙。
回归时构造续写,而不是重开一次补全
恢复时的正确动作是构造续写(continuation),并把输出上限设为「预算减去已花费」。
如果API支持assistant prefill(助手预填充),就把已存的前缀放进去,要求它在不重复的前提下继续。如果API不接受prefill,就发送一段简短指令加上未完成的尾部,而不是把整个原始对话加上「再写一篇新文章」的要求重新塞回去。
原文给出的gate逻辑用Python实现,核心是 continuation 函数的判断:
def continuation(job: dict, floor: int = 32) -> dict | None:
remain = job["budget_tokens"] - job["spent_tokens"]
if job["state"] != "open" or remain < floor or not job["prefix"]:
return None
return {
"assistant_prefill": job["prefix"],
"max_output_tokens": remain,
"instruction": "Continue from the prefill. Do not repeat it.",
}
当剩余预算低于你设定的floor,continuation 返回 None,你就停手——绝不「以防万一」地再发一次原始请求。None 意味着前缀已经吃掉了有用的预算,或者根本没有值得续写的前缀。一次完整的重放会直接无视你刚刚算出的结尾。
代码里还有一个工程细节值得留意:flush写文件采用了临时文件改名(temp-file rename)的方式,避免进程在写入中途崩溃时留下半截JSON。但原文坦承这不是分布式锁——两个worker共用同一个job id仍会互相覆盖游标,所以每个在途调用都要有自己的id。
Assistant Prefill(助手预填充)机制说明
Assistant Prefill 是部分LLM API提供的一项特性,允许调用方在请求中预先填入assistant角色的消息内容,模型会将其视为已生成的前缀并从该位置继续输出,而不会重新生成这段文本。Anthropic的Claude API对此有明确支持(在messages数组中最后一条消息角色设为assistant且不含stop_reason),OpenAI的部分接口通过suffix参数或特定prompt构造实现类似效果。
使用prefill的关键约束在于:模型不保证续写内容与"如果从未中断"的原始采样路径完全一致——这本质上是一次基于已有上下文的新采样,随机种子和KV cache状态均已不同。因此本文"什么时候不该用"一节特别指出,若任务要求字节级复现首次采样结果,续写方案并不适用。对于大多数文本生成任务而言,语义连贯性通常已经足够,精确一致性并非必要。
三个出口:续写、放弃、还是外溢
原文强调,你有三个出口,而且应该在调用之前就选好,而不是在恐慌中临时决定:
- 放弃(Abandon):当剩余量低于floor,或前缀为空时。
- 续写(Resume):当前缀是真实的,且剩余量仍在预算之内时。
- 外溢(Spill):仅当任务无法丢弃、且免费槽位已经失败时,才外溢到一条你愿意等待的通道。
关于外溢,作者说得很克制:外溢不是循环。一次外溢,然后交给人工,或者交给一个真正能预留工作量的队列。预留问题比这个游标古老得多,本文并不打算解决它。
(披露:原文声明本文作为 MonkeyCode 产品推广的一部分撰写。其适用场景很窄——MonkeyCode 的免费模型访问和免费服务器选项,可以在你阅读了产品的实时限制之后,承载一次「断流演练」,而且仅限于那些可以被放弃的任务。原文未记录任何配额、信用卡、区域或时长数字,因为这些数字会变动,且没有附上一手来源。)
比抢占更隐蔽的陷阱:输入重放
原文点出了一个更安静的错误赌注——输入重放(input replay)。
如果供应商在每次续写时都重新对prompt计费,那么一个巨大的system prompt会吞掉你「不重新生成前缀」省下的所有钱。应对方法是:在恢复调用上分别记录输入token和输出token。如果恢复后的输入几乎等于原始prompt加上存储的前缀,那你只是转移了成本,并没有消除它。
解法要么是把恢复的上下文收缩到你仍然需要的尾部,要么干脆对这种prompt形状停用续写。游标在它能阻止第二次完整补全时才真正值回票价——它无法让一个臃肿的prompt变便宜,也无法退还供应商已经计量过的token。
Prompt Caching与输入重放的关系
为缓解大型system prompt反复计费的问题,部分供应商推出了Prompt Caching功能(如Anthropic的Cache Control、OpenAI的Prompt Caching)。其原理是对超过一定长度阈值的前缀计算哈希,若短时间内相同前缀再次出现,则复用已缓存的KV cache,并对缓存命中的token给予折扣(通常为原价的10%–50%)。
然而这套机制存在几个不确定性:缓存有效期通常只有几分钟到数小时;跨请求的缓存命中率取决于供应商的负载均衡策略;且折扣幅度和缓存窗口均可能随时调整,不构成计费保证。因此本文建议在恢复调用时分别记录输入和输出token,主动验证输入成本是否真的被压缩,而不是默认缓存一定命中。若输入成本未被有效摊薄,续写方案对于超长system prompt的收益将大打折扣。
什么时候不该用这套方法
作者列出了几种应当跳过这个习惯的场景,相当坦诚:
- 无法把前缀持久化到进程之外时跳过。只活在内存里的游标会随抢占一起死掉,反而留下你本想避免的重放。
- 客户端隐藏token计数、你只能靠字符串长度猜
piece_tokens时跳过。被猜出来的游标是账本里的一个谎言。 - 要求与首次采样字节级一致时跳过。续写是一次全新的抽样(a new draw),而非恢复的采样器,除非供应商有明确文档。不要声称你没有买到的确定性。
- 任何在流结束前就改变世界状态的调用都要跳过。支付捕获、邮件发送、基础设施变更、面向客户的webhook,这些需要幂等键(idempotency key),且副作用应在文本完成之后运行,而不是在流里面。如果一个残缺的答案比沉默更糟,就不要把它吐到一条可能半句话就消失的通道上。
幂等键(Idempotency Key)的工程含义
幂等键是一种防止副作用被重复触发的工程模式:客户端在请求头中附带一个唯一标识符,服务端以此为键记录操作结果;若相同幂等键的请求再次到达(无论是网络重试还是客户端重发),服务端直接返回首次结果而不重新执行操作。支付系统(如Stripe)、消息队列的生产者端以及邮件发送服务通常都要求调用方传入幂等键。
将LLM流式续写与具有副作用的下游操作混合在同一个流程中,是一种高风险的架构选择:流可能在触发副作用之前中断,也可能在中断续写后被错误地重新触发,导致支付被扣两次或邮件被发两封。正确的模式是将LLM的文本生成与副作用执行严格解耦——先完成文本生成并持久化结果,再以幂等键驱动下游操作,两个步骤之间通过队列或事务日志衔接,而不是在一条可能随时断开的流里串联执行。
小结
这个习惯小到足以坚持:开一个job,文本到达时持续flush,死掉时只索要剩余部分,或者什么都不要。免费槽位可以承载这场演练,但它退不回你已经花掉的前缀,也不该成为你不愿失去的工作的最终承诺。对于那些「部分答案比沉默更糟」的关键副作用,免费容量从一开始就是错误的赌注。
相关推荐

AI数字分身实战:用DOT自动追踪Bug与用户反馈
DOTS 团队工程师演示如何用 AI 数字分身 DOT 跨平台收集用户反馈、自动归类问题主题、开线程调查 Bug 根因并直接提交 PR,展示 AI 代理参与缺陷分诊的完整工作流与可审计设计。

晶体管为何不再扁平?从平面到环绕栅极的演进
晶体管从平面到FinFET再到环绕栅极的演进:为何栅极需要从三面包裹到四面环绕,以抑制短沟道效应和泄漏电流,延续摩尔定律。

蜂群思维=单个RL智能体?多臂老虎机等价性证明
一篇论文证明蜂群通过局部模仿进行的集体决策,在数学上等价于单智能体强化学习。N个盲目模仿者相当于N个并行环境采样,有效学习率为1/N。本文解读Maynard-Cross Learning算法的推导与影响。