逐日AI

面试题库

共 328 题,当前筛选 6 题。

标签
还有 361 个标签
#api-design9#coding-agent9#distributed-systems8#multi-agent8#rag8#chunking7#debugging7#pipeline-design7#structured-output7#agent-loop6#mcp6#operations6#prompt-injection6#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 到底是什么

  • 流式回复到一半网络断了,前端和后端各要做什么?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

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
  • 一个 SSE 长连接服务上线,心跳、连接断开处理和优雅退出分别在解决什么问题?For a long-lived SSE service in production, what problems do heartbeats, disconnect handling and graceful shutdown each solve?
    国内高频海外高频深入#sse#reliability#deployment

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

    1. 这题的区分度在于三件事各自的失败现象完全不同,能分别说出现象的人一定真上过线。答成「都是为了稳定性」等于没答。
    2. 心跳解决的是「被中间设施误杀」。负载均衡和网关普遍有空闲超时,常见 60 到 120 秒,一段时间没有字节流动就关连接;而 Agent 天生有大量静默期——模型在思考、在调工具、在等慢接口。现象是连接莫名其妙断在一半,且本地直连时完全复现不了。实现上用 SSE 的注释行(冒号开头)做心跳,客户端会安静忽略,不用改客户端代码。
    3. 连接断开处理解决的是「花钱」。用户关掉页面之后服务端不会自动停,模型继续生成、token 继续计费,只是没人接收。这是流式服务里最贵的疏忽,而且测试环境暴露不出来,因为没人会中途关页面。做法是监听响应对象的关闭事件,判定是被掐断而不是正常收尾,就把上游请求一起中止。
    4. 这里有个必须说对的细节,说错会当场暴露没写过:Node 里要监听的是响应对象的 close,不是 request 的——request 的 close 在请求体读完时就触发,拿它当断线信号会把每一条正常请求都误判成客户端跑了,现象是每次只推出一两个片段就停。
    5. 优雅退出解决的是「发布时切断在途请求」。容器收到 SIGTERM 后应当先停止接受新连接,给在途的流一点收尾时间再退出,否则用户看到的是回复说了一半突然没了。前提是信号真的能传到进程——CMD 写成包管理器的话 PID 1 不是 node,SIGTERM 传不到,只能等超时被强杀。
    6. 可以预期的追问:收尾时间给多久?答案是要小于编排系统的终止宽限期(Docker 默认十秒、K8s 默认三十秒),超过就会被 SIGKILL,等于白设计;同时新连接要立刻拒绝,让负载均衡把流量挪走。

    How to reason about it · think before answering

    1. The discriminator is that the three have completely different failure symptoms. Someone who can describe each symptom has shipped one; 'they all improve stability' is a non-answer.
    2. Heartbeats prevent middleboxes from killing you. Load balancers and gateways commonly close idle connections after 60 to 120 seconds, and agents are full of silent gaps while the model reasons, calls a tool or waits on a slow API. The symptom is a stream that dies halfway for no visible reason and never reproduces against a local server. Implement it as an SSE comment line, which clients silently ignore, so no client change is needed.
    3. Disconnect handling is about money. When a user closes the tab the server does not stop on its own: the model keeps generating and tokens keep billing with nobody receiving. It is the most expensive oversight in streaming services, and staging never reveals it because nobody closes tabs mid-run. Watch for the response closing, distinguish a premature close from a normal finish, and abort the upstream request.
    4. One detail must be right or it exposes you immediately: in Node listen on the response object's close, not the request's. The request emits close once its body has been read, so using it as a disconnect signal misfires on every normal request and you see streams stopping after one or two chunks.
    5. Graceful shutdown is about deploys cutting live requests. On SIGTERM the process should stop accepting new connections, give in-flight streams a short window, then exit; otherwise users watch a reply stop mid-sentence. This assumes the signal actually reaches the process — if the container's PID 1 is a package manager, SIGTERM never arrives and the runtime kills you on timeout.
    6. Expect: how long is the window? Shorter than the orchestrator's termination grace period (10s by default in Docker, 30s in Kubernetes), or you get SIGKILLed anyway; and refuse new connections immediately so the load balancer drains traffic away.

    答题要点

    • 心跳防的是中间设施的空闲超时,Agent 的静默期常常超过网关的 60 到 120 秒,用 SSE 注释行实现,客户端无感
    • 断开处理防的是浪费:用户关页面后服务端不停就是纯烧 token,测试环境暴露不出来
    • Node 里要监听响应对象的 close 而不是 request 的——后者在请求体读完时就触发,会把正常请求误判成断线
    • 优雅退出防的是发布切断在途流:SIGTERM 后先停收新连接、给在途流收尾时间,收尾窗口要小于编排系统的终止宽限期
    • 前提是信号能传到进程:容器的 PID 1 必须是 node 本身,不能是包管理器

    Key points

    • Heartbeats defeat idle timeouts in middleboxes, since agent silence often exceeds a gateway's 60 to 120 seconds; SSE comment lines do it transparently
    • Disconnect handling stops waste: after a user closes the tab, an unaware server keeps burning tokens, and staging never shows it
    • In Node listen on the response's close, not the request's — the latter fires when the body is read and misclassifies normal requests as disconnects
    • Graceful shutdown stops deploys from cutting live streams: on SIGTERM refuse new connections and drain, within the orchestrator's grace period
    • It only works if the signal reaches the process, so PID 1 must be node itself rather than a package manager

D11 run 状态机、输出流回传、按 runId 保序、SSE 等待者、30s 打断合并

  • 多个客户端同时订阅同一次执行的流式输出,怎么保证每个客户端都收到完整且有序的内容?Several clients subscribe to the same run's streaming output at once. How do you guarantee each of them receives the full content in order?
    国内高频海外高频进阶#sse#ordering#fan-out

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

    1. 题眼有两个词:完整、有序。很多人只答有序,漏掉完整——而「完整」那一半恰好是最容易设计错的,因为它取决于你用了哪种读法。
    2. 先给一句能定调的判断:一次执行是执行的单位,一条连接是观看的单位,两者不是一对一。手机和电脑同开、两个标签页、重连瞬间新旧连接并存,都会让同一次执行上挂着多条流。想清楚这句话,「谁先连谁独占」这种锁的方案就自然被排除了。
    3. 接着点出「完整」的真正机关:广播读法与消费组是两种语义。消费组是分摊,一条消息只给一个消费者;这里要的是广播,每个订阅者都要看到全部。用消费组做扇出,结果就是两条连接各拿到半段话——这是这道题最常见的错误答案。
    4. 再答「有序」:每个片段带一个从 0 开始、连续、不跳号的序号,写进流;接收侧维护「下一个该交付的号」,小于它的丢弃,大于它的先入缓冲,连号了再批量推出去。序号同时写进 SSE 的 id 字段,客户端不用另记一套账。
    5. 然后是必须主动说的工程代价:缓冲要有上限。如果 5 号迟迟不到,6 号往后全在内存里排队,一万条连接同时这样就是一次内存事故。做法是给缓冲设条数上限和等待上限,超时就从库里补读,补不到就发 error 让客户端重连——能等,但不能无限等。
    6. 可以预期的追问:扇出实现怎么选?两种——每条连接各自去读一遍流(简单,代价是同一批数据被读 N 次),或进程内只读一次再广播给本地订阅者(省读取,但要维护订阅者表、要处理最后一个订阅者离开,跨实例仍要各读一次)。判据是每次执行的平均订阅者数,多数产品接近 1,那就选前者,别为不存在的规模提前写一层。

    How to reason about it · think before answering

    1. Two words carry the question: complete and ordered. Most candidates answer only ordering and drop completeness — which is the half that is easy to get structurally wrong, because it depends on which read primitive you pick.
    2. Set the frame first: a run is the unit of execution, a connection is the unit of viewing, and they are not one-to-one. Phone plus laptop, two browser tabs, or the overlap window during a reconnect all put multiple streams on one run. Once that is clear, 'first connection wins the lock' schemes fall away on their own.
    3. Name the trap in 'complete': broadcast reads and consumer groups are different semantics. A consumer group divides work — each message goes to exactly one consumer — while here every subscriber must see everything. Using a consumer group for fan-out gives you two connections each holding half the answer, and that is the classic wrong answer here.
    4. Then ordering: every chunk carries a sequence number starting at 0, contiguous, never skipping, and is written to the stream. The reader keeps a 'next to deliver' cursor, discards anything below it, buffers anything above it, and flushes contiguous runs. Put the same number in the SSE id field so the client keeps no separate bookkeeping.
    5. Volunteer the cost: the reorder buffer needs bounds. If chunk 5 is late, 6 onward pile up in memory, and ten thousand connections doing that is an outage. Cap the buffer size and the wait, then backfill the gap from the database, and if that fails emit an error event and let the client reconnect. Wait, but never wait forever.
    6. Expect the fan-out follow-up: either every connection reads the stream itself (simple, at the cost of reading the same data N times) or one read per process broadcast to local subscribers (fewer reads, but you now own a subscriber registry, teardown when the last one leaves, and still one read per instance). Decide by average subscribers per run — usually close to one, so take the simple path.

    答题要点

    • 一次执行是执行单位、一条连接是观看单位,两者不是一对一,不需要「谁先连谁独占」的锁
    • 输出流必须用广播读法而不是消费组:消费组是分摊,会让两条连接各拿到半段话
    • 每个片段带从 0 开始、连续、不跳号的序号,接收侧按序交付:小于当前号丢弃、大于当前号入缓冲、连号批量推
    • 序号同时写进 SSE 的 id 字段,客户端不必自己记账,也是重连续号的依据
    • 缓冲必须有条数与时间上限,超时从库里补读,补不到就发 error 让客户端重连

    Key points

    • A run is the unit of execution and a connection is the unit of viewing; they are not one-to-one, so no first-wins lock is needed
    • Read the output stream as a broadcast, not through a consumer group — a group divides messages and leaves each connection with half the answer
    • Tag every chunk with a contiguous sequence starting at 0; the reader discards older, buffers newer, and flushes contiguous ranges
    • Mirror that sequence into the SSE id field so clients need no extra bookkeeping and can resume from it
    • Bound the reorder buffer by size and time, backfill gaps from the database, and fall back to an error event plus reconnect
  • 流式接口的客户端断线重连后,怎么做到既不丢片段也不重复?After a streaming client reconnects, how do you deliver every missed chunk exactly once — no gaps, no duplicates?
    国内高频海外高频深入#sse#idempotency#reconnect

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

    1. 「不丢」和「不重复」要分开答。只答一半的人很多:说了从库里补发(不丢),却没说重叠部分怎么去重(不重复)。
    2. 推导链很短:客户端知道自己最后收到的编号 → 它重连时把这个编号带回来 → 服务端从下一号开始给 → 前提是编号连续不跳号。所以能不能重连,取决于当初有没有把序号设计成从 0 开始、连续、单调。序号一旦是时间戳或随机 id,这条链第一步就断了。
    3. 然后给三步实现和各自的坑:第一步换算,带回来的是「最后收到」的那一号,要加一,少加一重复一帧、多加一丢一个字,这是整段逻辑里唯一的算术也最常写错;第二步先从持久化里回放缺的部分,因为库里一定是全的;第三步再接上还在流动的那条流,两边必然重叠,靠「小于当前指针的一律丢弃」去重——幂等回放的全部秘密就是这一次比较。
    4. 这里要点出为什么必须双写:片段既进流也进库。只有流,重连时早期片段已被消费掉;只有库,就得轮询查库,首字延迟从几十毫秒涨到几百毫秒。代价是写放大,一次回答几百个片段就是几百行,生产里按批落库(每几十个片段或每两百毫秒一次)。
    5. 对齐一下协议细节:浏览器原生的事件源会自动把上次的编号放进重连请求头带回来;但大模型接口通常要用 POST,原生事件源只能发 GET,所以真实前端多是手写解析,重连时要自己把编号带上——这个细节能证明你真接过前端。
    6. 可以预期的追问:回放要保留多久?必须给两个边界——保留期(逐片段的行只对最近若干小时的执行保留,之后归档成一整条完整回复并删掉碎行)和回放上限(一次重连最多回放多少片段,超了就一次性发完整文本而不是逐字重演)。不定这两条,那张表会变成全库最大且 99% 的行写完十秒后再没人读。

    How to reason about it · think before answering

    1. Answer 'no gaps' and 'no duplicates' separately. Plenty of candidates cover only the first — they backfill from storage but never say how the overlap is deduplicated.
    2. The chain is short: the client knows the last id it received, it sends that id back on reconnect, the server resumes from the next one — and all of that requires contiguous, monotonic numbering. Whether resumption is possible at all was decided when you chose the sequence scheme; timestamps or random ids break the chain at step one.
    3. Then the three steps and their individual traps. Convert: the client reports the last id it *received*, so add one — off by minus one repeats a frame, off by plus one drops a character, and this is the only arithmetic in the whole flow and the most commonly wrong line. Replay: read the missing range from durable storage, which is always complete. Attach: resume the live stream, whose overlap with the replay is guaranteed, and drop anything below the cursor. That single comparison is all there is to idempotent replay.
    4. Explain why the dual write is mandatory: chunks go both to the stream and to the table. Stream only, and the early chunks are gone by reconnect time; table only, and you are polling the database, pushing time-to-first-token from tens to hundreds of milliseconds. The cost is write amplification — hundreds of rows per answer — so production batches the writes, every few dozen chunks or every couple hundred milliseconds.
    5. Get the protocol detail right: the browser's native event source replays the last id in a request header for you, but model endpoints generally need POST while that API only issues GET, so real frontends hand-roll the parser and must resend the id themselves. Mentioning this proves you have actually wired up the client side.
    6. Expect: how long do you keep replayable data? Give two bounds — a retention window (per-chunk rows only for runs from the last few hours, then collapsed into one complete message) and a replay cap (beyond N chunks, send the full text once instead of re-enacting it character by character). Without both, that table becomes the largest in the database while 99% of its rows are never read again after ten seconds.

    答题要点

    • 重连的前提是序号从 0 开始、连续、单调;序号是时间戳或随机 id 就无法续传
    • 客户端带回来的是「最后收到」的那一号,服务端要加一再开始,这是唯一的算术也最容易错
    • 先从库里回放缺的片段(库一定是全的),再接上还在流动的流,重叠部分靠「小于当前指针一律丢弃」去重
    • 片段必须双写:流服务当前挂着的连接,库服务等一下才回来的人;代价是写放大,生产里按批落库
    • 必须定保留期与回放上限:过期的执行归档成一整条完整回复,超长回放直接一次性发完整文本

    Key points

    • Resumption requires a contiguous, monotonic sequence starting at 0; timestamps or random ids make it impossible
    • The client reports its last received id, so the server resumes from that id plus one — the single most error-prone line
    • Replay the gap from durable storage first, then attach the live stream, discarding anything below the cursor to dedupe the overlap
    • Dual-write every chunk: the stream serves currently attached connections, the table serves clients that come back later; batch the writes in production
    • Set a retention window and a replay cap — archive old runs into one complete message and send full text instead of re-enacting long replays

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.