逐日AI

面试题库

共 328 题,当前筛选 12 题。

标签
还有 361 个标签
#coding-agent9#distributed-systems8#multi-agent8#rag8#chunking7#debugging7#pipeline-design7#structured-output7#agent-loop6#mcp6#operations6#prompt-injection6#sse6#tool-calling6#tool-design6#context5#context-engineering5#deployment5#embeddings5#hybrid-search5#message-bus5#scheduling5#system-prompt5#agentic-rag4#behavioral4#client4#concurrency4#consistency4#framework-design4#ingestion4#prompt-engineering4#rate-limiting4#retrieval4#routing4#trade-offs4#abstention3#agents-sdk3#caching3#communication3#context-window3#cost-control3#data-quality3#failure-modes3#image-generation3#interview-prep3#langgraph3#latency3#llm-as-judge3#long-context3#model-migration3#model-routing3#orchestration3#ordering3#progressive-disclosure3#prompt-basics3#prompt-caching3#protocol3#provider-abstraction3#ranking3#recall3#redis-streams3#responses-api3#resume3#retry3#runtime3#scalability3#skills3#state-management3#statelessness3#subagents3#timeline3#versioning3#workflow-engine3#access-control2#agent-design2#async-task2#auth2#checkpointing2#citation-verification2#citations2#claude-md2#code-review2#compaction2#context-rot2#contextual-retrieval2#cost-tradeoff2#data-modeling2#database2#distribution2#fallback2#ffmpeg2#golden-set2#grounding2#interview-process2#llm-basics2#long-term-memory2#media-pipeline2#memory2#multi-hop2#multi-tenancy2#oauth2#prioritization2#project-storytelling2#prompt-techniques2#query-rewriting2#react2#refusal2#reproducibility2#rerank2#retrieval-quality2#retrospective2#scripts2#sharding2#state-machine2#tool-permissions2#tooling2#tools2#tracing2#transport2#tts2#ux2#vector-database2#verification2#workflow2#agent-basics1#agent-quality1#agent-sdk1#agents-md1#altitude1#analytics1#approvals1#architecture-review1#async1#atomicity1#attention-budget1#av-sync1#backoff1#bi-encoder1#bm251#budget-control1#build-vs-buy1#cancellation1#candidate-selection1#capacity-planning1#career1#chain-of-thought1#ci1#circuit-breaker1#claude-code1#cli-design1#client-integration1#cloud1#compliance1#compression1#configuration1#confused-deputy1#consistent-hashing1#content-hash1#content-safety1#context-assembly1#context-compression1#context-management1#copyright1#coreference1#correctness1#cost-accounting1#cost-analysis1#cost-optimization1#cross-encoder1#cross-model1#customer-support1#dag1#decision-making1#decomposition1#degradation1#deliberate-practice1#design1#diagnostics1#dimensions1#docker1#documentation1#embedding-migration1#encoding1#engineering-judgement1#engineering-tradeoffs1#error-propagation1#escalation1#eval1#event-driven1#evidence1#failure-analysis1#fairness1#faithfulness1#fan-out1#feedback-loop1#fencing-token1#few-shot1#filter-pushdown1#filtering1#fine-tuning1#forking1#four-elements1#framework-selection1#frontend1#global-market1#graph-rag1#guardrails1#hallucination1#handoff1#handoffs1#headless1#hnsw1#hooks1#human-in-the-loop1#hybrid1#hyde1#incremental-recompute1#incremental-sync1#index-maintenance1#index-routing1#indexing1#information-retrieval1#instruction-hierarchy1#integration1#intent-routing1#interrupt-merge1#invalidation1#isolation1#iterative-scan1#ivfflat1#json-parsing1#json-schema1#just-in-time1#jwt1#knowledge-organization1#labeling1#latency-budget1#lease1#least-privilege1#llm-output-quality1#long-session1#loop-guard1#maintenance1#mcp-basics1#mental-model1#messages-api1#metadata1#methodology1#metrics1#migration1#mobile1#model-selection1#moderation1#modularity1#multi-turn1#multimodal1#nodejs1#normalisation1#notifications1#ocr1#offline-testing1#openai1#overlap1#pagination1#parent-child1#pdf-parsing1#performance1#permissions1#persistence1#pgvector1#pipeline-reliability1#portfolio1#primitives1#priority-queue1#proactive-messaging1#product-engineering1#production-readiness1#prompt1#prompt-assembly1#prompt-bloat1#prompt-design1#prompt-limits1#prompt-surface1#prompt-template1#prompt-versioning1#prompting1#protocol-versions1#quality1#quality-check1#quantization1#query-transformation1#quiet-hours1#rag-basics1#rank-fusion1#reasoning1#reconnect1#redis1#reflection1#replay1#reporting1#retrieval-failure1#retrieval-metrics1#retry-semantics1#retry-strategy1#review1#risk-assessment1#rollback1#rollout1#rrf1#safety1#sandbox1#sandboxing1#scaling1#schema-design1#schema-validation1#scoping1#secrets-management1#self-assessment1#self-introduction1#self-presentation1#self-reflection1#server-design1#service-architecture1#session-management1#sessions1#similarity1#skill-authoring1#skill-description1#skill-design1#spec1#split-brain1#stakeholder-communication1#star1#state-persistence1#stateless1#stdio-transport1#stopping-criteria1#storytelling1#subagent1#subscriptions1#subtitles1#task-graph1#team-governance1#test-set1#test-strategy1#testing1#thresholds1#timezone1#token-accounting1#token-budget1#tool-budget1#tool-execution1#tool-naming1#tool-schema1#trust-boundary1#tuning1#validation1#vector-index1#workflow-design1#xml-tags1#zero-downtime1

30 天从前端工程师到 Agent 工程师

D1 LLM API 基础:messages/roles、token、流式、temperature;Agent 到底是什么

  • 为什么 LLM 应用几乎都用流式输出?SSE 和 WebSocket 该怎么选?Why do LLM apps stream responses, and how do you choose between SSE and WebSockets?
    国内高频海外高频进阶#streaming#protocol

    分析过程 · 先想清楚再作答

    1. 第一问考的是对延迟指标的敏感度:要能区分「首字延迟」和「全文延迟」,并说出模型逐 token 生成决定了前者远小于后者。
    2. 把它翻译成产品语言:用户 1 秒内看到反馈 vs 对着空白等 20 秒,这是体验的分水岭,不是锦上添花。
    3. 第二问不要背优缺点表,先问自己「客户端需不需要频繁上行」——这一条几乎决定了答案。
    4. 只需要服务器往下推 token,SSE 就够:它跑在普通 HTTP 上,代理和负载均衡友好,还自带重连。需要语音、协同、频繁打断这类双向高频交互,才值得上 WebSocket。
    5. 给出多数产品的真实形态:请求走普通 POST,回复走 SSE,另配一个取消接口——顺势可以引到「POST 的 SSE 用不了 EventSource 的自动重连」这个坑。

    How to reason about it · think before answering

    1. The first half tests latency literacy: separate time-to-first-token from total latency and tie it to sequential generation.
    2. Translate to product terms: feedback within a second versus twenty seconds of blank screen.
    3. For the second half, skip the pros-and-cons table and ask whether the client needs frequent upstream messages.
    4. Server-to-client tokens only means SSE suffices: plain HTTP, proxy-friendly, with built-in reconnection. Voice, collaboration or frequent interrupts justify WebSockets.
    5. State the common shape: plain POST for the request, SSE for the reply, plus a cancel endpoint — which sets up the trap that POST-based SSE cannot use EventSource auto-reconnect.

    答题要点

    • 模型逐 token 生成,首字延迟远小于全文延迟;流式让用户 1 秒内看到反馈而不是等 20 秒
    • SSE 是单向、基于 HTTP 的文本协议,自动重连、穿透代理容易,天然适合服务器→客户端的 token 流
    • WebSocket 双向、更适合需要客户端频繁上行(语音、协同编辑、打断)的场景,但代理/负载均衡更麻烦
    • 多数聊天产品:请求用普通 HTTP POST,回复用 SSE;需要打断时再加一个取消接口

    Key points

    • Models emit tokens sequentially; time-to-first-token is far lower than full latency
    • SSE is one-way over HTTP with built-in reconnect and easy proxying, ideal for server→client token streams
    • WebSockets are bidirectional, better when the client sends often (voice, collaboration, interrupts) but harder to load-balance
    • Most chat products: plain POST for the request, SSE for the reply, plus a cancel endpoint
  • 流式回复到一半网络断了,前端和后端各要做什么?EventSource 的自动重连能用上吗?A streaming reply is cut off mid-way. What do the client and server each do, and can EventSource auto-reconnect help?
    国内高频海外高频进阶#streaming#reliability#sse

    分析过程 · 先想清楚再作答

    1. 这题的陷阱在后半句。很多人背过「SSE 自带重连」,就直接答自动重连能救——那是错的,必须先分清两种 SSE 用法。
    2. 浏览器原生 EventSource 确实按规范自动重连:重连时带 Last-Event-ID 请求头,服务器用 id: 打点、用 retry: 设间隔;但它只能发 GET,且要求响应 Content-Type 是 text/event-stream。
    3. 而 LLM chat API 必须 POST(messages 要放在请求体里),所以实际用的是 fetch 加手写 SSE 解析——EventSource 那套自动重连一行都用不上。
    4. 于是前端职责变成:自己判定断流、自己重试、自己保存已收到的部分。后端职责是让重试是安全的——响应可续、副作用幂等。
    5. 给出续写策略并说清边界:把已收到的内容作为上下文构造续写请求;但工具调用块和思考块无法部分恢复,只能从最近的完整文本块续。
    6. 可预期追问:非 200 响应会重连吗?按规范不会——状态码不是 200 或 Content-Type 不对,连接直接判定失败;服务器还可以用 204 主动叫停重连。

    How to reason about it · think before answering

    1. The trap is the second half: people who memorized 'SSE reconnects automatically' answer yes, which is wrong.
    2. Native EventSource does auto-reconnect per spec, sending Last-Event-ID, with the server marking events via id: and setting the interval via retry: — but it only issues GET and requires Content-Type text/event-stream.
    3. LLM chat APIs require POST because messages go in the body, so real clients use fetch plus hand-written SSE parsing, where none of that machinery applies.
    4. So the client owns detection, retry and buffering of what arrived; the server's job is making retries safe — resumable output and idempotent side effects.
    5. Give the continuation strategy and its limits: feed the received prefix back as context, but tool-use and thinking blocks cannot be partially recovered — resume from the last complete text block.
    6. Follow-up to expect: does a non-200 reconnect? Per spec no — a non-200 status or wrong Content-Type fails the connection, and a 204 tells the browser to stop reconnecting.

    答题要点

    • 先区分两种 SSE:浏览器原生 EventSource 自动重连并带 Last-Event-ID,但只能 GET;LLM API 走 POST,用不上这套
    • 所以前端要自己检测断流、自己重试,并保留已收到的部分内容
    • 续写策略:把已收到的内容作为上下文发起新请求,让模型接着写,而不是整轮重来
    • 边界:tool_use 和 thinking 块无法部分恢复,只能从最近的完整文本块续
    • 后端要保证重试安全:响应可续、工具副作用幂等,并对已产生的用量正确计费

    Key points

    • Separate the two SSE modes: native EventSource auto-reconnects with Last-Event-ID but is GET-only; LLM APIs use POST and cannot rely on it
    • The client must therefore detect the break, retry itself, and keep whatever text already arrived
    • Continuation: send the received prefix as context so the model resumes rather than restarting the turn
    • Limits: tool_use and thinking blocks cannot be partially recovered; resume from the last complete text block
    • The server must make retries safe: resumable responses, idempotent tool side effects, correct billing for tokens already produced
  • 用户切到后台或者直接关掉网页,回来后怎么恢复那条还在生成的回复?The user backgrounds the app or closes the tab. How do you restore a reply that was still being generated?
    国内高频海外高频深入#streaming#reliability#architecture

    分析过程 · 先想清楚再作答

    1. 先识别这题和「网络断了」不是同一个问题:客户端已经不存在了,任何写在前端的重试逻辑都不会执行。
    2. 由此推出唯一出路:生成过程必须能脱离这个客户端独立存活,也就是把流本身放到服务端持久化。
    3. 落到具体架构:发起请求时给这轮生成分配一个流 id,服务端一边把 token 推给当前连接,一边把同样的内容写进 Redis 之类的存储;会话记录里保存这个 activeStreamId。
    4. 恢复路径是另开一个 GET 端点:客户端带着会话 id 请求,服务端按 activeStreamId 找到那条流并接着推;找不到活跃流就返回 204,让前端知道没有需要恢复的东西。
    5. 说清代价,别只说方案:多了一份存储、一套过期清理、以及「同一条流可能被多个连接消费」的并发问题。
    6. 延伸:这套结构和普通聊天产品的「消息已持久化,重进会话直接读库」不同——区别在于回复还在生成中,需要的是可续的流而不是一条静态记录。

    How to reason about it · think before answering

    1. First separate this from a dropped connection: the client is gone, so no client-side retry will ever run.
    2. That leaves one option — the generation must outlive the client, which means persisting the stream server-side.
    3. Concretely: assign a stream id per generation; the server pushes tokens to the live connection while also writing them to storage such as Redis, and the chat record stores that activeStreamId.
    4. Recovery is a separate GET endpoint: the client asks with the chat id, the server locates the stream by activeStreamId and resumes; with no active stream it returns 204.
    5. Name the costs, not just the design: extra storage, expiry/cleanup, and concurrency when several connections consume the same stream.
    6. Extension: this differs from ordinary message persistence because the reply is still being produced — you need a resumable stream, not a static row.

    答题要点

    • 客户端已经不在了,前端重试无从谈起,必须让生成过程在服务端独立存活
    • 发起生成时分配流 id,服务端边推送边把内容写进 Redis,会话里记录 activeStreamId
    • 恢复走单独的 GET 端点:按会话 id 找到活跃流接着推,没有活跃流就返回 204
    • 代价:额外存储、过期清理,以及同一条流被多个连接消费的并发处理
    • 与「消息持久化后重新读库」的区别在于回复仍在生成中,需要的是可续的流

    Key points

    • The client is gone, so recovery must live server-side: the generation has to outlive the connection
    • Assign a stream id at start; the server writes tokens to Redis while streaming, and the chat stores activeStreamId
    • Resume through a dedicated GET endpoint that replays the active stream, returning 204 when there is none
    • Costs: extra storage, expiry and cleanup, and concurrent consumers of one stream
    • It differs from plain message persistence because the reply is still in flight, so you need a resumable stream
  • 移动端 App 里的对话,网络频繁抖动,你会怎么设计重连策略?On mobile, connectivity is flaky. How would you design the reconnection strategy for a chat feature?
    国内高频海外高频进阶#reliability#mobile#streaming

    分析过程 · 先想清楚再作答

    1. 先说明移动端和浏览器的差别:网络在 WiFi 与蜂窝之间切换、App 会被系统挂起、后台执行时间受限,所以不能照搬网页那套。
    2. 重试节奏用指数退避加随机抖动。抖动这一条常被忽略,但它是防止大面积断网恢复后所有客户端同时涌上来把服务打垮的关键。
    3. 要设上限:最大重试次数与最大退避间隔,超过就转成显式的「重新加载」按钮交给用户,而不是无限静默重试。
    4. 区分「短暂抖动」和「真的没网」:监听系统的网络状态变化,没网时直接停止重试并进入离线态,等网络恢复事件再立刻重连,比盲目定时重试省电得多。
    5. 结合上一题的服务端持久化:App 被系统杀掉后重进,靠会话 id 请求恢复端点,而不是指望本地缓存拼出完整回复。
    6. 最后补发送侧:用户在离线时发出的消息进本地队列,恢复后按序重发,且每条带幂等键,避免重复发送。

    How to reason about it · think before answering

    1. Start with what makes mobile different: network switches between WiFi and cellular, the OS suspends apps, background time is limited.
    2. Use exponential backoff with jitter; jitter is the commonly missed part that prevents a thundering herd when a wide outage clears.
    3. Set ceilings: max attempts and max interval, then surface an explicit reload action instead of retrying silently forever.
    4. Distinguish a brief blip from being genuinely offline: subscribe to OS connectivity events, stop retrying when offline, and reconnect on the restore event — far cheaper on battery than blind timers.
    5. Combine with server-side persistence: after the OS kills the app, resume by chat id rather than reconstructing from local cache.
    6. Finally the send path: queue outgoing messages while offline and replay them in order, each with an idempotency key.

    答题要点

    • 移动端特殊性:WiFi 与蜂窝切换、App 被挂起、后台执行时间受限,不能照搬网页策略
    • 指数退避加随机抖动,抖动用于避免大面积恢复时的重连风暴
    • 设最大重试次数与最大间隔,超过后转为显式的重新加载入口,不做无限静默重试
    • 监听系统网络状态:离线直接停重试进入离线态,收到恢复事件再重连,比定时轮询省电
    • 回复恢复依赖服务端持久化,靠会话 id 请求恢复端点;发送侧用本地队列加幂等键按序重发

    Key points

    • Mobile differs: network handoffs, OS suspension, limited background time — do not copy the web strategy
    • Exponential backoff with jitter, where jitter prevents a reconnect storm when an outage clears
    • Cap attempts and interval, then hand the user an explicit reload instead of retrying forever
    • Listen to OS connectivity events: stop while offline, reconnect on restore, which saves battery over polling
    • Resume replies via server-side persistence by chat id; queue outgoing messages with idempotency keys
  • 用户主动点「停止生成」和网络意外断开,在服务端看起来都是连接没了,怎么区分处理?A user pressing stop and a dropped connection both look like a closed connection server-side. How do you tell them apart?
    国内高频海外高频深入#streaming#reliability#ux

    分析过程 · 先想清楚再作答

    1. 先点破为什么要区分:主动停止是「用户不想要了」,应当立即释放算力并结束这轮;意外断开是「用户还想要」,理应保留结果供恢复。处理反了,用户要么白花钱,要么回来发现内容没了。
    2. 所以不能只靠 TCP 连接状态判断——它对两种情况的表现是一样的。必须有一个显式信号。
    3. 做法是给「停止」单独一个接口:前端点停止时先调这个接口,带上 run id,服务端据此把该轮标记为「用户取消」,再中止上游模型调用。
    4. 而单纯的连接关闭一律按「意外断开」处理:继续把已生成内容落盘、保留可恢复的流,等客户端回来续。
    5. 补一个现实约束:停止请求本身也可能因为断网而发不出去。所以服务端还需要兜底——比如流没有任何消费者超过一定时间就自行结束,避免算力空转。
    6. 延伸到计费:两种情况都要为已经产生的 token 计费,因为上游厂商已经收了钱;区别只在于要不要保留结果和是否继续生成。

    How to reason about it · think before answering

    1. Say why it matters: stop means the user no longer wants the output, so free compute and end the run; a drop means they still want it, so preserve the result for resumption.
    2. Connection state alone cannot distinguish them — it looks identical — so you need an explicit signal.
    3. Give stop its own endpoint: the client calls it with the run id before closing, and the server marks the run as user-cancelled and aborts the upstream call.
    4. Treat a bare connection close as an unexpected drop: keep persisting output and hold the stream for resumption.
    5. Add the real-world caveat: the stop request itself may fail to send when the network is down, so the server needs a fallback — end a stream with no consumer after a timeout.
    6. Extend to billing: both cases still owe for tokens already produced, since the upstream provider has charged; they differ only in whether output is retained.

    答题要点

    • 两者语义相反:主动停止要立即释放算力并结束,意外断开要保留结果等待恢复
    • TCP 连接状态无法区分,必须有显式信号:给停止单独一个接口,带 run id 标记为用户取消
    • 只收到连接关闭一律按意外断开处理,继续落盘并保留可恢复的流
    • 兜底:停止请求本身也可能发不出去,服务端需对长时间无消费者的流自行结束
    • 计费上两者都要为已产生的 token 记账,区别只在于是否保留结果、是否继续生成

    Key points

    • The semantics are opposite: stop frees compute immediately, a drop preserves output for resumption
    • Connection state cannot distinguish them, so add an explicit stop endpoint carrying the run id
    • Treat a bare close as an unexpected drop: keep persisting and hold the stream for resume
    • Fallback: the stop call may itself fail to send, so end streams with no consumer after a timeout
    • Both still bill for tokens already produced; they differ only in retention and whether generation continues

D7 封装成服务:Fastify + SSE + Docker(dg P07);W1 复盘

  • 流式返回大模型回复,你会选 SSE 还是 WebSocket?为什么?For streaming LLM responses, would you pick SSE or WebSocket, and why?
    国内高频海外高频基础#sse#streaming#api-design

    分析过程 · 先想清楚再作答

    1. 这题的题眼是「怎么选」,不是「有什么区别」。只背出「SSE 单向、WebSocket 双向」拿不到分,因为那是文档第一段。
    2. 先问自己一个问题,它几乎决定了答案:这条连接上客户端需不需要频繁上行?聊天补全是「一次请求、一路往回推」,上行只有最开始那一次,完全落在 SSE 的形状里;协同编辑、实时游戏、语音这种双向高频才轮到 WebSocket。
    3. 然后给 SSE 的三条实际好处:它就是普通 HTTP,鉴权头、Cookie、限流、日志、CDN、反向代理这一整套现成设施全部照用;服务端只是往响应里写字节,不需要额外的连接管理;协议是纯文本,出问题 curl 一下就能看。WebSocket 走的是升级后的独立协议,前面那套东西大多要重做一遍。
    4. 接着说 SSE 的两个真实限制,主动说破比被问出来强:一是浏览器原生的 EventSource 只能发 GET,而大模型接口必须 POST,所以真实前端都是 fetch 手写解析,规范里那套 Last-Event-ID 自动重连一行都用不上;二是 HTTP/1.1 下同域并发连接数有限制,多个标签页各开一条长连接会互相挤占,HTTP/2 之后这条基本消失。
    5. 结论要落到一句可判断的话:单向推送选 SSE,双向高频选 WebSocket;拿不准就先用 SSE,因为它的退路是加一个上行接口,而 WebSocket 的退路是重做整套基础设施。
    6. 可以预期的追问:那大模型产品里的「停止生成」按钮怎么办?答案是它根本不需要走同一条连接——另发一个普通的 POST 请求带上这次生成的 id,服务端收到就中止上游,SSE 那条连接自然结束。这个追问很能区分有没有真做过。

    How to reason about it · think before answering

    1. The hinge is 'how would you pick', not 'what is the difference'. Reciting 'SSE is one-way, WebSocket is two-way' scores nothing — that is the first paragraph of any doc.
    2. Ask one question that nearly decides it: does the client need frequent upstream messages on this connection? Chat completion is one request followed by a long push, which is exactly SSE's shape. Collaborative editing, realtime games and voice are what WebSocket is for.
    3. Give three practical wins for SSE: it is ordinary HTTP, so auth headers, cookies, rate limiting, logging, CDNs and reverse proxies all keep working; the server just writes bytes into a response, with no separate connection lifecycle to manage; and the wire format is plain text, so curl is your debugger. WebSocket runs an upgraded protocol where most of that tooling has to be rebuilt.
    4. Volunteer SSE's two real limits before they are raised. First, the browser's native EventSource can only issue GET, while model endpoints require POST, so real frontends hand-roll the parser with fetch and the spec's Last-Event-ID auto-reconnect never applies. Second, HTTP/1.1 caps concurrent connections per origin, so several tabs each holding a stream compete; HTTP/2 largely removes this.
    5. Land on a decision rule: one-way push means SSE, high-frequency bidirectional means WebSocket, and when unsure start with SSE — its escape hatch is adding one upstream endpoint, while WebSocket's escape hatch is rebuilding your infrastructure.
    6. Expect the follow-up: what about the 'stop generating' button? It does not need the same connection — send a plain POST carrying the run id, have the server abort upstream, and the SSE stream ends on its own. This one separates people who shipped it from people who read about it.

    答题要点

    • 先判断上行频率:一次请求、一路往回推的场景(聊天补全)用 SSE,双向高频(协同编辑、语音)用 WebSocket
    • SSE 就是普通 HTTP,鉴权、限流、日志、代理、CDN 这套设施全部照用,排查时 curl 就够
    • SSE 的限制要主动说:EventSource 只能 GET,而模型接口必须 POST,所以自动重连用不上;HTTP/1.1 下同域连接数有限
    • 拿不准先选 SSE:加一个上行接口就能补足,而换 WebSocket 要重做整套基础设施
    • 「停止生成」不用走同一条连接,另发一个 POST 带 run id 让服务端中止上游即可

    Key points

    • Decide by upstream frequency: one request plus a long push (chat completion) fits SSE; high-frequency bidirectional traffic needs WebSocket
    • SSE is plain HTTP, so auth, rate limiting, logging, proxies and CDNs all still apply, and curl is enough to debug it
    • Name SSE's limits yourself: EventSource is GET-only while model endpoints need POST, so spec auto-reconnect does not apply; HTTP/1.1 also caps per-origin connections
    • When unsure start with SSE — adding one upstream endpoint is cheaper than rebuilding infrastructure around WebSocket
    • A stop button does not need the same connection: POST the run id and abort upstream, and the stream ends by itself
  • 把一个本地跑的 Agent 脚本改造成生产服务,接口层要重点考虑哪些事?When turning a local agent script into a production service, what does the interface layer have to get right?
    国内高频海外高频进阶#api-design#service-architecture#streaming

    分析过程 · 先想清楚再作答

    1. 这题考的是「你知不知道脚本里有哪些隐含假设」。答成一份笼统的清单(鉴权、日志、监控)拿不到分,要说出脚本时代默认成立、服务里立刻不成立的那几条。
    2. 先把假设列出来,这是最能体现工程视角的一步:只有一个用户(历史可以放模块级变量)、串行执行(不会有两个请求同时改一份状态)、输入可信(参数是自己敲的)、进程和会话同生共死(Ctrl+C 之后不用交代)。四条在服务里全部不成立,而第一条最难查,因为它在本地单人测试时表现完美。
    3. 然后给出四个必须做的决定:接口形状(一次性 JSON 还是流式推送)、会话标识(客户端带 sessionId 还是服务端发 cookie,以及历史存哪里)、鉴权与限流(谁能调、多久能调一次、单次 token 上限)、错误怎么表达。
    4. 第四条要单独展开,它是这题真正的区分点:流式接口一旦写出 200 和第一个字节,状态码就已经发出去了,之后模型超时、余额不足、上游 500,都只能在流里补发一个约定好的 error 事件。所以推流之前必须把能校验的全部校验完,那是你最后一次能用状态码好好说话的机会。
    5. 再补一条生产视角:服务要有健康检查接口。没有它,编排系统和负载均衡就没法判断这个实例能不能接流量,滚动发布时会把请求打给一个还没起好的进程。
    6. 可以预期的追问:单次请求的 token 上限为什么要在接口层限制?因为 Agent 的成本是请求方触发、你来买单,不设上限就等于把钱包交给调用方——限流限的不只是 QPS,还有每次调用能烧多少钱。

    How to reason about it · think before answering

    1. This tests whether you can name the assumptions hidden in a script. A generic checklist (auth, logging, monitoring) scores nothing; name the assumptions that silently break.
    2. List them first: one user (so history can live in a module-level variable), serial execution (no two requests mutating the same state), trusted input (you typed the arguments yourself), and a process whose life equals the session's. All four break in a service, and the first is hardest to catch because single-user local testing looks perfect.
    3. Then give the four decisions: response shape (single JSON versus streamed events), session identity (client-supplied id versus server cookie, and where history is stored), authentication and rate limiting (who may call, how often, and the per-call token ceiling), and how errors are expressed.
    4. Expand the last one — it is where this question is actually won. Once a streaming endpoint has written 200 and the first byte, the status code is already on the wire, so a later timeout, out-of-credit or upstream 500 can only surface as an agreed error event inside the stream. Validate everything you can before the first byte, because that is your last chance to speak in status codes.
    5. Add a production note: ship a health endpoint. Without one, orchestrators and load balancers cannot tell whether an instance is ready, and rolling deploys send traffic to a process that has not finished booting.
    6. Expect the follow-up: why cap tokens per request at the interface layer? Because agent cost is triggered by the caller and paid by you — no cap means handing your wallet to the client. Rate limiting is about money per call, not just QPS.

    答题要点

    • 脚本的四个隐含假设在服务里全部不成立:单用户、串行、输入可信、进程与会话同生共死
    • 会话状态必须按 sessionId 隔离,且要意识到放进程内存意味着重启即丢、无法水平扩容
    • 四个接口决定:响应形状、会话标识、鉴权与限流(含单次 token 上限)、错误表达方式
    • 流式接口推流之后无法用状态码报错,必须约定一个流内的 error 事件,并把校验全部前置到第一个字节之前
    • 提供健康检查接口,否则编排系统无法判断实例能不能接流量

    Key points

    • A script's four assumptions all break in a service: single user, serial execution, trusted input, and a process that dies with the session
    • Session state must be keyed by session id, and in-process storage means data is lost on restart and blocks horizontal scaling
    • Four interface decisions: response shape, session identity, auth and rate limiting including a per-call token ceiling, and error semantics
    • A streaming endpoint cannot report errors by status code after the first byte, so define an in-stream error event and move all validation ahead of it
    • Expose a health endpoint, or orchestrators cannot tell whether the instance is ready for traffic

D25 前端侧 Agent 体验:流式渲染、工具调用可视化、打断/重试、SSE hooks

  • 前端怎么消费 SSE 并实现打字机效果?为什么一般不用浏览器自带的 EventSource?How does a frontend consume SSE to render a typewriter effect, and why do people usually avoid the built-in EventSource?
    国内高频海外高频基础#sse#streaming#frontend

    分析过程 · 先想清楚再作答

    1. 这题是送分题,但送分点在后半句。只答「用 EventSource 监听 message 事件」的,面试官会立刻追问鉴权怎么办——答不上来就说明没在真项目里接过。
    2. 先给正面答案的骨架:`fetch` 拿到响应后读 `res.body` 这个 ReadableStream,`TextDecoder` 解码成文本,按空行切帧,逐帧解析出 `event` 与 `data`,把文本增量追加到当前这条消息上。
    3. 为什么不用 `EventSource`,三个硬伤要一口气说全:只能发 GET、不能带自定义请求头(也就是放不进 Authorization)、不能带请求体。Agent 场景里消息体、幂等键、会话 id 都得走 body,三条全撞上。
    4. 紧接着说代价,这是区分「用过」和「读过」的地方:手写解析意味着 `EventSource` 自带的自动重连、`Last-Event-ID` 续传都要自己实现。不过带鉴权的场景里那个自动重连本来就不好用(它重连时同样带不了头),所以损失没听起来那么大。
    5. 可预期的追问一:帧被网络切成两半怎么办?答缓冲——按空行切完之后,最后一段可能是半截,`pop` 出来留到下一块再拼。**这个 bug 在本机直连时几乎不出现**,所以要专门构造切碎的报文来测。
    6. 可预期的追问二:为什么不用 WebSocket?答:SSE 是单向下行、走普通 HTTP、天然过代理和 CDN、实现和运维都更轻;只有需要频繁上行(协同编辑、语音)才值得上 WebSocket。这一条能主动说出来会很加分。

    How to reason about it · think before answering

    1. This is a warm-up question, but the second half is where it bites. Answering only 'use EventSource and listen for message events' invites an immediate follow-up about auth, and not having one shows you never wired it in a real project.
    2. Sketch the positive answer first: fetch the response, read res.body as a ReadableStream, decode with TextDecoder, split on blank lines into frames, parse event and data per frame, and append the text delta onto the current message.
    3. Then the three hard blockers on EventSource, stated together: GET only, no custom request headers (so no Authorization), and no request body. Agent requests need all of a message payload, an idempotency key and a session id in the body, so all three bite at once.
    4. Name the cost next — this separates having used it from having read about it. Hand-rolling means you also reimplement EventSource's auto-reconnect and Last-Event-ID resume. That said, its auto-reconnect is already unusable under auth because reconnects cannot carry headers either, so the loss is smaller than it sounds.
    5. Expected follow-up 1: what if a frame is split across chunks? Buffer it — after splitting on blank lines, pop the trailing partial segment and prepend it to the next chunk. This bug almost never reproduces on localhost, so you must feed deliberately fragmented payloads to test it.
    6. Expected follow-up 2: why not WebSocket? SSE is one-way downstream over plain HTTP, passes proxies and CDNs, and is far lighter to run. WebSocket earns its keep only when you need frequent upstream traffic such as collaborative editing or voice. Volunteering this scores well.

    答题要点

    • 用 fetch 读 res.body 这个 ReadableStream,TextDecoder 解码,按空行切帧,增量追加文本。
    • EventSource 三个硬伤:只能 GET、不能带自定义头(放不进 Authorization)、不能带请求体。
    • 代价是自动重连和 Last-Event-ID 续传要自己写——但带鉴权时那个自动重连本来也用不了。
    • 必须处理跨块的半截帧:切完之后最后一段留到下一块再拼,本机直连测不出这个 bug。
    • 不用 WebSocket 是因为 SSE 单向下行、走普通 HTTP、过代理和 CDN 更省事;需要频繁上行才换 WebSocket。

    Key points

    • Use fetch, read res.body as a ReadableStream, decode with TextDecoder, split frames on blank lines, append deltas.
    • EventSource has three blockers: GET only, no custom headers (no Authorization), no request body.
    • The cost is reimplementing auto-reconnect and Last-Event-ID resume — though auto-reconnect is unusable under auth anyway.
    • You must buffer partial frames across chunks; localhost testing will not surface this bug.
    • SSE beats WebSocket here: one-way, plain HTTP, proxy and CDN friendly. Switch only when you need frequent upstream messages.
  • 用户点了「停止生成」,前端调用 AbortController.abort() 之后,后端在做什么?The user hits Stop and the frontend calls AbortController.abort(). What is the backend doing at that moment?
    国内高频海外高频深入#streaming#cancellation#cost

    分析过程 · 先想清楚再作答

    1. 这题是本章题眼,也是一道**陷阱题**:题干里已经把「前端 abort 了」当成既成事实,等你顺着说「那就停了」。答「停了」的直接出局。
    2. 正确答案一句话:**后端什么都不知道,它还在跑。** 还在调模型、还在往库里写消息、还在按 token 计费。`abort` 只是让你这一端不再读了,它顶多让 TCP 连接断开,而后端是否感知得到连接断开、感知到之后做不做事,是另一回事。
    3. 怎么拆:把「谁知道这件事」画出来。用户知道 → 前端知道 → **中间断了** → 后端不知道。断掉的这一环必须用一个显式的请求补上:`POST /runs/:id/cancel`。所以打断是两步,不是一步。
    4. 给一个量化的对照最有说服力:同一段 70 个字的回复,在第 5 个字打断——两步打断的后端停在 5/70,只 abort 的后端照跑到 70/70。差 14 倍的 token,而且那 65 个字还会落进会话历史,下一轮当上下文重新发一遍,付第二遍钱。
    5. 生产视角的补充:cancel 收到之后**不要硬杀**,把 run 迁到 cancelled 状态、让当前这一步跑完再退出——硬杀会留下半写的消息和对不上的序号。而且 cancel 本身必须幂等,因为网络抖动时你会重试它。
    6. 可预期的追问:那能不能靠后端检测连接断开来自动停?可以做,而且应该做(作为兜底),但不能只靠它——反向代理和负载均衡常常会把连接维持一段时间,后端感知到断开可能已经是十几秒之后;而且用户点停止之后如果自动重连,连接根本没断。**兜底归兜底,显式 cancel 才是主路径。**

    How to reason about it · think before answering

    1. This is the core question of the chapter and a deliberate trap: the prompt states the abort as a given and waits for you to say 'so it stopped'. Saying that ends the conversation.
    2. The correct answer in one line: the backend knows nothing and is still running — still calling the model, still writing messages, still billing tokens. abort only stops your end from reading; at most it drops the TCP connection, and whether the backend notices, or acts on noticing, is a separate matter.
    3. Decompose by drawing who knows what: the user knows, the frontend knows, the chain breaks, the backend does not know. That broken link must be closed with an explicit request: POST /runs/:id/cancel. So stopping is two steps, not one.
    4. A quantified contrast lands best: on the same 70-character reply interrupted at character 5, the two-step version stops the backend at 5/70 while abort-only runs to 70/70. That is 14x the tokens, and those 65 characters also land in conversation history and get resent as context next turn, billing you twice.
    5. Production addendum: on cancel, do not hard-kill. Move the run to a cancelled state and let the current step finish, or you leave half-written messages and gaps in the sequence numbers. Also make cancel idempotent, because you will retry it when the network flakes.
    6. Expected follow-up: can the backend just detect the dropped connection and stop by itself? It can and should, as a safety net, but not as the only mechanism. Proxies and load balancers often hold connections open, so detection can lag by tens of seconds, and if the client auto-reconnects the connection never drops at all. The net is a net; the explicit cancel is the main path.

    答题要点

    • 后端完全不知情:还在调模型、还在写库、还在计费。abort 只让前端这一端停止读取。
    • 打断必须两步:abort(界面立刻响应)+ POST /runs/:id/cancel(后端真的停)。
    • 量化差别:同一段 70 字的回复在第 5 个字打断,两步是 5/70,只 abort 是 70/70。
    • 后端收到 cancel 不要硬杀,迁到 cancelled 状态让当前步跑完;cancel 必须幂等。
    • 靠后端检测连接断开只能当兜底:代理会维持连接、自动重连时连接根本没断。

    Key points

    • The backend has no idea: still calling the model, still writing, still billing. abort only stops your side reading.
    • Stopping is two steps: abort for instant UI response, plus POST /runs/:id/cancel to actually halt the run.
    • Quantified: interrupting the same 70-character reply at character 5 gives 5/70 with both steps versus 70/70 with abort alone.
    • On cancel, transition the run to cancelled and let the current step finish rather than hard-killing; make cancel idempotent.
    • Backend disconnect detection is only a safety net — proxies hold connections open and auto-reconnect means no disconnect at all.
  • 流式场景下前端的状态管理要注意什么?为什么不能每个 token 都 setState?What is different about frontend state management under streaming, and why not call setState on every token?
    国内高频海外高频进阶#react#streaming#performance

    分析过程 · 先想清楚再作答

    1. 这题考的是「你有没有在长回复下真的看过掉帧」。答「用 useState 存消息数组,收到 delta 就 setState」在功能上没错,但它暴露的是只在短回复上试过。
    2. 先算一笔账:流式一秒来几十个 token,每个 token 一次 setState 就是一秒几十轮完整渲染。而消息列表是越来越长的,每一轮的代价随对话轮数增长——所以卡顿在回复后半段和长会话里最明显,正好是最不该卡的时候。
    3. 做法是攒批:token 先追加进 ref(不触发渲染),一个定时器每 30 毫秒把攒下的一次性提交。30 毫秒约等于 33 帧每秒,肉眼仍是连续的打字机,渲染次数掉一到两个数量级——实测 200 个 token 只提交 8 次。
    4. 三个必须配套的细节:流结束时强制 flush 一次(否则最后不足一个批次的内容永远留在缓冲里,用户看到回复少半句);打断时也要 flush(让用户看到停在哪个字);缓冲状态必须放 ref 不放 state,否则你为了省渲染写的代码本身在触发渲染。
    5. 再往上一层是分层:**流式逻辑应该活在 React 外面。** 解析、事件归并、攒批都是纯函数,store 持有状态并暴露 subscribe 和 getSnapshot,React 侧只用 useSyncExternalStore 订阅。这样做的直接好处是**这套逻辑可以在没有浏览器的环境里跑单元测试**,而不是只能靠手点。
    6. 可预期的追问:为什么不直接用某个状态库?答:状态库解决的是跨组件共享和更新粒度,而流式的难点在生命周期(连接、取消、卸载清理)和批处理频率——这两件事没有哪个库替你做。面试官问这题想听的是你怎么想,不是你会用哪个库。

    How to reason about it · think before answering

    1. This question probes whether you have watched a long reply drop frames. 'Keep messages in useState and setState on each delta' is functionally correct but reveals you only tried short replies.
    2. Do the arithmetic first: streaming delivers tens of tokens per second, so one setState per token means tens of full render passes per second. The message list keeps growing, so each pass gets more expensive as the conversation goes — the jank peaks late in long replies and long sessions, exactly when it hurts most.
    3. The fix is batching: append tokens into a ref without rendering, and flush the accumulated text on a 30 ms timer. Thirty milliseconds is roughly 33 fps, still a smooth typewriter, while render count drops by one to two orders of magnitude — measured, 200 tokens produced 8 commits.
    4. Three details that must ship with it: force a final flush when the stream ends, or the last sub-batch stays in the buffer and the user sees a truncated reply; flush on interrupt too, so the user sees exactly where it stopped; and keep the buffer in a ref, not state, or the code you wrote to avoid renders is itself causing them.
    5. One level up is layering: streaming logic should live outside React. Parsing, event reduction and batching are pure functions; a store holds state and exposes subscribe and getSnapshot; React only calls useSyncExternalStore. The concrete payoff is that this logic can be unit tested with no browser instead of being click-tested.
    6. Expected follow-up: why not just use a state library? Libraries solve cross-component sharing and update granularity, while the hard parts here are lifecycle (connect, cancel, cleanup on unmount) and flush cadence — no library does those for you. The interviewer wants your reasoning, not your library list.

    答题要点

    • 每个 token 一次 setState 等于一秒几十轮全量渲染,而消息列表越长每轮越贵,长回复后半段必然掉帧。
    • 做法是攒批:token 进 ref 不触发渲染,30 毫秒定时 flush 一次,实测 200 个 token 只提交 8 次。
    • 必须配套:流结束和打断时强制 flush;缓冲放 ref 不放 state。
    • 流式逻辑(解析、归并、攒批)应该是 React 之外的纯函数,React 只用 useSyncExternalStore 订阅。
    • 这样分层的直接好处是能脱离浏览器做单元测试,而不是只能手点验证。

    Key points

    • One setState per token means tens of full renders per second, and each render costs more as the list grows — long replies jank at the end.
    • Batch instead: accumulate tokens in a ref and flush every 30 ms; measured, 200 tokens produced only 8 commits.
    • Ship the details with it: force a flush on stream end and on interrupt, and keep the buffer in a ref rather than state.
    • Keep parsing, event reduction and batching as pure functions outside React; subscribe via useSyncExternalStore.
    • The payoff of that split is unit-testable streaming logic with no browser in the loop.

D29 弱项补强 + 编码热身:rate limiter、LRU、并发控制、流式 JSON 解析

  • 为什么流式场景下不能直接用 JSON.parse?你会怎么做增量解析?Why can't you just call JSON.parse in a streaming response, and how would you parse incrementally?
    国内高频海外高频深入#streaming#json-parsing

    分析过程 · 先想清楚再作答

    1. 这题的区分度几乎全在你开口的第一句话。听到「流式 JSON 解析」就动手写状态机的人,会花二十多分钟写一个大概率有 bug 的东西;先反问一句「是一行一个完整 JSON,还是一个大对象被切成很多片」的人,已经赢了一半。
    2. 先回答为什么不能直接解析:网络分包不认语法边界,一次读取拿到的很可能是半个 JSON。直接扔给解析器只会抛异常,而且这个异常没有任何可恢复的信息。
    3. 然后做那个关键区分。情况 a 是 SSE:每条事件是一行以 data 开头的文本,行内是完整 JSON,LLM 场景 99% 是这一种,解法是行缓冲加逐行解析,二十行代码——把切分出来的最后一段(可能是半行)留在缓冲区里,等下一次读到更多数据再拼。情况 b 是单个大对象跨分片到达,才需要真正的增量解析。
    4. 情况 b 的核心是三个状态变量:括号深度(深度归零说明一个完整对象结束)、是否在字符串内部(字符串里的括号不能计入深度)、前一个字符是不是反斜杠(转义中的引号不切换字符串状态)。三者缺一不可,少一个遇到含括号的文本就算错深度。
    5. 还有一条几乎没人主动说、但一说就加分的坑:分片是按字节切的,一个汉字在 UTF-8 里占三个字节,边界可能落在中间。必须用流式解码器(TextDecoder 的 stream 选项、Python 的增量解码器、Java 的 InputStreamReader),否则会拿到一个永远补不回来的乱码字符。这是「半行缓冲」在字节层的同款问题。
    6. 可以预期的追问:那结尾那个终止标记怎么办?答案是它不是 JSON,必须在解析前先判断并直接返回,拿它去解析必然抛异常——这是这道题里最常见的一行 bug。

    How to reason about it · think before answering

    1. Almost all the signal is in your first sentence. Whoever starts writing a state machine will spend twenty-plus minutes on something probably buggy; whoever first asks 'is it one complete JSON per line, or one big object split across chunks?' has already won half the question.
    2. Answer the why first: network chunking ignores syntax boundaries, so a single read often holds half a JSON document. Handing that to a parser only throws, and the exception carries nothing you can recover from.
    3. Then draw the distinction. Case (a) is SSE: each event is one line prefixed with data, holding one complete JSON object. This covers 99% of LLM work, and the fix is line buffering plus per-line parsing — keep the trailing fragment in the buffer and stitch it onto the next chunk.
    4. Case (b) — one large object arriving in pieces — is the only case needing real incremental parsing, and it rests on three state variables: bracket depth (back to zero means a complete object), whether you are inside a string (brackets in text must not count), and whether the previous character was a backslash (an escaped quote must not toggle string state). Drop any one and text containing brackets breaks the depth count.
    5. One trap almost nobody volunteers: chunks are split on bytes, and a CJK character takes three bytes in UTF-8, so a boundary can land mid-character. Use a streaming decoder — TextDecoder with the stream option, an incremental decoder in Python, InputStreamReader in Java — or you get a replacement character you can never recover. It is the half-line problem one layer down.
    6. Expect the follow-up: what about the terminator line? It is not JSON, so check for it and return before parsing. Feeding it to the parser is the single most common one-line bug in this question.

    答题要点

    • 网络分包不认语法边界,一次读取可能拿到半个 JSON,直接解析必然抛异常且不可恢复
    • 先问清是哪一种:一行一个完整 JSON(SSE,占 LLM 场景的绝大多数)还是一个大对象跨分片到达
    • 前者只需行缓冲加逐行解析:把最后一段可能的半行留在缓冲区,等下一次读到更多数据再拼
    • 后者才需要状态机,核心是括号深度、是否在字符串内部、前一个字符是否为转义反斜杠三个状态
    • 字节层还有一个同款坑:UTF-8 多字节字符可能被分片切开,必须用流式解码器;结尾的终止标记不是 JSON,解析前要先判断

    Key points

    • Network chunking ignores syntax boundaries, so a read can hold half a document; parsing it throws an unrecoverable error
    • Ask which case it is first: one complete JSON per line (SSE, the overwhelming majority of LLM work) or one large object split across chunks
    • The first case only needs line buffering plus per-line parsing, keeping the trailing partial line for the next chunk
    • Only the second case needs a state machine, tracking bracket depth, inside-string, and escaped-previous-character
    • One layer down, a multi-byte UTF-8 character can be split across chunks, so use a streaming decoder; and the terminator line is not JSON, so check for it before parsing

14 天 RAG:从检索到可信回答

D6 生成这一侧:上下文怎么排、引用怎么标、什么时候必须拒答,以及流式回答

  • 流式输出的场景下,你怎么保证吐出去的内容不会因为引用校验失败而需要撤回?In a streaming setup, how do you make sure nothing you have already sent needs to be retracted because its citation failed verification?
    国内高频海外高频深入#streaming#citation-verification#api-design

    分析过程 · 先想清楚再作答

    1. 这题在考一个真实的架构矛盾:流式要尽早出字,引用校验要等话说完才能核对。看回答里有没有出现「取舍」两个字,以及有没有把代价说清楚。
    2. 先说清矛盾在哪:一旦一个 token 发到了浏览器就撤不回来,你在末尾才发现第三句引用是编的,那句话已经在用户屏幕上了,只能补一句「刚才那句请忽略」,体验比不流式还糟。
    3. 给方案:按句缓冲。攒够一个完整句子就立刻校验一次,通过了才把这句连同已核实的引用发出去,没通过就整句丢掉。代价是首字延迟从一个 token 变成一句话,通常两三百毫秒,用户几乎察觉不到,而错误引用一旦上屏赔的是信任。
    4. 补两个实现细节,它们能证明你写过:流式模式没法用 JSON 输出(要等右花括号闭合才能解析),所以改成纯文本加行内标记,但校验必须和非流式共用同一套;标记要从正文里剥掉,正文保持干净,编号单独走校验再作为结构化数据发出去。
    5. 再补一条顺序上的讲究:生成前就能判的两条拒答线(分数过低、材料冲突)要在流开始之前发出去,用户不会先看到半句回答再被收回;生成后才能判的那条,在按句缓冲之下表现为一句都没发出来,收尾补一个拒答事件即可。
    6. 可预期的追问:那用户体验上的流式感是不是就没了?没有,句级流式在中文长回答里仍然是明显的渐进呈现;真要更细,可以在句子发出前先流一个「正在核对」的占位态,但不要流未校验的正文。

    How to reason about it · think before answering

    1. This tests a real architectural conflict: streaming wants the first token out early, citation verification cannot run until a statement is complete. Listen for whether the candidate names the trade-off and prices it.
    2. Name the conflict: once a token reaches the browser you cannot take it back. Discovering at the end that the third sentence cited a fabricated block leaves you posting 'please ignore that last sentence', which is worse than not streaming at all.
    3. Give the solution: buffer by sentence. As soon as a complete sentence lands, verify it, and only then emit it together with its verified citations; drop the whole sentence otherwise. The cost is that time-to-first-token becomes time-to-first-sentence, typically a few hundred milliseconds, which users barely notice, whereas a bad citation on screen costs trust.
    4. Add two implementation details that prove you have built it. Streaming cannot use JSON output because JSON is only parseable once closed, so switch to plain text with inline markers, while keeping exactly the same verifier as the non-streaming path. Strip the markers out of the prose and send the numbers as structured data after verification.
    5. Add the ordering point: the two rules decidable before generation, low score and source conflict, should be emitted before the stream starts, so the user never sees half an answer being withdrawn. The rule that needs generation shows up as 'no sentence was ever emitted', so close the stream with a refusal event.
    6. Expected follow-up: does this kill the streaming feel? No. Sentence-level streaming is still visibly progressive on long answers. If you need finer granularity, stream a 'checking sources' placeholder, but never stream unverified prose.

    答题要点

    • 矛盾在于发出去的内容撤不回来,而引用只有一句说完才能核对。
    • 解法是按句缓冲:攒够一句校验一次,通过才发,没通过整句丢掉。
    • 代价是首字延迟从一个 token 变成一句话,这个代价必须付也付得起。
    • 流式用不了 JSON,改纯文本加行内标记,但校验逻辑与非流式共用同一套;标记从正文剥出,编号作为结构化数据单独发。
    • 生成前能判的拒答要在流开始之前发出去,生成后才能判的那条以「一句都没发」的形式收尾补事件。

    Key points

    • The conflict: emitted text cannot be recalled, while a citation can only be checked once its sentence is complete.
    • The fix is sentence-level buffering: verify each completed sentence, emit only if it passes, drop the whole sentence if it does not.
    • The cost is time-to-first-sentence instead of time-to-first-token, which is affordable and worth paying.
    • Streaming cannot use JSON, so use inline markers in plain text while sharing one verifier with the non-streaming path; strip markers from the prose and send numbers as structured data.
    • Emit pre-generation refusals before the stream opens; the post-generation one manifests as an empty stream and is closed with a refusal event.