面试题库
共 328 题,当前筛选 8 题。
课程全部30 天从前端工程师到 Agent 工程师5 天提示词工程零基础Claude 高效使用:从对话到 Claude CodeCodex 与 OpenAI Agents SDK 高效使用7 天 MCP:把工具接进任何 Agent7 天 Agent Skills:把经验做成可复用能力5 天上下文工程14 天 RAG:从检索到可信回答14 天用 Agent 搭一条 AI 短剧生产线
标签
全部#distributed-systems8#evaluation30#cost29#reliability24#architecture22#agent-skills21#security18#observability15#streaming12#error-handling11#idempotency10#system-design10
还有 361 个标签收起标签
#api-design9#coding-agent9#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 工程师
D10 分片与租约:userId 哈希→shard、SET NX + TTL + Lua 续约、同用户顺序、handoff
在一个多 worker 的 Agent 服务里,怎么保证同一个用户的消息严格按顺序被处理?In a multi-worker agent service, how do you guarantee that one user's messages are processed in strict order?
国内高频海外高频进阶#ordering#sharding#distributed-systems分析过程 · 先想清楚再作答
- 这题是系统设计小题,考的是你能不能把「顺序」拆成分层的保证,而不是丢一个中间件名字。只答「用 Kafka 按 key 分区」不算错,但没有回答「分区之后进程内怎么办」,会被追着问。
- 拆法是从消息进入系统到产生副作用,逐层点出谁在保顺序,一共四层。第一层入队有序:接入层落库时给同一会话的消息发连续 seq,并按 seq 投递,总线对同一条流是追加有序的,这层几乎免费。第二层消费者唯一:同一个分片同一时刻只有一个 worker 在读,靠租约实现——这是跨进程的那一半。
- 第三层进程内串行:同一个分片内不能并发处理两条消息。这一层最容易被自己破坏——为了提高吞吐把一批消息丢进 Promise.all 或线程池,顺序就在自己的代码里丢掉了。要明确说出「租约保住跨进程的顺序,await 保住进程内的顺序,缺一不可」。第四层在途优先:前任 worker 挂掉时手上可能有一条已领取但没确认的消息,接管者必须先把它 claim 回来再读新消息,否则新消息会插到旧消息前面。
- 紧接着说串行的代价,这是面试官判断你有没有上过线的地方:串行意味着一个用户的慢请求会挡住同一个分片上其他用户的消息,一次 20 秒的模型调用能让这个 worker 名下的几十个分片全部停摆。正确做法是按分片并行、分片内串行——每个持有的分片各起一条独立处理链。并行的单位是分片,不是消息。
- 主动划边界:这套机制只保证同一个用户的顺序,不保证跨用户的全局顺序。全局有序需要把并行度压到 1,那就没有分布式可谈了。顺序性和并行度是一对反比,分片的意义就是把「必须有序」的范围缩到刚好够用的最小值。
- 可预期的追问一:不用租约行不行?可以,Kafka 按 key 分区、或者让 Gateway 直连固定 worker(粘性路由)都能得到亲和性,但代价分别是分区数难改、以及 worker 挂掉时需要额外的故障转移机制——租约恰好把故障转移也一并解决了。追问二:能不能干脆让业务对乱序免疫?部分可以,比如把「追加消息」设计成幂等且可交换的写入,但只要存在不可逆的副作用(退款、发货),顺序就必须保。
How to reason about it · think before answering
- This is a small system-design question testing whether you can decompose ordering into layered guarantees rather than naming a middleware. 'Partition by key in Kafka' is not wrong, but it leaves 'and inside the process?' unanswered, which is exactly where they will push.
- Decompose it along the path from ingress to side effect, four layers. One, ordered ingress: the gateway assigns consecutive seq numbers per session on write and publishes in seq order; a single stream is append-ordered, so this layer is nearly free. Two, single consumer: only one worker reads a given shard at a time, enforced by the lease — that is the cross-process half.
- Three, in-process serialization: no two messages from the same shard may be handled concurrently. This is the layer people break themselves, by dropping a batch into Promise.all or a thread pool to raise throughput. Say it explicitly: the lease preserves order across processes, await preserves it inside one. Four, in-flight first: a killed predecessor may hold a delivered but unacknowledged message, so the successor must claim it back before reading anything new, otherwise a newer message jumps ahead of an older one.
- Then name the cost of serialization, which is where they judge whether you have shipped this: a single slow request blocks other users on the same shard, and one 20-second model call can stall every shard that worker owns. The right shape is parallel across shards, serial within a shard — one independent processing chain per held shard. The unit of parallelism is the shard, not the message.
- Volunteer the boundary: this only guarantees per-user order, never a global order across users. Global ordering requires parallelism of one, which defeats the point. Ordering and parallelism trade off directly, so sharding exists to shrink the 'must be ordered' scope to the smallest useful unit.
- Expect two follow-ups. Could you skip leases? Yes — Kafka key partitioning or sticky routing from the gateway to a fixed worker also gives affinity, at the cost of rigid partition counts or of needing a separate failover mechanism when a worker dies; the lease happens to solve failover at the same time. Could the business simply tolerate reordering? Partly, if appends are idempotent and commutative, but any irreversible side effect such as a refund or a shipment forces you to preserve order.
答题要点
- 把顺序拆成四层:入队有序(连续 seq)、消费者唯一(租约)、进程内串行(逐条 await)、在途消息优先被接管者 claim 回来
- 租约保住跨进程的顺序,await 保住进程内的顺序,缺一不可——用 Promise.all 提吞吐会当场毁掉顺序
- 并行的单位是分片不是消息:每个持有的分片各起一条独立处理链,否则一次慢调用会拖停这个 worker 的全部分片
- 只保证同一用户的顺序,不保证跨用户全局有序;顺序性和并行度是反比,分片就是把有序范围缩到最小
- 替代方案是 Kafka 按 key 分区或粘性路由,但它们不自带故障转移;只要存在不可逆副作用,顺序就必须保
Key points
- Decompose ordering into four layers: ordered ingress with consecutive seq, a single consumer per shard via the lease, in-process serialization with await, and claiming the predecessor's in-flight message first
- The lease preserves order across processes and await preserves it within one — reaching for Promise.all to raise throughput destroys it
- The unit of parallelism is the shard, not the message: one chain per held shard, or a single slow call stalls every shard that worker owns
- Only per-user order is guaranteed, never a global order; ordering trades off against parallelism, so sharding shrinks the ordered scope
- Alternatives are Kafka key partitioning or sticky routing, but neither brings failover; any irreversible side effect makes ordering mandatory
D11 run 状态机、输出流回传、按 runId 保序、SSE 等待者、30s 打断合并
怎么设计一次 Agent 执行(run)的状态机?需要覆盖哪些异常状态?How would you design the state machine for one agent run, and which failure states must it cover?
国内高频海外高频基础#state-machine#distributed-systems分析过程 · 先想清楚再作答
- 这题的区分度不在「能不能列出几个状态」,而在你有没有说出「为什么单进程时代不需要它」。答不出这一点,说明你只是抄过一张状态图。
- 先给动机:单进程里「执行到哪一步了」就是那个函数栈,状态存在于进程内存里,不需要名字。拆成 Gateway 与 Worker 之后,至少三方要同时回答同一个问题——接入层要判断还挂不挂 SSE,执行层要判断这条消息是否已被人领走,前端重开页面要判断上次的问题还在不在生成。三方不同进程,只能靠一张表对齐。
- 再给状态:pending 到 running 到 streaming 到 done 是正常路径,failed(重试耗尽)与 cancelled(被打断合并或用户取消)是两个随时可以走的异常出口。主动说明为什么 running 和 streaming 要分开:前者是「有人领走了但还没有一个字」,后者是「第一个字已出来」,这条线就是首字延迟的观测点,也是前端决定转圈还是打字机的依据。
- 结论要落到「状态机是用来挡写入的」:终态没有出边这一条最值钱。至少一次投递下「已经 done 的 run 又收到一个片段」是常态,没有转换表,那一笔会安静地写进库,用户看到回复末尾多出半句话,而日志里查不出是谁写的。
- 补一条纪律,这是有没有落地过的分水岭:所有写状态的地方都必须过同一个转换函数。绕过它直接执行一条更新语句,状态机就退化成注释了。
- 可以预期的追问:状态存哪、并发怎么办?答数据库那一行是唯一真相,转换用带条件的更新(更新时把当前状态写进 where 子句),失败说明有人抢先改过,这时候重读再决定,而不是覆盖。
How to reason about it · think before answering
- The discriminator is not listing states, it is explaining why a single-process service does not need them at all. Without that, you have only memorized a diagram.
- Start from motivation: in one process the call stack *is* the state. Once you split gateway and worker, three parties must answer the same question independently — the gateway decides whether to keep an SSE connection open, the worker decides whether someone already claimed the message, and a reopened browser tab asks whether the previous question is still generating. Different processes, so the answer has to live in a table.
- Then the states: pending to running to streaming to done on the happy path, with failed (retries exhausted) and cancelled (superseded by a merge, or user-cancelled) as exits available from anywhere. Volunteer why running and streaming are separate: running means claimed but no token yet, streaming means the first token is out. That boundary is your time-to-first-token probe and the frontend's cue to switch from spinner to typewriter.
- Land on the real purpose: the machine exists to reject writes. Terminal states having no outgoing edges is the most valuable row in the table. Under at-least-once delivery, a done run receiving one more chunk is routine, and without the table that chunk lands silently — the user sees half a sentence appended and the logs show nothing wrong.
- Add the discipline that separates shipped from read-about: every status write goes through one transition function. One raw UPDATE that bypasses it and the state machine is just a comment.
- Expect the follow-up on storage and concurrency: the database row is the single source of truth, and transitions are conditional updates that include the expected current status in the WHERE clause. Zero rows affected means someone moved first — re-read and decide, never blindly overwrite.
答题要点
- 单进程里状态就是函数栈;拆成 Gateway 与 Worker 后有三方要独立回答「这次执行到哪了」,必须落成一张表
- 正常路径 pending 到 running 到 streaming 到 done;异常出口 failed(重试耗尽)与 cancelled(打断合并或用户取消)
- running 与 streaming 分开,是为了观测首字延迟,也让前端知道该转圈还是该开始打字机效果
- 终态没有出边是核心:至少一次投递下的迟到片段会被当场挡住,而不是安静写进库
- 纪律:所有状态写入都过同一个转换函数,并用带当前状态条件的更新来处理并发
Key points
- In one process the call stack is the state; after splitting gateway and worker, three parties need the same answer, so it has to be a table
- Happy path pending, running, streaming, done; exits are failed (retries exhausted) and cancelled (merged or user-cancelled)
- Separating running from streaming gives you a time-to-first-token probe and tells the UI when to switch from spinner to typewriter
- Terminal states with no outgoing edges reject the late chunks that at-least-once delivery guarantees you will get
- Every status write goes through one transition function, implemented as a conditional update on the expected current status
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
怎么保证一个 cron 任务不会被重复投递或重复执行?幂等键应该怎么构造?How do you keep a cron job from being published or executed twice, and how should the idempotency key be built?
国内高频海外高频深入#idempotency#scheduling#distributed-systems分析过程 · 先想清楚再作答
- 题眼在「投递」和「执行」是两件事。很多人只答一半:要么只说消费组保证一条消息一个消费者(那只挡住了执行侧的重复),要么只说加锁(那只挡住了投递侧,还挡不干净)。完整答案要说清两侧各自的重复来源,以及一个能同时兜住的兜底。
- 先拆重复的来源:投递侧的重复来自多个调度器实例、调度器重启后的补发重放、以及消息总线本身的至少一次语义;执行侧的重复来自 Worker 处理到一半崩溃后消息被 XAUTOCLAIM 转交给别人。这两类重复用不同手段挡效率完全不同。
- 再给核心结论:不要用分布式锁去做互斥,用数据层的唯一约束做去重。原因是锁只能提供「大概率互斥」——租约到期而前任进程其实只是 GC 卡住的那一瞬间,两个调度器都会认为自己持有,各发一次;而唯一约束是在最终落库那一步判断的,无论上游发了几次,任务表里只会多一行。能在唯一约束上解决的问题,不要升级成分布式协调问题。
- 然后回答幂等键怎么构造,这是最容易翻车的一步:键必须是「任务 id 加计划触发的那一分钟」,绝不能用当前时刻。两个调度器实例的时钟不可能对齐到毫秒,一个在 09:00:00.120 醒来、另一个在 09:00:00.480 醒来,用 now 算出来的键不一样,去重完全失效。把秒和毫秒截掉之后,无论谁在这一分钟里的哪一刻醒来,算出的键都是同一个字符串。落到代码上就是 insert 加 on conflict do nothing,冲突说明已经有人建过这次执行,直接 ack 掉不执行。
- 补一句作用范围:这套只保证「同一个触发点只产生一次执行」,不保证「执行内部的副作用只发生一次」。如果这次执行要发短信、要扣款,那些副作用还得各自带自己的幂等键,因为 Worker 可能在发完短信之后、写完状态之前崩掉。这一层区分是加分项。
- 可以预期的追问:那漏发怎么办?答宁可多发不可少发——调度器启动时回看最近 N 分钟逐分钟重放,重复投递被幂等键吃掉。at-least-once 加幂等是分布式系统里最省心的一组搭配,反过来先追求 exactly-once 再补幂等,通常两头都做不好。
How to reason about it · think before answering
- The hinge is that publishing and executing are two separate problems. Most candidates answer half: either only the consumer group (which stops duplicate execution) or only a lock (which stops duplicate publishing, and imperfectly). A complete answer names the duplicate sources on both sides plus one backstop that covers both.
- Enumerate the sources: duplicate publishes come from multiple scheduler instances, from replay after a scheduler restart, and from the bus's own at-least-once semantics. Duplicate executions come from a worker crashing mid-processing and the message being reclaimed by another consumer. The two need different treatment.
- State the core conclusion: do not reach for a distributed lock, use a uniqueness constraint in the data layer. A lease only gives you probable mutual exclusion — in the instant when the TTL expires while the previous holder is merely stuck in GC, both schedulers believe they hold it and both publish. A uniqueness constraint is evaluated at the final insert, so no matter how many times upstream published, the table gains exactly one row. Do not escalate a problem solvable by a constraint into a distributed coordination problem.
- Then the key construction, which is where people fail: the key must be the task id plus the scheduled minute, never the current instant. Two scheduler clocks never align to the millisecond; one wakes at 09:00:00.120 and the other at 09:00:00.480, so keys built from now differ and dedup collapses. Truncate seconds and milliseconds and every instance computes the same string for that minute. In code this is an insert with on conflict do nothing; a conflict means the execution already exists, so ack the message and skip.
- Scope it honestly: this guarantees one execution per trigger point, not that side effects inside the execution happen once. If the run sends an SMS or charges a card, those side effects need their own idempotency keys, because the worker can crash after sending and before writing status. Making that distinction earns points.
- Expect: what about missed triggers? Prefer over-publishing to under-publishing — replay the last N minutes at startup and let the idempotency key absorb duplicates. At-least-once plus idempotency is the easiest combination in distributed systems; chasing exactly-once first and adding idempotency later usually achieves neither.
答题要点
- 投递重复和执行重复是两件事:前者来自多调度器实例、重启重放和总线的至少一次语义,后者来自 Worker 崩溃后消息被转交
- 用数据层唯一约束去重,不要用分布式锁互斥:租约过期而前任还活着的瞬间两个调度器都会各发一次,而唯一约束在落库那一步只放行一条
- 幂等键必须是任务 id 加计划触发的那一分钟,不能用当前时刻——两个实例的醒来时刻永远不同,用 now 会让去重完全失效
- 落到代码上是 insert 加 on conflict do nothing,冲突就直接 ack 不执行
- 这只保证一个触发点一次执行,执行内部的发短信、扣款等副作用要各自带幂等键;宁可多发不可少发,靠 at-least-once 加幂等兜底
Key points
- Duplicate publishing and duplicate execution are separate: the former comes from multiple schedulers, restart replay and at-least-once delivery; the latter from a crashed worker's message being reclaimed
- Dedupe with a database uniqueness constraint rather than a distributed lock: when a lease expires while the holder is only GC-stalled, both schedulers publish, whereas the constraint admits exactly one row
- Build the key from the task id plus the scheduled minute, never the current instant — instances never wake at the same millisecond, so a now-based key defeats dedup entirely
- In code this is an insert with on conflict do nothing; on conflict, ack the message and skip execution
- This guarantees one execution per trigger, not once-only side effects — SMS or payments inside the run need their own keys; prefer over-publishing and let at-least-once plus idempotency absorb it
D14 部署运维:compose 多 worker、心跳、健康检查、优雅停机、dev/prod 隔离;W2 复盘
多实例部署下,怎么设计心跳和健康检查?两者是同一件事吗?With multiple replicas, how do you design heartbeats and health checks? Are they the same thing?
国内高频海外高频进阶#observability#deployment#distributed-systems分析过程 · 先想清楚再作答
- 题眼是「两者是同一件事吗」。答「都是探活」直接失分——面试官想看你能不能把一个词拆成三个不同的问题,因为混起来会造成真事故。
- 先拆问题:存活探针回答「这进程要不要被重启」,就绪探针回答「现在能不能给我发流量」,心跳面板回答「集群此刻是什么状态」。三者的读者不同:前两个给编排系统,第三个给人。
- 再说心跳为什么不可省:编排系统只能看到进程存活,而 Worker 完全可以进程活着而活儿全停——事件循环被死循环占住、连接池耗尽后取消息全超时、宿主机 CPU 被邻居打满。这类假死恰好是编排系统看不见的那种,只有业务自己上报的心跳能发现。
- 方向也要答对:心跳是副本自己 push,不是 Gateway 逐个 pull。因为容器随时换 IP 和主机名,去问的一方需要一份永远在变的名单,而那份名单本身就得靠心跳维护,逻辑绕回来了。上报内容至少三样:时间戳判活、在跑任务数区分闲和忙、版本号在滚动发布时看新旧两批各剩几个。
- 最关键的一刀是隔离性:**不要把下游依赖查进就绪探针**。一个 Worker 失联导致所有 Gateway 的就绪探针同时转红,编排系统会把整个接入层摘光——一个非核心故障被自己升级成全站不可用。而实际上那个 Worker 失联根本不影响接单:消息还在流里,没确认的会被别人接手,它的租约会因 TTL 到期而易主。
- 可以预期的追问:那 Gateway 怎么判断某个 Worker 可不可用?答「它不判断,也不需要判断」——Gateway 从不指定某个 Worker 干活,派活由消费组和租约决定,心跳的用途是观测和告警,不是路由。答到这里就说明你真的想清楚了分层。
How to reason about it · think before answering
- The hinge is are they the same thing. Answering both check liveness loses the point — the interviewer wants to see you split one word into three distinct questions, because conflating them causes real outages.
- Separate them: a liveness probe answers should this process be restarted, a readiness probe answers can you send me traffic now, and a heartbeat dashboard answers what is the cluster's state. The audiences differ: the first two are for the orchestrator, the third is for a human.
- Then say why heartbeats are not optional: the orchestrator only sees process liveness, but a worker can be alive while doing no work at all — a blocked event loop, an exhausted connection pool timing out every read, a noisy neighbour saturating host CPU. This kind of zombie is exactly what the orchestrator cannot see, and only an application-level heartbeat catches it.
- Get the direction right too: replicas push their own heartbeat rather than the gateway polling each one. Containers change IP and hostname constantly, so a poller needs a roster that is always changing — and maintaining that roster is what heartbeats are for, so the logic is circular. Report at least three things: a timestamp for liveness, in-flight count to distinguish idle from overloaded, and a version so you can watch old and new replicas during a rollout.
- The sharpest point is isolation: do not query downstream dependencies inside a readiness probe. One worker going quiet would turn every gateway's readiness red, and the orchestrator would pull the entire ingress layer — turning a non-critical fault into a full outage. In reality that worker's absence does not stop intake at all: messages sit in the stream, unacked ones get claimed by someone else, and its lease changes hands when the TTL expires.
- Expect: so how does the gateway decide whether a worker is usable? Answer that it does not, and does not need to — the gateway never assigns work to a specific worker; the consumer group and the lease decide that. Heartbeat data is for observability and alerting, not routing. Getting here shows you actually understand the layering.
答题要点
- 一个词要拆成三个问题:存活探针(要不要重启)、就绪探针(能不能发流量)、心跳面板(集群什么状态),前两个给编排系统、第三个给人
- 编排系统只看得见进程存活,看不见假死(事件循环卡住、连接池耗尽、CPU 被抢),所以业务层心跳不可省
- 心跳必须是副本 push 而不是 Gateway pull:容器随时换 IP,pull 需要一份靠心跳才能维护的名单,逻辑绕回来了
- 上报时间戳、在跑任务数、版本号三样,分别用于判活、区分忙闲、观察滚动发布进度
- 不要把下游依赖查进就绪探针,否则一个 Worker 失联会让整个接入层被摘掉,把非核心故障升级成全站不可用
- Gateway 不判断 Worker 可用性——派活由消费组和租约决定,心跳只用于观测告警,不用于路由
Key points
- Split one word into three questions: liveness (restart me?), readiness (send me traffic?), heartbeat dashboard (what is the cluster doing?) — first two for the orchestrator, third for humans
- The orchestrator sees process liveness but not zombies (blocked loop, exhausted pool, stolen CPU), so an application-level heartbeat is mandatory
- Heartbeats must be pushed by replicas, not polled by the gateway: containers change IP constantly and polling needs a roster that heartbeats themselves maintain
- Report timestamp, in-flight count and version — for liveness, load, and rollout progress respectively
- Never query downstream dependencies in a readiness probe, or one quiet worker pulls the whole ingress layer and escalates a minor fault into an outage
- The gateway does not judge worker availability — the consumer group and lease assign work; heartbeats are for observability, not routing
系统设计:请设计一个 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
D19 跨服务 Agent 集成:用户级 JWT 铸造、JWKS 验签、inject/memory/usage 三类接口、幂等 externalId
跨服务调用的幂等键该怎么设计?由谁生成、存在哪、重复了返回什么?How do you design an idempotency key for cross-service calls — who generates it, where does it live, and what do you return on a repeat?
国内高频海外高频进阶#idempotency#distributed-systems#api-design分析过程 · 先想清楚再作答
- 这题的区分度全在实现细节上。概念谁都会说,能不能答对「谁生成、存在哪、返回什么」这三个具体问题,直接暴露你有没有真做过。
- 先立一条铁律:**幂等的最终裁判必须是数据库的唯一约束**,不是应用层的「先查一下有没有」。先查后插在单进程测试里永远是对的,一上多实例就出双份——两个副本同时查、同时发现没有、同时插入,这个时间窗压测时窄到复现不出来,上线后每天出几条脏数据。
- 再答「谁生成」:由**调用方**生成,因为只有它知道重试的那两次是同一件事;但键必须由事件内容决定,不能是每次重试重新生成的随机 UUID——那等于没有幂等。这条和 D8 的用户消息幂等是同一条判据。
- 跨服务比同服务多一个坑,这是本题最有价值的一点:**调用方给的 id 不能直接当键用**。两个不同的调用方各自造出 evt-1 是迟早的事,撞车之后的表现不是报错,而是后来那个用户静默收不到消息——他的事件被当成重复丢掉了,日志里干干净净。所以落库前要加命名空间,用「签发方 + 用户 id + 事件 id」三段拼,而且三段都取自验签后的令牌,伪造不了。
- 「返回什么」也是个坑:重复送达要返回 200 并附上第一次的结果,不要返回 409。重复不是错误,是分布式系统的常态;回 409 会让调用方的重试逻辑把它当失败处理,越重试越乱。
- 可以预期的追问:这张表会不会无限涨?答「会,所以要有保留期」——按业务能接受的重放窗口设一个 TTL(比如 7 天)定期清理,同时说明清理之后超期的重复请求会被当成新事件,这是一个明确的、可接受的取舍,不是漏洞。
How to reason about it · think before answering
- This question separates people entirely on implementation detail. Anyone can define idempotency; answering who generates the key, where it lives, and what a repeat returns shows whether you have actually built one.
- Start with the rule: the final arbiter must be a database uniqueness constraint, not an application-level check-then-insert. Check-then-insert always passes single-process tests and produces duplicates the moment you run two replicas — both check, both find nothing, both insert. The window is too narrow to reproduce under load testing and wide enough to produce dirty rows daily in production.
- Who generates it: the caller, because only the caller knows that two retries are the same event. But the key must be derived from the event itself, never a fresh random UUID per retry — that is idempotency in name only. Same criterion as the user-message case from day 8.
- Cross-service adds one trap worth the most points: never use the caller's raw id as the key. Two different callers will eventually both produce evt-1, and the failure is not an error — the second user silently receives nothing, because their event is treated as a duplicate and the logs look clean. Namespace it: issuer plus user id plus event id, all three taken from the verified token so none of them can be forged.
- What to return also matters: a repeat gets 200 with the original result, not 409. Repeats are normal in distributed systems; a 409 makes the caller's retry logic treat it as a failure and the situation compounds.
- Expect: does this table grow forever? Yes, so give it a retention window — a TTL matching the replay window the business tolerates, say seven days, with periodic cleanup. Say plainly that a duplicate arriving after cleanup is treated as new; that is a stated trade-off, not a hole.
答题要点
- 最终裁判是数据库唯一约束加 on conflict do nothing,先查后插在多实例下必然出双份
- 键由调用方生成,但必须由事件内容决定,随机 UUID 等于没有幂等
- 调用方给的 id 不能直接当键:加命名空间(签发方 + 用户 id + 事件 id),三段都取自验签后的令牌
- 撞车的后果不是报错而是另一个用户静默收不到消息,日志里看不出异常
- 重复送达返回 200 加第一次的结果,不要返回 409,否则调用方会当失败继续重试
- 幂等表要设保留期,超期后的重复会被当成新事件,这是明确取舍不是漏洞
Key points
- The arbiter is a unique constraint plus on conflict do nothing; check-then-insert duplicates as soon as you run two replicas
- The caller generates the key, but it must be derived from the event — a fresh UUID per retry is not idempotency
- Never use the caller's raw id: namespace it with issuer plus user id plus event id, all taken from the verified token
- A collision does not raise an error; it silently drops another user's event and leaves clean logs
- Return 200 with the original result on a repeat, never 409, or the caller's retry logic treats success as failure
- Give the table a retention window and state that post-cleanup repeats count as new events — a stated trade-off, not a hole
D21 评估与可观测:golden set、LLM-as-judge、tracing、失败率/成本面板;Pi vs LangGraph 总结;W3 复盘
多 Agent 系统的可观测性要看哪些东西?和单 Agent 有什么不一样?What does observability look like for a multi-agent system, and how does it differ from a single agent?
国内高频海外高频进阶#observability#tracing#distributed-systems分析过程 · 先想清楚再作答
- 题眼在「不一样」。答「加日志加监控」等于没答,要说清结构上的差别。
- 结构差别一句话:**单 Agent 的一次调用是一条线,多 Agent 是一棵树。** 一次请求走监督者路由、规划者拆三件、三个执行者并行、评审者打回一件、那件重跑、最后汇总——按时间平铺看不出谁在谁里面,也看不出哪两个是并行的。
- 所以 span 必须带**父指针**,这是全部关键:有它才是树,没它只是一张平铺列表,你知道发生过什么,却不知道谁触发了谁。一条 span 的字段少得出奇——id、父指针、名字、起止时刻、几个属性,就够还原整棵树。
- 父子关系怎么传下去也是个考点:**不要在每个函数上加一个 parentSpanId 参数**,每加一个节点都要改签名、漏一处断一截。用语言自带的隐式上下文——JS 的 AsyncLocalStorage、Python 的 contextvars、Swift 的 TaskLocal,Java 用 ScopedValue 或 ThreadLocal 配合线程池的显式传播。
- 然后说面板要回答哪四个问题:错了多少(通过率、路由准确率、降级率、兜底率)、慢在哪(p50/p95)、花了多少、**钱花在哪个角色身上**(按节点分摊)。最后一样是多 Agent 特有的,也最有用——实测执行者节点占了成本三分之一强,一眼就知道压成本先压哪儿。
- 还有一条地基性的:**面板不是另一套埋点,是 trace 的聚合**。同一份原始数据横着看是树、竖着堆是面板。两套数据来源迟早会对不上,然后没有人相信任何一个。
- 最后回指路由:路由决策是模型做的,同一句话下次未必给同样的答案,所以必须把**路由理由**一起记下来——当时不记,那次判断就永远丢了。这是多 Agent 里最容易漏、又最需要事后审计的一条。
How to reason about it · think before answering
- The hinge is differ. Saying add logs and metrics is a non-answer; name the structural difference.
- In one sentence: a single agent's call is a line, a multi-agent request is a tree. One request goes supervisor routing, planner splitting into three, three executors in parallel, a critic rejecting one, that one rerunning, then aggregation — flattened by time you cannot see nesting or which two ran concurrently.
- So spans must carry a parent pointer; that is the whole game. With it you have a tree, without it a flat list where you know what happened but not what triggered what. A span needs surprisingly few fields — id, parent, name, start and end, a few attributes — to reconstruct the entire tree.
- How the parent propagates is itself an interview point: do not thread a parentSpanId parameter through every function, because each new node then changes a signature and one omission breaks the chain. Use the language's implicit context — AsyncLocalStorage in JS, contextvars in Python, TaskLocal in Swift, and ScopedValue or ThreadLocal with explicit propagation across thread pools in Java.
- Then the four questions a dashboard must answer: how much is wrong (pass rate, routing accuracy, degradation rate, fallback rate), where is it slow (p50/p95), what did it cost, and which role spent the money (cost attributed per node). That last one is multi-agent specific and the most actionable — measured, executor nodes took over a third of spend, telling you immediately where to optimise.
- One foundational point: the dashboard is not a second instrumentation layer, it is an aggregation of traces. The same raw data read across is a tree and stacked up is a dashboard. Two separate sources will eventually disagree, after which nobody trusts either.
- Finally, tie back to routing: the routing decision is made by a model and the same sentence may route differently next time, so the routing rationale must be recorded — if you do not capture it then, that judgement is gone forever. It is the easiest thing to omit and the thing most needing post-hoc audit.
答题要点
- 结构差别:单 Agent 一次调用是一条线,多 Agent 是一棵树(路由→拆分→并行执行→评审打回→重跑→汇总)
- span 必须带父指针,否则只是平铺列表,看不出嵌套关系也看不出并行
- 父子关系用语言自带的隐式上下文传(AsyncLocalStorage / contextvars / TaskLocal),不要在每个函数签名上加参数
- 面板回答四个问题:错了多少、慢在哪、花了多少、钱花在哪个角色身上(最后一个是多 Agent 特有且最有用)
- 面板必须是 trace 的聚合而不是另一套埋点,两套数据源迟早对不上
- 路由理由必须记下来:路由是模型做的决策,当时不记那次判断就永远丢了
Key points
- Structural difference: a single agent call is a line, multi-agent is a tree (route, split, parallel execute, critic reject, rerun, aggregate)
- Spans need a parent pointer, or you have a flat list showing neither nesting nor parallelism
- Propagate parentage through implicit context (AsyncLocalStorage / contextvars / TaskLocal), not a parameter on every signature
- The dashboard answers four questions: how much is wrong, where it is slow, what it cost, and which role spent it — the last is multi-agent specific and most actionable
- The dashboard must be an aggregation of traces, not separate instrumentation; two sources will disagree
- Record the routing rationale: routing is a model decision, and uncaptured it is lost forever