面试题库
共 328 题,当前筛选 6 题。
课程全部30 天从前端工程师到 Agent 工程师5 天提示词工程零基础Claude 高效使用:从对话到 Claude CodeCodex 与 OpenAI Agents SDK 高效使用7 天 MCP:把工具接进任何 Agent7 天 Agent Skills:把经验做成可复用能力5 天上下文工程14 天 RAG:从检索到可信回答14 天用 Agent 搭一条 AI 短剧生产线
标签
全部#cost6#distributed-systems6#reliability6#idempotency5#message-bus5#operations4#architecture3#deployment3#observability3#redis-streams3#scalability3#database2
还有 29 个标签收起标签
#error-handling2#long-term-memory2#ordering2#rag2#scheduling2#security2#sharding2#sse2#state-machine2#atomicity1#chunking1#configuration1#consistent-hashing1#data-modeling1#fan-out1#fencing-token1#interrupt-merge1#lease1#pgvector1#reconnect1#redis1#reporting1#retrieval-quality1#schema-design1#split-brain1#stateless1#system-design1#tool-design1#vector-database1
30 天从前端工程师到 Agent 工程师
D11 run 状态机、输出流回传、按 runId 保序、SSE 等待者、30s 打断合并
用户在 Agent 还没回复完的时候又发来一条消息,应该怎么处理?A user sends another message while the agent is still answering the previous one. How should the system handle it?
国内高频海外高频进阶#interrupt-merge#state-machine#cost分析过程 · 先想清楚再作答
- 这题看起来是产品题,其实考的是你有没有想过「并发两次执行」的后果。答「排队处理」或「直接取消上一条」都不算错,但都不完整——面试官想听的是判据和代价。
- 先说清不处理会怎样:两次执行同时往同一个会话里写输出,前端看到两段交错的文字;而且第一次执行是基于不完整的信息跑的,它的答案注定要被推翻。这两条后果一说,方案的方向就定了——要合并,不要并发。
- 然后给可执行的判据,三个条件全中才合并:同一个会话、上一次执行正处于 running 或 streaming、距它创建不到 30 秒。命中就把新消息追加进同一次执行的输入并标记为需要重跑,不新建;超窗或上一次已完成就正常新建。把 pending 排除掉是有意的——那段窗口只有几毫秒,排除后判据不必考虑「执行侧正好在这一刻读输入」的竞态。
- 两个实现细节最能体现动手过:一是「需要重跑」这个标记不要写进业务表,它只在本次执行期间有意义,写进表里进程崩在半路就留下脏标记、重启后无限重跑,放一个带过期时间的键上更合适;二是重跑时序号必须接着往上加、不能重置,否则重连的客户端按上次收到的号续,会续到一段已经作废的历史上。
- 主动算一笔账,把「为了省钱」这个错误理由挡回去:按输入 2000、输出 500 个 token 估,单次约 0.0006 美元;不合并是两次跑完约 0.0012 美元,合并是第一遍被掐在三分之一处约 0.0004 美元加第二遍 0.0006 美元约 0.0010 美元,只省 17%,一天一万次改口也就两美元。所以合并的理由是体验,不是成本。
- 可以预期的追问:30 秒怎么定的?答它是产品判断不是推导结果——用户改口通常在 5 到 15 秒之间,窗口太短合并不到、太长会把新问题误并成补充;关键是这个数只在一处定义、被判据与前端提示共同引用,不要在代码里散落三份。
How to reason about it · think before answering
- It reads like a product question but tests whether you have thought through two concurrent runs. 'Queue it' or 'cancel the previous one' are not wrong, just incomplete — they want the criteria and the costs.
- Start with what happens if you ignore it: two runs write into the same conversation, so the UI shows two interleaved answers, and the first run was computed from incomplete input, so its answer is already wrong. Those two consequences point straight at merging rather than concurrency.
- Then give the actual test — all three must hold: same session, the previous run is running or streaming, and it was created less than 30 seconds ago. On a hit, append the new message to that run's input and flag it for a rerun instead of creating a new run; outside the window, or if the previous run finished, create a new one. Excluding pending is deliberate: that window lasts milliseconds, and excluding it keeps the rule free of races with the worker reading the input.
- Two implementation details show hands-on experience. First, the rerun flag does not belong in the business table — it is meaningful only during this execution, and persisting it means a crash mid-flight leaves a dirty flag that makes the run loop forever after restart; an expiring key is the right home. Second, on rerun the sequence must keep counting up rather than resetting, or a reconnecting client resuming from its last id lands in a history that has been invalidated.
- Volunteer the arithmetic to kill the 'saves money' answer: at roughly 2000 input and 500 output tokens, one answer costs about $0.0006. Not merging means two full runs, about $0.0012; merging means a first pass cut off a third of the way in (about $0.0004) plus a full second pass ($0.0006), about $0.0010 — a 17% saving, which is two dollars a day even at ten thousand corrections. Merging is a user-experience decision, not a cost optimization.
- Expect: where does 30 seconds come from? It is a product judgment, not a derivation — corrections usually arrive 5 to 15 seconds in, too short misses them and too long merges genuinely new questions into old ones. What matters is defining it once and referencing it from both the rule and the UI hint rather than scattering the constant.
答题要点
- 不合并的两个后果:两段输出交错写进同一个会话,且第一次执行基于不完整信息注定被推翻
- 判据三条全中才合并:同一会话、上一次执行处于 running 或 streaming、距创建不到 30 秒;否则正常新建
- 命中就把新消息追加进同一次执行的输入并标记需要重跑,标记放带过期时间的键上而不是业务表
- 重跑时序号继续往上加、绝不重置,否则断线重连会续到作废的历史上
- 合并省的钱有限(约 17%),真正的理由是不让两个回答同时对着用户说话
Key points
- Without merging you get two interleaved answers in one conversation, and the first was computed from incomplete input
- Merge only when all three hold: same session, previous run running or streaming, created under 30 seconds ago; otherwise create a new run
- On a merge, append to the same run's input and flag a rerun, keeping that flag in an expiring key rather than the business table
- Sequence numbers keep counting on rerun and are never reset, or reconnects resume into an invalidated history
- The cost saving is small (about 17%); the real reason is to avoid two answers talking over each other
D12 长期记忆:pgvector、embedding、chunking、memory_search 工具
为什么 Agent 需要额外的长期记忆,而不是把历史全部塞进上下文?Why does an agent need a separate long-term memory instead of stuffing all history into the context window?
国内高频海外高频基础#long-term-memory#rag#cost分析过程 · 先想清楚再作答
- 这题最容易答成「因为窗口装不下」。那只答对了一半,而且是不值钱的那一半——窗口一年比一年大,光靠这条理由,面试官会追问「等窗口到一百万 token 呢」,你就没词了。
- 先把两个问题拆开:上下文压缩解决的是「同一次会话里这一轮塞不下」,长期记忆解决的是「上个月说过的事想不起来」。前者在组装请求时做减法,后者做加法,触发时机、数据去向、失败后果都不同。能主动区分这两件事,是这题最大的区分度。
- 然后给成本账:200 条记忆、每条约 400 token 就是 8 万 token,按输入价 0.15 美元每百万 token 算,每一轮多付 0.012 美元;一天 20 轮就是 0.24 美元一个用户。只检索最相关的 5 条是 2000 token、每轮 0.0003 美元,差 40 倍。而且这笔钱是每轮重复付的,不是一次性的。
- 再给比钱更硬的理由:无关信息会降低命中率。200 条里跟这一轮相关的可能只有 1 条,剩下 199 条是噪声,模型会被带偏去回答一个用户没问的问题。**所以哪怕窗口无限大、token 免费,也该检索而不是全塞。** 这一句是这题的最优解。
- 落到做法上:把跨会话的用户事实与偏好抽成陈述句存进向量库,每轮按语义检索最相关的三五条注入请求——这就是 RAG 最小的一环。
- 可以预期的追问:什么信息该进长期记忆?答三问——跨会话之后还需要吗、会不会随时间失效、能不能靠检索捞回来。「用户住上海」三条都满足,「把刚才那段改成三句话」一条都不满足。
How to reason about it · think before answering
- The tempting answer is 'the window is too small'. That is half right and it is the cheap half — windows keep growing, and the interviewer will ask what you would do at a million tokens.
- Separate the two problems first: context compression solves 'this turn does not fit in one session', long-term memory solves 'I cannot recall what was said last month'. One subtracts at request-assembly time, the other adds. Naming that distinction unprompted is where the signal is.
- Then quantify: 200 memories at roughly 400 tokens each is 80k tokens; at 0.15 USD per million input tokens that is 0.012 USD every single turn, about 0.24 USD per user per day at 20 turns. Retrieving the top 5 is 2k tokens, 0.0003 USD per turn — a 40x gap, and it repeats every turn.
- Give the reason that beats cost: irrelevant context lowers accuracy. If one of 200 memories is relevant, the other 199 are noise that pull the model toward answering something nobody asked. So even with an infinite free window, you would still retrieve rather than dump.
- Land on practice: distil cross-session user facts and preferences into standalone statements, store them as vectors, and inject the three to five most relevant per turn — the minimal form of RAG.
- Expect the follow-up: what belongs in long-term memory? Three tests — is it still needed across sessions, does it expire, can retrieval find it again. 'Lives in Shanghai' passes all three; 'shorten that paragraph' passes none.
答题要点
- 压缩管「这一轮塞不下」,长期记忆管「上个月说过的事想不起来」,是两个问题、两套机制
- 全塞的成本是每轮重复付的:200 条约 8 万 token,每轮多 0.012 美元;检索 5 条只要 0.0003 美元
- 更硬的理由是准确率:无关记忆是噪声,会把模型带偏,所以窗口再大也该检索而不是全塞
- 做法是把跨会话的事实抽成陈述句、向量化存储,每轮按语义检索最相关的三五条注入
- 判断一条信息该不该进长期记忆:跨会话还需要吗、会不会失效、能不能被检索到
Key points
- Compression handles 'this turn does not fit'; long-term memory handles 'what did they say last month' — different problems, different machinery
- Dumping everything costs on every turn: 200 memories is about 80k tokens and 0.012 USD per turn versus 0.0003 USD for five retrieved ones
- The stronger reason is accuracy — irrelevant memories are noise, so you would retrieve even with an infinite window
- Practice: distil cross-session facts into statements, embed them, inject the top three to five per turn
- Admission test for a memory: still needed across sessions, does not expire, and is findable by retrieval
D13 cron 调度(中心调度→stream 投递)+ 成本计量(token→USD 台账、usage report)
服务部署了多个实例,定时任务为什么不能让每个实例各自起一个 cron?你会怎么做?When a service runs multiple replicas, why not let each replica start its own cron? What would you do instead?
国内高频海外高频基础#scheduling#distributed-systems#cost分析过程 · 先想清楚再作答
- 题眼在「多个实例」四个字。只答「会重复执行」拿不到分,因为那是现象;面试官想看你能不能把现象换算成业务后果和钱。
- 先把重复的代价说具体:3 个副本各起 cron,同一个任务被执行 3 次,用户收到 3 份一样的推送,你付 3 份模型调用的钱。而且这个倍数会跟着副本数走——扩容到 10 个副本,账单和骚扰量一起变成十倍,却不会触发任何告警,因为从每个进程自己的视角看它只是老实地执行了一次。
- 然后给出正确的形状:把「谁该在什么时候被执行」收进一个中心调度器,它命中 cron 之后只做一件事——往消息总线投递一条任务消息;执行侧照旧靠消费组分摊,一条消息只会被一个消费者拿到。关键认知是「定时任务不是一种新的执行方式,只是把按按钮的人从用户换成了钟表」,所以执行侧一行代码都不用改。
- 接着主动补上「那调度器自己不就成单点了吗」——这是必被追问的一句。答案分两层:调度器无状态、崩了拉起来就行,短暂不可用的代价只是几分钟内的任务延迟;真要高可用就起两个实例,靠投递时的幂等键去重,而不是靠给调度器加分布式锁。
- 最后点一句选型:任务量不大时中心调度器加消息总线足够;量大或者任务本身有依赖关系时,业界会换成专门的调度框架(带任务依赖、重试策略、补数),但底层的「中心决定 + 队列分发」结构是一样的。
- 可以预期的追问:调度器崩溃 90 秒,中间跨过的那一分钟怎么办?答启动时回看最近 N 分钟逐分钟重放,因为有幂等键兜底,重复投递无害——这正是 at-least-once 加幂等这组搭配能成立的地方。
How to reason about it · think before answering
- The hinge is the phrase multiple replicas. Saying it would run twice is only the symptom; the interviewer wants the business and dollar consequence.
- Make the cost concrete: three replicas each running cron means the job fires three times, users get three identical pushes, and you pay for three model calls. The multiplier tracks replica count, so scaling to ten makes both the bill and the spam tenfold, with no alert firing, because from each process's own point of view it ran exactly once.
- Give the right shape: move the decision of who runs when into one central scheduler whose only job, on a cron match, is to publish a task message onto the bus; the execution side keeps using a consumer group so one message reaches exactly one consumer. The key insight is that a scheduled task is not a new execution path, it just swaps the user for a clock as the thing pressing the button, so the worker code stays untouched.
- Volunteer the obvious follow-up: doesn't the scheduler become a single point of failure? Two layers. It is stateless, so a crash costs you a few minutes of task delay; if you truly need HA, run two instances and dedupe on the idempotency key at publish time rather than bolting a distributed lock onto the scheduler.
- Close with sizing: a central scheduler plus a bus is enough at modest volume. At high volume, or when tasks have dependencies, teams move to a dedicated workflow scheduler with dependency graphs, retry policy and backfill, but the underlying central-decision-plus-queue shape is identical.
- Expect: the scheduler was down for 90 seconds and skipped a minute — now what? Replay the last N minutes on startup, one minute at a time. The idempotency key makes redundant publishes harmless, which is exactly what makes at-least-once plus idempotency the easy combination.
答题要点
- 每个实例各自起 cron 等于同一个任务被执行 N 次:用户收到 N 份重复推送,模型调用花 N 倍的钱,倍数随副本数线性增长且不会触发告警
- 正确形状是中心调度器命中 cron 后只往消息总线投递一条消息,执行侧靠消费组保证一条消息只被一个 Worker 拿到
- 定时任务不是新的执行路径,只是把触发者从用户换成了钟表,所以 Worker 侧不需要任何改动
- 调度器是无状态的,崩了拉起来即可;需要高可用就起两个实例靠投递时的幂等键去重,不要给它加分布式锁
- 崩溃期间跨过的时间点靠启动时回看最近 N 分钟重放补上,幂等键保证重复投递无害
Key points
- Per-replica cron means the job runs N times: N duplicate pushes, N times the model spend, scaling linearly with replica count and silently
- The right shape is a central scheduler that publishes one message to the bus on a cron match, with a consumer group ensuring exactly one worker picks it up
- A scheduled task is not a new execution path — only the trigger changed from a user to a clock, so worker code is unchanged
- The scheduler is stateless: restart on crash, and if you need HA run two and dedupe on the idempotency key rather than adding a distributed lock
- Missed minutes are recovered by replaying the last N minutes at startup, which is safe because the idempotency key absorbs duplicates
让你从零设计一套 token 成本计量和台账系统,你会怎么做?How would you design a token cost metering and ledger system from scratch?
国内高频海外高频进阶#cost#observability#data-modeling分析过程 · 先想清楚再作答
- 这题在考「你有没有真的对过账」。区分度在两个地方:金额用什么类型存,以及金额是冗余存还是查询时现算。答不到这两点的方案,上线三个月就会被财务打回来。
- 先立判据:台账不是日志。日志是给排查问题用的,删了就删了;台账要拿去对账、要回答「这个月为什么涨了 40%」,所以每一笔钱都必须能追回到「谁、因为哪一次执行、用哪个模型、花了多少 token」。字段设计全部由这条判据推出来。
- 然后给字段和理由,一一对应:user_id 回答该算谁头上、run_id 回答属于哪次执行(允许为空,因为有系统级批量开销)、model 存调用当时那一个(fallback 会让同一段业务落到不同模型上)、kind 区分 chat 和 embedding(两者量级和增长曲线完全不同)、prompt_tokens 与 completion_tokens 分开存(输入输出单价差三到四倍,只存 total 就算不回金额,也看不出是提示词太长还是模型太啰嗦)。
- 接着是两个最能体现经验的判断。第一,金额用定点类型:数据库用 numeric,代码里用 Decimal 或 BigDecimal,绝不用双精度浮点累加,否则十万条之后总额和逐条相加对不上。第二,cost_usd 要在写入那一刻算好并冗余存,不要查询时用当前价格表现算——价格会变,历史账单不能跟着一起变,这是台账和报表最本质的区别。
- 还要主动说记账的时机和事务边界:记账放在「拿到 usage 字段」那一刻,而不是「业务成功」那一刻,因为失败的调用同样产生费用,尤其 fallback 会一次业务跨两三次收费调用。台账写入不必和业务同事务(丢一条只是几厘钱,锁住台账表却会卡住用户对话),可以异步加重试,用 run_id 加调用序号做唯一约束防重;但如果产品有额度限制,配额扣减必须同事务,否则用户能靠并发把额度刷穿。
- 可以预期的追问:厂商调价了历史数据怎么办?答案是价格表本身要有生效时间和版本号,台账里既存算好的金额也可以存价格版本,这样重算和审计都有依据。
How to reason about it · think before answering
- This question tests whether you have ever reconciled a bill. The discriminators are the numeric type you store money in, and whether cost is stored or computed at query time. A design missing either gets rejected by finance within a quarter.
- Set the criterion first: a ledger is not a log. Logs exist for debugging and can be dropped; a ledger has to reconcile against the vendor invoice and answer why the bill grew 40% this month, so every charge must trace back to who, which run, which model, and how many tokens. Every field falls out of that.
- Then walk the fields with reasons: user_id says whose budget it hits; run_id says which execution it belongs to and is nullable because some spend is system-level batch work; model records the one actually used, since fallback routes the same workload to different providers; kind separates chat from embedding because their volumes and growth curves differ completely; prompt_tokens and completion_tokens are stored separately because input and output differ three- to four-fold in price, and a single total can neither reproduce the amount nor tell you whether the prompt is bloated or the model is verbose.
- Now the two judgments that show experience. First, money uses fixed-point: numeric in the database, Decimal or BigDecimal in code, never accumulated in binary floats, or the total will diverge from the sum of rows after a hundred thousand entries. Second, cost is computed at write time and stored redundantly, not recomputed from the current price table — prices change, and history must not change with them. That is the essential difference between a ledger and a report.
- Volunteer the timing and transaction boundary: record at the moment you receive the usage field, not at business success, because failed calls still cost money and a fallback spans two or three billable calls per business operation. Ledger writes need not share the business transaction — losing a row costs fractions of a cent, while locking the ledger table stalls user conversations — so write asynchronously with retries and a uniqueness constraint on run id plus call index. The exception is quota enforcement: if the product caps spend, the decrement must be transactional or concurrent requests will blow through the cap.
- Expect: what happens to history when the vendor changes prices? The price table itself needs effective dates and a version, and the ledger stores both the computed amount and the price version, so recomputation and audit both have a basis.
答题要点
- 台账不是日志:每一笔钱要能追回到谁、哪一次 run、哪个模型、多少 token,字段设计全由这条判据推出
- prompt_tokens 与 completion_tokens 必须分开存,因为输入输出单价差三到四倍,只存 total 既算不回金额也看不出问题出在哪一侧
- 金额用定点类型(数据库 numeric、代码 Decimal/BigDecimal),不要用浮点累加,否则总额和逐条相加对不上
- cost_usd 在写入那一刻算好并冗余存,不要查询时按当前价格现算——价格会变,历史账单不能跟着变
- 记账时机是拿到 usage 字段那一刻而不是业务成功那一刻,失败调用和 fallback 同样产生费用;台账可异步写入加重试,但配额扣减必须和业务同事务
Key points
- A ledger is not a log: every charge must trace to a user, a run, a model and a token count, and the schema follows from that
- Store prompt and completion tokens separately, since input and output prices differ three- to four-fold and a single total can neither reproduce the amount nor localize the problem
- Use fixed-point money (numeric in the database, Decimal or BigDecimal in code); float accumulation makes totals disagree with the sum of rows
- Compute cost at write time and store it, rather than recomputing from today's price table, so history stays stable when prices change
- Record at the moment usage is returned, not at business success — failed calls and fallbacks still cost money; ledger writes can be async with retries, but quota decrements must be transactional
一份 LLM 应用的 usage report 通常要覆盖哪些维度?这些维度分别用来做什么决策?Which dimensions should a usage report for an LLM product cover, and what decision does each one drive?
国内高频海外高频进阶#observability#cost#reporting分析过程 · 先想清楚再作答
- 这题最容易答成罗列维度:按用户、按天、按模型、按功能……列得越全越显得没想过。区分度在后半句——每个维度对应的是哪一类行动。列不出行动,说明你只做过报表没用过报表。
- 先给三个主维度和它们各自的行动类型:按用户切是商业动作(谁该涨价、谁在滥用、定价分层能不能覆盖成本);按天切是排障动作(对齐发布时间线,找出是哪次上线让成本跳了台阶);按模型和调用类型切是优化动作(验证分层路由有没有真省到钱、embedding 的量是不是失控了)。三个维度对应三个不同的看板受众。
- 然后升一层,指出绝对金额没有信息量,真正有用的是带分母的单位经济学指标:每次执行成本(当月总成本除以 run 数)、每用户月成本(除以活跃用户数)、每美元产出(完成的业务动作数除以总成本)。前两个用来判断定价能不能覆盖成本,第三个用来判断这套系统值不值得继续投入。
- 举一个能落地的用法证明你真用过:每次执行成本这个比值是把尺子。如果用户数没涨而单次成本涨了,几乎一定是某次上线让提示词变长了,或者某个工具的返回体膨胀了——这个信号通常比超时告警早好几天出现,所以成熟团队会把成本曲线和错误率、延迟并排挂在值班大盘上。
- 最后补一个大多数人会漏的维度:失败与降级。失败的调用照样收费,fallback 会让一次业务操作跨两三次收费调用。报表里不单独切出这一块,你和厂商账单的差额就会恰好集中在故障期,也就是最需要看清成本的时候。
- 可以预期的追问:报表要做到什么实时度?答案是分层——按天的汇总离线跑就够,但配额和预算护栏需要近实时的当月累计,通常用一张按用户按月的汇总表增量更新,而不是每次请求都扫一遍明细。
How to reason about it · think before answering
- The trap is listing dimensions: by user, by day, by model, by feature. Length signals you have not thought about it. The discriminator is the second half — which action each dimension drives. No action means you built reports but never used one.
- Give three primary dimensions with their action type: by user is a commercial action (who to reprice, who is abusing, whether tiering covers cost); by day is a debugging action (align with the release timeline to find which deploy stepped the cost up); by model and call kind is an optimization action (did tiered routing actually save money, is embedding volume running away). Three dimensions, three different dashboard audiences.
- Then go up a level: absolute dollars carry no information. What matters are unit-economics ratios with a denominator — cost per run (monthly cost over run count), cost per active user per month, and business actions completed per dollar. The first two say whether pricing covers cost; the third says whether the system deserves further investment.
- Prove you have used it with a concrete pattern: cost per run is a ruler. If user count is flat but cost per run climbs, it is almost always a deploy that lengthened the prompt or a tool whose response body grew. That signal usually appears days before latency alerts, which is why mature teams put the cost curve next to error rate and latency on the on-call dashboard.
- Add the dimension most people miss: failures and fallbacks. Failed calls are still billed, and a fallback spans two or three billable calls per business operation. Without slicing that out, your gap against the vendor invoice concentrates exactly during incidents, when you most need cost clarity.
- Expect: how fresh does the report need to be? Tier it — daily rollups can run offline, but quota and budget guardrails need near-real-time month-to-date totals, usually from an incrementally updated per-user monthly summary table rather than scanning the detail rows on every request.
答题要点
- 按用户切是商业动作(定价分层、异常账号),按天切是排障动作(对齐发布找成本跳变),按模型和调用类型切是优化动作(验证分层路由、盯 embedding 用量)
- 绝对金额没有信息量,要看带分母的指标:每次执行成本、每用户月成本、每美元产出
- 每次执行成本是把尺子:用户数没涨而单次成本涨了,通常是提示词变长或工具返回体膨胀,比超时告警早好几天出现
- 必须单独切出失败与降级的开销,否则和厂商账单的差额会集中在故障期
- 实时度要分层:按天汇总可离线跑,预算护栏需要近实时的当月累计,用增量汇总表而不是每次扫明细
Key points
- By user drives commercial decisions, by day drives debugging, and by model or call kind drives optimization — three dimensions, three audiences
- Absolute dollars say nothing; use ratios with a denominator: cost per run, cost per active user per month, and business actions per dollar
- Cost per run is a ruler: flat users with rising per-run cost usually means a longer prompt or a bloated tool response, and it shows days before latency alerts
- Slice out failed and fallback calls, or your gap against the vendor invoice concentrates during incidents
- Tier the freshness: daily rollups offline, near-real-time month-to-date totals from an incremental summary table for budget guardrails
D14 部署运维:compose 多 worker、心跳、健康检查、优雅停机、dev/prod 隔离;W2 复盘
系统设计:请设计一个 IM Agent 平台——用户在即时通讯软件里和一个 AI 助手对话,助手能调用工具、记住长期偏好、还能定时主动推送。要求支撑十万日活。System design: design an IM agent platform where users chat with an AI assistant inside a messaging app. The assistant calls tools, remembers long-term preferences, and proactively pushes scheduled messages. Target 100k daily active users.
国内高频海外高频深入#system-design#distributed-systems#cost#operations分析过程 · 先想清楚再作答
- 先别画图。系统设计题最常见的死法是听完就开始画框,二十分钟后面试官发现你解的是另一道题。花三到五分钟问清四件事:一是流量形状(十万日活对应多少并发会话、峰谷比多少),二是延迟要求(首字节要多快,是否必须流式),三是工具的性质(只读查询还是有写操作和副作用),四是主动推送的合规边界(能不能在深夜推、每天上限几条)。这四个答案会实质改变架构,问它们本身就是分数。
- 然后给主干,一句话先定形状:**接入层无状态、消息总线解耦、Worker 有状态且按用户分片、状态全在数据库**。接着按数据流走一遍:IM 平台的 webhook 打到接入层,接入层只做鉴权、限流、落库、投递四件事,立刻返回 202;执行侧从总线取活、跑 Agent 循环、把输出片段回传;主动推送由一个中心调度器按时间投递进同一条总线。**关键论点是接入层耗时确定、执行层耗时不确定,把它们放在一个进程里意味着一次慢的模型调用会占住一个本该毫秒级返回的连接**——这是整道题的立论基础,要主动说出来。
- 再逐个模块给出选择和理由。存储:sessions / runs / messages 三张表,runs 单独存在是因为只有它能回答「这次到底跑完没有」,幂等靠 runs 上的唯一约束而不是先查后插。总线:Redis Streams 的消费组做分摊,语义是至少一次,恰好一次靠消费端幂等做出来;反复失败的消息投递三次后进死信流。顺序:消费组的分配单位是一条消息而业务要求的串行单位是一个用户,所以按 userId 哈希到固定数量分片,每个分片同一时刻只有一个 Worker 持有租约。记忆:pgvector 存 embedding,检索包成一个工具交给模型自己决定要不要查,且不给它身份参数——身份只能来自会话。
- 主动推送这一块要单独讲透,因为它是这道题区别于普通聊天服务的地方。中心调度器命中时间点后只投一条消息,执行侧照旧;幂等键锚在「计划触发的那一分钟」,所以调度器崩溃重启后回看重放不会重复推送。合规上要有时区、静默时段、每日上限三道闸,而且这三道闸必须在投递前判断而不是在推送时判断——否则你已经花了模型调用的钱才发现不该推。
- 然后主动给出容量和成本的数字感,这是高级候选人的分水岭。十万日活、人均十轮对话是一百万次模型调用;按输入输出各一千 token、每百万 token 输入 0.15 美元输出 0.60 美元估算,一天大约七百五十美元。这个数字立刻推出三件事必须做:token 用量要按调用记账并换算成美元(否则你无法定位是哪个用户或哪个功能在烧钱)、要有分层降级(超预算的用户切便宜模型而不是直接拒绝)、以及上下文长度是主要成本杠杆(所以要压缩历史、控制检索条数)。
- 最后收在可运维性上,也就是这一周的落点:多副本部署、心跳发现假死、就绪探针只查自己必需的依赖、优雅停机让发版不掐断对话、dev 与 prod 用键名前缀隔离。**每个机制都要配一句「它失效时会怎样」**——租约会脑裂所以要有自杀规则和护栏令牌、心跳会误判所以面板转红只告警不自动摘流量、停机会超时所以等待要有上限。说不出失效模式的机制,面试官会认为你只是读过。
- 可以预期的追问,按出现频率排:单点在哪(调度器无状态可重启,Redis 和 Postgres 靠托管服务的主备);怎么灰度(新旧 Worker 同时在线,靠消息里的版本字段决定走哪套提示词);用户在助手回复中途又发一句怎么办(三十秒内的改口合并进同一次执行,而不是并发开两个);成本再降一半怎么做(缓存高频问答、压缩历史、把简单意图路由到小模型)。
How to reason about it · think before answering
- Do not start drawing. The most common way to fail a design question is to hear the prompt and immediately sketch boxes, only for the interviewer to realise twenty minutes later that you solved a different problem. Spend three to five minutes on four questions: traffic shape (how many concurrent sessions does 100k DAU imply, and what is the peak-to-trough ratio), latency (how fast must first byte be, is streaming required), the nature of the tools (read-only lookups, or writes with side effects), and the compliance boundary on proactive pushes (may you push at night, what is the daily cap). All four change the architecture materially, so asking them is itself worth points.
- Then state the trunk in one sentence: stateless ingress, a message bus for decoupling, stateful workers sharded by user, all state in the database. Walk the data flow: the messaging platform's webhook hits ingress, which does only auth, rate limiting, persistence and publish, and returns 202 immediately; the execution side pulls work, runs the agent loop, and streams output fragments back; proactive pushes come from a central scheduler publishing onto the same bus. The load-bearing argument is that ingress latency is bounded while execution latency is not, so putting them in one process means one slow model call occupies a connection that should have returned in milliseconds — say this out loud, it is the premise of the whole answer.
- Then justify each module. Storage: sessions, runs and messages, with runs existing separately because only it can answer whether this attempt actually finished; idempotency comes from a unique constraint on runs, not from check-then-insert. Bus: Redis Streams consumer groups for fan-out, at-least-once semantics, with exactly-once manufactured by consumer-side idempotency, and messages that fail three times moved to a dead-letter stream. Ordering: the consumer group's unit of assignment is one message while the business requires serialisation per user, so hash userId into a fixed set of shards and let exactly one worker hold each shard's lease. Memory: embeddings in pgvector, retrieval wrapped as a tool the model chooses to call, with no identity parameter — identity only ever comes from the session.
- Treat proactive push as its own section, because it is what separates this from an ordinary chat service. The central scheduler publishes one message on a time match and the execution side is unchanged; the idempotency key is anchored to the scheduled minute, so replaying after a scheduler restart cannot double-send. For compliance you need timezone, quiet hours and a daily cap — and all three must be evaluated before publishing rather than at send time, or you have already paid for the model call before discovering you should not have pushed.
- Then volunteer capacity and cost numbers, which is what separates senior candidates. 100k DAU at ten turns each is a million model calls; at roughly a thousand tokens in and out, with input at $0.15 and output at $0.60 per million tokens, that is about $750 a day. That number immediately implies three requirements: meter token usage per call and convert to dollars (otherwise you cannot tell which user or feature is burning money), build tiered degradation (push over-budget users to a cheaper model rather than refusing them), and recognise that context length is the dominant cost lever (so compress history and cap retrieved items).
- Land on operability, which is this week's payoff: multiple replicas, heartbeats to surface zombies, readiness probes that only check their own hard dependencies, graceful shutdown so deploys do not cut conversations, and dev/prod isolation via key prefixes. Pair every mechanism with what happens when it fails — leases can split-brain so you need a self-fencing rule and fencing tokens, heartbeats produce false positives so a red dashboard alerts a human rather than auto-draining, shutdown can time out so the wait needs a ceiling. A mechanism without a stated failure mode reads as something you only read about.
- Expect, in rough order of frequency: where are the single points (the scheduler is stateless and restartable; Redis and Postgres rely on managed primary/replica); how do you roll out safely (old and new workers coexist and a version field in the message selects the prompt set); what if the user sends another message mid-reply (merge a change of mind within thirty seconds into the same execution rather than running two concurrently); and how would you halve the cost (cache frequent answers, compress history, route simple intents to a smaller model).
答题要点
- 先用三到五分钟问清四件事:流量形状、延迟要求、工具是否有副作用、主动推送的合规边界——它们会实质改变架构
- 主干一句话:接入层无状态、消息总线解耦、Worker 有状态且按用户分片、状态全在数据库;立论是接入层耗时确定而执行层不确定
- 存储 sessions / runs / messages 三张表,幂等靠 runs 上的唯一约束;总线用 Redis Streams 消费组,至少一次加消费端幂等,三次失败进死信
- 顺序靠 userId 哈希分片加租约:消费组的分配单位是一条消息,而业务要求的串行单位是一个用户
- 记忆用 pgvector 并包成工具交给模型自己决定是否检索,不给身份参数——身份只能来自会话
- 主动推送由中心调度器投递,幂等键锚在计划触发的那一分钟;时区、静默时段、每日上限三道闸必须在投递前判断
- 给出成本数字感:十万日活人均十轮约一百万次调用、一天约七百五十美元,由此推出计量记账、分层降级、压上下文三件事
- 收在可运维性:多副本、心跳查假死、就绪探针只查自己的依赖、优雅停机、dev/prod 前缀隔离
- 每个机制都配一句失效模式:租约会脑裂、心跳会误判、停机会超时——说不出失效模式等于只是读过
Key points
- Spend three to five minutes clarifying four things: traffic shape, latency targets, whether tools have side effects, and the compliance boundary on proactive pushes
- State the trunk in one sentence: stateless ingress, bus for decoupling, stateful workers sharded by user, all state in the database — premised on bounded ingress latency versus unbounded execution latency
- Storage is sessions/runs/messages with idempotency from a unique constraint on runs; the bus is Redis Streams consumer groups, at-least-once plus consumer idempotency, dead-lettering after three failures
- Ordering comes from hashing userId into shards plus leases: the consumer group assigns per message while the business serialises per user
- Memory is pgvector exposed as a tool the model may call, with no identity parameter — identity comes only from the session
- Proactive push flows through a central scheduler with the idempotency key anchored to the scheduled minute; timezone, quiet hours and daily caps are enforced before publishing
- Bring numbers: 100k DAU at ten turns is ~1M calls and ~$750/day, which implies metering, tiered degradation, and context length as the main cost lever
- Land on operability: replicas, heartbeats for zombies, readiness probes scoped to own dependencies, graceful shutdown, dev/prod prefix isolation
- Pair each mechanism with its failure mode — leases split-brain, heartbeats false-positive, shutdown times out; a mechanism without one reads as book knowledge