想象一家深夜食堂的直播厨房:大厨正在为一位客人定制套餐,每做好一道菜就立刻端到桌上,客人边看边吃。突然客人举手喊了一声“停,后面的别上了”。这时真正考验团队的,不是“停止”这个动作本身,而是怎么优雅地收场:厨房要把火关掉、把已经切好的菜放回冰箱、把已经端上桌的菜保留好、把账单按“已经吃过的菜”结清楚,还要记住这位客人的口味,方便他过会儿说“继续”时无缝接上。 大模型的流式输出也一样。用户看到一半不满意,点下“停止”按钮,背后要发生一连串事情:前端发送取消信号、连接关闭、服务端停止生成、GPU资源回收、Token计费结算、部分结果保存、上下文保留。这节课,我们就用这家“直播厨房”的视角,把流式中断处理的七个关键环节一次讲透。

岗位关联

掌握中断处理,能让你的 LLM 应用在用户取消时既不浪费算力、不丢结果、不花冤枉钱,是 AI 产品工程化的基本功。

目录
  1. 可视化演示
  2. 中断触发方式
  3. 信号传递与连接关闭
  4. 服务端资源回收
  5. Token 计费规则
  6. 部分结果处理
  7. 上下文会话管理
  8. 产品交互体验
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

中断触发方式

深夜食堂的直播厨房里,客人有三种方式叫停:举手喊“停”、直接起身离开、或者停止观看直播。大厨并不总能立刻知道客人已经离开,他还在按原计划切菜。流式输出也一样——用户想中断大模型生成,可以有三个入口:点击界面上的“停止”按钮、关闭浏览器标签页、或通过代码里的 AbortController 主动取消请求。所谓 AbortController,就是前端向浏览器喊停的哨子,一声令下,底层请求就会被标记为“取消”。

这三种方式分别对应不同的信号来源,但最终都指向同一个目标:让正在进行的请求进入取消状态。点击停止按钮是最直接的用户动作;关闭标签页会销毁页面上下文,浏览器随即取消请求;AbortController 则让开发者能在任意时刻主动调用 abort(),不需要用户真的去点按钮。

在实际代码里,前端通过 fetch 或 XHR 发起流式请求时,通常会挂一个 AbortController。用户点“停止”时,前端先停止把新到的增量文本渲染到界面,再调用 abort(),让浏览器关闭底层连接。服务端收到连接断开或取消标志后,才会真正停止生成。所以“中断触发”从来不是单一路径,而是多个入口合力发出的刹车信号。

就像客人在第三道菜之后喊停,后厨要记住的是“三道菜”这个节点,而不是从头再数一遍菜单。先明确中断从哪个入口、哪个时刻开始,后面所有优雅退出的动作才有一个可靠的锚点。

tinterrupt=min⁡{tclick,  tabort,  tclose}\displaystyle t_{\text{interrupt}} = \min\{ t_{\text{click}},\; t_{\text{abort}},\; t_{\text{close}} \}
通俗理解:真正的中断时刻,是最早到来、最先触发的那个取消动作。
  1. 三个候选时刻\{ t_{\text{click}}, t_{\text{abort}}, t_{\text{close}} \} 分别代表按钮点击、AbortController 调用和页面关闭发生的时刻,它们是三种可能的触发源。
    {tclick,  tabort,  tclose}\{ t_{\text{click}},\; t_{\text{abort}},\; t_{\text{close}} \}
  2. 取最小值\min 表示取集合中最早的时刻,最早触发的信号决定了中断的起点。
    min⁡{tclick,  tabort,  tclose}\min\{ t_{\text{click}},\; t_{\text{abort}},\; t_{\text{close}} \}
  3. 中断时刻确定t_{\text{interrupt}} 是后续资源回收和计费截止的时间锚点,它越准,整个取消流程越干净。
    tinterruptt_{\text{interrupt}}

信号传递与连接关闭

餐厅里客人的喊声并不会自动传进厨房,需要服务员穿过走廊把话带到。流式场景里也一样,用户点击停止后,“取消”这个信号要穿过网络,让服务端真的知道。SSE 和 WebSocket 是两种常见的流式传输通道,相当于传话的两条走廊。SSE 是服务端到客户端的单向长连接,客户端不能往这条连接里发“停止”消息,只能把连接关掉。WebSocket 是双向通道,客户端可以直接发 Close 帧,相当于拿起电话说“我这边讲完了”。

在 SSE 场景下,前端通常用 AbortController 关闭连接。服务端什么时候才能意识到连接断了?答案是:下一次向这条连接写入数据时。如果一个生成任务卡在 GPU 上做长推理,连接断开事件不会立刻打断推理,而要等这次推理返回、服务端尝试写下一个 chunk 时,才会发现写入失败,从而触发取消。WebSocket 的 Close 帧更明确,服务端收到后可以立即把生成任务标记为取消,但也要等当前这一步计算结束才能安全停掉。

这里有一个容易混淆的“半关闭”语义:一端停止发送,但仍然可以接收对方的消息。就像你先说“我讲完了”,但还拿着电话听对方回应。服务端收到 Close 帧后,不能只清理连接对象,还要把正在运行的生成任务标记为取消,否则模型可能继续在后台计算,白白消耗资源。

服务员不用每时每刻盯着客人,而是在端下一盘菜时发现没人接,才知道客人已经离开。流式服务也不必提前预知中断,只要在下一次“上菜”时检查盘子有没有被接住,就能及时收手。

State={active,write succeedsclosed,write fails\displaystyle \text{State}=\begin{cases}\text{active},& \text{write succeeds}\\ \text{closed},& \text{write fails}\end{cases}
通俗理解:服务端通过“下一次写入成不成功”来判断客人还在不在,这是断线感知的关键机制。
  1. 写入动作\text{write} 是服务端向客户端推送一个数据块的行为,它是断线检测的探针。
    write\text{write}
  2. 写入成功当服务端发现 \text{write succeeds} 时,连接状态记为 \text{active},流水线继续运行。
    active,write succeeds\text{active}, \quad \text{write succeeds}
  3. 写入失败当 \text{write fails} 发生时,连接状态记为 \text{closed},服务端应开始取消任务并回收资源。
    closed,write fails\text{closed}, \quad \text{write fails}

服务端资源回收

大厨接到“别上了”之后,要立刻关燃气、把切好的菜放回冰箱、把占着的灶台腾出来。在 LLM 服务端,这个动作叫资源回收:取消生成任务、释放显存和内存、清理请求生命周期里挂着的各类资源。一次流式请求往往不是一个孤立函数,前端连接、网关、模型推理服务,甚至下游工具调用都串在一起,中断信号必须沿调用链逐层传递。

对大模型来说,最重的资源是推理过程中缓存的中间状态,通常称为 KV Cache。它就像厨师提前备好的半成品食材,堆在灶台一侧。请求一旦取消,这些缓存必须尽快释放,否则高并发场景下,显存会被堆积的“半成品”占满,后面的请求反而排不上队。工程上常用取消令牌或标志位,让生成循环在下一次迭代前看一眼“要不要退出”。

更规范的做法是为每个生成请求分配唯一 request_id,并注册取消回调。中断触发时,取消回调会从任务队列、缓存表和计数器里移除这个请求,让它不再占用 GPU、CPU 和网络连接。这样,一次用户取消不只是“停止输出”,而是把整条链路占用的资源还给系统。

就像没有被及时关火的灶台会把锅烧干,没有被回收的 KV Cache 会让整条推理链路越来越慢。优雅退出的第一步,不是嘴上说“停了”,而是真正把火关掉、把台面腾出来。

Rreclaim=∑i=1NMi×Ii\displaystyle R_{\text{reclaim}} = \sum_{i=1}^{N} M_i \times I_i
通俗理解:被取消任务占用的显存和内存全部加总,就是这次中断能还给系统的资源总量。
  1. 任务总数N 表示当前服务上所有可能占用资源的生成任务,包括正在运行和排队中的任务。
    NN
  2. 单个任务占用M_i 表示第 i 个任务占用的显存或内存,它通常和序列长度、模型规模有关。
    MiM_i
  3. 取消标记I_i 取 1 表示该任务已被取消,取 0 表示仍在运行,它决定这一项是否被计入回收量。
    Ii∈{0,1}I_i \in \{0, 1\}
  4. 求和回收\sum_{i=1}^{N} 把所有已取消任务占用的资源相加,得到本次中断可释放的总资源。
    ∑i=1NMi×Ii\sum_{i=1}^{N} M_i \times I_i

Token 计费规则

客人喊停后,侍者要结账。账单既不是按整桌菜算,也不是只收还没上的菜,而是按已经端上桌的菜收费。LLM 的计费逻辑也一样:已经消耗的 token 都要计费。一次请求的 token 分为两部分:输入侧的 prompt tokens,也就是用户输入的提示词;输出侧生成的 completion tokens,也就是模型回答的内容。用户中断时,模型可能已经生成了几十上百个 token,这些字已经消耗了 GPU 算力,所以必须计入账单。

未生成的 token 没有产生计算,自然不计费。但“未生成”的边界要注意:客户端可能只收到了 50 个 token,服务端因为网络缓冲实际生成了 60 个。这种情况下,以服务端实际生成并记录的 token 数为准。因此用户越早喊停,能省下的输出成本越多;但已经发生的算力,不会因为取消而消失。

在工程实现上,流式接口返回的增量数据里通常带有 usage 信息,或在请求结束时返回 token 统计。为了成本控制,我们要把每次增量累计到计数器,而不是中断后重新数一遍文本。还要保存 prompt token 数、已生成 token 数和停止位置,方便账单溯源和用户申诉。

账单不是惩罚,而是算力消耗的镜子。让用户看到“已生成 120 个 token,费用约 0.003 元”,比默默扣费更能建立信任。

Bill=(Nprompt+Ngenerated)×Ptoken\displaystyle \text{Bill} = (N_{\text{prompt}} + N_{\text{generated}}) \times P_{\text{token}}
通俗理解:账单等于输入 token 数加上中断前已生成的 token 数,再乘以单价。
  1. 输入侧数量N_{\text{prompt}} 是用户发给模型的提示词 token 数,无论是否中断都计入成本。
    NpromptN_{\text{prompt}}
  2. 输出侧数量N_{\text{generated}} 是中断前已经生成并记录的 token 数,它是成本控制的核心变量。
    NgeneratedN_{\text{generated}}
  3. 单价P_{\text{token}} 表示每个 token 的计费价格,通常按模型规格提前约定。
    PtokenP_{\text{token}}
  4. 总价计算(N_{\text{prompt}} + N_{\text{generated}}) \times P_{\text{token}} 把输入和输出相加后乘以单价得到本次请求费用。
    (Nprompt+Ngenerated)×Ptoken(N_{\text{prompt}} + N_{\text{generated}}) \times P_{\text{token}}

部分结果处理

中断之后,已经端上桌的菜不应该被倒进垃圾桶。用户虽然没吃到完整套餐,但已经上桌的几道菜可以继续吃。大模型的输出也一样,已经生成的部分内容是有价值的,应当保存并展示。流式输出的原始数据通常是一串增量块(chunk),每个块里是几个 token。中断时,客户端可能只收到了前 k 个块,需要把这些块按顺序拼接起来,形成当前已生成文本。

拼接时要注意顺序不能乱,尤其涉及流式 JSON、Markdown 代码块时,可能出现残缺的括号或未闭合的代码段。比如用户让模型写一段 Python 代码,中断时可能刚写到 def func():,后面还没有函数体。这种半截代码不是错误,而是“生成到一半”的正常状态,前端要给出完整性提示,避免用户误以为系统故障。

保存策略上,不要让客户端等中断后才一次性向服务端要结果,而应在收到每个 chunk 时就增量落盘,写入内存或数据库。这样即使页面刷新、网络抖动,用户也能立刻看到已生成内容。展示策略上,要明确区分“完整回答”和“中断回答”,用“已停止,内容生成到此处”这类提示,让用户知道边界在哪里。

半成品不是失败品。把已上桌的菜摆放整齐,告诉客人“这是三道菜的进度”,用户会觉得整个系统是体面的,而不是在事故中仓皇收场。

Dsaved=⋃i=1kΔi,Dcomplete=Dsaved∪⋃i=k+1nΔi\displaystyle D_{\text{saved}} = \bigcup_{i=1}^{k} \Delta_i,\quad D_{\text{complete}} = D_{\text{saved}} \cup \bigcup_{i=k+1}^{n} \Delta_i
通俗理解:已经保存的内容是前 k 段碎片按顺序拼起来的结果,继续生成则把后面的碎片再接上去。
  1. 增量块\Delta_i 表示服务端第 i 次推送的文本片段,它是流式输出的最小单位。
    Δi\Delta_i
  2. 已保存结果D_{\text{saved}} = \bigcup_{i=1}^{k} \Delta_i 表示把前 k 个片段按顺序拼接,得到中断时的半成品。
    Dsaved=⋃i=1kΔiD_{\text{saved}} = \bigcup_{i=1}^{k} \Delta_i
  3. 完整结果D_{\text{complete}} 表示在已保存结果之后继续拼接剩余片段,最终得到完整回答。
    Dcomplete=Dsaved∪⋃i=k+1nΔiD_{\text{complete}} = D_{\text{saved}} \cup \bigcup_{i=k+1}^{n} \Delta_i

上下文会话管理

客人想继续吃,厨房不能重新买菜、重新点火。他要记得“这位客人之前点过什么、已经上过哪几道菜”。对应到 LLM,就是保留会话上下文,让模型在续接时知道前面说了什么。中断发生时,客户端的对话历史里已经有一条用户的原始消息,还可能有一条 assistant 的部分回复。要继续生成时,不能只把原始 prompt 再发一遍。

正确的做法是把系统提示词、用户消息、已生成的 assistant 部分一起放进新的请求。这样模型续写时能接着“本周完成了流式中断模块”往下写,而不是重新开始讲一遍。断点续接的本质是状态恢复:用户说“继续”,前端把保存的 partial_text 和原 prompt 拼成新的对话上下文,发送给模型,模型从中断处继续。

上下文管理还要考虑上下文窗口和会话过期。已保存的部分结果要设置生命周期:短时间内的“继续生成”可以复用;超过一定时间或用户主动关闭会话,就要清理,避免占用存储和 context 空间。系统可以通过会话 ID 关联消息,也可以记录最后活跃时间,定期把过期会话归档或删除。

像保留点菜单一样保留上下文,用户隔几分钟回来说“继续”,系统就能无缝接上;如果这单太久没人认领,再把菜单收进文件柜,等下次重新点菜。

Cnext=Csystem+Cprompt+Dsaved\displaystyle C_{\text{next}} = C_{\text{system}} + C_{\text{prompt}} + D_{\text{saved}}
通俗理解:下一次请求的上下文,是系统提示词、用户原始输入和已保存部分结果的拼接。
  1. 系统提示词C_{\text{system}} 是模型需要遵守的系统级指令,比如“你是严谨的周报助手”。
    CsystemC_{\text{system}}
  2. 用户消息C_{\text{prompt}} 是用户本次会话输入的原始内容,它是上下文的起点。
    CpromptC_{\text{prompt}}
  3. 已保存结果D_{\text{saved}} 是中断前生成的 assistant 内容,它让续接有据可依。
    DsavedD_{\text{saved}}
  4. 拼接上下文C_{\text{next}} 是下一次请求携带的完整上下文,模型基于它继续生成。
    Cnext=Csystem+Cprompt+DsavedC_{\text{next}} = C_{\text{system}} + C_{\text{prompt}} + D_{\text{saved}}

产品交互体验

优雅退出还差最后一块拼图:让客人看见“厨房已经收到指令、现在是什么状态”。好的服务员不会在客人喊停后消失,而是告诉他“已经上了三道菜,剩下两道退掉了”。产品交互上,要有明确的停止态提示:显示“已停止生成”、保留的部分内容、本次已消耗的 token 和费用,以及“重新生成”按钮。

对成本较高的长文本请求,可以在用户点击停止时加一次二次确认,防止误触。停止按钮也不应紧挨着“发送”按钮,否则很容易手滑。产品设计上,要区分“主动停止”和“被动断开”:主动停止有明确的状态和账单;被动断开要提示用户“连接已断开,已生成内容已保存”,并提供恢复入口。

从工程角度看,产品交互是中断流程的最后一环:中断信号触发后,前端要更新按钮状态、隐藏加载动画、展示部分结果;后端要上报取消事件、写入计费日志。产品经理可以把“用户平均取消率”“取消后会话续接率”当作体验指标,持续优化防误触和续接入口。

服务员的一句“好的,马上停”,比沉默的等待更让人安心。产品交互就是把这份安心做成可见状态:让用户知道自己的喊停被听见、被处理、被记录。

Rvalid=1−NmisNstop\displaystyle R_{\text{valid}} = 1 - \frac{N_{\text{mis}}}{N_{\text{stop}}}
通俗理解:有效停止率等于停止次数中排除掉误触后的占比,它衡量停止设计是否真正好用。
  1. 停止总次数N_{\text{stop}} 表示用户点击停止的总次数,它是统计口径的总和。
    NstopN_{\text{stop}}
  2. 误触次数N_{\text{mis}} 表示用户不小心点错造成的停止,这是要优化的坏指标。
    NmisN_{\text{mis}}
  3. 误触比例\frac{N_{\text{mis}}}{N_{\text{stop}}} 表示所有停止动作中误触的占比,占比越高体验越差。
    NmisNstop\frac{N_{\text{mis}}}{N_{\text{stop}}}
  4. 有效停止率R_{\text{valid}} = 1 - \frac{N_{\text{mis}}}{N_{\text{stop}}} 越接近 1,说明停止交互越可靠。
    Rvalid=1−NmisNstopR_{\text{valid}} = 1 - \frac{N_{\text{mis}}}{N_{\text{stop}}}

AI 应用场景

智能客服的“停止回答”按钮

阶段:在线推理与交互

解决的问题:传统方案里,用户对长回答不满意时只能干等,要么手动刷新页面,造成已生成内容全部丢失,同时服务端还在继续计算。引入流式中断处理后,前端用 AbortController 发送取消信号,服务端取消生成任务,已生成的内容保留并展示,计费只按实际消耗 token 结算,避免算力和费用的双重浪费。

具体案例:用户问“请介绍贵司全部产品”,看到第一段后觉得太啰嗦,点击“停止”。系统立刻停止新内容渲染,显示“已回答 180 字,已停止生成”,并保留对话上下文。用户接着问“继续”,模型基于已生成的 180 字继续补充,而不是从头开始。

长文档生成器的“中途暂停,稍后继续”

阶段:会话状态管理

解决的问题:长文档生成耗时长、成本高,如果用户中途离开就必须重新生成,体验极差。利用上下文会话管理机制,系统把原始 prompt 和已生成部分一起保存,下次请求时作为上下文传入,实现断点续接。

具体案例:法务人员让 AI 起草一份合同,生成到 60% 时被会议打断,点击“暂停”。系统把草稿和 token 消耗保存到会话中。会议结束后,用户在同一个会话点击“继续”,模型从上次中断处继续写出剩余条款。

直播字幕的“打断修正”

阶段:流式结果处理

解决的问题:实时字幕流式输出时偶尔出错,用户希望立刻停止并修正。通过中断后保留部分结果,用户可以对已生成字幕直接编辑,再把修正后的文本作为上下文继续生成,避免整段重来。

具体案例:直播字幕把“流式中断”识别成“牛市中段”,用户点击停止并手动改成“流式中断”,模型读取修正后的上下文,继续输出后续字幕。中断前的内容没有被清空,而是作为人工纠偏的依据。

Python 演示

python73 行
import time  class StreamingSession:    """模拟一个支持中断和续接的流式生成会话."""     def __init__(self, prompt, price_per_token=0.02):        self.prompt = prompt        self.partial_text = ""        self.generated_count = 0        self.billed_tokens = 0        self.price_per_token = price_per_token        self._stopped = False     def request_stop(self):        self._stopped = True     def _bill(self):        self.billed_tokens = len(self.prompt) + self.generated_count     def stream(self, chunks):        try:            for chunk in chunks:                if self._stopped:                    break                self.partial_text += chunk                self.generated_count += 1                yield chunk                time.sleep(0.05)        finally:            self._bill()     def resume(self, chunks):        self._stopped = False        try:            for chunk in chunks:                if self._stopped:                    break                self.partial_text += chunk                self.generated_count += 1                yield chunk                time.sleep(0.05)        finally:            self._bill()     def cost(self):        return round(self.billed_tokens * self.price_per_token, 4)  def main():    session = StreamingSession("请写一份周报", price_per_token=0.02)    chunks = ["本周完成", "了流式", "中断模块", "联调", "下周计划", "做性能", "压测"]     gen = session.stream(chunks)    for token in gen:        if session.generated_count >= 3:            session.request_stop()            gen.close()            break     print("已保存部分结果:", session.partial_text)    print("已计费 token 数:", session.billed_tokens)    print("当前费用:", session.cost())     for token in session.resume(chunks[3:]):        pass     print("续接后的完整结果:", session.partial_text)    print("最终费用:", session.cost())  if __name__ == "__main__":    main()
目的:用一个模拟的流式生成会话,演示用户中途喊停后如何保留部分结果、按已生成 token 计费,并在之后用上下文续接。
  1. self.partial_text += chunk每收到一个增量块就存进半成品文本,中断后不会丢内容。
  2. if self._stopped: break每次产出新块前检查取消标志,用户喊停就立刻退出生成循环。
  3. gen.close()关闭生成器,触发 finally 里的记账逻辑,让计费立即生效。
  4. self._bill()按“prompt 长度 + 已生成数量”更新计费,未生成的块不计费。
  5. session.resume(chunks[3:])用保存的上下文继续生成,模拟用户说“继续”后的断点续接。

常见误区

误区用户点停止后,前端按钮已经禁用、界面不再渲染,所以模型应该已经停了。

正解前端停止渲染不等于后端停止生成。大模型在服务端按增量生成,客户端断开后可能还有缓冲中的 token 继续产出。必须通过 AbortController 或 WebSocket Close 帧把取消信号传到服务端,服务端还要在生成循环里检查取消标志,才能安全停下。

为何常见:UI 层先停了,给人“已经取消”的错觉,但服务端可能仍在 GPU 上消耗算力。

误区我主动停止生成了,没有拿到完整答案,这次调用不应该收费。

正解计费按实际消耗 token 计算。prompt tokens 全部计费,已生成的 output tokens 也计费,只有未生成的 token 不计费。中断只帮你避免未来的消耗,不能免掉已经发生的算力成本。

为何常见:用户视角是“我没得到完整答案”,但计费视角是“服务端已经跑了一段算力”。

误区半截回答留着没用,还可能让模型下一轮生成出错,不如全部清掉。

正解应该把已生成部分作为 assistant 内容保留在会话历史,等用户说“继续”时拼进下一次请求,让模型续写而不是重写。半截代码块需要加完整性提示,但不应粗暴清空。

为何常见:恐惧半截结果污染上下文,其实半截结果也是一种有效状态,关键是在展示层说清楚“生成到哪了”。

误区只要 HTTP 连接断开,服务端马上就知道,不用做额外处理。

正解HTTP/SSE 是单向长连接,服务端通常在下一次写入 chunk 时才发现连接已断开。如果模型卡在某次长推理里,要等推理返回才能检查。需要心跳/注释帧及时发现,并在模型调用层设置取消传播。

为何常见:把 TCP 断开想象成即时电话挂断,忽略了服务端还沉浸在本地计算任务中。

面试题

Q1. 用户点击“停止”后,前端首先应做什么?

单选题 · easy

  • A.立刻把页面上的文本全部清空
  • B.停止本地渲染并发送取消请求← 正确答案
  • C.马上刷新浏览器重新加载页面
  • D.等待服务端自己发现断线

答案:B。前端要先停止渲染,让用户不再看到新内容,同时把取消信号发出去。清空文本会丢掉已有结果,刷新页面会丢失上下文,等待断线发现则浪费时间且可能无法真正取消。

Q2. 一次流式生成了 8 个 token 后用户中断,计费时通常应计算?

单选题 · easy

  • A.只算未生成的 token,生成的不算
  • B.只算提示词的 token,输出免费
  • C.算提示词和已生成的 8 个 token← 正确答案
  • D.全部 0 个 token,因为是用户主动停

答案:C。输入侧的 prompt tokens 和输出侧已生成的 token 都已经消耗算力,都要计费;未生成的 token 不计费。这就像已经端上桌的菜,客人退货也要结账。

Q3. 以下哪些方式可以触发流式中断?

多选题 · mid

  • A.用户点击页面上的停止按钮← 正确答案
  • B.浏览器标签页被用户关闭← 正确答案
  • C.调用 AbortController 的 abort 方法← 正确答案
  • D.服务端模型自动增加上下文窗口

答案:ABC。停止按钮、页面关闭和 AbortController 都是客户端主动取消的入口。模型自动增加上下文窗口与中断无关。

Q4. 在 SSE 流式输出中,服务端通常在什么时机感知到用户中断?

单选题 · mid

  • A.用户点击停止按钮的那一刻
  • B.TCP 连接一关闭就立刻中断推理
  • C.下一次向连接写入内容时发现失败← 正确答案
  • D.模型权重被动态卸载的时候

答案:C。SSE 是单向长连接,服务端不能实时监听关闭事件,只能在下一次写入时感知失败。这像服务员端下一道菜时才发现客人已经走了。

Q5. 用户中断后希望“稍后继续生成”,应用层应如何保存会话?

单选题 · mid

  • A.清空全部历史,只保留系统提示词
  • B.保留原始提示词和已生成的部分结果← 正确答案
  • C.只保存最后输出的一个 token
  • D.把中断前内容复制到用户剪贴板

答案:B。续接的关键是把原始 prompt 和已生成的 assistant 部分一起作为上下文。清空历史会让模型重写,只保存一个 token 则失去上下文语义。

Q6. 服务端收到中断信号后,合理的资源回收动作包括?

多选题 · mid

  • A.取消当前模型的生成任务← 正确答案
  • B.释放该请求占用的显存和内存← 正确答案
  • C.通知下游链路取消级联调用← 正确答案
  • D.立刻重启整个推理服务进程

答案:ABC。取消任务、释放显存、级联取消都是合理的清理动作。重启服务会影响其他用户,属于严重误操作。

Q7. WebSocket 半关闭语义的正确理解是?

单选题 · hard

  • A.两端同时关闭消息通道并销毁连接
  • B.一端停止发送但仍可接收对方消息← 正确答案
  • C.连接关闭后服务端不再需要清理资源
  • D.关闭帧只能由客户端发送给服务端

答案:B。半关闭像电话里“我讲完了,但还听着”。它不是瞬间销毁连接,服务端仍要按协议处理后续消息并完成资源清理。

Q8. 中断后已经生成的部分内容,最好的处理方式是?

单选题 · easy

  • A.直接丢弃并告诉用户请求失败
  • B.保留展示并说明生成在此中断← 正确答案
  • C.强制模型继续生成完整结果
  • D.只在控制台打印,不展示给用户

答案:B。部分结果有业务价值,应保留并展示,同时提示“生成到此处中断”。强制继续违背用户意志,只打印不展示则完全失去意义。

Q9. 要减少“误触停止”带来的成本和体验问题,产品可采取?

多选题 · hard

  • A.对高成本请求二次确认暂停← 正确答案
  • B.停止后展示已消耗 token 数← 正确答案
  • C.停止后提供“重新生成”入口← 正确答案
  • D.将停止按钮放在手机屏幕边缘

答案:ABC。二次确认、成本可视化和重新生成入口都能降低误触影响。把按钮放屏幕边缘反而更容易误触。

Q10. 某 API 返回了 1000 个 token,但客户端在 500 个 token 时断开,服务端日志显示实际生成了 600 个 token。计费以下哪个说法正确?

单选题 · extreme

  • A.按客户端接收的 500 个计费
  • B.按服务端已生成的 600 个计费← 正确答案
  • C.按模型完整生成的 1000 个计费
  • D.按 prompt token 数单独计费

答案:B。计费以服务端实际生成为准,而不是客户端收到的数量。已生成的 600 个 token 消耗了算力,因此计入账单;未生成的 400 个不计费。这也说明尽早取消能减少多余消耗。