面试题库
共 328 题,当前筛选 61 题。
还有 92 个标签收起标签
30 天从前端工程师到 Agent 工程师
D1 LLM API 基础:messages/roles、token、流式、temperature;Agent 到底是什么
为什么 LLM 应用几乎都用流式输出?SSE 和 WebSocket 该怎么选?Why do LLM apps stream responses, and how do you choose between SSE and WebSockets?
国内高频海外高频进阶#streaming#protocol分析过程 · 先想清楚再作答
- 第一问考的是对延迟指标的敏感度:要能区分「首字延迟」和「全文延迟」,并说出模型逐 token 生成决定了前者远小于后者。
- 把它翻译成产品语言:用户 1 秒内看到反馈 vs 对着空白等 20 秒,这是体验的分水岭,不是锦上添花。
- 第二问不要背优缺点表,先问自己「客户端需不需要频繁上行」——这一条几乎决定了答案。
- 只需要服务器往下推 token,SSE 就够:它跑在普通 HTTP 上,代理和负载均衡友好,还自带重连。需要语音、协同、频繁打断这类双向高频交互,才值得上 WebSocket。
- 给出多数产品的真实形态:请求走普通 POST,回复走 SSE,另配一个取消接口——顺势可以引到「POST 的 SSE 用不了 EventSource 的自动重连」这个坑。
How to reason about it · think before answering
- The first half tests latency literacy: separate time-to-first-token from total latency and tie it to sequential generation.
- Translate to product terms: feedback within a second versus twenty seconds of blank screen.
- For the second half, skip the pros-and-cons table and ask whether the client needs frequent upstream messages.
- Server-to-client tokens only means SSE suffices: plain HTTP, proxy-friendly, with built-in reconnection. Voice, collaboration or frequent interrupts justify WebSockets.
- State the common shape: plain POST for the request, SSE for the reply, plus a cancel endpoint — which sets up the trap that POST-based SSE cannot use EventSource auto-reconnect.
答题要点
- 模型逐 token 生成,首字延迟远小于全文延迟;流式让用户 1 秒内看到反馈而不是等 20 秒
- SSE 是单向、基于 HTTP 的文本协议,自动重连、穿透代理容易,天然适合服务器→客户端的 token 流
- WebSocket 双向、更适合需要客户端频繁上行(语音、协同编辑、打断)的场景,但代理/负载均衡更麻烦
- 多数聊天产品:请求用普通 HTTP POST,回复用 SSE;需要打断时再加一个取消接口
Key points
- Models emit tokens sequentially; time-to-first-token is far lower than full latency
- SSE is one-way over HTTP with built-in reconnect and easy proxying, ideal for server→client token streams
- WebSockets are bidirectional, better when the client sends often (voice, collaboration, interrupts) but harder to load-balance
- Most chat products: plain POST for the request, SSE for the reply, plus a cancel endpoint
流式回复到一半网络断了,前端和后端各要做什么?EventSource 的自动重连能用上吗?A streaming reply is cut off mid-way. What do the client and server each do, and can EventSource auto-reconnect help?
国内高频海外高频进阶#streaming#reliability#sse分析过程 · 先想清楚再作答
- 这题的陷阱在后半句。很多人背过「SSE 自带重连」,就直接答自动重连能救——那是错的,必须先分清两种 SSE 用法。
- 浏览器原生 EventSource 确实按规范自动重连:重连时带 Last-Event-ID 请求头,服务器用 id: 打点、用 retry: 设间隔;但它只能发 GET,且要求响应 Content-Type 是 text/event-stream。
- 而 LLM chat API 必须 POST(messages 要放在请求体里),所以实际用的是 fetch 加手写 SSE 解析——EventSource 那套自动重连一行都用不上。
- 于是前端职责变成:自己判定断流、自己重试、自己保存已收到的部分。后端职责是让重试是安全的——响应可续、副作用幂等。
- 给出续写策略并说清边界:把已收到的内容作为上下文构造续写请求;但工具调用块和思考块无法部分恢复,只能从最近的完整文本块续。
- 可预期追问:非 200 响应会重连吗?按规范不会——状态码不是 200 或 Content-Type 不对,连接直接判定失败;服务器还可以用 204 主动叫停重连。
How to reason about it · think before answering
- The trap is the second half: people who memorized 'SSE reconnects automatically' answer yes, which is wrong.
- 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.
- 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.
- 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.
- 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.
- 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
移动端 App 里的对话,网络频繁抖动,你会怎么设计重连策略?On mobile, connectivity is flaky. How would you design the reconnection strategy for a chat feature?
国内高频海外高频进阶#reliability#mobile#streaming分析过程 · 先想清楚再作答
- 先说明移动端和浏览器的差别:网络在 WiFi 与蜂窝之间切换、App 会被系统挂起、后台执行时间受限,所以不能照搬网页那套。
- 重试节奏用指数退避加随机抖动。抖动这一条常被忽略,但它是防止大面积断网恢复后所有客户端同时涌上来把服务打垮的关键。
- 要设上限:最大重试次数与最大退避间隔,超过就转成显式的「重新加载」按钮交给用户,而不是无限静默重试。
- 区分「短暂抖动」和「真的没网」:监听系统的网络状态变化,没网时直接停止重试并进入离线态,等网络恢复事件再立刻重连,比盲目定时重试省电得多。
- 结合上一题的服务端持久化:App 被系统杀掉后重进,靠会话 id 请求恢复端点,而不是指望本地缓存拼出完整回复。
- 最后补发送侧:用户在离线时发出的消息进本地队列,恢复后按序重发,且每条带幂等键,避免重复发送。
How to reason about it · think before answering
- Start with what makes mobile different: network switches between WiFi and cellular, the OS suspends apps, background time is limited.
- Use exponential backoff with jitter; jitter is the commonly missed part that prevents a thundering herd when a wide outage clears.
- Set ceilings: max attempts and max interval, then surface an explicit reload action instead of retrying silently forever.
- Distinguish a brief blip from being genuinely offline: subscribe to OS connectivity events, stop retrying when offline, and reconnect on the restore event — far cheaper on battery than blind timers.
- Combine with server-side persistence: after the OS kills the app, resume by chat id rather than reconstructing from local cache.
- Finally the send path: queue outgoing messages while offline and replay them in order, each with an idempotency key.
答题要点
- 移动端特殊性:WiFi 与蜂窝切换、App 被挂起、后台执行时间受限,不能照搬网页策略
- 指数退避加随机抖动,抖动用于避免大面积恢复时的重连风暴
- 设最大重试次数与最大间隔,超过后转为显式的重新加载入口,不做无限静默重试
- 监听系统网络状态:离线直接停重试进入离线态,收到恢复事件再重连,比定时轮询省电
- 回复恢复依赖服务端持久化,靠会话 id 请求恢复端点;发送侧用本地队列加幂等键按序重发
Key points
- Mobile differs: network handoffs, OS suspension, limited background time — do not copy the web strategy
- Exponential backoff with jitter, where jitter prevents a reconnect storm when an outage clears
- Cap attempts and interval, then hand the user an explicit reload instead of retrying forever
- Listen to OS connectivity events: stop while offline, reconnect on restore, which saves battery over polling
- Resume replies via server-side persistence by chat id; queue outgoing messages with idempotency keys
D2 工具调用原理:JSON Schema、tool_use 循环;不用框架手写 Agent Loop
什么是 ReAct 模式?它和你手写的工具调用循环是什么关系?What is the ReAct pattern, and how does it relate to a hand-rolled tool-calling loop?
国内高频海外高频进阶#react#agent-loop#tool-calling分析过程 · 先想清楚再作答
- 这题最容易答成名词解释。区分度在于你能不能指出 ReAct 和那个 while 循环是同一个东西,而不是两套并列的技术——把它们说成两样,面试官会认为你只读过博客没写过代码。
- 先给历史脉络:ReAct 出现时模型接口还没有工具字段,做法是在提示词里跟模型约定一套纯文本格式,让它交替吐出 Thought、Action、Action Input,你用正则把动作抠出来执行,再把 Observation 拼回提示词里继续。
- 再做映射,这是拿分的一步:今天的 function calling 把这套口头约定固化成了协议——Thought 对应 message.content,Action 对应结构化的 tool_calls,Observation 对应你追加回去的那条 role 为 tool 的消息。所以 ReAct 是那个循环的名字,不是另一种实现。
- 把取舍说出来:文本版脆在解析,模型少写一个换行、把参数写成 JSON、把 Action 和 Thought 换个顺序,正则就崩;结构化版把这个包袱交给了服务端,是今天的默认选择。但文本版没死——本地小模型、老接口不支持 tools 字段时,回退到「提示词约定 + 正则」仍是唯一可行的兜底,代价是解析失败率自己扛。
- 可以预期的追问:要不要让模型显式写出 Thought?它多花 token,但复杂任务的准确率通常更好,日志也终于可读。这是一个可调旋钮,不是必选项,按任务复杂度决定。
- 第二个追问:ReAct 和先规划后执行(Plan-and-Execute)有什么区别?ReAct 每一步都重新决策,边走边看,适合环境会变、信息要边查边补的任务;先规划后执行一次性出完整计划,步数和成本更可控,但对中途出现的意外不敏感。真实系统常常混用:先出一个粗计划,每一步内部再走 ReAct。
How to reason about it · think before answering
- The trap is answering with a definition. What separates candidates is whether you can say that ReAct and the while loop you wrote are the same thing rather than two parallel technologies.
- Give the history first: when ReAct appeared, model APIs had no tool field. The trick was a prompt-level convention — the model emitted Thought, Action and Action Input as plain text, you regex-extracted the action, ran it, and pasted the Observation back into the prompt.
- Then map it, which is where the points are: function calling froze that convention into the protocol. Thought became message.content, Action became structured tool_calls, Observation became the tool-role message you append. ReAct is the name of your loop, not an alternative to it.
- State the trade-off: the text version is brittle at the parsing layer — a missing newline, JSON where plain text was expected, or a reordered Thought and Action all break the regex. Structured tool calls hand that problem to the server, which is why they are the default today. The text version is still alive though: local small models and older endpoints without a tools field leave you no other option, and you own the parse failure rate.
- Expect: should the model write its Thought out loud? It costs tokens, but accuracy on multi-step tasks usually improves and your logs finally become readable. Treat it as a dial, not a requirement.
- Second follow-up: ReAct versus plan-and-execute? ReAct re-decides at every step, which suits environments that change or information you have to gather as you go; plan-and-execute commits to a full plan up front, giving predictable step counts and cost but reacting poorly to surprises. Production systems often nest them: a coarse plan on the outside, a ReAct loop inside each step.
答题要点
- ReAct 是 Reasoning 加 Acting,让模型交替进行推理与行动,观察结果后再决定下一步
- 原始形态靠提示词约定纯文本格式加正则解析;function calling 把这套约定固化进了 API 协议
- 三步一一对应代码:Thought 是 message.content,Action 是 tool_calls,Observation 是回填的 role 为 tool 的消息
- 结构化调用的好处是不用自己解析,代价是依赖模型支持 tools 字段;不支持时只能回退到文本版并自担解析失败率
- 与先规划后执行相比,ReAct 每步重新决策、更适应变化,但步数与成本不如前者可控
Key points
- ReAct is Reasoning plus Acting: the model alternates thinking and acting, observing each result before deciding the next step
- The original form was a prompt convention parsed by regex; function calling froze that convention into the API protocol
- The three words map to code: Thought is message.content, Action is tool_calls, Observation is the tool-role message you append
- Structured calls remove the parsing burden but require model support; without it you fall back to text ReAct and own the failure rate
- Versus plan-and-execute, ReAct adapts better to change but has less predictable step count and cost
工具执行报错时,应该怎么把错误信息传给模型?有没有不该传的?When a tool fails, how should the error reach the model — and what must never reach it?
国内高频海外高频进阶#tool-calling#error-handling分析过程 · 先想清楚再作答
- 题眼在后半句。只答「catch 住、打日志、返回错误」是普通后端思维,答不出「在 Agent 里错误是给模型的反馈」就拿不到区分度分。
- 先分类,判据是一句话:这个错误模型改得动吗?参数格式不对、缺了必填项、值不在枚举里、单位没去掉——模型改得动,回传,并且要把「正确的样子」写进错误文案,否则它只会换个花样再错一次。反过来,数据库连不上、下游服务 500、凭证过期,模型改一万遍参数也没用,这类该由代码决定重试还是终止,回传只是让它空转烧钱。
- 结论落到形式上:值得回传的错误要变成一条正常的 role 为 tool 的消息,tool_call_id 照样对上,而不是抛异常终止循环。抛了用户看到 500;回传了模型往往下一轮就自己改对,这是 Agent 稳定性最便宜的一份来源。
- 接着答「不该传的」:绝不回传原始异常堆栈。堆栈里有文件路径、内部服务名,有时还有连接串,它会原封不动进入下一次请求,也可能被模型复述给用户;而且动辄上千 token,每一轮都跟着历史重发。回给模型的必须是你自己写的一句话,原始堆栈只进日志。
- 可以预期的追问:模型一直改不对怎么办?错误也要计入步数,撞上步数上限就终止并给用户一句交代;再进一步,同一个工具连续失败若干次可以直接把它从这一轮的可用工具里摘掉,逼模型换条路。
- 第二个追问:这和模型层的 fallback 是一回事吗?是同一套判断的两侧——那边问「换一家 provider 有没有可能变好」,这边问「让模型改一改有没有可能变好」,都是先分类再决定重试,一刀切重试在两边都是错的。
How to reason about it · think before answering
- The second half is the discriminator. 'Catch it, log it, return an error' is ordinary backend thinking; the insight they want is that inside an agent loop an error is feedback to the model, not a failure notification.
- Classify first, with one test: can the model fix this? Malformed arguments, a missing required field, a value outside the enum, a unit that should not be there — the model can fix those, so return them, and spell out what correct looks like or it will simply fail differently next time. A database that is down, a 5xx from a downstream service, an expired credential — no amount of re-prompting helps, so code decides whether to retry or abort.
- Then the mechanics: a returnable error becomes an ordinary tool-role message with the matching tool_call_id, not an exception that unwinds the loop. Throwing gives the user a 500; returning usually gets the model to correct itself on the very next turn, which is the cheapest reliability you will ever buy.
- Now the 'never' half: never hand back a raw stack trace. It carries file paths, internal service names and sometimes connection strings, it enters the next request verbatim, the model may recite it to the user, and it costs a thousand tokens re-sent every turn. Send a sentence you wrote; keep the stack in your logs.
- Expect: what if the model never gets it right? Failed calls still count against the step budget, and hitting the cap should end the run with an honest message. Going further, a tool that fails N times in a row can be dropped from the available set for that run, forcing a different route.
- Second follow-up: is this the same as provider fallback? Two sides of one judgment. There you ask whether another provider could plausibly succeed; here you ask whether the model could plausibly fix it. Blanket retry is wrong in both places.
答题要点
- 先分类:模型改得动的错误(参数格式、缺字段、枚举越界)才值得回传,外部故障应由代码决定重试或终止
- 回传的形式是一条正常的 role 为 tool 的消息,tool_call_id 照常对应,而不是抛异常中断循环
- 错误文案里要写清「正确的样子」,模型才知道该怎么改,否则它只会换个花样再错一次
- 绝不回传原始异常堆栈:内部路径与服务名会进入下一次请求、可能被复述给用户,还白白吃掉上千 token
- 报错同样计入步数上限;同一工具连续失败可以临时摘掉,避免模型在原地打转
Key points
- Classify first: only model-fixable errors (bad arguments, missing fields, enum violations) are worth returning; infrastructure failures are the code's decision
- Return it as an ordinary tool-role message with the matching tool_call_id, not as an exception that kills the loop
- Write what correct looks like into the message, otherwise the model just fails a different way
- Never return raw stack traces: internal paths leak into the next request and to users, and they burn a thousand tokens every turn
- Failed calls count against the step budget, and a repeatedly failing tool can be removed from the available set
D3 Pi SDK 上手:三层架构、Agent Loop 对照(dg P01/P02/M02/M03)
选择使用 Agent 框架还是手写 Agent,你会怎么权衡?How do you decide between adopting an agent framework and hand-rolling the loop?
国内高频海外高频进阶#framework-design#engineering-tradeoffs分析过程 · 先想清楚再作答
- 题眼在「权衡」。答「框架更快」或者「手写更可控」都只说了一半,面试官想听的是你有没有一条能当场执行的判据,而不是立场。
- 给判据:问自己「我需不需要看见并改动这段循环里的每一步」。需要就手写——学习调试阶段、合规审计要求每次模型调用和工具调用都可拦截可留痕、或者场景本身只有一两个工具两三轮循环,那点代码量的收益抵不过一整棵依赖树。
- 不需要就用框架,判断标准是这几件事你是不是迟早都要做:工具数量上去、要把每一步实时推给前端、会话要能重启后继续、上下文满了要压缩、要随时换模型。这些凑齐了就是一个小型框架,自己写等于重新发明一个没人帮你测的版本。
- 然后主动说出框架的三笔代价,这是区分度所在:一是排障栈变深,工具没被调用可能是描述、schema、钩子拦截三种完全不同的原因;二是你继承了一堆没写过的默认值,模型、系统提示词、内置工具都是别人替你选的;三是升级会改变你没测过的行为,代码一行没动线上表现却变了,这类问题最难定位。
- 结论要给出可落地的折中:先手写一遍把循环吃透,再上框架;上了框架也要显式覆盖掉默认值,并把框架版本锁死。这样既拿到了开发速度,也没把行为的控制权整个交出去。
- 可以预期的追问:那你怎么评估一个框架好不好?答看它的分层能不能让你「只要一半」——只要模型调用层、循环自己写行不行;必须整包吞下的框架,迟早要为用不上的那一半付代价。
How to reason about it · think before answering
- The hinge word is 'decide'. 'Frameworks are faster' and 'hand-rolling is more controllable' are each half an answer; what earns points is a criterion you can apply on the spot rather than a preference.
- Offer the criterion: ask whether you need to see and change every step inside the loop. If yes, hand-roll — during learning and debugging, under compliance rules that require every model call and tool call to be interceptable and auditable, or when the scenario really is two tools and three turns and the saved lines do not justify a large dependency tree.
- If no, take the framework, and justify it by what you will inevitably need anyway: more tools, streaming every step to a UI, sessions that survive a restart, compaction when context fills up, swapping models on demand. Assemble all of those yourself and you have written a small framework — an untested one.
- Then volunteer the three costs, which is where the signal is: debugging spans more layers, so a tool that never runs could be a bad description, a schema rejection, or a hook that blocked it; you inherit defaults you never wrote, including the model, the system prompt, and the built-in tools; and upgrades change behavior you never tested, which is brutal to diagnose because your own code did not change.
- Land on a practical middle: hand-roll once to internalize the loop, then adopt a framework, override its defaults explicitly, and pin its version. You keep the delivery speed without handing over control of behavior.
- Expect the follow-up: how do you judge a framework? By whether its layering lets you take only half of it — model layer only, loop your own. Anything you must swallow whole will eventually bill you for the half you do not use.
答题要点
- 判据是「需不需要看见并改动循环里的每一步」,需要就手写,不需要就用框架
- 手写更合适:学习调试、合规要求每步可拦截可留痕、场景极简、对依赖体积与冷启动敏感
- 框架更合适:工具多、要事件流、要会话持久化与压缩、要多模型——这些凑齐等于自己造一个框架
- 框架的三笔代价:排障栈变深、继承一堆没写过的默认值、升级会改变没测过的行为
- 折中做法:先手写吃透循环再上框架,显式覆盖默认值并锁死版本
Key points
- The criterion is whether you need to see and modify every step of the loop
- Hand-roll for learning and debugging, for compliance that demands interceptable and auditable steps, for genuinely tiny scenarios, and where dependency size or cold start matters
- Use a framework once you need many tools, an event stream, persistent sessions, compaction, and model swapping — building all of that is writing a framework yourself
- Three costs: deeper debugging surface, inherited defaults you never wrote, and upgrades that shift untested behavior
- The middle path: hand-roll once, then adopt, override defaults explicitly, and pin the version
D4 模型接入与系统提示词:多 provider 抽象与 fallback、覆盖默认人设(dg P03/P04/M04)
设计模型 fallback 策略时要权衡哪些因素?What trade-offs shape a model fallback strategy?
国内高频海外高频进阶#model-routing#reliability#cost分析过程 · 先想清楚再作答
- 题眼在「权衡」两个字——面试官不要你背一个 for 循环,他要看你知不知道 fallback 是有代价的。
- 先拆出第一个关键判断:不是所有错误都该 fallback,而分类的依据不是状态码的首位数字,是「换一家有没有可能变好」。400 请求体不合法、403 被安全策略拦截,换谁都一样,重试只是把同一个 bug 再犯一遍、白花两倍的钱和时间;408 超时、429 限流、5xx 服务端故障是对方的问题,换一家大概率能成。最容易答错的是 402 余额不足和 404 模型被下线或改名——它们同属 4xx、长得像「你的问题」,其实换一家完全可能成功;401 则要看凭证怎么管,三家共用一把网关 key 时换了也没用,各有各的 key 时 A 被吊销切到 B 完全能救。分类是 fallback 的第一步,不是重试。
- 再说成本:切换意味着同一段 prompt 你付了两次钱,三家链路最坏是三倍成本。这条一定要主动说出来,它区分了「写过」和「上过线」。
- 然后是延迟:串行 fallback 的总耗时是各家超时值的累加。如果每家给 15 秒、三家串下来用户要等 45 秒,那还不如早点失败。所以超时值必须按 provider 分别设,且要设总预算上限。
- 最后是雪崩,这是最容易被追问的点:主 provider 限流时你把全部流量瞬间压到备用上,很可能把备用也压垮。所以要加熔断——连续失败 N 次就暂时摘掉该 provider,过一段时间放少量流量试探。
- 可以预期的追问:怎么知道该摘多久?答案是指数退避 + 半开状态试探,和数据库连接池的熔断是同一套思路。
How to reason about it · think before answering
- The word 'trade-offs' is the hinge: they are not asking for a for-loop, they want to know you understand fallback has costs.
- First key judgment: not every error deserves a fallback, and the test is not the leading digit of the status code but whether another provider could plausibly succeed. A 400 (malformed body) or 403 (blocked by safety policy) fails everywhere, so retrying repeats your own bug at double the cost and latency; a 408, 429 or 5xx is theirs and usually succeeds elsewhere. The two that people get wrong are 402 (out of credit) and 404 (model retired or renamed): both are 4xx, both look like your fault, and both are fixed by switching. A 401 depends on how credentials are managed — one shared gateway key fails everywhere, but per-provider keys mean a revoked key on A is survivable on B. Classification comes before retry.
- Cost: switching means paying for the same prompt twice, up to 3x across a three-provider chain. Volunteering this separates people who shipped from people who only read about it.
- Latency: serial fallback accumulates timeouts. Three providers at 15s each means a 45s wait — worse than failing fast. Timeouts must be per-provider with an overall budget.
- Thundering herd is the most common follow-up: when the primary rate-limits, shifting all traffic at once can take down the backup too. Hence circuit breaking — drop a provider after N consecutive failures, then probe with a trickle.
- Expect: how long do you drop it for? Exponential backoff with a half-open probe — the same pattern as database connection pool breakers.
答题要点
- 先分类再重试,判据是「换一家有没有可能变好」而不是状态码首位:400/403 不该切,408/429/5xx 该切,402 余额不足和 404 模型下线同样该切,401 取决于三家是否共用同一把凭证
- 成本:每次 fallback 都要重付一遍 prompt 的钱,链路越长最坏成本越高
- 延迟:串行 fallback 的耗时是各超时值累加,必须按 provider 分设超时并设总预算
- 雪崩防护:主 provider 故障时全量流量压向备用会把备用也压垮,需要熔断 + 指数退避 + 半开试探
Key points
- Classify before retrying, judging by whether another provider could plausibly succeed rather than the leading digit: 400/403 must not fail over; 408/429/5xx should; so should 402 (out of credit) and 404 (model retired); 401 depends on whether the providers share one key
- Cost: every fallback re-pays for the same prompt, so worst-case cost scales with chain length
- Latency: serial fallback sums the timeouts, so set per-provider timeouts plus an overall budget
- Thundering herd: shifting full traffic to the backup can topple it too — use circuit breaking with exponential backoff and half-open probes
系统提示词(system prompt)在 Agent 里起什么作用?为什么不能用框架默认的?What does the system prompt do in an agent, and why not rely on the framework default?
国内高频海外高频进阶#prompt-engineering#system-prompt分析过程 · 先想清楚再作答
- 前半句是送分题,后半句才是区分度所在——很多人答得出 system prompt 是干什么的,答不出「默认值有什么坑」。
- 先说作用:它是唯一一段在整段对话里权重稳定、不会被后续几十轮稀释的指令,用来设定身份、能力边界和输出格式。
- 再答「为什么不能用默认的」,要给出三条具体后果而不是泛泛说「不够定制」:一是它不知道你的业务边界,用户问业务外的问题它会热情地答;二是它不约束输出格式,前端样式会被冷不丁冒出的 Markdown 标题打乱;三是最要命的——它会随框架升级而变化,你测好的所有行为建立在一段看不见的文本上,出 bug 时你的代码一行没动,极难排查。
- 结论落到工程做法:生产环境的 system prompt 是拼出来的模板,结构是「人设 + 能力边界 + 输出要求 + 动态上下文」,最后一块每次请求现拼。
- 可以预期的追问:动态上下文里最容易漏什么?答「当前时间」——模型没有时钟,不告诉它今天几号,它算不出「三天前下的单」是哪天。这个细节很能体现有没有真做过。
- 第二个追问:prompt 怎么测试?答案是把它当配置而不是代码——存库、加版本号、支持按比例灰度,因为你没法写单元测试断言「模型语气变友好了」。
How to reason about it · think before answering
- The first half is a warm-up; the discriminating half is why the default is dangerous.
- State the role: it is the one instruction block whose weight stays stable across dozens of turns, setting identity, capability boundaries and output format.
- Then give three concrete consequences rather than 'not customized enough': it does not know your business boundary so it happily answers off-topic questions; it does not constrain output format so stray Markdown headings break your UI; and worst, it changes when the framework updates — your tested behavior rests on invisible text, and the bug appears with zero code changes.
- Land on practice: a production system prompt is assembled from a template — persona, capability boundary, output requirements, dynamic context — with the last part rebuilt per request.
- Expect: what gets forgotten in dynamic context? The current time. Models have no clock; without today's date they cannot resolve 'the order I placed three days ago'.
- Second follow-up: how do you test a prompt? Treat it as configuration, not code — store it, version it, roll it out to a percentage, because you cannot unit-test 'the tone got friendlier'.
答题要点
- system prompt 设定身份、能力边界与输出格式,是对话里权重最稳定、不被后续轮次稀释的一段指令
- 框架默认人设不知道你的业务边界,会热情回答业务外的问题,浪费 token 且跑题
- 默认人设不约束输出格式,模型可能吐出 Markdown 标题打乱前端样式
- 最危险的是默认值会随框架升级而变化,代码一行没动却出现行为回归,极难排查
- 生产做法:显式拼模板(人设 + 能力边界 + 输出要求 + 动态上下文),当作配置存储、加版本号、可灰度
Key points
- The system prompt sets identity, capability boundaries and output format, and keeps stable weight across turns
- A default persona does not know your business boundary and will cheerfully answer off-topic questions
- It does not constrain formatting, so stray Markdown can break your UI
- Most dangerous: defaults change on framework upgrades, producing behavior regressions with no code change
- Production practice: assemble it explicitly, treat it as versioned configuration, and roll changes out gradually
如何在成本和延迟之间给不同任务选择合适的模型?How do you pick the right model per task, balancing cost against latency?
国内高频海外高频进阶#model-routing#cost#latency分析过程 · 先想清楚再作答
- 这题考的是「你有没有真的在花自己的钱」。答「用最好的模型」是最差的答案,答「按需选择」太空,要给出可执行的分档维度。
- 先建立核心事实:不同模型的价格能差 50 倍以上,而你的任务里很大一部分根本不需要最强的模型。用旗舰模型做意图识别,等于开跑车去楼下取快递。
- 然后给出三个可操作的路由维度:任务类型(分类抽取走便宜模型,长文推理走强模型)、延迟要求(前台用户在等就走低延迟,后台批处理可以慢而便宜)、输入长度(超长上下文只有部分模型支持且价格陡增)。
- 结论要落到数字上才有说服力:1 万轮对话每轮 2000 token,全走旗舰约 300 元一天;把六成粗活改走小模型后降到 125 元左右,一年省六万多,用户感知不到差别。
- 还要主动说出实现上的取舍:先按任务类型静态分档,不要一上来就做「让模型判断该用哪个模型」的动态路由——那个方案本身又要多一次模型调用,延迟和成本可能得不偿失,等有真实数据再优化。
- 可以预期的追问:怎么验证降档没有损失质量?答案是准备 golden set,对同一批输入跑两档模型,用人工或 LLM-as-judge 比对准确率,把降档决策建立在数据上而不是感觉上。
How to reason about it · think before answering
- This question tests whether you have ever spent your own money. 'Use the best model' is the worst answer; 'it depends' is too vague — give actionable routing dimensions.
- Establish the core fact: model pricing spans 50x or more, and much of your workload does not need the strongest model. Using a flagship for intent detection is driving a sports car to fetch a parcel downstairs.
- Give three routing dimensions: task type (classification and extraction go cheap, long-form reasoning goes strong), latency requirement (foreground users need low latency, background batches can be slow and cheap), and input length (only some models handle very long context, and pricing rises steeply).
- Quantify it: 10k conversations a day at 2000 tokens each costs roughly 300 CNY/day on a flagship; routing the 60% of grunt work to a small model drops it to about 125 CNY/day, saving 60k+ CNY a year with no perceptible quality change.
- Volunteer the implementation trade-off: start with static tiers by task type. Dynamic routing that asks a model which model to use adds another model call, and the latency and cost may not pay for themselves — optimize once you have real data.
- Expect: how do you verify the cheaper tier did not hurt quality? A golden set — run both tiers over the same inputs and compare with human or LLM-as-judge scoring, so the decision rests on data rather than vibes.
答题要点
- 不同模型价格能差 50 倍以上,用旗舰模型做意图识别是明显的浪费
- 三个路由维度:任务类型(分类抽取 vs 推理生成)、延迟要求(前台 vs 后台)、输入长度(是否需要超长上下文)
- 实现上给调用层加 tier 参数,按任务静态分档挑起始 provider,fallback 逻辑完全复用
- 先静态分档再考虑动态路由,让模型判断该用哪个模型本身要多一次调用,可能得不偿失
- 用 golden set 对比两档模型的准确率,把降档决策建立在数据上
Key points
- Model pricing spans 50x or more, so a flagship doing intent detection is obvious waste
- Three routing dimensions: task type, latency requirement, and input length
- Add a tier parameter to the call layer, pick the starting provider statically, and reuse the fallback chain
- Prefer static tiers first — dynamic model-picks-model routing adds a call and may not pay off
- Validate downgrades against a golden set rather than intuition
D5 工具系统与事件驱动:参数校验、错误回传让模型自纠错、事件订阅(dg P05/P06/M05/M07)
工具调用报错或参数非法时,你怎么让模型自己纠正而不是直接失败?When a tool call fails validation or errors out, how do you get the model to correct itself instead of failing the whole turn?
国内高频海外高频进阶#tool-calling#error-handling分析过程 · 先想清楚再作答
- 这题在考你有没有真的做过工具循环。只答「把错误告诉模型」是及格线,区分度在两个地方:错误信息长什么样,以及你有没有给它设刹车。
- 先给机制,一句话就能说清:错误不是异常,是数据。工具成功时你把结果包成一条 tool 消息追加进 messages 再继续循环,失败时走同一条路,只是内容换成错误描述。异常一路抛出、终止这一轮,是最常见的错误做法。
- 再给判据:好错误信息有三个要素——错在哪个字段、期望是什么、一个合法示例。对比三档就很清楚:「工具执行失败」模型只能原样重试或放弃;「order_id 格式不正确」它知道错在哪却不知道对的长什么样,可能试出一个新错法;「参数 order_id 需要 SO 开头加 8 位数字,例如 SO20260901,你传的是数字 12345」基本一次改对。另外校验要一次报全部错误,报了第一条就返回会让模型多跑好几轮。
- 然后主动说代价,这是面试官等的:一次自纠错等于多两条消息加一次完整的模型调用,延迟和 token 都翻倍;更凶的是死循环——错误信息含糊时模型会以近乎相同的方式反复重试。所以必须设三道闸:单工具连续失败 2 次就停手转人工、整轮工具调用总次数上限、整轮 token 预算,哪个先到都终止。
- 最后划一条边界,它和 D4 的 fallback 判据同源:判断依据是「模型改参数有没有可能变好」。校验失败、订单不存在、日期超范围——回传。数据库连不上、下游 503、密钥过期——模型改一百遍参数也没用,应该直接失败并告警,回传只会让它朝错误方向瞎试。
- 可以预期的追问:回传的错误信息里能放什么?只能放字段名、期望格式和示例;栈信息、SQL、内部路径、真实表名一律不能进,因为模型会把它复述给用户。
How to reason about it · think before answering
- This checks whether you have actually built a tool loop. 'Tell the model about the error' is the passing grade; the discriminators are what the error text looks like and whether you put brakes on the loop.
- State the mechanism in one line: an error is data, not an exception. On success you append the result as a tool message and continue the loop; on failure you take the same path with error text as the content. Throwing all the way out and killing the turn is the common mistake.
- Give the quality bar: a good error names the field, states the expectation, and shows one valid example. Compare three tiers — 'tool failed' leaves the model to retry blindly or give up; 'order_id has the wrong format' tells it where but not what, so it may invent a new wrong form; 'order_id must be SO plus 8 digits, e.g. SO20260901, you sent the number 12345' usually gets fixed in one shot. Also report every validation error at once; returning on the first one costs extra round trips.
- Volunteer the cost, which is what they are waiting for: one self-correction adds two messages and a full model call, doubling latency and tokens. Worse is the infinite loop when the error text is vague. So set three brakes — stop after two consecutive failures of the same tool and hand off to a human, cap total tool calls per turn, and cap the token budget per turn.
- Draw the boundary, which shares its logic with the D4 fallback rule: the test is whether changing arguments could plausibly help. Validation failures, 'order not found', 'date out of range' — feed back. Database unreachable, downstream 503, expired key — no argument change will help, so fail loudly and alert instead of letting the model flail.
- Expect the follow-up: what may go into the error text? Field names, expected formats and examples only. Stack traces, SQL, internal paths and real table names must never reach the model, because it will repeat them to the user.
答题要点
- 错误不是异常是数据:把它包成一条 tool 消息追加进 messages,和成功结果走同一条路,模型下一轮就能看到
- 好错误信息三要素:错在哪个字段、期望是什么、给一个合法示例;校验要一次报全部错误
- 自纠错不免费:多两条消息加一次模型调用,延迟和 token 翻倍
- 必须设三道闸:单工具连续失败 2 次转人工、整轮工具调用总次数上限、整轮 token 预算
- 判据是「模型改参数有没有可能变好」:校验失败该回传,数据库连不上、下游 503 该直接失败并告警
- 回传文本只能有字段名、期望格式和示例,不能带栈信息、SQL 和内部路径
Key points
- An error is data: append it as a tool message on the same path as a successful result so the model sees it next turn
- A good error names the field, states the expectation and shows a valid example; report all validation errors at once
- Self-correction is not free — two extra messages plus a full model call double latency and tokens
- Set three brakes: hand off after two consecutive failures of one tool, cap tool calls per turn, cap the token budget
- The test is whether changing arguments could help: feed back validation errors, but fail loudly on unreachable databases or downstream 503s
- Never put stack traces, SQL or internal paths into text the model will read
Agent 的事件系统一般会暴露哪些生命周期事件?为什么不能只等最终返回值?Which lifecycle events does an agent runtime typically expose, and why is waiting for the final return value not enough?
国内高频海外高频进阶#event-driven#observability分析过程 · 先想清楚再作答
- 这题看起来是背清单,实际考的是你有没有做过带界面的 Agent。只报事件名不解释用途,会被判成看过文档但没接过前端。
- 先说动机:一次带工具的循环短则几秒长则几分钟,中间要调模型、调工具、可能还失败重试,而返回值只有最后一句话。对调用方来说中间全是黑盒——不知道它在干什么,也不知道该不该再等。
- 再报清单并各配一句用途:run:start / run:end / run:error 是一轮的开始与两种结束;model:delta 是模型吐出的文本片段,前端拿它做打字机效果;tool:proposed 是模型决定要调工具但还没执行,权限确认就挂在这个事件上;tool:start / tool:end / tool:error 是工具执行的三个出口,tool:end 带耗时;approval:required 让界面弹确认框。
- 然后说出这套设计真正的价值:同一条事件流同时喂三个消费者——界面渲染进度、日志系统做链路追踪、计量系统拿 run:end 的 token 数算成本。不为三件事写三套埋点,这是架构判断而不是 API 罗列。
- 补两条实现纪律,能显著拉开差距:事件必须带 runId 和自增序号,因为跨进程传输后顺序不保证;监听器里不写业务逻辑,且监听器抛错不能炸掉主循环——事件是旁路不是主干。
- 可以预期的追问:model:delta 一个 token 一条事件会不会太多?会,所以要按时间窗合批,攒 50 毫秒推一次,用户感知不到差别而消息量掉一个数量级。
How to reason about it · think before answering
- It looks like a listing question but it really tests whether you have shipped an agent with a UI. Reciting event names without saying what each one is for reads as documentation-deep only.
- Start with the motivation: a tool-using loop runs from seconds to minutes, calling models and tools and sometimes retrying, while the return value is just the final sentence. Everything in between is a black box to the caller, who cannot tell whether to keep waiting.
- List them with a purpose each: run:start, run:end and run:error mark the turn and its two endings; model:delta carries text fragments for the typewriter effect; tool:proposed fires when the model has chosen a tool but has not executed it, which is where the approval gate hangs; tool:start, tool:end and tool:error are the three exits of execution, with duration on tool:end; approval:required tells the UI to show a confirmation card.
- Then name the real payoff: one event stream feeds three consumers — the UI renders progress, logging gets distributed tracing, and metering reads token counts off run:end. One stream instead of three instrumentation layers is an architecture answer, not an API listing.
- Add two implementation rules that separate candidates: every event carries a runId and a monotonic sequence number because ordering is not guaranteed once events cross processes, and listeners must contain no business logic and never let an exception escape into the main loop. Events are a side channel, not the trunk.
- Expect the follow-up: isn't one event per token too many? Yes, so batch on a time window — flush every 50ms, which is imperceptible to users and cuts message volume by an order of magnitude.
答题要点
- 动机:一轮循环几秒到几分钟,返回值只有最后一句话,中间全是黑盒,调用方无法判断该不该继续等
- 常见事件:run:start / run:end / run:error、model:delta、tool:proposed、tool:start / tool:end / tool:error、approval:required
- 同一条事件流同时喂界面、日志链路追踪和成本计量三个消费者,不用写三套埋点
- 事件要带 runId 和自增序号,跨进程后顺序不保证,消费端要能自己排序
- 监听器不写业务逻辑,且抛错不能影响主循环;model:delta 要按 50 毫秒时间窗合批
Key points
- Motivation: a turn takes seconds to minutes and only returns the final sentence, so the caller cannot tell whether to keep waiting
- Typical events: run:start/end/error, model:delta, tool:proposed, tool:start/end/error, approval:required
- One stream serves the UI, distributed tracing and cost metering — no need for three instrumentation layers
- Every event carries a runId and a sequence number since ordering is not guaranteed across processes
- Listeners hold no business logic and must not throw into the main loop; batch model:delta on a 50ms window
D6 消息、上下文工程与压缩、会话存储/恢复/分叉(dg M06/M08/M09/M10)
长对话里上下文放不下了,你会怎么压缩?什么时候触发、压掉什么、保留什么?When a long conversation outgrows the context window, how do you compress it — when do you trigger, what do you drop, and what do you keep?
国内高频海外高频进阶#context-engineering#compression#cost分析过程 · 先想清楚再作答
- 这题的区分度不在「用摘要」三个字上,几乎人人都答得出。区分度在你有没有说出触发时机和保留清单——只答「让模型总结一下前面的对话」的,面试官会判定你没在长对话上线过。
- 先把问题拆成三问再逐个答:什么时候压、压掉什么、保留什么。这个拆法本身就是加分项,因为它说明你把压缩当成一个策略而不是一个函数。
- 触发用阈值不用定时器,也不能等报错。给一个具体数字并解释它:历史占用到预算的七成就动手,因为摘要本身是一次模型调用,有延迟也可能失败,卡到九成再压,一旦摘要超时下一轮就直接撞窗口上限了——七成是留给自己的抢救时间。
- 压掉的是过程性内容:中间推理、已经被消费完的工具原始返回值、用户后来推翻的需求。它们的共同点是价值已经沉淀进后面的结论里。保留的是系统提示词(它不属于历史)、最近若干条原文、以及用户明确声明过的约束和事实——后者写错了模型会当场失忆。
- 再补一条别人不会说的:切口必须对齐到一轮的开头。切在 assistant 的 tool_calls 和对应的 tool 结果中间,下一次请求就有了悬空调用,多数厂商的 API 直接返回 400。这一条最能证明你真的调过。
- 可以预期的追问有两个。一是摘要该用哪个模型:用便宜的小模型就行,摘要是抽取任务不是推理任务,这也接上了 D4 的分层路由。二是摘要调用失败了怎么办:降级到不摘要的滑动窗口(直接丢最早的几轮),保证请求发得出去,别让压缩失败连带整轮对话失败。
How to reason about it · think before answering
- Saying 'summarize it' earns nothing — everyone says that. The signal is whether you name a trigger point and a keep-list; without those you sound like someone who never ran a long conversation in production.
- Split it into three questions before answering: when to compress, what to drop, what to keep. The split itself scores, because it frames compression as a policy rather than a function.
- Trigger on a threshold, not a timer, and never on an error. Give a number and justify it: compress at roughly 70% of the history budget, because summarizing is itself a model call that can be slow or fail. Waiting until 90% means one timed-out summary call and the next turn slams into the window limit.
- Drop the process: intermediate reasoning, raw tool payloads already consumed, requirements the user later reversed — their value has already settled into later conclusions. Keep the system prompt (it is not history), the most recent turns verbatim, and any constraint or fact the user stated explicitly. Getting that last one wrong makes the model visibly forget.
- Add the detail others miss: the cut must land on a turn boundary. Slicing between an assistant tool_calls message and its matching tool result leaves a dangling call, and most providers reject that request with a 400. This is the line that proves hands-on experience.
- Two follow-ups to expect. Which model summarizes? A cheap small one — summarization is extraction, not reasoning, which ties back to tiered routing. And what if the summary call fails? Degrade to a plain sliding window that drops the oldest turns, so a failed compression never fails the whole turn.
答题要点
- 阈值触发:历史占用到预算七成就压,因为摘要本身是一次会失败、有延迟的模型调用,必须留抢救余量
- 压过程、留结论:丢中间推理和已消费的工具原始返回,保留系统提示词、最近若干条原文、用户明确声明的约束与事实
- 切口必须对齐到一轮开头,切在 tool_calls 与 tool 结果之间会让下一次请求返回 400
- 压缩是有损且不可逆的:原始历史另存一份只追加,发给模型的是压缩版,需要回溯或分叉时读原始版
- 摘要用便宜的小模型;摘要失败要能降级成滑动窗口,别让压缩失败连累整轮对话
Key points
- Threshold-triggered at about 70% of the history budget, because the summary call itself is a slow, fallible model call that needs headroom
- Drop process, keep conclusions: discard intermediate reasoning and consumed raw tool payloads; keep the system prompt, the recent turns verbatim, and explicit user constraints and facts
- Align the cut to a turn boundary — slicing between tool_calls and its tool result makes the next request fail with a 400
- Compression is lossy and irreversible: keep an append-only original, send the compressed version, and read the original when you need to backtrack or fork
- Summarize with a cheap small model, and degrade to a sliding window if the summary call fails so compression failure never fails the turn
会话的持久化、恢复和分叉分别解决什么问题?实现时各有什么坑?What problems do session persistence, restore, and forking each solve, and what goes wrong in each?
国内高频海外高频进阶#session-management#persistence#forking分析过程 · 先想清楚再作答
- 题干把三件事并列,考的其实是你能不能分清它们各自的动机——很多人会把三个都答成「存下来」,那就丢掉了全部区分度。
- 先一句话各给一个动机:持久化解决「进程重启和跨机器请求」,恢复解决「加载回来还能接着聊」,分叉解决「同一段历史要走出两条不同的后续」。动机不同,所以数据结构的要求也不同。
- 持久化的关键选择是只追加还是快照。答只追加并给理由:写入不受历史长度影响、能回放到任意一步、有审计轨迹;快照只是读加速手段,工程上常见的是「只追加为准 + 定期快照」。这一条直接决定了分叉能不能做。
- 恢复的两个坑要主动说。一是把当时的系统提示词一起存进了历史,里面有「现在时间」这类动态上下文,三天后读出来模型的日期判断全错——系统提示词不进持久化历史,每次现拼。二是存档存在了工具调用中途,最后一条是没有配对结果的 tool_calls,直接发出去就是 400,加载后必须做完整性校验,补一条「执行被中断」的结果或丢弃这条尾巴。
- 分叉最容易被忽视的是「父会话只读」这条语义。分叉不是回滚:回滚砍掉历史继续用,是破坏性的;分叉复制前 k 条长出新枝,两边都能继续。实现上要深拷贝,直接引用父会话的消息对象会让两条分支互相污染。
- 可以预期的追问:分叉多了存储怎么办?答按父引用加偏移存、读时拼接,代价是读路径变复杂;再顺手补一句成本要能顺着 parentId 聚合成一棵树,否则账算不清是哪个用户的哪次重试花的钱。
How to reason about it · think before answering
- The question lists three things side by side, so it is really testing whether you can separate their motivations. Answering 'they all save the conversation' throws away the entire signal.
- Give one motivation each in a sentence: persistence survives process restarts and multi-instance routing, restore lets a loaded history keep the conversation going, forking lets one history grow two different futures. Different motivations imply different data structures.
- The key persistence choice is append-only versus snapshot. Choose append-only and justify it: writes are independent of history length, any point can be replayed, and you keep an audit trail. Snapshots are a read optimization, so production usually means append-only as the source of truth plus periodic snapshots. This choice is what makes forking possible at all.
- Volunteer the two restore traps. First, persisting the system prompt inside the history: it carries dynamic context like the current time, so a session loaded three days later has the model reasoning from a stale date. Rebuild the system prompt fresh on every load. Second, a session saved mid tool call ends with an unmatched tool_calls message; replaying it verbatim gets a 400, so validate on load and either append an 'execution interrupted' tool result or drop the dangling tail.
- For forking, the overlooked point is that the parent stays read-only. Forking is not rollback: rollback truncates and mutates, forking copies the first k messages into a new branch and both sides continue. Deep-copy the messages — sharing the parent's objects lets the branches contaminate each other.
- Expect the follow-up on storage: reference the parent plus an offset and stitch on read, at the cost of a more complex read path. Add that cost must aggregate up the parentId tree, or you cannot tell which user's retry burned which tokens.
答题要点
- 持久化解决进程重启与跨实例,选只追加:写入不受历史长度影响、可回放任意一步、有审计轨迹;快照只是读加速
- 恢复要现拼系统提示词,不能把带「现在时间」的那份存进历史,否则读出来日期判断全错
- 恢复必须做完整性校验:尾部悬空的 tool_calls 要补一条中断结果或丢弃,否则下一次请求返回 400;压缩水位也要一起恢复
- 分叉是复制前 k 条并记住父会话与切点,父会话只读——这是它和破坏性回滚的根本区别,实现上必须深拷贝
- 分叉的代价是存储放大与成本归属,规模上来后改成存父引用加偏移,账要能顺着 parentId 聚合成树
Key points
- Persistence handles restarts and multiple instances; prefer append-only for constant-cost writes, replayability and an audit trail, with snapshots purely as a read optimization
- On restore, rebuild the system prompt fresh — persisting the one containing the current time makes the model reason from a stale date
- Validate on restore: a dangling tool_calls tail needs an 'interrupted' tool result or must be dropped, or the next request returns 400; restore the compression watermark too
- A fork copies the first k messages and records parent and cut point, leaving the parent read-only — that is what separates it from destructive rollback, and it requires a deep copy
- Forking costs storage amplification and muddled cost attribution; at scale store a parent reference plus offset and aggregate spend up the parentId tree
D7 封装成服务:Fastify + SSE + Docker(dg P07);W1 复盘
把一个本地跑的 Agent 脚本改造成生产服务,接口层要重点考虑哪些事?When turning a local agent script into a production service, what does the interface layer have to get right?
国内高频海外高频进阶#api-design#service-architecture#streaming分析过程 · 先想清楚再作答
- 这题考的是「你知不知道脚本里有哪些隐含假设」。答成一份笼统的清单(鉴权、日志、监控)拿不到分,要说出脚本时代默认成立、服务里立刻不成立的那几条。
- 先把假设列出来,这是最能体现工程视角的一步:只有一个用户(历史可以放模块级变量)、串行执行(不会有两个请求同时改一份状态)、输入可信(参数是自己敲的)、进程和会话同生共死(Ctrl+C 之后不用交代)。四条在服务里全部不成立,而第一条最难查,因为它在本地单人测试时表现完美。
- 然后给出四个必须做的决定:接口形状(一次性 JSON 还是流式推送)、会话标识(客户端带 sessionId 还是服务端发 cookie,以及历史存哪里)、鉴权与限流(谁能调、多久能调一次、单次 token 上限)、错误怎么表达。
- 第四条要单独展开,它是这题真正的区分点:流式接口一旦写出 200 和第一个字节,状态码就已经发出去了,之后模型超时、余额不足、上游 500,都只能在流里补发一个约定好的 error 事件。所以推流之前必须把能校验的全部校验完,那是你最后一次能用状态码好好说话的机会。
- 再补一条生产视角:服务要有健康检查接口。没有它,编排系统和负载均衡就没法判断这个实例能不能接流量,滚动发布时会把请求打给一个还没起好的进程。
- 可以预期的追问:单次请求的 token 上限为什么要在接口层限制?因为 Agent 的成本是请求方触发、你来买单,不设上限就等于把钱包交给调用方——限流限的不只是 QPS,还有每次调用能烧多少钱。
How to reason about it · think before answering
- This tests whether you can name the assumptions hidden in a script. A generic checklist (auth, logging, monitoring) scores nothing; name the assumptions that silently break.
- List them first: one user (so history can live in a module-level variable), serial execution (no two requests mutating the same state), trusted input (you typed the arguments yourself), and a process whose life equals the session's. All four break in a service, and the first is hardest to catch because single-user local testing looks perfect.
- Then give the four decisions: response shape (single JSON versus streamed events), session identity (client-supplied id versus server cookie, and where history is stored), authentication and rate limiting (who may call, how often, and the per-call token ceiling), and how errors are expressed.
- Expand the last one — it is where this question is actually won. Once a streaming endpoint has written 200 and the first byte, the status code is already on the wire, so a later timeout, out-of-credit or upstream 500 can only surface as an agreed error event inside the stream. Validate everything you can before the first byte, because that is your last chance to speak in status codes.
- Add a production note: ship a health endpoint. Without one, orchestrators and load balancers cannot tell whether an instance is ready, and rolling deploys send traffic to a process that has not finished booting.
- Expect the follow-up: why cap tokens per request at the interface layer? Because agent cost is triggered by the caller and paid by you — no cap means handing your wallet to the client. Rate limiting is about money per call, not just QPS.
答题要点
- 脚本的四个隐含假设在服务里全部不成立:单用户、串行、输入可信、进程与会话同生共死
- 会话状态必须按 sessionId 隔离,且要意识到放进程内存意味着重启即丢、无法水平扩容
- 四个接口决定:响应形状、会话标识、鉴权与限流(含单次 token 上限)、错误表达方式
- 流式接口推流之后无法用状态码报错,必须约定一个流内的 error 事件,并把校验全部前置到第一个字节之前
- 提供健康检查接口,否则编排系统无法判断实例能不能接流量
Key points
- A script's four assumptions all break in a service: single user, serial execution, trusted input, and a process that dies with the session
- Session state must be keyed by session id, and in-process storage means data is lost on restart and blocks horizontal scaling
- Four interface decisions: response shape, session identity, auth and rate limiting including a per-call token ceiling, and error semantics
- A streaming endpoint cannot report errors by status code after the first byte, so define an in-stream error event and move all validation ahead of it
- Expose a health endpoint, or orchestrators cannot tell whether the instance is ready for traffic
把一个 Node 服务打包成 Docker 镜像,Dockerfile 里有哪些关键决定?What are the key decisions in a Dockerfile that packages a Node service?
国内高频海外高频进阶#docker#deployment#nodejs分析过程 · 先想清楚再作答
- 这题看着是背步骤,其实考的是「你有没有为构建速度和安全性做过取舍」。把 FROM、COPY、RUN、CMD 顺着念一遍是最没有区分度的答法。
- 第一个决定是指令顺序,也是唯一能立刻量化收益的:镜像是逐层叠出来的,某层的输入没变就复用缓存。所以先只拷 package.json 和 lockfile、装完依赖再拷源码——改一行业务代码只让最后两层失效,依赖那层照旧命中;反过来一上来就 COPY 全部,改一个字都要重装依赖。
- 第二个是基础镜像钉版本。写 latest 等于让镜像在某天悄悄升到下一个大版本,可复现性当场归零,而可复现正是用容器的全部理由。
- 第三个是运行时配置:容器里必须监听 0.0.0.0,只听 127.0.0.1 的话它只在容器内部可达,宿主机做了端口映射也连不上——这个坑在本机跑的时候完全正常,所以特别常见。另外 EXPOSE 只是声明意图,真正开端口的是 docker run 的 -p。
- 第四个是安全:用非 root 用户跑业务进程(容器和宿主机共用内核,逃逸后 root 的破坏面大得多),.dockerignore 排除 node_modules(宿主机的二进制在 Linux 容器里跑不起来,还会让构建上下文暴涨)和 .env(密钥打进镜像等于发给每个能拉到镜像的人,运行时用 --env-file 传)。
- 可以预期的追问,也是长连接服务最该主动说的一条:CMD 要用数组形式直接起 node,让它当 PID 1。写成 pnpm start 的话 PID 1 是包管理器,docker stop 的 SIGTERM 未必传得到 node,优雅退出代码永远不执行,只能等十秒超时被 SIGKILL——对 SSE 服务,那意味着所有在途的流被硬切。
How to reason about it · think before answering
- It looks like a recipe question, but it tests whether you have ever traded off build speed against security. Reading FROM, COPY, RUN, CMD in order is the least differentiating answer.
- The first decision is instruction order, the only one with an immediately measurable payoff. Images are stacked layers and a layer whose inputs are unchanged is reused, so copy the manifest and lockfile first, install, then copy source. Editing one line of code then invalidates only the last two layers instead of forcing a full reinstall.
- Second, pin the base image. Using latest means the image silently jumps a major version some morning, which destroys the reproducibility that was the whole reason to containerize.
- Third, runtime configuration: bind to 0.0.0.0 inside a container. Binding 127.0.0.1 leaves the service reachable only from inside, so a published port still refuses connections — and it works perfectly on your laptop, which is why it is so common. Also note EXPOSE only documents intent; the port is actually published by docker run -p.
- Fourth, security: run as a non-root user, since containers share the host kernel and root widens the blast radius of an escape. Keep node_modules out via .dockerignore (host binaries will not run in a Linux container and the build context balloons) and keep .env out too, passing secrets at runtime with --env-file.
- Expect the follow-up, and it is the one a streaming service should volunteer: use the exec-form CMD to launch node directly so it becomes PID 1. With pnpm start, PID 1 is the package manager, SIGTERM from docker stop may never reach node, your graceful shutdown never runs, and the container is SIGKILLed after the timeout — cutting every in-flight SSE stream.
答题要点
- 指令顺序决定缓存命中:先拷依赖清单装依赖,再拷源码,改代码不会触发重装依赖
- 基础镜像钉版本不用 latest,可复现是用容器的全部理由
- 容器里监听 0.0.0.0;EXPOSE 只是声明,真正开端口靠 docker run -p
- 用非 root 用户运行;.dockerignore 排除 node_modules 与 .env,密钥运行时用 --env-file 注入
- CMD 用数组形式直接起 node 让它当 PID 1,SIGTERM 才能传到进程,优雅退出才有效
Key points
- Instruction order drives cache hits: copy the manifest, install, then copy source, so code edits do not reinstall dependencies
- Pin the base image instead of latest — reproducibility is the entire point of containerizing
- Bind 0.0.0.0 inside the container; EXPOSE only documents intent while docker run -p publishes the port
- Run as a non-root user, and keep node_modules and .env out via .dockerignore, injecting secrets at runtime
- Use exec-form CMD to run node as PID 1 so SIGTERM reaches it and graceful shutdown actually executes
D8 为什么 Gateway/Worker 分离;Postgres 表设计(sessions/runs/messages)+ Drizzle
什么是无状态服务?它对水平扩展意味着什么?Worker 算不算有状态?What makes a service stateless, what does that mean for horizontal scaling, and are workers stateful?
国内高频海外高频进阶#stateless#scalability分析过程 · 先想清楚再作答
- 这题的陷阱是字面理解。很多人答成「不保存任何数据」,那是错的——无状态服务当然会写数据库。区分度在于你能不能给出准确定义。
- 准确定义只有一句:无状态指的是**状态不留在处理请求的那个进程身上**,因此任意一台实例都能处理任意一个请求。把它翻译成一个自检问题就很好用:随便杀掉一台实例,有没有任何用户的数据只存在于那台机器上?答「没有」才是无状态。
- 再推出水平扩展的三个后果:新实例不需要预热或同步数据,接上负载均衡立刻能干活;任意实例可以随时被杀,滚动发布和抢占式实例才成立;不需要会话粘连,而粘连一旦存在,扩容时的重新分配就会打断老用户的会话。
- Worker 那一问要答得有分寸:它持有的不是用户数据,而是一次执行的进度(跑到第几轮、调了哪些工具、后面还会加上一个租约)。用户数据始终在数据库里。所以说它有状态,指的是「手上有活没交代完」,后果是不能随便杀——必须优雅停机,先拒绝新任务再等手头的跑完。
- 可以预期的追问:内存缓存算不算破坏了无状态?答案是看丢了会不会出错。纯粹用于加速、丢了只是变慢的缓存不破坏无状态;一旦某个用户的会话只存在于某台机器的内存里,你就已经在偷偷依赖粘连了,扩容那天必然出事。
How to reason about it · think before answering
- The trap is reading the word literally. Many candidates say 'it stores nothing', which is wrong — stateless services write to databases all day. The discriminator is whether you can define it precisely.
- One sentence does it: stateless means state does not live in the process handling the request, so any instance can serve any request. Turn it into a self-check: kill a random instance — does any user's data exist only there? Only 'no' is stateless.
- Derive three scaling consequences: a new instance needs no warm-up or data sync and starts serving the moment it joins the load balancer; any instance can be killed at will, which is what makes rolling deploys and spot instances viable; and no sticky sessions are needed, whereas stickiness means rebalancing during a scale-up cuts existing conversations.
- Answer the worker half carefully: it holds execution progress, not user data — which turn it is on, which tools it called, and later a lease. User data always lives in the database. So 'stateful' here means 'holding unfinished work', and the consequence is that you cannot kill it freely: drain first, refuse new work, let the current run finish.
- Expect the follow-up: does an in-memory cache break statelessness? It depends on whether losing it causes wrong behavior. A pure accelerator that only costs latency is fine; the moment a user's session exists only in one machine's memory you are silently relying on stickiness, and the next scale-up will prove it.
答题要点
- 无状态的准确含义是状态不留在处理请求的进程里,任意实例都能处理任意请求,而不是「不存数据」
- 自检方法:随便杀一台实例,是否有用户的数据只存在于那一台上
- 水平扩展的三个前提:新实例无需预热、任意实例可被随时杀掉、不需要会话粘连
- Worker 的有状态指的是持有一次执行的进度而不是用户数据,后果是必须优雅停机而不能随便杀
- 只加速、丢失只降速的缓存不破坏无状态;承载唯一副本的内存数据等于隐式的会话粘连
Key points
- Stateless means the state does not live in the request-handling process, so any instance serves any request — not that nothing is stored
- Self-check: kill any instance and ask whether any user's data existed only there
- Three scaling prerequisites: no warm-up, any instance disposable, no sticky sessions
- Workers are stateful in the sense of holding run progress, not user data, so they need graceful drain rather than a hard kill
- A pure accelerator cache is fine; in-memory data that is the only copy is implicit stickiness
sessions / runs / messages 这三张表你会怎么设计主键与索引?为什么不用自增主键?How would you design primary keys and indexes for sessions, runs and messages, and why avoid auto-increment ids?
国内高频海外高频进阶#database#schema-design#idempotency分析过程 · 先想清楚再作答
- 这题看着像八股,其实每一个选择背后都有一个具体约束。判断标准是:你能不能为每个决定说出「不这么做会发生什么」。
- 先讲为什么是三张表而不是一张:粒度不同。会话是长期容器,一次执行有生命周期且可能失败重来,消息是不可变事实。少了「一次执行」这一层,你就没有地方回答「这次跑完没有」「该不该重试」「这轮花了多少钱」。
- 主键选文本型的应用侧 id(UUID 或 ULID),理由是接入层必须在写库之前就把 id 放进 202 响应体返回给客户端;自增主键要等数据库插完才知道值,那次往返就被卡在用户的等待路径上,而且多实例无法预分配。附带好处是将来分库分表不用重编号。
- 索引按查询路径建,不按直觉建:按用户拉会话列表要 sessions 的 user_id 索引,按会话拉历史要 messages 的 session_id 索引,外键列本身要索引否则删除父行会全表扫。多余的索引不是免费的,每个都让写入变慢。
- 两条唯一约束才是这套设计的灵魂:runs 的幂等键唯一,挡住重复投递;messages 的「run id 加序号」复合唯一,既保证同一次执行的输出顺序稳定,又让断线重连可以按序号幂等回放。序号要从 0 开始、连续、不跳号,否则续传就找不到断点。
- 可以预期的追问:ULID 和 UUIDv4 选哪个?答 ULID 或 UUIDv7——它们按时间有序,插入时集中在 B 树右端,不像 UUIDv4 那样随机分布导致页分裂和缓存命中率下降。这个细节能直接体现你关心过写入性能。
How to reason about it · think before answering
- It looks like a trivia question, but every choice sits on a concrete constraint. The test is whether you can say what breaks if you choose otherwise.
- Start with why three tables rather than one: the grains differ. A session is a long-lived container, a run has a lifecycle and can fail and be retried, a message is an immutable fact. Without the run layer there is nowhere to answer 'did this finish', 'should we retry', or 'what did this turn cost'.
- Use text primary keys generated in the application (UUID or ULID), because the gateway must put the id into the 202 response before the row is written. Auto-increment ids are only known after the insert, which parks a round trip in the user's wait path and cannot be pre-allocated across instances. A bonus is that sharding later needs no renumbering.
- Index by query path, not by instinct: sessions need an index on user_id to list a user's conversations, messages need one on session_id to load history, and foreign key columns need indexes or deleting a parent row triggers a full scan. Extra indexes are not free — each one slows writes.
- The two unique constraints carry the design: a unique idempotency key on runs blocks duplicate delivery, and a composite unique on run id plus sequence in messages both fixes output ordering for one run and lets a reconnect replay idempotently by sequence. The sequence must start at zero and never skip, otherwise resume cannot find the cut point.
- Expect the follow-up: ULID or UUIDv4? Choose ULID or UUIDv7 — they are time-ordered so inserts land at the right edge of the B-tree, whereas random UUIDv4 scatters writes, splits pages and hurts cache hit rates. Mentioning this shows you have watched write performance.
答题要点
- 三张表对应三种粒度:会话是长期容器、run 是一次有生命周期的执行、message 是不可变事实;少了 run 就无法回答是否跑完、该不该重试、花了多少钱
- 主键用应用侧生成的文本 id,因为 Gateway 要在写库之前把 runId 放进 202 响应里,自增主键必须等插入完成且无法跨实例预分配
- 索引按实际查询路径建:sessions 的 user_id、messages 的 session_id、以及外键列;多余索引会拖慢写入
- 两条唯一约束是灵魂:runs 的幂等键唯一挡重复投递,messages 的「run id 加序号」复合唯一保证保序与幂等回放
- id 优先选 ULID 或 UUIDv7 这类时间有序的方案,避免随机 UUID 造成的页分裂与缓存失效
Key points
- Three tables for three grains: a long-lived session, a run with a lifecycle, and immutable messages; without runs you cannot answer completion, retry or cost questions
- Application-generated text ids, because the gateway must return the run id in the 202 before the write, and auto-increment ids cannot be pre-allocated across instances
- Index the real query paths — user_id on sessions, session_id on messages, plus foreign key columns; extra indexes slow writes
- Two unique constraints carry the design: a unique idempotency key on runs, and a composite unique on run id plus sequence in messages for ordering and idempotent replay
- Prefer time-ordered ids such as ULID or UUIDv7 over random UUIDv4 to avoid page splits and cache misses
D9 Redis Streams 消息总线:XADD/XREADGROUP/XACK/XAUTOCLAIM、consumer group、毒消息
XACK 和 XAUTOCLAIM 分别解决什么问题?XACK 放在业务处理之前和之后有什么区别?What problems do XACK and XAUTOCLAIM each solve, and what changes if you XACK before instead of after doing the work?
国内高频海外高频进阶#message-bus#redis-streams#error-handling分析过程 · 先想清楚再作答
- 题眼在后半句。前半句背文档就能答,后半句在考你知不知道 ack 的时机直接决定了整个系统的投递语义——答不出这一点,面试官会判定你没在生产里管过队列。
- 先把两个命令的分工说清:XACK 是「销号」,把消息从 pending 清单里删掉,代表这件事真的做完了;XAUTOCLAIM 是「接手」,把闲置超过阈值的 pending 消息改判给另一个消费者,代表原来那个人可能已经死了。一个负责正常收尾,一个负责异常兜底。
- 然后回答时机问题,用一句话定性:先 ack 再干活是 at-most-once,先干活再 ack 是 at-least-once。前者进程一崩消息就人间蒸发,pending 清单里查不到、XAUTOCLAIM 也捡不回来;后者最坏是重复执行,而重复可以用幂等挡掉,丢单挡不掉。所以除了埋点日志这类丢一条无所谓的场景,一律先干活再 ack。
- 补一个大多数人漏掉的点:处理失败时正确的动作是**什么都不做**,让消息留在 pending 里等 XAUTOCLAIM。很多人会在 catch 里顺手 ack 掉,那等于把失败的消息静默丢弃,比不重试更糟——因为你连丢了什么都不知道。
- 再补一条 XAUTOCLAIM 的参数取舍:空闲阈值要大于「一次正常处理的耗时上限」。给太小会把还在正常处理的消息抢走,同一件事被跑两遍;给太大则故障恢复变慢。但要说清,调大阈值只降低重复概率,不消灭重复,兜底始终是消费端的唯一约束。
- 可以预期的追问:为什么用 XAUTOCLAIM 而不是 XCLAIM?答:XCLAIM 要你先 XPENDING 查出候选 id 再点名认领,两步之间还有竞态;XAUTOCLAIM 自己扫 pending 并返回游标,一条命令搞定,是 Redis 6.2 之后的推荐做法。
How to reason about it · think before answering
- The hinge is the second half. The first half is documentation; the second asks whether you know that ack timing decides the delivery semantics of the whole system.
- Split the two commands: XACK clears a message from the pending list, meaning the work is genuinely finished; XAUTOCLAIM reassigns a pending message that has been idle past a threshold, meaning its previous owner may be dead. One is the normal path, the other is the failure path.
- Then answer the timing question categorically: ack-then-work is at-most-once, work-then-ack is at-least-once. In the first, a crash makes the message vanish — it is not in the pending list, so XAUTOCLAIM cannot recover it. In the second, the worst case is duplicate execution, and duplicates can be blocked by idempotency while lost work cannot. Always work first, except for fire-and-forget telemetry.
- Add the point most people miss: on failure the correct action is to do nothing and leave the message pending for XAUTOCLAIM. Acking inside the catch block silently discards failures, which is worse than no retry because you no longer know what you lost.
- Add the parameter trade-off: the idle threshold must exceed the worst-case normal processing time. Too small and a healthy in-flight message gets stolen and executed twice; too large and recovery is slow. Be explicit that tuning it only lowers the probability of duplicates — the real backstop is a uniqueness constraint on the consumer side.
- Expect: why XAUTOCLAIM rather than XCLAIM? XCLAIM needs an XPENDING scan first and then a named claim, with a race in between; XAUTOCLAIM scans and returns a cursor in one command, and is the recommended approach since Redis 6.2.
答题要点
- XACK 负责正常收尾:把消息从 pending 清单里销号,代表这件事真的做完了;重复 ack 返回 0,天生幂等
- XAUTOCLAIM 负责异常兜底:把闲置超过阈值的 pending 消息改判给另一个消费者,解决「消费者死了它手上的消息怎么办」
- 先 ack 再干活是 at-most-once,崩溃就丢单;先干活再 ack 是 at-least-once,最坏是重复,可以用幂等挡
- 处理失败时不要 ack,让消息留在 pending 里等接手;在 catch 里顺手 ack 等于静默丢弃失败
- 空闲阈值要大于正常处理耗时的上限,但调大只降低重复概率,兜底仍是消费端唯一约束
Key points
- XACK is the happy-path close-out: it clears the message from the pending list; repeat acks return 0, so it is naturally idempotent
- XAUTOCLAIM is the failure backstop: it reassigns pending messages idle past a threshold, answering 'what happens to work held by a dead consumer'
- Ack-before-work is at-most-once and loses work on a crash; work-before-ack is at-least-once and at worst duplicates, which idempotency can absorb
- Never ack on failure — leave the message pending for takeover; acking in the catch block silently discards failures
- The idle threshold should exceed worst-case processing time, but tuning it only reduces duplicates; uniqueness constraints are the real guarantee
Redis Streams 和 Kafka 该怎么选?什么情况下 Streams 明显不够用?How do you choose between Redis Streams and Kafka, and when is Streams clearly not enough?
国内高频海外高频进阶#message-bus#redis-streams#architecture分析过程 · 先想清楚再作答
- 这题的坏答案是「看数据量」。吞吐从来不是第一判据——单机 Redis 每秒几万条 XADD 毫无压力,绝大多数业务的量级根本碰不到天花板。答成「量小用 Streams、量大用 Kafka」会被认为没做过选型。
- 换成两个真正的判据来推:一、这些消息需要保留多久;二、会不会有第二类消费方。生命周期是「执行一次就没用了」、且只有执行层这一个消费方,Streams 完全够用,还省掉一整套运维;需要「三个月内任意时间点重放」、或者同一份数据要同时喂给实时执行、离线数仓、风控三条链路,那就该上 Kafka。
- 再补三条结构性差异:Streams 是内存为主、保留全靠你自己 MAXLEN 或 XTRIM,Kafka 是磁盘顺序写、保留几周是常态;Streams 一个组里加多少消费者都行,Kafka 的消费者数受分区数限制,多了就有人空转;顺序保证的粒度不同,Streams 是单条流内有序而组内分配随机,Kafka 是同 key 落同分区、分区内有序。
- 然后主动说出那条最能体现深度的话:Redis 的持久化是有损的。AOF 默认每秒刷盘,最坏丢最后一秒的写入;主从异步复制,故障切换时未同步的消息会消失。所以用 Streams 时架构上必须有一个真相之源——本课是 Postgres 的 runs 表,流只是触发器,丢了消息那个 run 还停在 pending,补投任务会把它捡回来。把总线当唯一数据源是最危险的误用。
- 结论落成一句可复用的判断:Streams 适合「触发执行」,Kafka 适合「数据管道」。前者的消息是一次性的命令,后者的消息是需要被多方反复读取的事实。
- 可以预期的追问:那 RabbitMQ、SQS 呢?答:RabbitMQ 强在复杂路由和延迟队列(Streams 没有原生延迟投递,要自己带「下次可执行时间」重投);SQS 强在零运维,代价是没有回放、也没有严格顺序(FIFO 队列另算)。把判据说成「保留时长、消费方数量、路由复杂度、运维预算」四条,比背产品参数强得多。
How to reason about it · think before answering
- The bad answer is 'it depends on volume'. Throughput is never the first criterion — a single Redis node handles tens of thousands of XADDs per second, and most workloads never approach that ceiling. 'Small volume Streams, large volume Kafka' reads as never having run a real evaluation.
- Use two real criteria instead: how long the messages must be retained, and whether a second class of consumer will appear. If a message is useless once executed and the execution layer is the only consumer, Streams is plenty and saves an entire operational surface. If you need replay from any point in the last three months, or the same data must feed real-time execution, an offline warehouse and a risk engine, choose Kafka.
- Add three structural differences: Streams is memory-first with retention you enforce yourself via MAXLEN or XTRIM, while Kafka does sequential disk writes and keeps weeks by default; a Streams group takes any number of consumers, while Kafka consumers are capped by partition count and extras idle; ordering granularity differs — Streams orders a single stream but dispatches randomly within a group, Kafka pins a key to a partition and orders within it.
- Then volunteer the line that shows real depth: Redis persistence is lossy. AOF fsyncs once per second by default, so the last second of writes can vanish, and replication is asynchronous, so a failover can drop unreplicated messages. Using Streams therefore requires a source of truth elsewhere — here the Postgres runs table, with the stream acting only as a trigger; a lost message leaves the run pending and a sweeper republishes it. Treating the bus as the only datastore is the dangerous misuse.
- Land on a reusable rule: Streams suits triggering work, Kafka suits data pipelines. One carries one-shot commands, the other carries facts that many parties re-read.
- Expect: what about RabbitMQ or SQS? RabbitMQ wins on complex routing and delayed delivery (Streams has no native delay, you republish with a next-eligible timestamp); SQS wins on zero operations at the cost of replay and strict ordering (FIFO queues aside). Framing the criteria as retention, number of consumers, routing complexity and operational budget beats reciting product specs.
答题要点
- 第一判据不是吞吐,是「消息要保留多久」和「会不会有第二类消费方」
- 只有执行层一个消费方、消息执行完即失效:Streams 够用,且大概率你已经有 Redis,零新增运维
- 需要长期保留与任意时间点回放、或多条下游链路共用同一份数据:选 Kafka
- 结构差异:Streams 内存为主、保留靠自己裁剪、组内分配随机;Kafka 磁盘顺序写、按 key 分区且分区内有序、消费者数受分区限制
- Redis 持久化有损(AOF 每秒刷盘、异步复制),所以真相之源必须是数据库,流只当触发器,靠补投任务兜底
- 一句话判断:Streams 适合触发执行,Kafka 适合数据管道
Key points
- The first criterion is not throughput but retention length and whether a second class of consumer will exist
- One consumer class and messages that expire on execution: Streams is enough, and you probably already run Redis
- Long retention with arbitrary replay, or one dataset feeding several downstream pipelines: pick Kafka
- Structural differences: Streams is memory-first with self-managed trimming and random in-group dispatch; Kafka is sequential-disk, key-partitioned with in-partition ordering, and caps consumers at partition count
- Redis persistence is lossy (per-second AOF fsync, async replication), so the database must be the source of truth with the stream as a trigger plus a republish sweeper
- One-line rule: Streams triggers work, Kafka moves data
一条消息反复处理失败怎么办?请设计一个毒消息隔离机制。What do you do with a message that keeps failing? Design a poison-message isolation mechanism.
国内高频海外高频进阶#message-bus#error-handling#reliability分析过程 · 先想清楚再作答
- 这题在考你有没有踩过「一条坏消息拖垮整条流」。判断标准很简单:你的回答里有没有出现一个具体的阈值和一个具体的落地位置,没有就是在讲概念。
- 先把故障模式说清楚:按 at-least-once 的规矩,失败就不 ack、留在 pending 等重投,于是一条无论谁来都会失败的消息进入死循环——投递、失败、闲置超时、被接手、再失败。它自己永远好不了,还持续占用消费者的处理能力。
- 然后给机制,三个动作缺一不可:一、判定依据用 pending 清单自己记的投递次数,不要另建计数表;二、超过阈值(本课固定 3 次)就把消息搬到一条死信流,字段里带上原始消息 id、投递次数和失败原因;三、对原流 XACK,同时把这次执行标成失败并写入错误原因。只搬不 ack,它还躺在 pending 里等着被接手;只 ack 不搬,消息和失败原因一起消失,用户永远停在「正在思考」。
- 阈值的取值要给出权衡:定 1 会让一次网络抖动就把本来能成功的消息判死;定 10 会在一条必死的消息上浪费十次执行的钱和时间。3 次配合每次之间的空闲阈值,足够熬过绝大多数瞬时故障。
- 还要主动说出一个缺口:Redis Streams 没有原生的指数退避,重投时机由空闲阈值决定。想要退避就得自己把消息重新投递并带上「下次可执行时间」,那已经是在实现延迟队列了——这一条能体现你知道 Streams 的边界在哪。
- 可以预期的追问:死信流建完就完了吗?答:不。死信条数必须接进告警,它从 0 变成非 0 通常意味着有一类输入你的代码处理不了,是真 bug 而不是运气差;还要留一个重放入口——把死信里的字段原样投回原流即可,因为幂等键还在,重放不会产生重复执行。见过团队把死信建起来半年没打开过,那等于把故障静音了。
How to reason about it · think before answering
- This question probes whether you have ever watched one bad message stall an entire stream. The test is simple: does your answer contain a concrete threshold and a concrete place where isolation happens? If not, you are talking theory.
- Describe the failure mode first: under at-least-once you do not ack on failure, so the message stays pending and gets redelivered. A message that fails for everyone therefore loops forever — delivered, failed, idle timeout, claimed, failed — never recovering while continuously consuming worker capacity.
- Then give the mechanism, three actions and all of them required. One, use the delivery count the pending list already tracks rather than building a counter table. Two, past the threshold (three deliveries in this course) move the message to a dead-letter stream carrying the original id, delivery count and failure reason. Three, XACK the original stream and mark the run failed with the error recorded. Moving without acking leaves it pending for another takeover; acking without moving makes both the message and its reason disappear, leaving the user stuck on 'thinking'.
- Justify the threshold: one delivery kills messages that a single network blip would have let through; ten wastes ten executions of money and time on a message that can never succeed. Three deliveries, spaced by the idle threshold, survives almost all transient faults.
- Volunteer a limitation: Redis Streams has no native exponential backoff — redelivery timing is governed by the idle threshold. Backoff requires republishing the message with a next-eligible timestamp, which means building a delay queue yourself. Naming this shows you know where Streams ends.
- Expect: is creating the dead-letter stream the end of it? No. Its depth must be alerted on, since going from zero to non-zero usually means a class of input your code cannot handle — a real bug, not bad luck. Keep a replay path too: republish the stored fields back to the original stream, and because the idempotency key is preserved, replay cannot cause duplicate execution. Teams that build a dead-letter stream and never open it have simply muted their failures.
答题要点
- 故障模式:at-least-once 下失败不 ack,一条永远失败的消息会无限重投并持续占用消费者
- 判定依据用 pending 清单里记的投递次数,不需要另建计数表
- 阈值固定 3 次:定 1 会误杀瞬时故障,定 10 会在必死消息上浪费十次执行成本
- 隔离动作三件缺一不可:搬到死信流(带原始 id、投递次数、失败原因)、对原流 XACK、把这次执行标成失败并写入原因
- Redis Streams 没有原生指数退避,重投时机由空闲阈值决定,要退避得自己实现延迟投递
- 死信流要接告警并留重放入口;幂等键还在,重放不会导致重复执行
Key points
- Failure mode: under at-least-once you do not ack on failure, so an always-failing message is redelivered forever and keeps consuming worker capacity
- Use the delivery count already tracked in the pending list rather than a separate counter table
- Fix the threshold at three deliveries: one kills transient failures, ten wastes ten executions on a message that can never succeed
- Isolation needs all three actions: move to a dead-letter stream with original id, delivery count and reason; XACK the original stream; mark the run failed with the error stored
- Redis Streams has no native exponential backoff — redelivery timing follows the idle threshold, so backoff means implementing delayed republishing yourself
- Alert on dead-letter depth and keep a replay path; the idempotency key survives, so replay cannot duplicate execution
D10 分片与租约:userId 哈希→shard、SET NX + TTL + Lua 续约、同用户顺序、handoff
租约为什么必须配合 TTL?续约为什么要用 Lua 脚本,而不是先 GET 再 PEXPIRE?Why must a lease carry a TTL, and why renew it with a Lua script instead of GET followed by PEXPIRE?
国内高频海外高频进阶#lease#redis#atomicity分析过程 · 先想清楚再作答
- 这题有两个考点,第二个才是区分度。第一个考点其实是在问「你知不知道租约和分布式锁不是一回事」——先把这条说清:锁的语义是互斥(我持有、你等待,我主动 release 你才拿得到),租约的语义是带过期时间的所有权(持有者不 release 也会失效,因为它可能永远不会回来了)。
- 由此推出 TTL 的必要性:持有者会被 kill -9、会断网、会整台机器掉电,它没有机会归还。没有 TTL 就是一把永不释放的锁,那个 shard 从此永久荒废,只能靠人工介入。TTL 的全部意义是「不需要任何人干预,所有权会自己失效」。
- 顺手说出 TTL 的取舍,证明你调过:太短则一次垃圾回收停顿或网络抖动就丢租约,shard 反复易主、用户会话来回搬家;太长则真死了之后要等满一个 TTL 才有人接手。常见口径是 TTL 30 秒、续约间隔取 TTL 的三分之一(10 秒),这样能连续失败两次而不丢租约。
- 第二个考点是原子性。两步写法的失败时间线要具体讲出来:GET 返回「是我的」,紧接着的两毫秒里租约恰好到期被 Redis 删除、另一个 worker SET NX 抢到,然后你的 PEXPIRE 执行成功——你续的是对手的租约,而自己还以为持有。如果第二步用的是 SET 而不是 PEXPIRE,你还会把对手的名字覆盖成自己,两个进程一起动手。
- 结论要落到通用原理上:检查和改动必须是一个不可分割的动作(compare-and-swap)。Redis 单线程执行命令,一整段 EVAL 对其他客户端就是一个原子步骤,所以 Lua 在这里不是为了性能,是为了把 GET 和 PEXPIRE 粘成一条。等价手段还有 Redis 函数、或用 WATCH 加事务重试,但 Lua 最直接。
- 可预期的追问:续约返回 0 应该怎么办?答「立刻放手」——把这个 shard 从持有集合里删掉、停止取消息、手上那条没做完的不许再写。返回 0 只打一行警告日志然后继续跑,是脑裂最常见的来源。再加一条自杀规则:距上次成功续约超过 TTL 的三分之二就主动全部放手。
How to reason about it · think before answering
- There are two things being tested and the second is the discriminator. The first is really 'do you know a lease is not a lock': a lock means mutual exclusion (I hold, you wait, you get it when I release), while a lease means ownership with an expiry (it lapses even if the holder never releases, because the holder may never come back).
- That gives you the necessity of the TTL: holders get kill -9'd, lose the network, lose the whole machine — they never get to hand anything back. Without a TTL you have a lock that is never released and a shard that is permanently orphaned until a human intervenes.
- Volunteer the TTL trade-off to show you have tuned this: too short and a GC pause or a network blip costs you the lease, so shards flap and sessions keep migrating; too long and a genuinely dead worker's shards sit idle for a full TTL. A common setting is a 30 second TTL renewed every 10 seconds (one third), which tolerates two consecutive renewal failures.
- The second point is atomicity, and you should spell out the failing interleaving: GET says the lease is yours, then within two milliseconds it expires, Redis drops it, another worker wins it with SET NX, and your PEXPIRE succeeds — you have just extended your rival's lease while believing you still hold the shard. If step two is SET rather than PEXPIRE you also overwrite their owner field and both processes start working.
- Land on the general principle: check-and-mutate must be indivisible (compare-and-swap). Redis executes commands single-threaded, so one EVAL is a single atomic step to every other client — Lua here is not about performance, it is about fusing GET and PEXPIRE. Redis Functions or WATCH plus a transaction retry are equivalent, but Lua is the most direct.
- Expect the follow-up: what should a renewal returning 0 do? Let go immediately — drop the shard from the held set, stop consuming, and refuse to write the in-flight item. Logging a warning and carrying on is the most common source of split brain. Add a self-kill rule too: if the last successful renewal is older than two thirds of the TTL, release everything.
答题要点
- 租约不是锁:锁是互斥,租约是带过期时间的所有权;持有者可能永远不会回来,所以所有权必须能自己失效
- 没有 TTL 就是永不释放的锁,持有者被 kill 之后那个 shard 永久荒废
- TTL 30 秒、续约间隔 10 秒(TTL 的三分之一),留出连续两次续约失败的余量
- 两步续约的窗口里租约可能已易主,你的 PEXPIRE 会替对手延长任期,而自己仍以为持有
- Lua 的作用是把「比较持有者」和「续期」粘成一个原子步骤,不是为了性能;续约返回 0 必须立刻放手
Key points
- A lease is not a lock: locks give mutual exclusion, leases give ownership with an expiry, because the holder may never return
- Without a TTL you have a never-released lock and a permanently orphaned shard once the holder is killed
- A 30 second TTL renewed every 10 seconds leaves headroom for two consecutive renewal failures
- In the two-step window the lease may already have changed hands, so your PEXPIRE extends a rival's term while you still think you hold it
- Lua fuses the ownership check and the extension into one atomic step; a renewal returning 0 means let go immediately
在一个多 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 打断合并
多个客户端同时订阅同一次执行的流式输出,怎么保证每个客户端都收到完整且有序的内容?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分析过程 · 先想清楚再作答
- 题眼有两个词:完整、有序。很多人只答有序,漏掉完整——而「完整」那一半恰好是最容易设计错的,因为它取决于你用了哪种读法。
- 先给一句能定调的判断:一次执行是执行的单位,一条连接是观看的单位,两者不是一对一。手机和电脑同开、两个标签页、重连瞬间新旧连接并存,都会让同一次执行上挂着多条流。想清楚这句话,「谁先连谁独占」这种锁的方案就自然被排除了。
- 接着点出「完整」的真正机关:广播读法与消费组是两种语义。消费组是分摊,一条消息只给一个消费者;这里要的是广播,每个订阅者都要看到全部。用消费组做扇出,结果就是两条连接各拿到半段话——这是这道题最常见的错误答案。
- 再答「有序」:每个片段带一个从 0 开始、连续、不跳号的序号,写进流;接收侧维护「下一个该交付的号」,小于它的丢弃,大于它的先入缓冲,连号了再批量推出去。序号同时写进 SSE 的 id 字段,客户端不用另记一套账。
- 然后是必须主动说的工程代价:缓冲要有上限。如果 5 号迟迟不到,6 号往后全在内存里排队,一万条连接同时这样就是一次内存事故。做法是给缓冲设条数上限和等待上限,超时就从库里补读,补不到就发 error 让客户端重连——能等,但不能无限等。
- 可以预期的追问:扇出实现怎么选?两种——每条连接各自去读一遍流(简单,代价是同一批数据被读 N 次),或进程内只读一次再广播给本地订阅者(省读取,但要维护订阅者表、要处理最后一个订阅者离开,跨实例仍要各读一次)。判据是每次执行的平均订阅者数,多数产品接近 1,那就选前者,别为不存在的规模提前写一层。
How to reason about it · think before answering
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
用户在 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 工具
pgvector 和专用向量数据库相比,优劣分别是什么?你会怎么选?How does pgvector compare with a dedicated vector database, and how would you choose?
国内高频海外高频进阶#vector-database#pgvector#architecture分析过程 · 先想清楚再作答
- 这题考的是选型判断力,不是产品参数背诵。开口就报「Milvus 支持分布式、Qdrant 过滤更强」是最没有区分度的答法——面试官想知道你按什么判据选,以及你有没有算过运维成本。
- 先给三个提问维度,把选型变成可推导的:数据量到什么量级、要不要和业务表在同一个事务里提交、过滤条件复不复杂。这三问能覆盖绝大多数真实场景。
- pgvector 的赢面几乎全在后两问上:记忆表和业务表在同一个库,写记忆和更新执行记录可以放进同一个事务;按用户过滤就是普通 where 条件;备份、监控、连接池、迁移工具全部复用。**多一个有状态服务的运维成本,通常比向量检索的性能更早成为瓶颈**——这句话最能体现你上过线。
- 再诚实地说它的天花板:单表到千万级向量时 HNSW 索引构建吃内存、写入放大明显,ANN 与元数据过滤的融合不如专用库,水平扩展只能靠 Postgres 自己那一套。不肯说缺点的人会被认为在推销。
- 结论要能写死:百万级以内、需要和业务表一起过滤或同事务提交、团队人手紧,用 pgvector;上千万条、检索本身就是主要负载、有专人维护,上专用库。别在第一天就选专用库。
- 可以预期的追问:以后想换库,迁移成本大不大?答案会让很多人意外——换库不用重算 embedding,向量是模型产出的,跟存它的库无关,导出导入即可,成本主要在双写和灰度。真正要全量重算的是换 embedding 模型,那才是硬锁定。
How to reason about it · think before answering
- This is a judgment question, not a feature-recital. Opening with 'Milvus does sharding, Qdrant filters better' carries no signal — they want your decision criteria and whether you have priced the operational overhead.
- Offer three questions that make the choice derivable: what scale, does it need to commit in the same transaction as business tables, and how complex is the metadata filtering.
- pgvector wins on the last two: memories live in the same database as the business tables, so writing a memory and updating a run share one transaction; filtering by user is an ordinary WHERE clause; backups, monitoring, pooling and migrations are all reused. The line that shows operational experience is that one more stateful service usually becomes the bottleneck before vector search performance does.
- Be honest about the ceiling: past roughly ten million vectors in one table, HNSW index builds eat memory and write amplification shows; ANN plus metadata filtering is weaker than a purpose-built engine; horizontal scaling is whatever Postgres gives you. Refusing to name downsides reads as salesmanship.
- Commit to a rule: under a million vectors, needing joins or shared transactions, small team — pgvector. Tens of millions, retrieval as the primary workload, someone owning the service — dedicated store. Do not start with the dedicated store on day one.
- Expect the follow-up on migration cost. Switching stores does not require re-embedding — vectors belong to the model, not the store, so export and import; the cost is dual-write and rollout. Switching the embedding model is what forces a full recompute, and that is the real lock-in.
答题要点
- 三个判据:数据量级、要不要和业务表同事务提交、元数据过滤复不复杂
- pgvector 的优势是同库同事务、普通 SQL 过滤、运维零新增——少一个有状态服务往往比性能更值钱
- pgvector 的天花板:千万级向量时索引构建吃内存、写入放大、ANN 与过滤融合弱、扩展受限于 Postgres
- 专用向量库给的是分布式分片、更强的过滤与 ANN 融合、混合检索,代价是多一个要备份要监控的有状态服务
- 换向量库不用重算 embedding;换 embedding 模型才要全量重算,真正的锁定点是模型不是库
Key points
- Three criteria: scale, need for same-transaction commits with business tables, and filtering complexity
- pgvector gives one database, one transaction, ordinary SQL filters and zero new operations — often worth more than raw performance
- Its ceiling: memory-hungry index builds and write amplification at tens of millions, weaker ANN-plus-filter fusion, scaling limited to Postgres
- Dedicated stores buy sharding, better filtered ANN and hybrid search, at the price of another stateful service to back up and monitor
- Changing stores needs no re-embedding; changing the embedding model does — the lock-in is the model, not the database
chunking 的切分策略会怎么影响检索效果?切多大合适?How does the chunking strategy affect retrieval quality, and how do you pick a chunk size?
国内高频海外高频进阶#chunking#rag#retrieval-quality分析过程 · 先想清楚再作答
- 题眼在「怎么影响」。只回答一个数字(比如「切 500 字」)会被追着问为什么,所以要先把两个方向的失效模式讲出来,数字才有落点。
- 切太碎的失效模式:单张卡片脱离上下文。「他说要 42 码」检索命中了也没用,代词失去指代,模型拿到一句悬空的话反而更容易编。
- 切太整的失效模式更反直觉,也是这题真正的区分点:一块横跨三个主题时,它的向量是这几个主题的平均值,结果对哪个 query 都不太像,命中率反而下降。**块越大信息越全,却越难被检索到**——能说出这句话基本就过了。
- 然后给可操作的口径:目标 400 字符、相邻块重叠 80 字符,并优先在句号、换行这类自然边界收尾。重叠的作用要说清楚——一句关键的话被切口劈开时,两块各拿半句,重叠保证它至少在其中一块里是完整的。
- 补上代价,这是工程视角:重叠 80 除以 400 等于 20% 的存储放大,向量也跟着多一份;内容高度重叠的两块可能一起被检索出来,白占返回名额,所以要按内容去重。
- 可以预期的追问:怎么验证切分策略好不好?答案是准备一批 query 与标注好的期望命中,量召回率和 top-k 命中率,改切分参数后重跑对比——切分是可以被度量的,不该靠感觉调。第二个追问是「对话数据要不要原样切」,答不要:先让模型抽成陈述句再切,否则大量寒暄句会把向量拉平。
How to reason about it · think before answering
- The hinge is 'how does it affect'. Naming a number alone invites a why, so describe both failure modes first and let the number follow.
- Too small: a chunk loses its context. 'He wants size 42' retrieves fine but resolves to nothing — pronouns dangle and the model is more likely to fabricate.
- Too large is the counter-intuitive half and the real discriminator: a chunk spanning three topics gets a vector that averages them, so it looks only vaguely like any query and recall drops. Bigger chunks carry more information yet are harder to retrieve.
- Give an operational default: target 400 characters with 80 characters of overlap, ending on natural boundaries such as sentence stops or newlines. Explain the overlap — when a key sentence lands on a cut, each side holds half of it, and the overlap guarantees at least one chunk holds it whole.
- Add the costs: 80 over 400 is 20% storage amplification plus an extra vector per duplicated span, and near-duplicate chunks can both surface and waste result slots, so deduplicate by content before returning.
- Expect: how do you validate a chunking strategy? Build a query set with labelled expected hits and measure recall and top-k hit rate, then re-run after changing parameters — chunking is measurable, not a matter of taste. Second follow-up: should raw dialogue be chunked as-is? No — have the model distil it into standalone statements first, or filler turns flatten the vectors.
答题要点
- 切太碎:单块脱离上下文,代词失去指代,命中了也用不上
- 切太整:一块横跨多个主题,向量被平均,对任何 query 都不够像,命中率反而下降
- 可操作口径:目标 400 字符、重叠 80 字符,优先在句号或换行这类自然边界收尾
- 重叠的作用是保证被切口劈开的句子至少在一块里完整;代价是约 20% 的存储放大和可能的重复命中
- 别直接切对话原文,先抽成陈述句;切分效果要用标注好的 query 集测召回率,而不是凭感觉
Key points
- Too small: chunks lose context, pronouns dangle, and a hit is useless
- Too large: one chunk spans several topics, its vector averages them, and recall drops for every query
- Working default: target 400 characters with 80 characters of overlap, cutting on sentence or newline boundaries
- Overlap keeps a split sentence whole in at least one chunk, at roughly 20% storage amplification plus possible duplicate hits
- Distil dialogue into standalone statements before chunking, and validate with a labelled query set measuring recall
D13 cron 调度(中心调度→stream 投递)+ 成本计量(token→USD 台账、usage report)
让你从零设计一套 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 复盘
多实例部署下,怎么设计心跳和健康检查?两者是同一件事吗?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
什么是优雅停机?为什么直接 kill 进程有风险?请说出具体步骤。What is graceful shutdown, and why is killing a process outright risky? Walk through the steps.
国内高频海外高频进阶#deployment#reliability#operations分析过程 · 先想清楚再作答
- 这题考的是「你有没有真的发过版」。答「等任务跑完再退出」只是定义,面试官要的是代价、步骤和顺序。
- 先把代价说具体。发版、缩容、机器维护、抢占式实例回收都会先发 SIGTERM、等宽限期、超时 SIGKILL。SIGKILL 拦不住,落到正在跑 Agent 循环的 Worker 身上:这次的 run 永远停在 running,用户界面一直转圈;模型调用的钱已经付了,回复却没落库;没确认的消息要等空闲阈值到了才被别人接手,用户白等一轮。一次发版掐断几十次对话,这就是日常代价。
- 然后给三步,强调顺序不能变:第一步拒新——把开关拨过去,消费循环下一轮不再从流里取消息(已经读到手上还没开始的那几条,留在 pending 里由别人接手,比硬扛完一整批更快);第二步等手头这次执行跑完,但要有上限;第三步主动交还租约、从心跳面板注销,然后退出。
- 第二步的上限是加分点:一次卡死的模型调用会让你永远等不到,而宽限期一到照样 SIGKILL。与其被动挨刀,不如自己认输退出——没确认的消息还在 pending 里,别人会接手重做。本课取 20 秒,取法是「一次正常执行的耗时上限」再留余量。
- 第三步也是加分点:租约本来靠 TTL 到期自然易主,但那是为进程猝死准备的。计划内下线你明知道自己要走,主动交还能让接手方下一轮扫描就上岗,而不是白等一个 TTL。交还必须带条件——只删还写着自己名字的那把牌子,否则租约已过期、别人刚抢到时,你就把对方的值班牌撕了。
- 最后两件配套的事,漏一件前面全白做:宽限期的配置必须大于代码里的等待上限(代码等 20 秒而 compose 默认只等 10 秒,第 10 秒就 SIGKILL,三步只走到一半);以及信号得真的传到你的进程(启动命令写成包管理器,PID 1 就是包管理器,SIGTERM 未必传得到,停机代码一次都不会执行)。
How to reason about it · think before answering
- This question tests whether you have actually shipped a release. Reciting finish in-flight work before exiting is just the definition; the interviewer wants the cost, the steps, and the ordering.
- Make the cost concrete. Deploys, scale-downs, host maintenance and spot reclamation all send SIGTERM, wait a grace period, then SIGKILL. SIGKILL cannot be trapped, and landing it on a worker mid-agent-loop means: the run is stuck in running forever while the user watches a spinner; you already paid for the model call but never persisted the reply; the unacked message waits for the idle threshold before anyone claims it. One deploy cuts off dozens of conversations — that is the everyday cost.
- Then give three steps and stress that the order is fixed. One, stop accepting work: flip a flag so the consume loop stops reading from the stream (messages already fetched but not started stay in pending for someone else, which is faster than forcing a whole batch through). Two, wait for the in-flight execution, but with a ceiling. Three, proactively release leases, deregister from the heartbeat dashboard, and exit.
- The ceiling in step two earns points: a hung model call means you wait forever, and the grace period will SIGKILL you anyway. Better to concede and exit — the unacked message is still pending and someone will redo it. This course uses 20 seconds, derived from the upper bound of a normal execution plus margin.
- Step three also earns points: leases normally change hands via TTL expiry, but that path exists for sudden death. On a planned shutdown you know you are leaving, so releasing proactively lets the successor take over on its next scan instead of waiting out a full TTL. The release must be conditional — delete only the badge that still bears your name, or you will tear down the badge of whoever just claimed it after your lease expired.
- Finish with two companions; miss either and the rest is wasted. The configured grace period must exceed the wait ceiling in code (code waits 20s while compose defaults to 10s, so SIGKILL lands at second 10 and your three steps only half-run). And the signal must actually reach your process (if the entrypoint is a package manager, PID 1 is the package manager, SIGTERM may never arrive, and your shutdown code never runs once).
答题要点
- 直接 kill 的具体代价:run 永远停在 running、用户界面一直转圈、模型的钱已付但回复没落库、没确认的消息要等空闲阈值才被接手
- 三步且顺序不能变:拒绝新任务 → 等手头的跑完(有上限)→ 主动交还租约并注销心跳,然后退出
- 等待必须有上限(本课 20 秒):卡死的模型调用会让你永远等不到,宽限期一到照样被 SIGKILL,不如自己认输,消息还在 pending 里
- 主动交还租约让接手方下一轮就上岗,而不是白等一个 TTL;交还必须条件化,只删还写着自己名字的那把
- 宽限期配置必须大于代码里的等待上限,否则三步只执行到一半(compose 的 stop_grace_period / K8s 的 terminationGracePeriodSeconds)
- 信号要真传到进程:用 exec 形式直接起业务进程,别让包管理器当 PID 1
Key points
- Concrete cost of a hard kill: the run is stuck in running, the user stares at a spinner, the model call is paid for but the reply is unsaved, and the unacked message waits out the idle threshold
- Three steps in a fixed order: refuse new work, wait for in-flight work with a ceiling, then release leases and deregister before exiting
- The wait needs a ceiling (20s here): a hung model call never returns and the grace period kills you anyway, so concede — the message is still pending for someone else
- Releasing leases proactively lets the successor start on its next scan instead of waiting a full TTL; the release must be conditional on still owning it
- The configured grace period must exceed the in-code wait ceiling, or the three steps only half-run (stop_grace_period / terminationGracePeriodSeconds)
- Make sure the signal reaches your process: exec the business process directly rather than letting a package manager be PID 1
滚动发布时,如何避免正在处理的任务被打断?During a rolling deploy, how do you keep in-flight tasks from being interrupted?
国内高频海外高频进阶#deployment#reliability#operations分析过程 · 先想清楚再作答
- 这题是上一题的应用题,区别在于它要求你把编排系统那一侧也讲进来——只讲进程内的三步只答了一半。
- 完整骨架是两侧配合:编排系统先摘流量(把就绪探针转红,让负载均衡不再把新请求打过来)、再发 SIGTERM、然后等宽限期;进程在这段时间里把手头的活做完、交还所有权、干净退出。这一句话就是答案的主干,剩下都是细节。
- 然后区分两类副本,这是拿分点。Gateway 有入站连接,摘流量对它有意义;Worker 没有任何入站连接,它是自己去总线取活的,所谓「摘流量」对它就是「自己不再取新消息」——也就是停机三步的第一步。**同一个词在两类副本上是两种机制**,能说清这一点说明你理解拉与推的差别。
- 接着讲批次与顺序:一次只换一部分副本(compose 里手动分批,K8s 里靠 maxUnavailable / maxSurge),保证任何时刻都有足够的存活副本接得住流量。心跳面板上的版本号字段这时派上用场——你能看到新旧两批各剩几个,而不是盲发。
- 还要提一句状态兼容:滚动发布期间新旧代码同时在线,所以数据库迁移必须向后兼容(先加可空列、再双写、最后才删旧列),消息格式也不能一次性改。这是很多人漏掉的一层——进程停得再优雅,新旧版本读不了同一份数据照样出事故。
- 可以预期的追问:如果一次执行本来就要跑 5 分钟,宽限期不可能等那么久怎么办?答案不是把宽限期拉到 5 分钟,而是让任务可中断可重入——把长任务切成可保存进度的小步(D11 的 run 状态机和 D9 的 at-least-once 加幂等正好提供了这个基础),被打断的那一步由下一个副本接着做。
How to reason about it · think before answering
- This is the applied version of the previous question, and the difference is that it demands the orchestrator's side too — describing only the in-process steps answers half of it.
- The full skeleton is both sides cooperating: the orchestrator first removes traffic (turns readiness red so the load balancer stops sending new requests), then sends SIGTERM, then waits out the grace period; the process uses that window to finish in-flight work, hand back ownership, and exit cleanly. That sentence is the trunk; everything else is detail.
- Then distinguish the two kinds of replica, which is where the points are. A gateway has inbound connections, so draining traffic means something for it. A worker has no inbound connections at all — it pulls work from the bus, so draining for it means stop fetching new messages, which is step one of graceful shutdown. The same word is two different mechanisms on the two replica types, and saying so shows you understand pull versus push.
- Next, batching and ordering: replace only a subset at a time (manual batches in compose, maxUnavailable / maxSurge in Kubernetes) so enough replicas are always alive to absorb traffic. This is where the version field in the heartbeat payload pays off — you can see how many old and new replicas remain instead of deploying blind.
- Also mention state compatibility: during a rolling deploy old and new code run simultaneously, so schema migrations must be backward compatible (add a nullable column, dual-write, drop the old column last) and message formats cannot change in one shot. Many candidates miss this layer — however gracefully processes stop, two versions that cannot read the same data will still cause an incident.
- Expect: what if a single execution legitimately takes five minutes and the grace period cannot wait that long? The answer is not to stretch the grace period to five minutes but to make the task interruptible and resumable — break long work into steps that checkpoint progress (the run state machine from D11 plus at-least-once with idempotency from D9 give you exactly this), so the next replica continues the interrupted step.
答题要点
- 完整骨架是两侧配合:编排系统先摘流量、再发 SIGTERM、等宽限期;进程在这段时间做完手头的活、交还所有权、干净退出
- Gateway 和 Worker 的「摘流量」是两种机制:前者靠就绪探针转红让负载均衡停止转发,后者靠自己不再从总线取新消息
- 分批替换(maxUnavailable / maxSurge 或手动分批),保证任何时刻有足够存活副本;心跳里的版本号让你看到新旧两批各剩几个
- 新旧代码同时在线,所以数据库迁移必须向后兼容(加可空列 → 双写 → 最后删旧列),消息格式不能一次性改
- 长任务不该靠拉长宽限期解决,而要做成可中断可重入:切成能保存进度的小步,被打断的那步由下一个副本接着做
Key points
- The full skeleton is both sides: orchestrator drains traffic, sends SIGTERM, waits the grace period; the process finishes in-flight work, hands back ownership, exits cleanly
- Draining means two different things for gateways and workers: readiness turning red versus the worker itself stopping its fetch from the bus
- Replace in batches (maxUnavailable / maxSurge or manual) so enough replicas stay alive; the version field in heartbeats shows how many old and new remain
- Old and new code run concurrently, so migrations must be backward compatible (nullable column, dual-write, drop last) and message formats cannot change in one step
- Long tasks are not solved by a longer grace period but by being interruptible and resumable — checkpointed steps that the next replica can continue
D15 多 Agent 模式全景(Router/Supervisor、Planner-Executor、Critic、Swarm、Blackboard)与何时不该用;LangGraph 入门
LangGraph 里节点、边、状态分别扮演什么角色?如果不用框架,你自己会怎么实现?In LangGraph, what roles do nodes, edges and state play? If you had no framework, how would you implement it yourself?
国内高频海外高频进阶#langgraph#orchestration#state-management分析过程 · 先想清楚再作答
- 题眼在后半句。只答三个概念的定义,面试官会认为你读过文档;能说出「不用框架会难受在哪」,才证明你知道框架替你解决了什么。这类题的通用解法是:先讲自己手写的版本,再讲框架把哪几处收敛了。
- 先给手写版:一个循环,里面一串条件判断决定下一步走哪,中间用一个大对象在各步之间传数据。写到第三个分支就会撞上三件事——两步都往同一个字段写,是覆盖还是追加,你要在每个分支里手写一遍合并逻辑;中间过程全在局部变量里,出错只能靠打印;进程一挂就从头重来,已经花掉的模型调用钱白付。
- 然后一一对上:节点是一个普通函数,读全量状态、返回只含改动字段的增量;边是节点之间的连接,无条件边写死顺序,条件边在运行时决定去哪;状态是一张字段表,每个字段是一条独立通道,通道上挂着合并规则。
- 重点讲第三条,因为它是最容易被略过、也最值钱的一条:**合并规则是声明在字段上的,不是写在节点里的**。这意味着新增节点时不需要考虑「我该怎么和别人的写入合并」,字段自己知道;也意味着并行写同一个字段时行为是确定的,而不是取决于谁先返回。
- 配两个具体的坑,证明你真跑过:一是在节点里原地修改状态(比如直接往数组里 push)会绕过合并规则,单线程时察觉不到,并行时变成偶发覆盖;二是加了节点没连边不会报错,表现只是那个节点永远不执行,只能靠逐节点追踪发现。
- 可以预期的追问:那你为什么不直接自己写?答:三要素本身很轻,核心逻辑几十行就能手写出来——框架真正值钱的是检查点与恢复、并行执行、以及每一步的可观测,这三样自己写的成本远高于三要素本身。顺带说明 Java 和 Swift 没有官方 LangGraph,真要在这两门语言里做,就是把这三要素手写一遍。
How to reason about it · think before answering
- The hinge is the second half. Defining the three concepts only proves you read the docs; explaining what hurts without a framework proves you know what it buys you. The general move for this family of questions is: describe your hand-rolled version first, then name what the framework collapsed.
- Hand-rolled version: a loop, a chain of conditionals picking the next step, and one big object carrying data between steps. By the third branch you hit three walls — when two steps write the same field, is it overwrite or append, and you hand-write that merge in every branch; intermediate state lives in local variables so debugging means print statements; a crash restarts from zero and the model calls you already paid for are wasted.
- Then map them: a node is an ordinary function that reads the whole state and returns a delta containing only what it changed; edges connect nodes, unconditional ones fix the order and conditional ones decide at runtime; state is a table of fields where each field is its own channel carrying a merge rule.
- Dwell on the third, which is the most skipped and most valuable point: the merge rule is declared on the field, not written inside the node. Adding a node therefore requires no thought about how to combine with other writers, and parallel writes to one field behave deterministically instead of depending on who returns first.
- Add two concrete traps to show you have actually run this: mutating state in place inside a node bypasses the merge rule — invisible single-threaded, an intermittent overwrite once things run in parallel; and adding a node without wiring an edge raises no error at all, it simply never executes, which only per-node tracing reveals.
- Expect: so why not just write it yourself? Because the three primitives are genuinely light — a few dozen lines. What the framework actually sells is checkpointing and recovery, parallel execution, and per-step observability, all of which cost far more to build than the primitives. Mention too that there is no official LangGraph for Java or Swift, so in those languages you do hand-roll exactly these three.
答题要点
- 节点是普通函数:读全量状态,返回只含改动字段的增量,不在节点里原地改状态
- 边决定执行顺序:无条件边写死,条件边在运行时决定下一步去哪(Supervisor 就靠它)
- 状态是一张字段表,每个字段一条通道,通道上挂合并规则——规则声明在字段上而不是写在节点里
- 不用框架会撞三堵墙:合并逻辑在每个分支手写一遍、中间过程只能靠打印、进程挂了从头重来
- 两个真实的坑:原地改状态绕过合并规则(并行时偶发覆盖)、加了节点没连边不报错只是永不执行
- 框架真正值钱的不是这三要素,而是检查点恢复、并行执行和逐步可观测
Key points
- A node is a plain function: read the full state, return a delta of changed fields only, never mutate in place
- Edges set execution order: unconditional edges are fixed, conditional edges decide the next hop at runtime — that is what a supervisor uses
- State is a table of fields, each field a channel carrying a merge rule declared on the field rather than inside nodes
- Without a framework you hit three walls: hand-written merges in every branch, no visibility into intermediate steps, and full restart after a crash
- Two real traps: in-place mutation bypasses the merge rule and causes intermittent overwrites under parallelism; an unwired node raises no error, it just never runs
- What the framework really sells is checkpoint recovery, parallel execution and per-step observability — not the three primitives themselves
从单 Agent 升级到多 Agent,通常是被什么信号触发的?升级之后系统会多付出什么?What signals typically trigger the move from a single agent to a multi-agent system, and what does the upgrade cost you?
国内高频海外高频进阶#multi-agent#cost#architecture分析过程 · 先想清楚再作答
- 这题考的是「你是被业务逼着拆的,还是照着博客拆的」。答「业务变复杂了」等于没答,面试官要的是**可观测的信号**:什么现象出现时你才动手。
- 给五个按出现顺序排的信号:一是提示词开始互相打架(加一条规则,另一个指标就掉);二是工具列表长到自己都要查文档;三是某一步的失败需要单独处理,不该整轮重来;四是想给某一步单独换模型;五是评估颗粒度不够,只能整体打分好或不好。
- 第四个信号要展开讲,它是唯一一个反常识的:多 Agent 通常更贵,但按步换模型是它唯一能省钱的场景——分诊这种短判断走便宜的小模型,拟方案走大模型。单 Agent 做不到按步换模型。这一条在面试里是明显的亮点。
- 然后主动给代价,不给代价的回答会被当成布道:延迟按步数乘倍数(原来两秒变六秒,而用户耐心大约三秒);成本按调用次数线性涨,因为每一步都要把当前状态重新塞进上下文,典型是三倍;调试难度按状态维度涨,出错要同时回答路由对不对、每个子 Agent 拿到的状态对不对、合并有没有互相覆盖。
- 再补一句反向判断,证明你不是无脑拆:工具太多的第一反应应该是合并工具、收敛描述,拆 Agent 是第二反应;质量差的第一反应应该是把单 Agent 版本调到最好,那个版本还会成为多 Agent 的对照基线。
- 可以预期的追问:拆完怎么证明比原来好?答:留住单 Agent 版本当基线,用同一批标准样本集跑 A/B,比准确率也比每次对话的成本与延迟。说不出对照基线的人,通常也说不清自己为什么拆。
How to reason about it · think before answering
- This question tests whether business pain forced the split or a blog post did. Answering the business got complex is a non-answer; the interviewer wants observable signals — what symptom made you act.
- Give five, in the order they usually appear: prompts start fighting each other (add one rule, another metric drops); the tool list grows until you need the docs yourself; one step's failure needs isolated handling instead of redoing the whole turn; you want a different model for one specific step; and evaluation granularity is too coarse to say more than good or bad.
- Expand on the fourth, the counter-intuitive one: multi-agent is usually more expensive, but per-step model selection is the one case where it saves money — a short triage decision on a cheap small model, a drafting step on a larger one. A single agent cannot swap models per step. This lands well in interviews.
- Then volunteer the costs, or the answer reads as evangelism: latency multiplies by step count (two seconds becomes six, while user patience is about three); cost grows linearly with calls because every step re-sends the current state as context, typically three times; and debugging cost grows with state dimensions, since a failure now requires checking routing, each sub-agent's input state, and whether merges overwrote each other.
- Add the reverse check to show you are not splitting reflexively: too many tools should first prompt consolidation and tighter descriptions, with splitting as the second response; poor quality should first prompt tuning the single-agent version to its best, which then becomes the baseline the multi-agent version is measured against.
- Expect: how do you prove the split helped? Keep the single-agent version as a baseline and A/B both against the same golden set, comparing accuracy alongside per-conversation cost and latency. People who cannot name a baseline usually cannot explain why they split either.
答题要点
- 五个可观测信号:提示词互相打架、工具多到要查文档、某一步需要独立重试、想按步换模型、评估颗粒度不够
- 按步换模型是多 Agent 唯一能省钱的场景:短判断走小模型、拟方案走大模型,单 Agent 做不到
- 代价一:延迟按步数乘倍数,两秒变六秒,而用户对客服机器人的耐心大约三秒
- 代价二:成本线性涨,每一步都要把状态重新塞进上下文,典型是原来的三倍
- 代价三:调试难度按状态维度涨,所以多 Agent 和链路追踪必须一起上
- 反向判断:工具多先合并再拆分,质量差先把单 Agent 调到最好——那个版本还是多 Agent 的对照基线
Key points
- Five observable signals: prompts fighting each other, a tool list you must look up, one step needing isolated retries, wanting a different model per step, and evaluation too coarse to act on
- Per-step model selection is the only case where multi-agent saves money: small model for triage, larger model for drafting — impossible in a single agent
- Cost one: latency multiplies with step count, two seconds becomes six, while patience for a support bot is about three
- Cost two: spend grows linearly with calls since every step re-sends state as context, typically three times the original
- Cost three: debugging cost grows with state dimensions, so multi-agent and tracing have to ship together
- Reverse check: consolidate tools before splitting, and tune the single agent to its best first — that version becomes your baseline
D16 Supervisor 动态路由:structured output 路由、override、routingReason
为什么要让模型输出 structured output 而不是自然语言来做路由?自然语言到底差在哪?Why use structured output rather than natural language for routing? What exactly goes wrong with free text?
国内高频海外高频进阶#structured-output#routing#reliability分析过程 · 先想清楚再作答
- 这题最容易答成「结构化更规范、更好解析」——这是形容词,不是理由。面试官想听的是一个具体的失败场景,最好是你真的调过的那种。
- 把最锋利的一刀先亮出来:**自然语言路由的失败是静默的**。模型回「我觉得这个可以让查订单的同事看一下」,它其实判对了,但说的是人话不是 id,正则匹配不上就落进兜底,日志里只留下一个 smalltalk。模型是对的、解析是错的,而它和「模型判错了」在日志里长得一模一样。你会去调提示词,调两天才发现问题在那三行正则。
- 然后给三个漏洞,一条一条对上结构化输出解决了什么:输出会漂移(今天回「订单查询」明天回「查订单」,正则永远追不上,模型小版本升级你就掉准确率);没有置信度(自然语言里没有「我有多大把握」这个信息,你没法区分它很确定还是在猜);拼错或自造的路由名要到运行时才炸(枚举是一道编译期就存在的闸门)。
- 接着说清机制,别停在「用 zod 更规范」:把 schema 发进请求(response_format 里的 json_schema),模型的解码过程被枚举约束;回来之后**用同一份声明再校验一遍**。一份声明两用,请求与校验不会漂移。
- 关键的反直觉点,答到这里就拉开差距了:**结构化输出不等于不用校验**。不是所有网关、所有模型都严格执行 schema,降级到备用模型时更说不准。所以解析函数的返回类型应该是「一段待校验的东西」,而不是「已经是 RouteDecision」。
- 可以预期的追问:那不支持 json_schema 的模型怎么办?答案是退回「few-shot 加严格提示词加自己校验」,闸门仍然在你的枚举校验那一步——真正兜底的从来不是模型的自觉,是你的解析层。
How to reason about it · think before answering
- The trap is answering structured output is cleaner and easier to parse. Those are adjectives, not reasons. The interviewer wants a concrete failure you have actually debugged.
- Lead with the sharpest point: free-text routing fails silently. The model replies I think the order desk should look at this — it judged correctly, but it spoke prose, not an id. Your regex misses, you fall through to the default, and the log shows only smalltalk. A correct model with a broken parser looks exactly like a wrong model, so you spend two days tuning a prompt that was never the problem.
- Then list three holes and map each to what structured output fixes: wording drifts across versions so regexes never catch up; there is no confidence signal, so you cannot tell certainty from guessing; and an invented route name only explodes at runtime, whereas an enum is a gate that exists before the request is even sent.
- Explain the mechanism rather than stopping at zod is nicer: send the schema in the request (response_format with a json_schema), so decoding is constrained by the enum, then validate the response with the same declaration. One declaration used twice means request and validation cannot drift apart.
- The counterintuitive point that separates candidates: structured output does not remove the need to validate. Not every gateway or model enforces the schema strictly, and a fallback model may not at all. Your parse function should return something-to-be-validated, not an already-typed decision.
- Expect: what if the model does not support json_schema? Fall back to few-shot plus a strict prompt plus your own validation. The real gate was never the model's discipline; it is your parsing layer.
答题要点
- 自然语言路由的失败是静默的:模型判对了但说的是人话,正则匹配不上就落兜底,和判错在日志里完全一样
- 三个漏洞:措辞会漂移(正则追不上)、没有置信度(分不清确定与猜)、自造的路由名要到运行时才炸
- 机制是一份声明两用:schema 随请求发出去约束解码,回来后用同一份声明校验,请求与校验不会漂移
- 枚举是编译期就存在的闸门,把「拼错的路由名」从线上事故降级成一次解析失败
- 结构化输出不等于不用校验:网关和降级模型未必严格执行 schema,解析函数的返回类型应该是「待校验」而不是「已经是」
Key points
- Free-text routing fails silently: a correct judgement in prose misses your regex and falls through, looking identical to a wrong judgement in the logs
- Three holes: wording drifts, there is no confidence signal, and invented route names only fail at runtime
- One declaration used twice: the schema constrains decoding in the request and validates the response, so the two cannot drift
- An enum is a gate that exists before the call, turning a misspelled route from an incident into a parse failure
- Structured output does not remove validation — gateways and fallback models may not enforce the schema, so parsing must return an unvalidated value
routingReason 这类调试信息在生产系统里有什么价值?只是打日志而已吗?What is a field like routingReason actually worth in production? Is it just logging?
国内高频海外高频进阶#observability#routing#debugging分析过程 · 先想清楚再作答
- 这题看着像水题,其实在筛「有没有真的排查过线上问题」。答「方便调试」就结束的人,基本没值过班。
- 先给一条不可回避的事实:**路由决策是模型做的,而模型不可复现**。同一句话下次未必给同样的判断,你没法重跑一遍去看「当时是怎么想的」。所以理由必须在当时就写下来,否则那次判断永远丢了。这一条把 routingReason 从「日志」抬到了「唯一的审计证据」。
- 然后给三个具体用途,每个都要能落地:一是把「模型判错了」和「解析或兜底出错了」分开,前缀写成 fallback 加原因码,一眼就能分辨;二是攒下一版提示词的素材,把一周内落进兜底的请求按原因分组,会看到集中的几类意图缺描述;三是它是离线评估的输入——标准样本集要评的不只是最终回答,还有分诊准不准,而这件事只有当时记了判断和理由才评得了。
- 写法上有个细节值得主动说:**结构化的壳加自然语言的芯**。前缀(fallback 加原因、override 加目标)用来聚合统计,后面那句人话用来看具体这一单。整条都写成自然语言,就退回成本章批判的那种东西了。
- 再补一条容易被忽略的:兜底和人工改派都不要擦掉模型的原判,原样拼进理由里。否则一周后没人说得清这一单是模型判错了还是本来就被人改过——多写二十个字符,省掉一次翻遍代码的排查。
- 可以预期的追问:这些字段会不会带来隐私或成本问题?答案是会,所以理由里只写判断依据不写用户原文,长度设上限(比如 120 字),并且和链路追踪共用同一个 run 标识,别另起一套。
How to reason about it · think before answering
- This looks like a throwaway question but it screens for whether you have ever been on call. Anyone who stops at it helps with debugging has not.
- Start with the fact you cannot design around: the routing decision is made by a model, and models are not reproducible. The same sentence may be judged differently next time, so you cannot re-run to see what it was thinking. The reason must be captured at decision time or it is gone forever — that is what turns this field from a log line into the only audit evidence you have.
- Then give three concrete uses. One, it separates a wrong model judgement from a parsing or fallback problem, provided the prefix carries a cause code. Two, it is raw material for the next prompt revision: group a week of fallbacks by cause and the missing intent descriptions jump out. Three, it feeds offline evaluation — a golden set should score routing accuracy, not just the final answer, and that is only scorable if the decision and its reason were recorded.
- Mention the shape: a structured prefix wrapping a human sentence. The prefix (fallback plus cause, override plus target) is what you aggregate on; the sentence is what you read for one specific case. Making the whole field prose puts you right back in the failure mode this chapter argues against.
- Add the detail people skip: neither a fallback nor a human override should erase the model's original judgement — carry it into the reason. Otherwise nobody can later tell whether the model got it wrong or a human redirected it. Twenty extra characters save an afternoon of archaeology.
- Expect: do these fields create privacy or cost problems? Yes, so record the basis for the decision rather than the user's raw text, cap the length, and reuse the same run identifier as your tracing instead of inventing a parallel one.
答题要点
- 路由决策由模型做出且不可复现,理由必须在当时写下来,否则那次判断永远丢了——它是唯一的审计证据
- 用途一:把「模型判错」和「解析或兜底出错」分开,靠 fallback 加原因码一眼分辨
- 用途二:把一周内落进兜底的请求按原因分组,直接得到下一版分诊提示词该补什么
- 用途三:它是离线评估的输入,分诊准确率这个指标只有记了当时的判断与理由才评得了
- 写法是结构化的壳加自然语言的芯:前缀用于聚合统计,人话用于看具体这一单
- 兜底与人工改派都要保留模型原判;理由只写判断依据不写用户原文,长度设上限,并复用链路追踪的 run 标识
Key points
- The decision comes from a model and is not reproducible, so the reason must be captured at decision time — it is the only audit evidence you get
- Use one: it separates a wrong model judgement from a parsing or fallback failure, via a cause code in the prefix
- Use two: grouping a week of fallbacks by cause tells you exactly what the next routing prompt is missing
- Use three: it feeds offline evaluation, since routing accuracy can only be scored if the decision and reason were recorded
- Shape it as a structured prefix around a human sentence: aggregate on the prefix, read the sentence for one case
- Keep the model's original judgement through fallbacks and overrides; store the basis rather than raw user text, cap the length, and reuse the tracing run id
D17 Planner–Executor–Critic + 共享工作区:workspace state、toolBudget、并行 fan-out、review 回路
为什么要给每个子任务设 toolBudget 这样的预算?超了预算之后你会怎么处理?Why give each subtask a tool-call budget, and what do you do when it runs out?
国内高频海外高频进阶#cost-control#reliability#agent-design分析过程 · 先想清楚再作答
- 这题的题眼在后半句。前半句几乎人人会答「防止成本失控」,真正拉开差距的是超限之后的动作——答「抛异常」的人基本没做过面向用户的 Agent。
- 先把「为什么」说具体。子任务卡住的典型形态不是报错,而是反复查、反复不满意、再查——模型不会喊累,它会把额度花光为止。整轮对话的成本封顶是外层的闸,子任务预算是内层的闸;粒度细到单件事的好处是超支时你能精确指出是哪一件失控了,而不是只看到这次对话贵了。
- 再点一个容易被忽略的设计点:预算必须是子任务级的,不是单次执行级的。有评审回路时,被打回重做也得计费,否则打回两次实际额度就翻三倍,这道闸等于没设。
- 结论是超限的处理:降级返回已有结果并打上标记,不抛错。理由要说透——抛错等于把「这件事只做了一半」升级成「整个请求失败」,用户等了几秒最后看到一句服务异常,可他其实只是没拿到三件事里的一件。给出两件半的答案并说明哪半件没做成,永远比一个错误页有用。
- 降级标记本身也要说:它让降级变成可观测、可统计的事实,而不是日志里的一句话。上层据此决定要不要转人工,监控据此画降级率——两个平均分一样的系统,降级率百分之三十和百分之三完全不是一回事。
- 可以预期的追问:预算该设多少?答案是从「这件事正常需要几次工具调用」反推再留一点余量,不是拍脑袋取整数;同时要有第二个维度的闸(挂钟时间或 token 数),因为一次超长的工具调用同样能拖垮请求,而它只算一次。
How to reason about it · think before answering
- The hinge is the second half. Everyone can say it controls cost; what separates people is what happens when the budget runs out. Answering throw an exception usually means you have never shipped a user-facing agent.
- Make the why concrete: a stuck subtask rarely errors — it queries, dislikes the result, and queries again. The model never gets tired; it will spend whatever you allow. A per-conversation cap is the outer gate, a per-subtask budget is the inner one, and the finer grain tells you which piece went out of control instead of only that the conversation was expensive.
- Add the design point people miss: the budget must be per subtask, not per execution. With a review loop, retries have to draw on the same budget, or two rejections triple the real allowance and the gate is meaningless.
- The conclusion is the exhaustion path: degrade — return what you already have with a flag — rather than throw. Explain why: throwing upgrades this piece is half done into the whole request failed. The user waited several seconds and gets an error page, when in reality only one of three pieces is missing. Two and a half answers plus a clear note beats an error page every time.
- Say something about the flag too: it turns degradation into an observable, countable fact instead of a log line. The layer above decides whether to escalate to a human, and monitoring plots a degradation rate — two systems with the same average score but 30 percent versus 3 percent degradation are not the same system.
- Expect: how big should the budget be? Derive it from how many tool calls the task normally needs plus margin, not a round number pulled from the air. And pair it with a second dimension — wall-clock or tokens — because one very slow tool call can ruin a request while counting as a single call.
答题要点
- 子任务卡住的典型形态是反复查而不是报错,模型会把额度花光为止;整轮封顶是外层闸,子任务预算是内层闸,细粒度让你能定位到是哪一件失控
- 预算必须是子任务级而不是单次执行级,否则被评审打回两次实际额度就翻三倍
- 超限必须降级返回已有结果并标记,不能抛错——抛错把「做了一半」升级成「整个请求失败」,用户连已经查到的部分都拿不到
- 降级标记让降级率变成可统计指标,上层据此决定转人工,评估据此区分两个平均分相同的系统
- 预算大小从这件事正常需要几次工具调用反推并留余量,同时配一个时间或 token 维度的闸
Key points
- A stuck subtask loops rather than errors, and the model will spend whatever you allow; a conversation cap is the outer gate, a subtask budget the inner one that localises the blowup
- The budget must be per subtask, not per execution, or two review rejections triple the real allowance
- On exhaustion, degrade and flag rather than throw — throwing upgrades half done into whole request failed and discards what was already retrieved
- The degradation flag makes the degradation rate a real metric for escalation and evaluation
- Size the budget from the task's normal tool-call count plus margin, and pair it with a wall-clock or token gate
Critic 的评审回路怎么防止陷入死循环?除了次数上限还有什么要防的?How do you keep a Critic review loop from spinning forever, and what else needs guarding besides a retry cap?
国内高频海外高频进阶#reflection#loop-guard#reliability分析过程 · 先想清楚再作答
- 问「除了次数上限还有什么」,说明面试官已经预设你会答上限,真正在考的是你有没有真的跑过这条回路。只答上限的人拿基础分,能说出另外两种失效方式的才算过。
- 第一种就是无限打回:每改一版评审者挑一个新毛病,永远收敛不了。上限的作用不是省钱,是**保证流程一定会结束**。本课取最多打回 2 次、共 3 次执行,这个量级的取法是「一次有效的修改通常在第二次就完成,第三次还不行说明判据本身有问题」。
- 第二种是打回不说人话:评审者只回一句「不合格」,执行者拿不到可执行信息,第二稿原样再交一遍,于是必然打满上限、白烧三倍的钱。所以打回必须带具体理由,而且理由要回写进子任务的目标里带给执行者——「缺了退款结论,请补上」才是可执行的,「质量不佳」不是。
- 第三种最危险也最少被提到:评审者和执行者用同一个模型、同一套提示词时,它倾向于认可自己的输出。同一个模型对「什么算好答案」的偏好是一致的,让它复核自己刚写的东西,通过率会高得离谱,这道工序等于没有。缓解手段按性价比排:给评审者一份可核对的客观验收要求;换一个不同的模型来评审,哪怕更便宜;把评审做成逐条打分而不是一句结论。
- 还要点一句框架的兜底与业务上限的区别:编排框架通常自带一个递归步数上限,但那是最后一道保险丝,不能当业务上限用——它是全图的,你不知道是哪条回路失控;而且它触发时抛异常,你连已有结果都拿不到,正好违背「降级返回」的原则。
- 可以预期的追问:上限用完了返回什么?答:返回已有结果并标记降级,同时把最后一次的评审意见一起带出去,让上层能判断要不要转人工——这条回路的价值不只是修好,还包括「修不好时说清楚差在哪」。
How to reason about it · think before answering
- Asking what else besides a cap tells you the interviewer already expects the cap. What is really being tested is whether you have run this loop for real. The cap earns baseline credit; naming the other two failure modes is what passes.
- Failure one is infinite rejection: every revision draws a new complaint and nothing converges. The cap exists to guarantee termination, not to save money. Two rejections and three executions is a reasonable default, because an effective fix usually lands on the second attempt — if the third still fails, the rubric itself is the problem.
- Failure two is a rejection with no actionable content. If the reviewer only says not good enough, the executor has nothing to act on and resubmits the same thing, burning the full cap. Rejections must carry a specific reason, and that reason must be written back into the subtask goal. Missing the refund conclusion, please add it is actionable; poor quality is not.
- Failure three is the dangerous one people rarely mention: when reviewer and executor share a model and a prompt, the reviewer tends to approve its own output. A single model has consistent preferences about what a good answer looks like, so pass rates go implausibly high and the review step becomes theatre. Mitigations by value: give the reviewer an objective, checkable rubric; use a different model even a cheaper one; score item by item rather than emitting one verdict.
- Also distinguish the framework's safety net from your business cap: orchestration frameworks usually ship a recursion limit, but that is a last-resort fuse — it is graph-wide so you cannot tell which loop ran away, and it throws, which means you lose the partial results you were supposed to degrade to.
- Expect: what do you return once the cap is used up? Return what you have, flag it as degraded, and carry the last review comment out with it so the layer above can decide whether to escalate. The loop's value is not only fixing things — it is stating precisely what could not be fixed.
答题要点
- 次数上限的作用是保证流程一定会结束,不是省钱;本课取最多打回 2 次、共 3 次执行
- 打回必须带具体、可执行的理由并回写进子任务目标,只说「不合格」会让执行者原样重交、必然打满上限
- 最危险的是评审者与执行者同模型同提示词,它倾向于认可自己的输出,通过率虚高、这道工序等于没有
- 缓解手段:给客观可核对的验收要求、换一个模型来评审、逐条打分而不是一句结论
- 框架自带的递归上限只是保险丝,不能当业务上限:它是全图的、触发时抛异常,连已有结果都拿不到
- 上限用完要返回已有结果加降级标记,并把最后一次评审意见带出去,供上层决定是否转人工
Key points
- A retry cap exists to guarantee termination, not to save money — two rejections, three executions total
- Rejections must carry specific, actionable reasons written back into the subtask goal; not good enough guarantees an identical resubmission and a maxed-out cap
- The most dangerous failure is a reviewer sharing model and prompt with the executor: it approves its own output, pass rates inflate, and the step becomes theatre
- Mitigate with an objective checkable rubric, a different model for review, and item-by-item scoring instead of a single verdict
- The framework's recursion limit is a fuse, not a business cap: it is graph-wide and it throws, so you lose the partial results you meant to degrade to
- When the cap is spent, return what you have with a degraded flag plus the last review comment so the layer above can escalate
D18 历史保真与摘要、多模态占位、checkpointer 持久化
长对话做摘要时,怎么保证关键信息不丢?在多 Agent 场景下这件事有什么特别的?When summarizing a long conversation, how do you keep the critical information from being lost — and what is different about this in a multi-agent system?
国内高频海外高频进阶#context-compression#multi-agent#reliability分析过程 · 先想清楚再作答
- 题眼在后半句。只答「保留用户约束、保留最近几轮」是单 Agent 的标准答案,能过但不出彩;面试官问「多 Agent 有什么特别的」,是在看你有没有真的在协作图里踩过这个坑。
- 先把单 Agent 那半答扎实:触发用阈值不用定时器,本课口径是消息超过 20 条或估算超过 8000 token(一个字符算一个 token,故意高估,低估会让阈值永远触发不了);保留最近 6 条原文不动;切口必须对齐到一轮的开头,切在工具调用与工具结果之间会让下一次请求出现悬空消息,多数厂商直接返回 400。
- 然后给出多 Agent 那半的关键差别:单 Agent 里摘要丢的是**细节**,多 Agent 里摘要丢的是**判据**。评审者判一份产出合不合格,靠的是分清「这句是待验收的产出、那句是验收要求」;一段把发言人抹平的流水摘要读起来通顺,但评审拿它做不了任何判断。
- 所以多 Agent 的摘要有一条额外硬要求:每条被压掉的消息,在摘要里都要留下「第几条 + 谁说的」这两个坐标。实现只有一行——把转录写成带序号和角色前缀的形式再交给模型。
- 再补一条边界,这条最能显出你写过:**摘要只对自然语言历史动手,不碰任何结构化字段**。把共享工作区压成一句话,「按 id 找到某条子任务、比对验收要求」就整个失效了,结构化数据压成自然语言就再也回不去。附件字段更是碰不得——它存的是引用不是内容,摘要掉等于把那个对象变成孤儿。
- 可以预期的追问:摘要用哪个模型、失败了怎么办?答可以用更便宜的小模型(它只做归纳不做推理),失败时的正确行为是**跳过这一轮压缩继续跑**并告警,而不是让整次执行失败——阈值定在七八成就是为了留出这次抢救余量。
How to reason about it · think before answering
- The hinge is the second half. Answering only keep user constraints and the last few turns is the standard single-agent answer — passable, not memorable. Asking what is different in multi-agent is asking whether you have actually hit this in a collaboration graph.
- Get the single-agent half solid first: trigger on thresholds, never a timer. This course uses more than 20 messages or an estimated 8000 tokens, counting one character as one token — deliberately high, because underestimating means the threshold never fires. Keep the last 6 messages verbatim. Align the cut to a turn boundary: cutting between a tool call and its result produces a dangling message and most vendors return 400.
- Then name the real difference: in a single agent a summary loses detail; in a multi-agent graph a summary loses the criteria. A critic decides whether output passes by telling apart what is being reviewed from what the requirement was. A smooth narrative summary that flattens speakers reads fine and is useless to the critic.
- So multi-agent summarization has one extra hard requirement: every compressed message must leave behind two coordinates — its index and its speaker. The implementation is one line: build the transcript with numbered, role-prefixed entries before handing it to the model.
- Add the boundary that shows you have shipped this: summarize natural-language history only, never structured fields. Compressing the shared workspace into a sentence kills every lookup by task id and every comparison against an acceptance requirement, and structured data does not come back. Attachments are even more off-limits — they hold a reference, not content, so summarizing one orphans the underlying object.
- Expect the follow-up: which model writes the summary, and what if it fails? A cheaper small model is fine since the job is condensation, not reasoning. On failure the correct behaviour is to skip this round of compression, keep running, and alert — not to fail the whole execution. Setting the threshold at seventy or eighty percent exists precisely to leave that rescue room.
答题要点
- 触发用阈值不用定时器:超过 20 条或估算超过 8000 token,token 按一字符一 token 保守高估
- 保留最近 6 条原文不动,切口必须对齐到一轮开头,否则会出现有调用没结果的悬空消息、请求直接 400
- 多 Agent 的差别:摘要丢的不是细节而是判据,评审者靠「谁在第几步说的」区分产出与验收要求
- 所以每条被压掉的消息都要在摘要里留下条号与发言人,实现就是把转录写成带序号和角色的形式
- 只压自然语言历史,不碰共享工作区这类结构化字段,更不能碰存引用的附件字段
- 摘要可用更便宜的小模型;摘要调用失败时跳过这一轮压缩并告警,不要让整次执行失败
Key points
- Trigger on thresholds, not timers: more than 20 messages or an estimated 8000 tokens, counting one character as one token to stay conservative
- Keep the last 6 messages verbatim and align the cut to a turn boundary, or you ship a dangling tool call and the request 400s
- The multi-agent difference: a summary loses criteria, not just detail — the critic needs to know who said what and at which step
- So every compressed message keeps its index and speaker in the summary; the implementation is a numbered, role-prefixed transcript
- Summarize natural-language history only — never the shared workspace or other structured fields, and never the attachment references
- A cheaper small model is fine for summarizing; if the call fails, skip compression for this round and alert rather than failing the run
checkpointer 在多 Agent 系统里解决了什么问题?它的代价是什么?What problem does a checkpointer solve in a multi-agent system, and what does it cost?
国内高频海外高频进阶#checkpointing#cost#operations分析过程 · 先想清楚再作答
- 这题的下半句才是考点。只答「能恢复、能容错」是功能介绍,任何文档都写着;面试官想听的是你有没有算过这笔账,以及知不知道它会在哪里疼。
- 先把价值说具体,用钱和时间说:一次带评审回路的多 Agent 执行要调九次模型,跑到第七次进程被换版本重启,没有检查点就是九次全废、用户界面还停在转圈。有检查点则从上一个签字点接着跑,已经跑完的节点一次都不重跑——它买的是「失败的粒度从一整次执行降到一个节点」。
- 顺带说清它解锁的另外三件事,这三样单靠重试做不到:**人工审批闸口**(在某个节点前停下等人点确认,状态就停在那儿)、**时间旅行调试**(回到出问题那一步之前看状态长什么样)、**分叉对比**(从同一个检查点跑两种走法,比较结果,这也是评估的基础设施)。
- 然后是代价,三笔要说全。第一笔,**状态越大写得越慢**,而且是每一步都写一份——一次请求写六个检查点,状态里多一个字节就要多写六遍。所以附件存引用不存内容,检索结果存文档 id 不存全文。
- 第二笔,**存储本身有上限**。用 jsonb 存的话,硬上限很远,但单行超过大约两 KB 就会被挪到外存、每次读写多一次 IO,所以真正的工程线是「别让单个检查点变成几百 KB」,而不是那个理论上限。
- 第三笔也是最容易被忽略的:**版本兼容**。检查点是长期存活的数据,你每改一次状态形状就欠下一笔迁移债,而缺字段读出来通常不报错,只是静默给出 undefined 或 NaN。这一条决定了状态字段要尽早占好位子——图状态的形状一旦被持久化,改字段就不是改代码,是数据迁移。
- 可以预期的追问:那检查点要不要清理?答要,按会话线设保留期与归档策略,否则这张表会随日活线性膨胀;另外要留意它是敏感数据——图状态里有完整对话,删除用户数据时这张表必须一起处理。
How to reason about it · think before answering
- The second half is the real question. Answering it enables recovery and fault tolerance is a feature blurb any doc carries. The interviewer wants to know whether you have done the arithmetic and where it hurts.
- Make the value concrete in money and time: one multi-agent run with a review loop costs nine model calls. If the process is restarted for a deploy at call seven, without checkpoints all nine are wasted and the user is still watching a spinner. With them the run continues from the last signed-off point and no completed node re-runs. What you bought is a smaller unit of failure — a node instead of a whole run.
- Mention the three things it unlocks that retries alone cannot: human approval gates (pause before a node and the state simply waits), time-travel debugging (go back to just before the bad step and inspect state), and forking for comparison (run two variants from one checkpoint) — which is also the infrastructure evaluation is built on.
- Then the costs, all three. First, bigger state means slower writes, and it is written at every step: one request produces six checkpoints, so a byte added to state is six bytes written. Hence attachments hold references, not content, and retrieval results hold document ids, not full text.
- Second, the store has limits. With jsonb the hard cap is far away, but a row past roughly two kilobytes gets pushed to out-of-line storage and costs an extra IO on every read and write. The real engineering line is do not let a single checkpoint reach hundreds of kilobytes, not the theoretical cap.
- Third, the one people forget: version compatibility. Checkpoints are long-lived data, so every change to the state shape incurs migration debt, and missing fields usually do not throw — they silently yield undefined or NaN. This is why state fields should be reserved early: once a shape is persisted, changing a field is a data migration, not a code edit.
- Expect the follow-up: do checkpoints need cleanup? Yes — retention and archival per thread, or the table grows linearly with active users. Also treat it as sensitive data: graph state contains full conversations, so it must be included whenever you delete a user's data.
答题要点
- 核心价值:把失败的粒度从「一整次执行」降到「一个节点」,九次模型调用的执行不会因为一次重启全废
- 还解锁三件重试做不到的事:人工审批闸口、时间旅行调试、从同一个检查点分叉对比(也是评估的基础设施)
- 代价一,状态越大写得越慢,而且每一步都写一份——所以附件存引用、检索结果存文档 id
- 代价二,存储有上限:jsonb 单行超过约两 KB 就外存、多一次 IO,工程线是别让单个检查点到几百 KB
- 代价三,版本兼容:状态形状改一次就欠一笔迁移债,缺字段静默给出 undefined 或 NaN;所以字段要尽早占位
- 运维上还要有保留期与归档,并把它当敏感数据处理——图状态里有完整对话,删用户数据时必须一起删
Key points
- Core value: it shrinks the unit of failure from a whole run to a single node, so a nine-call run is not wasted by one restart
- It also unlocks three things retries cannot: human approval gates, time-travel debugging, and forking from one checkpoint to compare variants — the substrate evaluation is built on
- Cost one: bigger state writes slower, and it is written at every step — hence references for attachments and document ids for retrieval results
- Cost two: storage limits — a jsonb row past roughly two kilobytes goes out-of-line and costs an extra IO, so the practical line is keeping a checkpoint well under hundreds of kilobytes
- Cost three: version compatibility — every change to the state shape is migration debt, and missing fields silently yield undefined or NaN, which is why fields should be reserved early
- Operationally you need retention and archival, and you must treat it as sensitive data: graph state holds full conversations and must be purged with the user's data
D19 跨服务 Agent 集成:用户级 JWT 铸造、JWKS 验签、inject/memory/usage 三类接口、幂等 externalId
两个服务之间调用,你会用服务级令牌还是用户级令牌?分别适用于什么场景?For service-to-service calls, would you use a service token or a user token? When does each apply?
国内高频海外高频进阶#auth#security#api-design分析过程 · 先想清楚再作答
- 题眼在「分别」。答「用户级更安全」就把一道设计题做成了口号题——面试官想看你能不能说出两者各自成立的条件,以及选错的具体代价。
- 先给判断依据,一句话就能拆开:这次调用**有没有一个具体的用户在背后**。有,就必须是用户级;没有(拉配置、上报指标、跑对账批处理),服务级才是对的,硬塞一个用户 id 进去反而是伪造审计记录。
- 然后把用户级的三条理由说成代价而不是优点:服务级令牌泄露一次等于全量用户数据泄露,用户级泄露一张只丢一个用户且十五分钟自动作废;服务级在审计日志里只能查到「某服务调了一次」,查不到替谁操作;下游做用户级权限判断时,服务级令牌逼着它去信请求体里的 userId,而那是调用方可以随便写的。
- 补一句生产视角:两者不是二选一,真实系统里常常是「服务级令牌用来换用户级令牌」——调用方先用自己的服务凭证证明自己是谁,再申请一张代表某个用户的短期令牌。这样服务凭证只出现在铸造这一步,不出现在每一次业务调用里。
- 可以预期的追问一:令牌泄露了怎么办?答案要分两层——短有效期(本课 15 分钟)是止损的主力,撤销列表按 jti 拉黑是补充;不要上来就说「用黑名单」,那等于给每次验签加一次数据库查询,把无状态验签的好处全赔进去了。
- 可以预期的追问二:那 scope 该切多细?给一条可操作的判据——按「读写不对称的风险」切,读错了泄露信息、写错了污染数据且会持续影响后续每一轮对话,所以 read 和 write 必须分开;再细就要看有没有真实的调用方只需要其中一半。
How to reason about it · think before answering
- The hinge is each. Answering user tokens are safer turns a design question into a slogan — the interviewer wants the conditions under which each one is correct, and the concrete cost of choosing wrong.
- Give the deciding question first: is there a specific user behind this call? If yes, it must be a user token. If not — fetching config, reporting metrics, running a reconciliation batch — a service token is the right answer, and stuffing in a user id would fabricate audit history.
- Then state the three reasons as costs, not virtues. A leaked service token means every user's data at once; a leaked user token means one user, and it expires in fifteen minutes. Audit logs with a service token only show that some service called, never on whose behalf. And a downstream service doing per-user authorization is forced to trust a userId in the request body, which the caller writes freely.
- Add the production view: it is rarely either-or. Real systems use the service credential to obtain user tokens — the caller proves who it is once, then mints a short-lived token representing one user. The service credential then appears only at the minting step, never on every business call.
- Expect: what if a token leaks? Answer in two layers — a short lifetime (fifteen minutes here) does most of the containment, and a jti denylist is the supplement. Do not lead with a denylist: it puts a database lookup in front of every verification and gives away the whole point of stateless verification.
- Expect: how fine-grained should scopes be? Offer a usable rule — split along asymmetric risk. A bad read leaks information; a bad write poisons data that keeps influencing every later turn. So read and write always split; finer than that only if a real caller genuinely needs just one half.
答题要点
- 判断依据是「这次调用背后有没有一个具体用户」:有就用用户级,没有(配置、指标、对账批处理)才用服务级
- 服务级令牌泄露的爆炸半径是全量用户,用户级只影响一个用户且短期自动失效
- 审计要能落到人:只有 sub 字段能回答「当时是替谁操作的」
- 下游要做用户级权限判断时,服务级令牌逼着它去信请求体里的 userId,而那是调用方可以伪造的
- 生产里常见组合:服务凭证只用来换取代表某个用户的短期令牌,不出现在每次业务调用里
- 泄露后的止损顺序是短有效期优先、jti 撤销列表补充,别一上来就上黑名单换掉无状态验签
Key points
- The deciding question is whether a specific user stands behind the call: yes means user token, no (config, metrics, reconciliation) means service token
- A leaked service token exposes every user; a leaked user token exposes one and expires on its own
- Auditing has to reach a person — only the sub claim answers who the call was made on behalf of
- With a service token the downstream must trust a userId in the request body, which the caller can forge
- Common production shape: the service credential only buys short-lived per-user tokens and never appears on business calls
- After a leak, short lifetimes do the containment and a jti denylist supplements it — do not trade away stateless verification by default
跨服务调用的幂等键该怎么设计?由谁生成、存在哪、重复了返回什么?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
D20 定时 Job 与主动关怀:时区、quiet hours、每日上限、通知 provider 抽象
quiet hours 和每日发送上限这两条规则你会怎么实现?它们应该放在链路的哪一步判断?How would you implement quiet hours and a per-user daily cap, and where in the pipeline should they be evaluated?
国内高频海外高频进阶#rate-limiting#quiet-hours#cost-control分析过程 · 先想清楚再作答
- 这题有两个题眼,很多人只答了前一个。第一个是「怎么判断」(细节题),第二个是「放在哪一步」(架构题),后者才是拿分点。
- 先讲 quiet hours 的判断。把时刻折成从午夜起算的分钟数之后,绝大多数人第一次都会写成 start 小于等于 now 且 now 小于 end。这对午休那种同日区间是对的,对 22:00 到 08:00 恒为 false——start 是 1320、end 是 480,条件永远不成立,于是半夜照发。正确写法是 start 小于 end 时用「且」,start 大于 end(跨午夜)时换成「或」。这个 bug 恶劣在只在跨午夜的配置上错,用 13:00 到 14:00 写的单元测试全绿。
- 接着是命中之后怎么办:推迟,不是丢弃。判据不该由发送方临时决定,而应该由消息自己带一个过期时刻——过期时刻早于窗口结束的丢弃,其余一律推迟到窗口结束。限时取消提醒过了今晚就没意义,账单提醒明早发一样有效。另外要提一句惊群:所有推迟的消息会算出同一个到期时刻,要加一个按用户标识哈希得出的抖动(不能用随机数,否则线上复现不了)。
- 再讲每日上限的两个细节。一是「一天」必须是**用户本地日历日**,写成 UTC 日的话东八区用户早上八点前发的会算进昨天的额度。二是必须先占坑再判断——原子自增拿返回值比上限,超了再把名额还回去;先查后写在并发下两条候选会同时读到同一个值然后一起发出去。渠道明确拒绝时也要把名额还回去。
- 最后是架构题那一半,也是最值钱的一段:三道闸必须在**生成内容之前**判断,不是在发送那一步。顺序错了程序照样跑通、发出的消息也一样,唯一区别是每条被拦下的消息你都已经付过一次模型调用的钱。按 1000 用户每天各 3 条候选、拦掉四成、单条约 0.00075 美元算,一个月白花约 27 美元,比这批用户的正常对话开销还高,而且监控上完全看不出来——只有把候选数和实际发送数并排摆出来才看得见差额。
- 可以预期的追问:三道闸内部谁先谁后?答时区、安静时段、每日上限,上限必须最后。因为被安静时段推迟的消息明早才发,不该占掉今天的名额;顺序反了用户会发现自己明明没收到几条却被限流了。
How to reason about it · think before answering
- There are two hinges here and most candidates only answer the first. One is how to evaluate the rules (a details question), the other is where in the pipeline (an architecture question) — the second is where the points are.
- Start with quiet hours. Once you fold times into minutes-from-midnight, almost everyone first writes start less-or-equal now and now less-than end. That is correct for a same-day window like a lunch break, but for 22:00 to 08:00 it is always false: start is 1320, end is 480, the condition never holds, and you push at 3am. The fix is to use and when start is before end, and or when start is after end. What makes this bug nasty is that it only misfires on the cross-midnight config, so a unit test written around 13:00 to 14:00 passes.
- Then what to do on a hit: defer, do not drop. The decision should not be the sender's mood — attach an expiry to each candidate and drop only when it expires before the window ends, deferring everything else to the window's end. A thirty-minute cancellation warning is worthless tomorrow; a billing summary is just as valid at 8am. Mention the thundering herd too: every deferred message resolves to the same due instant, so add jitter derived from a hash of the user id, never a random number, or you cannot reproduce incidents.
- Now two details on the daily cap. First, the day must be the user's local calendar day; keying on the UTC date charges an East-Asian user's 8am message to yesterday's budget. Second, increment first and check the returned value, then give the slot back if it exceeded — a read-then-write races, letting two candidates read the same count and both go out. Return the slot on a hard rejection from the channel as well.
- Finish with the architecture half, which is the valuable part: all three gates must run before the content is generated, not at the send step. Get the order wrong and the program still works and sends the same messages; the only difference is that you paid for a model call on every message you then threw away. At 1000 users, three candidates each per day, forty percent blocked and roughly $0.00075 per message, that is about $27 a month wasted — more than the normal conversational spend for the same cohort — and it is invisible in monitoring. Only putting candidate count next to sent count reveals the gap.
- Expect: what order do the three gates run in? Timezone, quiet hours, daily cap, with the cap last. A message deferred to tomorrow morning must not consume today's quota; reverse the order and users get rate-limited despite having received almost nothing.
答题要点
- 跨午夜的安静时段:start 小于 end 用「且」,start 大于 end 换成「或」,朴素写法对 22:00-08:00 恒为 false
- 命中安静时段是推迟到窗口结束而不是丢弃;只有自带的过期时刻早于窗口结束才丢
- 推迟会造成惊群,要加按用户标识哈希得出的抖动,不能用随机数
- 每日上限的「天」必须是用户本地日历日,不是 UTC 日
- 计数要先占坑再判断(原子自增后比上限,超了还回去),先查后写在并发下会超发
- 三道闸必须在生成内容之前判断,装晚了每条被拦的消息都已经付过模型调用的钱(示例量级约 27 美元每月);闸内顺序是时区、安静时段、每日上限,上限最后
Key points
- Cross-midnight quiet hours need or when start is after end; the naive and version is always false for 22:00-08:00
- On a hit, defer to the end of the window rather than drop; only drop when the message's own expiry precedes that
- Deferral causes a thundering herd, so add jitter hashed from the user id, never a random value
- The day in a daily cap must be the user's local calendar day, not the UTC date
- Increment atomically then compare and release on overflow; read-then-write over-sends under concurrency
- Run all three gates before generating content — otherwise every blocked message has already been paid for (about $27/month at the example scale); order them timezone, quiet hours, daily cap, with the cap last
模型调用、支付、通知渠道你都做过 provider 抽象。通知这一份接口和另外两份有什么不同?You have abstracted model calls, payments and notification channels behind providers. How does the notification interface differ from the other two?
国内高频海外高频进阶#provider-abstraction#api-design#retry-semantics分析过程 · 先想清楚再作答
- 这题在考你是「会套模式」还是「懂模式」。把三者说成一回事——都是接口加多个实现、换厂商不改业务——只答到了共性那一层,面试官真正想看的是你有没有识别出差异并把它写进接口。
- 先给共性,一句话带过:都是把「会被替换的东西」推到接口后面,业务代码只认接口,选择点集中在一处。这一层是对的,但不构成区分度。
- 然后给三条差异,这是拿分点。第一,收下不等于送达:支付网关返回成功钱就划走了,通知渠道返回成功只表示它收下了,真正送达是过一会儿通过回执异步告诉你的。所以返回值只能叫 accepted 不能叫 delivered,而且必须带渠道侧的消息标识,回执回来时靠它对上号。
- 第二,限流的层次不同:渠道自带每秒条数上限并会用 429 加重试间隔告诉你稍后再来,这是**渠道维度的技术约束**;而每日发送上限是**用户维度的礼貌约束**。两者混成一个概念就没法分别调整——一个说的是这条线路挤不下了,一个说的是这个人今天已经被打扰够了。
- 第三,没有撤销:支付有退款,通知发出去就撤不回来,取消只在交给渠道之前有效。所以接口里不能出现 cancel——在接口上留一个做不到的操作比根本没有这个操作更危险,调用方会真的去用它。
- 可以预期的追问:那失败重试怎么设计?答分三类:限流按渠道给的时长退避重试;参数错(正文非法、用户已退订)不可重试,直接放弃并把当天的名额还回去;服务端错误或超时可重试但必须带同一个幂等键——数据库里多一行你能删掉,用户手机上多响一声删不掉。再补一条判断抽象好坏的判据:新接一个渠道时如果它只需要实现「把这条消息发出去」,抽象就对了;如果它还得知道现在是不是安静时段、这是今天第几条,说明业务规则泄进了渠道层。
How to reason about it · think before answering
- This question separates applying a pattern from understanding one. Saying all three are the same — an interface with several implementations so you can swap vendors without touching business code — only covers the shared part; the interviewer wants to see whether you spotted the differences and encoded them in the interface.
- Acknowledge the commonality in one line: each pushes a replaceable dependency behind an interface, business code depends only on the interface, and the selection point lives in exactly one place. Correct, but not differentiating.
- Then give three differences, which is where the points are. First, accepted is not delivered: when a payment gateway returns success the money has moved, but when a notification channel returns success it has merely taken the message, and actual delivery arrives later as an asynchronous receipt. So the result is accepted, never delivered, and it must carry the provider-side message id so the receipt can be correlated.
- Second, throttling lives at a different layer: the channel has its own per-second ceiling and tells you to come back later with a 429 plus a retry interval — a channel-level technical constraint — while the daily cap is a user-level courtesy constraint. Collapsing them into one concept makes them impossible to tune separately: one says this line is congested, the other says this person has been interrupted enough today.
- Third, there is no undo: payments have refunds, notifications do not. Once handed to the channel the message is gone, and cancel only means anything before that handoff. So the interface must not expose a cancel method — leaving an operation that cannot work is worse than not having it, because callers will actually use it.
- Expect: how do you design retries then? Three classes. Throttling backs off for the interval the channel gave you. Parameter errors (invalid body, unsubscribed user) are not retryable, so give up and return the daily slot. Server errors and timeouts are retryable but must carry the same idempotency key — you can delete a duplicate row, you cannot un-buzz a phone. Add a test for the abstraction itself: if a new channel only has to implement send the message, the boundary is right; if it also needs to know whether it is quiet hours or which message of the day this is, business rules have leaked into the channel layer.
答题要点
- 共性是把可替换依赖推到接口后面、选择点集中一处,但这只是及格线
- 收下不等于送达:返回值叫 accepted 不叫 delivered,必须带渠道侧消息标识以便异步回执对号
- 限流分两层:渠道的每秒上限是技术约束,每日发送上限是用户维度的礼貌约束,不能合并
- 通知没有撤销,接口里不能有 cancel;留一个做不到的操作比没有更危险
- 重试分三类:限流按渠道给的时长退避、参数错不可重试并归还名额、服务端错误可重试但必须带同一个幂等键
- 判断抽象切没切对:新渠道只需实现发送就对了,还要知道安静时段和当天条数就说明业务泄进了渠道层
Key points
- The shared part is pushing a replaceable dependency behind an interface with a single selection point — that is only the baseline
- Accepted is not delivered: name the result accepted and carry a provider message id so async receipts can be correlated
- Throttling has two layers: the channel's per-second ceiling is technical, the daily cap is a user-level courtesy rule, and they must stay separate
- Notifications have no undo, so the interface must not expose cancel — an unimplementable operation is worse than none
- Three retry classes: back off for the channel's interval on throttling, give up and release the slot on parameter errors, retry server errors with the same idempotency key
- Test the boundary: a new channel should only implement send; needing to know quiet hours or today's count means business rules leaked into the channel
D21 评估与可观测:golden set、LLM-as-judge、tracing、失败率/成本面板;Pi vs LangGraph 总结;W3 复盘
怎么评估一个 Agent 的效果?和传统后端服务的测试有什么不同?How do you evaluate an agent's quality, and how does it differ from testing a conventional backend service?
国内高频海外高频进阶#evaluation#testing#agent-quality分析过程 · 先想清楚再作答
- 题眼是「不同」。只答「建一个测试集跑准确率」拿不到分——那是机器学习的标准答案,面试官想看你能不能说清 Agent 这个场景特殊在哪。
- 根子上的差别只有一句:**同样的输入,Agent 不保证给同样的输出**。传统测试的断言是「等于」,而 Agent 的产出没有唯一正确答案,只有「够不够好」。断言从等值变成了判分,整套方法论跟着变。
- 这条差别连锁出三个后果,说全了这题就稳了:一是**跑没跑通判断不了质量**——流程没抛错,不等于回复里写明了退款结论;二是**改动的影响是弥散的**,改一个字的提示词可能只影响一类请求,人肉抽查五条恰好没覆盖到,你会得出「没影响」然后上线;三是**多 Agent 又难一层**,一次请求走路由、拆分、并行执行、评审、汇总五道工序,任何一道歪了都表现成「最后那段话不太对」,不分开量就不知道该改哪块。
- 所以给出定位:**评估不是测试,评估是给一个随机系统建立一条可比较的基线。** 它的产物不是「通过」或「不通过」,而是一个能和上一次比的数字。既然要比,样本集就必须固定。
- 然后落到具体做法:一个小而稳的 golden set(本课 15 条),每条写清期望走哪条路由和一份必备信息清单;用 LLM-as-judge 对照清单打分;把评估结果和链路追踪接到同一块面板上。**清单是关键**——它把「这答得好吗」这种没法验的问题,换成了「这几件事写没写」这种能验的问题。
- 可以预期的追问:那还需要单元测试吗?需要,而且分工很清楚——工具函数、状态迁移、reducer 这些确定性的部分照旧用单元测试断言等值,评估只负责模型产出那一段。把两者混成一套,你会得到一堆随机失败的测试,然后所有人开始无视 CI。
How to reason about it · think before answering
- The hinge is differ. Answering build a test set and measure accuracy is the textbook ML answer and misses the point; the interviewer wants to know whether you can articulate what makes agents special here.
- The root difference is one sentence: the same input does not guarantee the same output. Conventional tests assert equality, but an agent's output has no single correct answer, only good enough. Once the assertion changes from equality to scoring, the whole methodology changes with it.
- That difference cascades into three consequences, and covering all three secures the question. First, whether it ran tells you nothing about quality — the flow not throwing does not mean the reply stated the refund conclusion. Second, the blast radius of a change is diffuse: a one-word prompt edit may affect only one class of request, and hand-checking five samples that happen to miss that class yields no impact, then you ship. Third, multi-agent adds a layer: one request passes routing, planning, parallel execution, review and aggregation, and any one of them going wrong surfaces as that last paragraph seems off — without measuring each stage you cannot tell which to fix.
- So frame it: evaluation is not testing. Evaluation establishes a comparable baseline for a stochastic system. Its output is not pass or fail but a number you can compare against last time — and to compare, the sample set must be frozen.
- Then get concrete: a small stable golden set (15 items here), each declaring its expected route and a checklist of facts the reply must contain; an LLM-as-judge scoring against that checklist; and evaluation results joined to tracing on one dashboard. The checklist is the key move — it converts is this a good answer, which cannot be verified, into were these facts stated, which can.
- Expect: do you still need unit tests? Yes, with a clean split — deterministic parts (tool functions, state transitions, reducers) keep asserting equality in unit tests, while evaluation covers only the model-generated segment. Merge the two and you get a suite that fails randomly, after which everyone starts ignoring CI.
答题要点
- 根本差别:同样的输入 Agent 不保证同样的输出,断言从「等于」变成「够不够好」
- 三个后果:跑通不等于质量合格、改动影响弥散(抽查会漏)、多 Agent 里五道工序任一歪了都表现成同一个症状
- 定位:评估不是测试,是给随机系统建一条可比较的基线,产物是能和上次比的数字而不是通过与否
- 做法:小而稳的 golden set + 每条的必备信息清单 + LLM-as-judge 打分 + 与 tracing 同源的面板
- 清单是关键,它把「答得好吗」换成「这几件事写没写」,从没法验变成能验
- 单元测试仍然需要,负责确定性部分;两者混在一起会让 CI 随机变红,最后被所有人无视
Key points
- The root difference: identical input does not guarantee identical output, so the assertion shifts from equality to good enough
- Three consequences: running is not quality, change impact is diffuse (sampling misses it), and in multi-agent any of five stages failing looks like the same symptom
- Framing: evaluation is not testing — it establishes a comparable baseline for a stochastic system, yielding a number rather than pass/fail
- Method: a small stable golden set, a required-facts checklist per item, an LLM-as-judge, and a dashboard sharing tracing's data source
- The checklist is the key move: it converts is this good into were these facts stated — unverifiable into verifiable
- Unit tests remain for deterministic parts; merging the two makes CI fail randomly until everyone ignores it
多 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
什么时候该用 LangGraph 这类编排框架,什么时候不该用?When should you reach for an orchestration framework like LangGraph, and when should you not?
国内高频海外高频进阶#architecture#framework-selection#langgraph分析过程 · 先想清楚再作答
- 这题最怕答成特性对比表。面试官想听的是判据,而且是能反过来说「不该用」的判据——只会说该用的人,通常是没被框架坑过的人。
- 先给三条该拆的判据(一条都不命中就别拆,也别引框架):**提示词里出现了互斥的行为要求**(既要严谨又要俏皮,调好一个另一个就坏);**工具多到选错率明显上升**;**某一步需要独立的失败与重试语义**(比如查库存失败该重试,拟退款方案失败该转人工,两者不能共用一套策略)。
- 然后给框架本身的判据,核心是一句:**要让多个角色并行写同一份状态,就必须显式;不需要并行,显式就是纯负担。** LangGraph 的价值是把合并规则声明在字段上——三个执行者并行写同一个工作区,谁的写入怎么合并,这件事必须有地方声明。反过来,一两个工具、循环最多两轮的场景,一个 while 加一个 switch 就够了,引入框架是净亏。
- 对比 Pi 这类高层 SDK 时,用维度而不是特性:上手成本(Pi 默认值多所以快,代价是模型和人设都是它替你挑的)、状态管理的显式程度(Pi 的历史在会话内部你感知不到,所以想改「同一个工作区怎么合并」时根本没有位置可改)、调试形态(Pi 给事件流是一条时间线,LangGraph 给逐节点增量和检查点是一棵可回放可分叉的树——**线性问题看时间线更快,多角色问题必须看树**)。
- 跨语言这条值得单独提,因为它常被忽略:**Java 和 Swift 都没有 LangGraph**,跨语言团队要么统一到 TS/Python,要么自己手写同一套结构。选框架的时候把这条算进去,比上线后再发现便宜。
- 可以预期的追问:那你怎么选?给一句可执行的:**你更怕看不见的默认值,还是更怕写不完的样板?** 怕前者选显式框架,怕后者选高层 SDK。这句话比任何特性表都实用。
How to reason about it · think before answering
- The trap is answering with a feature matrix. The interviewer wants criteria, specifically criteria that can also say do not use it — people who can only argue for adoption usually have not been burned by a framework.
- Start with three criteria for splitting at all (if none holds, do not split and do not add a framework): the prompt contains mutually exclusive behavioural demands (rigorous and playful at once, where tuning one breaks the other); tools have grown numerous enough that selection error is visibly rising; or some step needs its own failure and retry semantics (an inventory lookup should retry, a refund draft should escalate to a human, and they cannot share one policy).
- Then the framework criterion, which is one sentence: if multiple roles write the same state concurrently, it must be explicit; if you do not need concurrency, explicitness is pure overhead. LangGraph's value is declaring merge rules on the field — with three executors writing one workspace, how those writes combine has to be declared somewhere. Conversely, with two tools and a loop that runs at most twice, a while and a switch suffice and a framework is a net loss.
- When comparing against a higher-level SDK like Pi, use dimensions rather than features: onboarding cost (Pi's defaults make it fast, at the price of it choosing your model and persona); explicitness of state (Pi keeps history inside the session, so when you want to change how one workspace merges there is no place to change it); and debugging shape (Pi gives an event stream, one timeline; LangGraph gives per-node deltas and checkpoints, a replayable and forkable tree — linear problems read faster as a timeline, multi-role problems require the tree).
- Cross-language deserves its own mention because it is routinely forgotten: neither Java nor Swift has LangGraph, so a polyglot team either standardises on TS/Python or hand-writes the same structure. Pricing that in during selection is cheaper than discovering it after launch.
- Expect: so how do you choose? Give something actionable: do you fear invisible defaults more, or endless boilerplate more? Fear the former and pick the explicit framework; fear the latter and pick the high-level SDK. That sentence is more useful than any feature table.
答题要点
- 先答该不该拆:提示词有互斥的行为要求、工具多到选错率上升、某步需要独立的失败与重试语义——一条不命中就别拆也别引框架
- 框架判据一句话:多个角色并行写同一份状态就必须显式;不需要并行,显式就是纯负担
- LangGraph 的价值是把合并规则声明在字段上;Pi 的历史在会话内部,想改合并方式根本没有位置可改
- 调试形态不同:事件流是一条时间线,逐节点增量加检查点是一棵可回放可分叉的树;线性问题看时间线,多角色问题必须看树
- Java 和 Swift 都没有 LangGraph,跨语言团队要么统一栈要么手写同一套结构,选型时就要算进去
- 一句可执行的选型判据:更怕看不见的默认值就选显式框架,更怕写不完的样板就选高层 SDK
Key points
- First decide whether to split at all: mutually exclusive prompt demands, rising tool-selection error, or a step needing its own retry semantics — none holding means no split and no framework
- The framework criterion in one line: concurrent writes to shared state require explicitness; without concurrency, explicitness is pure overhead
- LangGraph's value is declaring merge rules on the field; Pi keeps history inside the session, leaving nowhere to change merge behaviour
- Different debugging shapes: an event stream is a timeline, per-node deltas plus checkpoints are a replayable forkable tree — timelines for linear problems, trees for multi-role ones
- Neither Java nor Swift has LangGraph, so polyglot teams standardise or hand-write the structure — price that in at selection time
- An actionable heuristic: fear invisible defaults, choose the explicit framework; fear endless boilerplate, choose the high-level SDK
D22 安全:prompt injection、工具最小权限、沙箱思路、密钥管理
Agent 要执行不受信任的代码或命令时,有哪些沙箱隔离思路?你们选了哪一档,为什么?When an agent has to run untrusted code or commands, what sandboxing options do you have? Which tier would you pick and why?
国内高频海外高频进阶#sandboxing#security#tool-execution分析过程 · 先想清楚再作答
- 这题的区分度不在于能背出几种隔离手段,而在于你说不说得出每一档挡住了什么、放过了什么。只说「我们用了沙箱」等于没说,面试官下一句一定是「那它挡得住外发数据吗」。
- 先说清这类工具为什么特殊:白名单管的是「能不能调」,但「执行一段你给的东西」这类工具一旦进了工具表,白名单就退化成一张通行证,因为危险面在参数里不在工具名里。所以要换一种手段——不判断这段代码坏不坏,而是收窄它能触碰的东西。这和权限侧强制是同一个思路,只是对象从工具换成了进程。
- 然后按代价从低到高给三档。进程级:独立子进程、超时必杀、环境变量白名单、只读工作目录;挡住崩溃传染、死循环挂住主进程、密钥被读走;挡不住网络外发和读系统里的其他文件。容器级:无网络、只读 rootfs、非 root、CPU 与内存限额、进程数限额、用完即弃;把外发和越界读写也挡掉;挡不住内核漏洞逃逸。microVM:独立内核的轻量虚拟机,挡住多数逃逸,代价是冷启动和成本。
- 给选型判据,这是面试官真正想听的:代码是你写的、只是参数不可信,进程级够用;代码本身来自模型或用户,最低容器级;要跑第三方任意代码还对外提供服务,上 microVM。
- 点一个高频实现坑:很多人起了子进程就以为隔离了,却把父进程的环境变量整个传过去——进程是独立了,密钥跟着过去了,子进程一句读环境变量就把 API key 打印出来。子进程的环境必须是白名单拷出来的新对象,而不是继承。
- 可以预期的追问:超时之后怎么办?要用能真正杀死进程的信号,并且把「被超时杀掉」当成一个独立的失败类型上报,而不是混进普通报错——它通常意味着有人在试资源耗尽,而不是代码写错了。
How to reason about it · think before answering
- The spread here is not how many isolation techniques you can name, it is whether you can say what each tier stops and what it lets through. 'We use a sandbox' says nothing, and the next question will be 'does it stop data exfiltration?'
- First explain why these tools are special: an allowlist governs whether a tool may be called, but for a tool whose whole job is 'run this thing I hand you', the allowlist degrades into a hall pass, because the danger lives in the arguments rather than the name. So you switch technique — instead of judging whether the code is bad, you shrink what it can reach. Same idea as permission enforcement, applied to a process instead of a tool.
- Then give three tiers by cost. Process level: a separate child process, a hard timeout, an environment-variable allowlist, a read-only working directory; stops crash propagation, hung loops and secret theft; does not stop network exfiltration or reads elsewhere on the host. Container level: no network, read-only rootfs, non-root user, CPU/memory/pid limits, disposable per run; adds exfiltration and out-of-bounds access; does not stop a kernel escape. MicroVM: a lightweight VM with its own kernel, stops most escapes, at the price of cold start and cost.
- Give the selection rule, which is what the interviewer actually wants: if you wrote the code and only the arguments are untrusted, process level is enough; if the code itself comes from the model or a user, container level is the floor; if you run arbitrary third-party code as a service, go to microVM.
- Call out the classic implementation bug: people spawn a child process and assume they are isolated, then hand it the parent's entire environment. The process is separate but the secrets went with it, and one line reading an environment variable prints your API key. The child's environment must be a fresh object copied from an allowlist, never inherited.
- Expect the follow-up: what happens on timeout? Use a signal that actually kills the process, and report 'killed by timeout' as its own failure class rather than folding it into generic errors — it usually means somebody is probing for resource exhaustion, not that the code has a bug.
答题要点
- 执行类工具的危险面在参数里,白名单管不住,要靠隔离:不判断代码坏不坏,而是收窄它能触碰的东西
- 进程级:子进程 + 超时必杀 + 环境变量白名单 + 只读工作目录;挡崩溃、死循环、密钥泄漏,挡不住外发
- 容器级:无网络、只读 rootfs、非 root、CPU 内存与进程数限额、用完即弃;挡外发与越界读写,挡不住内核逃逸
- microVM:独立内核,挡多数逃逸,代价是冷启动与成本;判据是代码来自谁、要不要对外提供服务
- 最常见的实现坑是把 process.env 整个传给子进程——进程隔离了,密钥跟着过去了
Key points
- For execute-style tools the danger is in the arguments, so an allowlist cannot help; isolate instead — shrink what the code can reach rather than judging it
- Process level: child process, hard timeout, environment allowlist, read-only workdir; stops crashes, hangs and secret theft, not exfiltration
- Container level: no network, read-only rootfs, non-root, CPU/memory/pid limits, disposable; stops exfiltration and out-of-bounds access, not kernel escapes
- MicroVM: own kernel, stops most escapes, costs cold start and money; choose by who wrote the code and whether you serve it publicly
- The classic bug is handing the child process the whole parent environment — isolated process, leaked secrets
Agent 系统里的密钥应该怎么管理?它绝对不能出现在哪些地方,轮换要怎么做才能不停机?How should secrets be managed in an agent system? Where must they never appear, and how do you rotate them without downtime?
国内高频海外高频进阶#secrets-management#security#observability分析过程 · 先想清楚再作答
- 这题看着是送分题,但有一个专属于 Agent 的答案点,答不出来就只是通用后端水平:密钥不能进 LLM 上下文。面试官问的是 Agent 系统,这一条就是他在等的。
- 先给四不入,一条一句:不入代码(写死在源码里等于给了所有有仓库读权限的人,而且删掉那一行 git 历史里还在);不入日志(最高频的泄漏渠道,没人故意打印密钥,但「把请求头整个打出来方便排查」每个团队都干过);不入 LLM 上下文;不入错误信息(返回给前端的报错和抛给上游的异常都是对外出口)。
- 把第三条展开,这是本题的差异点:密钥一旦进了上下文,就意味着它会被送到模型厂商、被存进会话历史、被写进 trace,然后在某一次提示词注入里被完整地念出来。正确的形态是 Agent 需要的是「能调用某个 API」这个能力,而不是那把钥匙本身——密钥留在工具的实现里,模型只看得到工具名和参数。
- 再给落地手段:日志出口统一脱敏,不靠调用方自觉。靠每个人写日志时记得手动打码,一定会漏。做法是在唯一的日志出口做替换,两条路一起用——进程里已知的密钥值整段替换,再用通用形状兜底那些不是从环境变量来的密钥。异常处理那一支也要走同一个出口,堆栈里经常夹着带密钥的连接串。
- 存储与轮换:本地开发用 .env 加 gitignore;线上走密钥管理服务,进程启动时按自己的身份去取,不要把值烤进镜像或写进部署清单。轮换要双活——同时允许新旧两把 key,流量切到新 key、观察到没有旧 key 的调用了再吊销,一次性替换必然在某个副本上留下失败窗口。
- 可以预期的追问:轮换周期定多久?周期是次要的,真正要演练的是「能不能在 5 分钟内换掉一把疑似泄漏的 key」。答得出这一句,说明你想的是事故响应而不是合规打卡。
How to reason about it · think before answering
- It reads like a giveaway, but there is one answer point specific to agents, and missing it makes you sound like a generic backend engineer: secrets must never enter the LLM context. The interviewer asked about an agent system, and that is the line he is waiting for.
- Give the four 'nevers', one line each. Never in code — hardcoding hands the secret to everyone with read access, and deleting the line does not remove it from git history. Never in logs — the highest-frequency leak channel; nobody prints a secret on purpose, but 'log the whole request header so we can debug' is universal. Never in the LLM context. Never in error messages — responses to the frontend and exceptions thrown upstream are both outbound channels.
- Expand the third one, since it is what differentiates the answer: once a secret is in the context it will be sent to the model vendor, stored in conversation history, written into traces, and eventually read out loud by some prompt injection. What the agent needs is the capability to call an API, not the key itself — the key stays inside the tool implementation, and the model only ever sees the tool name and its arguments.
- Then the mechanics: redact at a single logging exit rather than trusting callers. Relying on everyone to mask by hand guarantees a miss. Do it in the one place logs leave the process, with two passes — replace known secret values from the environment, then catch the rest with generic shape patterns. Route the exception path through the same exit, because stack traces routinely carry connection strings with credentials.
- Storage and rotation: dotenv plus gitignore locally; in production a secret manager the process reads at startup under its own workload identity, never values baked into an image or a deployment manifest. Rotate dual-key: accept old and new simultaneously, shift traffic to the new one, confirm the old one has no remaining callers, then revoke. A single-shot swap always leaves a failure window on some replica.
- Expect the follow-up: how often do you rotate? The interval is secondary — what you should actually rehearse is whether you can revoke and replace a suspected-leaked key within five minutes. Saying that shows you are thinking about incident response rather than a compliance checkbox.
答题要点
- 四不入:不入代码、不入日志、不入 LLM 上下文、不入错误信息
- Agent 特有的一条是不入上下文——进了上下文就会被送到厂商、存进历史、写进 trace,并可能被注入念出来
- Agent 需要的是「能调用某个 API」的能力而不是钥匙本身,密钥留在工具实现里
- 日志出口统一 redact,不靠调用方自觉;异常路径走同一个出口,堆栈里常夹着连接串
- 线上走密钥管理服务按身份拉取;轮换用双活,新旧同时有效、切流量、确认无旧调用再吊销
Key points
- Four nevers: never in code, never in logs, never in the LLM context, never in error messages
- The agent-specific one is the context — anything there reaches the vendor, the history and the traces, and can be read out by an injection
- The agent needs the capability to call an API, not the key; the key stays inside the tool implementation
- Redact at one logging exit instead of trusting callers, and route the exception path through it too
- Use a secret manager with workload identity in production, and rotate dual-key: accept both, shift traffic, verify no old callers, then revoke
D23 MCP 与 Skills:协议、server/client、与 function calling 区别;Claude Agent SDK 一览
MCP 里 server 和 client 分别承担什么角色?server 能暴露哪几类东西,传输方式有哪些?What roles do the MCP server and client play, what can a server expose, and which transports exist?
国内高频海外高频进阶#mcp#protocol#transport分析过程 · 先想清楚再作答
- 这题看着是背概念,实际区分度在两个小地方:一是能不能把宿主和 client 分开说,二是知不知道 tools 之外还有别的原语。只答「server 提供工具、client 调用工具」是及格线以下。
- 先把三个角色摆清楚:server 是能力提供方,一个独立进程;client 是宿主里负责跟某一个 server 说话的那一小块,一个 client 只连一个 server;宿主是你的 Agent 应用,它同时持有多个 client。很多人把宿主和 client 当成一个东西,一问「连三个 server 怎么办」就露馅。
- server 侧三种原语要一起说,并且要说清谁来选:tools 是可执行的动作,由模型来挑;resources 是按 URI 读的只读数据;prompts 是可复用的提示词模板,后两者通常由用户或宿主来挑。这句「谁来选」比原语名字本身更能体现你真读过协议——把一份大文档做成 resource 而不是 tool,等于把花不花这笔 token 的决定权从模型手里收回给人。
- client 侧也能声明能力让 server 反过来请求宿主:sampling 是让宿主跑一次模型补全,roots 是告诉 server 哪些目录可见,elicitation 是请宿主向用户要一条输入。知道有这三样、不展开,分寸刚好。
- 传输两种:stdio 用于本地子进程,Streamable HTTP 用于远程。这里有个时间戳式的加分点——旧的 HTTP 加 SSE 双端点传输已经被标为 legacy,只为兼容老客户端保留;把它当现行方案讲,等于告诉对方你看的是去年的文章。
- 可以预期的追问:stdio server 有什么特别要注意的?答 stdout 被 JSON-RPC 独占,所有日志必须走 stderr,否则 client 会收到解析不了的消息;另外子进程的生命周期归宿主管,退出时要杀掉,不然留一堆孤儿进程。
How to reason about it · think before answering
- This looks like recall, but it discriminates on two small things: whether you separate host from client, and whether you know there are primitives beyond tools. 'Server provides tools, client calls them' is below the bar.
- Lay out three roles: the server is the capability provider and its own process; the client is the piece inside the host that talks to exactly one server; the host is your agent application, holding several clients at once. People who conflate host and client fall apart the moment you ask how they would connect to three servers.
- Cover all three server-side primitives and say who chooses each: tools are executable actions chosen by the model; resources are read-only data addressed by URI; prompts are reusable templates — the latter two are normally chosen by the user or host. That 'who chooses' framing shows you actually read the spec: modelling a large document as a resource rather than a tool moves the decision to spend those tokens from the model back to a human.
- The client side declares capabilities too, letting the server call back into the host: sampling asks the host to run a model completion, roots tells the server which directories are visible, elicitation asks the host to collect user input. Naming them without elaborating is the right level of detail.
- Two transports: stdio for a local subprocess, Streamable HTTP for remote. The dated detail worth knowing is that the older two-endpoint HTTP+SSE transport is now legacy, kept only for backwards compatibility — presenting it as current signals you read last year's blog posts.
- Expect the follow-up: anything special about stdio servers? Stdout is reserved for JSON-RPC, so every log line must go to stderr or the client receives unparseable messages; and the host owns the subprocess lifecycle, so it must reap the child on exit or leave orphans behind.
答题要点
- server 是能力提供方(独立进程),client 是宿主里连接单个 server 的那一块,宿主可以同时持有多个 client
- server 侧三种原语:tools 由模型挑,resources 是按 URI 读的只读数据,prompts 是可复用模板,后两者通常由人来挑
- client 侧还能声明 sampling、roots、elicitation,让 server 反过来请求宿主做事
- 传输两种:stdio(本地子进程)与 Streamable HTTP(远程);旧的 HTTP 加 SSE 已是 legacy,不要当现行方案讲
- stdio server 的 stdout 被 JSON-RPC 独占,日志必须走 stderr;子进程生命周期由宿主负责回收
Key points
- The server is the capability provider in its own process; a client connects to exactly one server; the host holds many clients
- Three server-side primitives: tools chosen by the model, resources as URI-addressed read-only data, prompts as reusable templates — the latter two usually chosen by a human
- Clients can declare sampling, roots and elicitation so the server can call back into the host
- Two transports: stdio for local subprocesses and Streamable HTTP for remote; the old HTTP+SSE transport is legacy
- On stdio, stdout belongs to JSON-RPC so logs must go to stderr, and the host must reap the child process
什么场景下应该考虑用 MCP,而不是直接写 function calling?不该用的时候硬上会付出什么代价?When should you reach for MCP instead of plain function calling, and what does it cost when you shouldn't?
国内高频海外高频进阶#mcp#architecture#trade-offs分析过程 · 先想清楚再作答
- 题眼在后半句。只会说「MCP 更标准更解耦」的人,等于说「微服务更解耦」——听起来对,但没有判据,面试官会立刻追问「那你们所有工具都做成 MCP server 了吗」。
- 先给判据,而且要是可执行的三条:能力要被多个宿主复用、能力由另一个团队或第三方维护、需要不改宿主代码就能增删能力。命中任意一条才考虑,**一条都不命中就直接写本地函数**——把默认答案摆成「不上」,这条比三条判据本身更能体现工程判断。
- 每条判据配一句为什么:多宿主复用把 N 乘 M 变成 N 加 M;别人维护时进程边界就是责任边界,他们改他们的、你不用发版;热插拔让加一个内部工具从一次发布降级成一次配置变更。
- 然后老实说代价,这是区分「用过」和「读过」的地方:多一个进程要保活、多一次握手要处理超时与重连、排障链路从一段变三段——工具没被调用,现在可能是模型没选、可能是 schema 翻译时丢了字段、也可能是 server 压根没起来。stdio 的子进程还要你自己回收,否则留孤儿进程。
- 还有一条容易被忽略但很加分:MCP 不改变你的成本结构。工具描述照样每轮都进上下文,工具多了照样会让模型选错——D5 那条「工具超过一定数量就该合并描述」在接了 MCP 之后一字不变,甚至更需要,因为现在别人可以往你的工具列表里塞东西。
- 可以预期的追问:那内部工具一律不上 MCP 吗?不是。有一类值得例外——你希望它能被 IDE 里的助手和运维机器人一起用,那第一条判据就命中了,即使它是你自己维护的。
How to reason about it · think before answering
- The hinge is the second half. Answering only 'MCP is more standard and decoupled' is like saying 'microservices are more decoupled' — true-sounding but with no criterion, and the interviewer will immediately ask whether you turned every tool into an MCP server.
- Give three actionable criteria: the capability must be reused by more than one host, owned by another team or a third party, or added and removed without changing host code. Any one of them justifies MCP; none of them means write a local function. Making 'no' the default answer shows more engineering judgment than the criteria themselves.
- Attach a reason to each: multi-host reuse turns N times M into N plus M; external ownership makes the process boundary the responsibility boundary, so their change is not your release; hot-swapping demotes adding an internal tool from a deployment to a config change.
- Then state the costs honestly, which is where shipped experience shows: another process to keep alive, another handshake with its own timeouts and reconnects, and a debugging path that went from one hop to three — a tool that never got called might mean the model did not pick it, the schema lost fields in translation, or the server never started. On stdio you also own reaping the child process.
- One more point that is easy to miss and scores well: MCP does not change your cost structure. Tool descriptions still enter the context every turn, and more tools still degrade tool selection. The rule that you should consolidate tools past a certain count survives MCP unchanged — arguably it matters more, because now other people can add entries to your tool list.
- Expect the follow-up: so internal tools never go through MCP? Not quite. If you want the same capability available to an IDE assistant and an ops bot as well, the first criterion is met even though you own the code.
答题要点
- 三条判据,命中任意一条才考虑 MCP:多宿主复用、由他人维护、需要不改代码增删能力
- 默认答案是不上:三条都不命中就直接写本地函数,这是更好的工程决策
- 代价是多一个进程要保活、多一次握手要处理超时、排障从一段链路变成三段
- MCP 不改变成本结构:工具描述照样每轮进上下文,工具过多照样会让模型选错,该合并还是要合并
- 第三方能力接进来之后,工具列表不再完全由你掌控,这本身就是需要设计的一件事
Key points
- Three criteria, any one justifies MCP: reuse across hosts, ownership by another team, or add/remove without touching host code
- The default is no — if none of the three apply, a local function is the better engineering decision
- Costs: another process to supervise, another handshake with timeouts, and a debug path that grows from one hop to three
- MCP does not change your cost structure: descriptions still enter context every turn and too many tools still hurt selection
- Once third-party capabilities are attached, your tool list is no longer fully under your control, which is itself a design problem
D24 RAG 进阶:hybrid search、rerank、引用、recall 评估
两路检索结果怎么合并?为什么不能直接加权求和?How do you merge two retrieval rankings, and why not just take a weighted sum of the scores?
国内高频海外高频进阶#rag#rrf#ranking分析过程 · 先想清楚再作答
- 题眼在后半句。前半句答「RRF」谁都会,后半句「为什么不能加权求和」才是筛人的地方——它考的是你有没有真的看过两路分数的分布。
- 怎么拆:先问自己两个分数是不是同一个量纲。余弦相似度有界(0 到 1)且分布密集,同一批候选常常只差 0.02;BM25 无上界,命中几个稀有词就能到 12 分。**不同量纲的数相加,等于让量纲大的那一路单方面决定结果**,权重只是在调「它说了算的程度」。
- 更麻烦的是它不稳定:权重在这批语料上调好了,换一批语料分布就变了,得重调。这是一个永远还不完的技术债。
- 结论:改用名次。RRF 把每一路的名次折算成 `1/(k + rank)` 再相加,k 取 60。名次是无量纲的,不需要任何标定。k 的作用是压平头部差距,让「两路都进前列」压过「一路排第一」——共识优先于单点自信。
- 一个能当场手算的例子很加分:两路排名 [a,b,c] 与 [c,d,a],a 得 1/61 + 1/63 ≈ 0.0323;而分数直接相加的版本会把 BM25 里 12 分的 c 顶到第一。
- 可预期的追问:同分了怎么办?必须显式定序(比如按 id),否则结果取决于哈希表遍历顺序,同一份输入在不同语言、不同运行里给出不同排序——评估集量出来的数字也就不可复现了。这一条答出来会非常加分,因为它说明你真的跑过多次。
How to reason about it · think before answering
- The second half is the real question. Anyone can say 'RRF'; explaining why weighted sums fail is what separates people who have looked at the score distributions.
- Decompose it: are the two scores even the same unit? Cosine similarity is bounded in 0 to 1 and tightly clustered — candidates often differ by 0.02. BM25 is unbounded and a few rare-term hits reach 12. Adding them lets the larger-magnitude channel decide everything; the weight only tunes how much it dominates.
- Worse, it is unstable. Weights tuned on one corpus drift on the next, so you re-tune forever.
- Conclusion: fuse ranks, not scores. RRF maps each rank to 1/(k + rank) and sums, with k = 60. Ranks are unitless and need no calibration. k flattens the head of the list so that 'top-ranked in both channels' beats 'first in one channel' — consensus over single-source confidence.
- A hand-checkable example helps: rankings [a,b,c] and [c,d,a] give a = 1/61 + 1/63 ≈ 0.0323, while a raw score sum promotes c on the strength of its BM25 12.
- Expected follow-up: what about ties? You must break them explicitly, e.g. by id. Otherwise ordering depends on hash-map iteration order and differs across languages and runs, which makes your evaluation numbers irreproducible. Mentioning this signals you actually ran it more than once.
答题要点
- 用 RRF:每一路的名次折算成 1/(k + rank) 再相加,k 取 60。
- 不能加权求和是因为两个分数量纲不同——余弦有界密集、BM25 无上界,相加等于让 BM25 单方面决定结果。
- 而且权重不可迁移:这批语料调好,换一批就得重调,是还不完的债。
- 名次是无量纲的,不需要标定;k 压平头部差距,让两路共识压过单路自信。
- 同分必须显式定序(按 id),否则结果依赖哈希表遍历顺序,评估数字不可复现。
Key points
- Use RRF: map each channel's rank to 1/(k + rank) and sum, with k = 60.
- Weighted sums fail because the scores are different units — bounded, tightly clustered cosine versus unbounded BM25, so BM25 decides the outcome.
- Weights also do not transfer: tuned on one corpus, they drift on the next.
- Ranks are unitless and need no calibration; k flattens the head so cross-channel consensus outweighs single-channel confidence.
- Break ties explicitly (by id) or ordering depends on hash iteration order and your evaluation numbers stop being reproducible.
重排(rerank)一般怎么实现?它解决了初步检索的什么问题,代价是什么?How is reranking usually implemented, what problem does it solve, and what does it cost?
国内高频海外高频进阶#rag#rerank#latency分析过程 · 先想清楚再作答
- 这题最容易答成「再排一次序,更准」。面试官想听的是**为什么第一轮不能直接排准**,以及**为什么重排不能对全库做**。
- 怎么拆:第一轮的排序依据是「检索信号」——余弦距离或词频统计,它们是为了能在百万条里快速筛选而设计的,代价就是粗。重排换了一种算法:把 query 和候选**拼在一起**送进同一个模型算相关度(cross-encoder),精度高得多,但复杂度是每条候选一次前向,没法对全库做。所以它必须跟在一个宽召回后面。
- 结论要区分两种实现:教学 / 原型可以用 LLM 批量打分(一次调用给 40 条打 0 到 10 分),生产用专门训练的 cross-encoder 重排模型。**代价说清楚:多一次 100 到 300 毫秒的调用,外加一台推理机器**——它不是按 token 计费的 API,是要占资源的。
- 把它放进漏斗里说最清楚:召回决定天花板,重排决定天花板上的东西能不能排到前五。实测的样子是——加了关键词那一路,recall@20 从 83% 涨到 95%(天花板抬高);再加重排,recall@20 只到 98%,但 recall@5 从 80% 跳到 91%、MRR 从 0.732 到 0.908。
- 可预期的追问一:重排能不能提高召回?不能。它不引入新候选,只重排已有的那批——所以看 recall@20 判断重排效果是错的指标。
- 可预期的追问二:为什么不用 LLM 打分上生产?延迟不可控、成本按 token 走、分数会随提示词措辞漂移,而且没法做批量离线蒸馏。
How to reason about it · think before answering
- The lazy answer is 'sort again, more accurately'. What the interviewer wants is why the first pass cannot rank well, and why reranking cannot run over the whole corpus.
- Decompose: the first pass ranks by retrieval signals — cosine distance or term statistics — which are designed to scan millions of items fast, and coarseness is the price. Reranking changes the algorithm: query and candidate go into one model together (a cross-encoder), which is far more accurate but costs one forward pass per candidate. Hence it must sit behind a wide recall stage.
- Distinguish two implementations. For teaching or prototypes, batch-score with an LLM (0-10 for 40 candidates in one call). Production uses a trained cross-encoder reranker. Name the cost: an extra 100-300 ms hop plus an inference box — it is not a per-token API, it consumes capacity.
- Framing it as a funnel is clearest: recall sets the ceiling, reranking decides whether what is under the ceiling reaches the top five. Measured: adding the keyword channel lifts recall@20 from 83% to 95%; adding reranking moves recall@20 only to 98%, but recall@5 jumps from 80% to 91% and MRR from 0.732 to 0.908.
- Expected follow-up 1: does reranking improve recall? No. It introduces no new candidates, so recall@20 is the wrong metric to judge it by.
- Expected follow-up 2: why not ship LLM scoring to production? Unpredictable latency, per-token cost, scores that drift with prompt wording, and no clean path to offline distillation.
答题要点
- 第一轮按检索信号粗排(余弦、词频),为的是能在大库里快速筛,代价是粗。
- 重排把 query 和候选拼在一起过同一个模型(cross-encoder),精度高但每条一次前向,只能对几十条做。
- 教学版可用 LLM 批量打 0 到 10 分;生产用专用重排模型,代价是多一次 100 到 300 毫秒的调用加一台推理机器。
- 重排不提高召回,它提高的是 recall@5 与 MRR——实测 80% → 91%、0.732 → 0.908,而 recall@20 只从 95% 到 98%。
- 所以判断重排效果要看前 k 小的指标,不要看 recall@20。
Key points
- The first pass ranks by retrieval signals so it can scan a large index fast; coarseness is the trade.
- Reranking feeds query and candidate through one model together (cross-encoder): much sharper, but one forward pass per candidate, so only tens of items.
- Batch LLM scoring works for teaching; production uses a dedicated reranker, costing an extra 100-300 ms hop plus an inference box.
- Reranking does not raise recall — it raises recall@5 and MRR (measured 80% to 91%, 0.732 to 0.908) while recall@20 barely moves from 95% to 98%.
- So judge a reranker by small-k metrics, never by recall@20.
D25 前端侧 Agent 体验:流式渲染、工具调用可视化、打断/重试、SSE hooks
流式场景下前端的状态管理要注意什么?为什么不能每个 token 都 setState?What is different about frontend state management under streaming, and why not call setState on every token?
国内高频海外高频进阶#react#streaming#performance分析过程 · 先想清楚再作答
- 这题考的是「你有没有在长回复下真的看过掉帧」。答「用 useState 存消息数组,收到 delta 就 setState」在功能上没错,但它暴露的是只在短回复上试过。
- 先算一笔账:流式一秒来几十个 token,每个 token 一次 setState 就是一秒几十轮完整渲染。而消息列表是越来越长的,每一轮的代价随对话轮数增长——所以卡顿在回复后半段和长会话里最明显,正好是最不该卡的时候。
- 做法是攒批:token 先追加进 ref(不触发渲染),一个定时器每 30 毫秒把攒下的一次性提交。30 毫秒约等于 33 帧每秒,肉眼仍是连续的打字机,渲染次数掉一到两个数量级——实测 200 个 token 只提交 8 次。
- 三个必须配套的细节:流结束时强制 flush 一次(否则最后不足一个批次的内容永远留在缓冲里,用户看到回复少半句);打断时也要 flush(让用户看到停在哪个字);缓冲状态必须放 ref 不放 state,否则你为了省渲染写的代码本身在触发渲染。
- 再往上一层是分层:**流式逻辑应该活在 React 外面。** 解析、事件归并、攒批都是纯函数,store 持有状态并暴露 subscribe 和 getSnapshot,React 侧只用 useSyncExternalStore 订阅。这样做的直接好处是**这套逻辑可以在没有浏览器的环境里跑单元测试**,而不是只能靠手点。
- 可预期的追问:为什么不直接用某个状态库?答:状态库解决的是跨组件共享和更新粒度,而流式的难点在生命周期(连接、取消、卸载清理)和批处理频率——这两件事没有哪个库替你做。面试官问这题想听的是你怎么想,不是你会用哪个库。
How to reason about it · think before answering
- This question probes whether you have watched a long reply drop frames. 'Keep messages in useState and setState on each delta' is functionally correct but reveals you only tried short replies.
- Do the arithmetic first: streaming delivers tens of tokens per second, so one setState per token means tens of full render passes per second. The message list keeps growing, so each pass gets more expensive as the conversation goes — the jank peaks late in long replies and long sessions, exactly when it hurts most.
- The fix is batching: append tokens into a ref without rendering, and flush the accumulated text on a 30 ms timer. Thirty milliseconds is roughly 33 fps, still a smooth typewriter, while render count drops by one to two orders of magnitude — measured, 200 tokens produced 8 commits.
- Three details that must ship with it: force a final flush when the stream ends, or the last sub-batch stays in the buffer and the user sees a truncated reply; flush on interrupt too, so the user sees exactly where it stopped; and keep the buffer in a ref, not state, or the code you wrote to avoid renders is itself causing them.
- One level up is layering: streaming logic should live outside React. Parsing, event reduction and batching are pure functions; a store holds state and exposes subscribe and getSnapshot; React only calls useSyncExternalStore. The concrete payoff is that this logic can be unit tested with no browser instead of being click-tested.
- Expected follow-up: why not just use a state library? Libraries solve cross-component sharing and update granularity, while the hard parts here are lifecycle (connect, cancel, cleanup on unmount) and flush cadence — no library does those for you. The interviewer wants your reasoning, not your library list.
答题要点
- 每个 token 一次 setState 等于一秒几十轮全量渲染,而消息列表越长每轮越贵,长回复后半段必然掉帧。
- 做法是攒批:token 进 ref 不触发渲染,30 毫秒定时 flush 一次,实测 200 个 token 只提交 8 次。
- 必须配套:流结束和打断时强制 flush;缓冲放 ref 不放 state。
- 流式逻辑(解析、归并、攒批)应该是 React 之外的纯函数,React 只用 useSyncExternalStore 订阅。
- 这样分层的直接好处是能脱离浏览器做单元测试,而不是只能手点验证。
Key points
- One setState per token means tens of full renders per second, and each render costs more as the list grows — long replies jank at the end.
- Batch instead: accumulate tokens in a ref and flush every 30 ms; measured, 200 tokens produced only 8 commits.
- Ship the details with it: force a flush on stream end and on interrupt, and keep the buffer in a ref rather than state.
- Keep parsing, event reduction and batching as pure functions outside React; subscribe via useSyncExternalStore.
- The payoff of that split is unit-testable streaming logic with no browser in the loop.
失败重试怎么设计才不会产生重复副作用?工具调用过程要不要暴露给用户?How do you design retry so it does not duplicate side effects, and should the tool-call process be visible to the user?
国内高频海外高频进阶#idempotency#retry#ux分析过程 · 先想清楚再作答
- 这题把两件事绑在一起问,考的是你能不能看出它们的共同点:**都是「把不可见的中间状态变成可控的」**。分开答也行,但点出这层关系会显得成熟。
- 重试这一半的推导链:重试意味着同一句话可能被执行两遍 → 两倍 token,还可能两次不可逆的工具调用(比如退款打两次钱)→ 所以要幂等 → 幂等键必须由**客户端在第一次发送时生成**并在重试时原样带上 → 后端拿它做唯一约束,命中就把已有 run 的流接回来,而不是新建。
- 关键判据要说清:**什么时候该换新键?** 判据是「要发送的内容变没变」,不是「用户点了哪个按钮」。同一句话重试用同一个键;用户改了内容重新发,那是新的一句话,必须换新键。
- 顺带提一句这一招的复用面会很加分:落库去重、定时任务防止一个 tick 被消费两次、跨服务调用防重复投递、前端重试——同一个形状用在四个层面,最终裁判永远是数据库的唯一约束,不是应用层的先查后写。
- 工具可视化这一半:中间过程要暴露,理由有三条——用户能判断要不要打断(不然他只能盲等);等待变得可以忍受(十几秒的转圈会让人刷新页面,而刷新意味着这一轮的钱白花);出问题时用户能说清「卡在查订单那一步」,客服和你都省事。
- 可预期的追问:全都暴露会不会泄露内部实现?会,所以要过滤——工具名用人话不用函数名,参数里的用户标识、内部 id、密钥一律不显示,错误显示归类后的原因而不是原始堆栈。**可视化的是过程,不是内部结构。**
How to reason about it · think before answering
- The question bundles two topics, and the test is whether you see what they share: both turn invisible intermediate state into something the user can act on. Answering them separately is fine, but naming the link reads as senior.
- Chain for retry: retrying means the same message may execute twice, costing double tokens and possibly duplicating irreversible tool calls such as issuing a refund twice. Hence idempotency. The key must be generated by the client on the first attempt and resent unchanged on retry, and the backend enforces it with a unique constraint, reattaching to the existing run instead of creating a new one.
- State the decision rule clearly: when do you mint a new key? The rule is whether the content being sent changed, not which button the user pressed. Same message retried keeps the key; edited content is a new message and needs a new key.
- Mentioning how far this pattern reaches scores well: write deduplication, cron ticks consumed exactly once, cross-service delivery, and frontend retry — the same shape at four layers, with the database's unique constraint always the final arbiter rather than an application-level check-then-write.
- For tool visibility: expose the process, for three reasons. The user can decide whether to interrupt instead of waiting blind; waiting becomes tolerable, since a spinner for fifteen seconds invites a page refresh that wastes the whole turn; and when something breaks the user can say 'it hung on looking up my order', which saves everyone time.
- Expected follow-up: does exposing everything leak internals? It can, so filter. Show human-readable tool names rather than function names, hide user identifiers, internal ids and secrets from the arguments, and show classified error reasons rather than raw stack traces. You are surfacing the process, not the internal structure.
答题要点
- 重试要带客户端首次生成的幂等键,后端用唯一约束命中后把已有 run 的流接回来,不新建。
- 换不换键的判据是「内容变没变」:同一句话重试用同一个键,改了内容才换新键。
- 同一招在落库、定时任务、跨服务调用、前端重试四处复用,最终裁判永远是数据库的唯一约束。
- 工具调用要可视化:用户才能判断要不要打断、等待变得可忍受、出问题时说得清卡在哪一步。
- 但要过滤:工具名用人话、参数里的内部 id 与密钥不显示、错误显示归类原因而不是原始堆栈。
Key points
- Retry carries the idempotency key minted on the first attempt; the backend hits a unique constraint and reattaches to the existing run.
- The rule for minting a new key is whether the content changed — same message keeps the key, edited content gets a new one.
- The same pattern recurs in write dedup, cron ticks, cross-service delivery and frontend retry, always arbitrated by a database unique constraint.
- Make tool calls visible so users can decide whether to interrupt, tolerate the wait, and describe where it hung.
- But filter: human-readable tool names, no internal ids or secrets in the arguments, classified error reasons instead of raw stack traces.
D26 系统设计专题:Agent 平台 / 客服 Agent / 多租户 / 成本控制
一个多租户的 Agent 服务,数据隔离和计费隔离要怎么设计?什么时候该从共享表升级到独立库?For a multi-tenant agent service, how do you design data isolation and billing isolation, and when do you move from a shared table to a dedicated database per tenant?
国内高频海外高频进阶#system-design#multi-tenancy#isolation分析过程 · 先想清楚再作答
- 这题的题眼在「隔离」是复数。只答数据隔离的候选人非常多,而多租户翻车最多的其实是资源那一层——一个租户的洪峰打穿别人的处理能力,数据一条都没串,用户照样投诉。所以第一句先把三层摆出来:数据、资源、计费,缺哪一层对应一类事故。
- 数据这一层,判断一个人有没有真做过就看一句话:他说「每条查询都带 tenant_id」还是「靠数据库的行级安全兜底」。前者迟早会漏一处,而漏掉的那处通常是最新加、最没被测过的功能。正确说法是行级安全是闸门,应用层那句 where 只是优化——这和幂等的最终裁判必须是数据库唯一约束,是同一种思路:能在数据层强制的,不要指望每个人写代码时都记得。
- 资源这一层给两件具体的东西:每个租户一个独立限流桶,以及 worker 按租户标识哈希分片。分片这一招和「按用户哈希保住同一用户顺序」是同一套机制,只是哈希的输入换了,目的从保序变成隔离洪峰。Agent 场景里噪声邻居格外突出,因为单次执行可能跑三十秒,一个租户灌一千条进来,别人就得排队。
- 计费这一层最简单也最容易漏:token 用量台账加一列租户标识,写入时打标。账单、配额、超支降级三件事全靠它。顺带说一句成本口径——单轮 2000 输入加 500 输出约 0.0006 美元,日活 1 万人均 5 轮约每月 900 美元,能报出这个量级说明你真的算过每租户成本。
- 然后回答升级判据,这是本题的第二个区分点。三档是共享表加租户列、schema 级、库级;**判据不是租户数量,是「有没有单个租户能把别人拖垮」和「有没有合规硬要求」**。答「超过一百个租户就该分库」是典型的凭感觉,因为一百个小租户共享一张表毫无问题,而一个受监管的大客户哪怕只有一个也可能必须物理隔离。代价要一起说:库级隔离看着干净,但迁移脚本、备份、监控、连接池全部乘以租户数,运维成本是陡增不是线性。
- 可以预期的追问,也是最见功力的一问:幂等键在多租户下要不要变?答案是键的算法不用变,但**作用域必须带上租户标识**。不带的话两个租户的客户端各自生成了同一个字符串(都用订单号 order-1024),后来那个会被唯一约束当成重复请求挡掉——一个租户的写入被另一个租户的历史请求吞掉,两边日志都完全正常,是多租户里最难查的一类 bug。
How to reason about it · think before answering
- The hinge is that 'isolation' is plural. Plenty of candidates answer only data isolation, but the layer that actually breaks in production is resources: one tenant's spike starves everyone else, no rows leak, and users still complain. Open with all three — data, resources, billing — and note that each missing layer maps to its own class of incident.
- On data, one sentence separates people who shipped this from people who read about it: 'every query carries tenant_id' versus 'row-level security is the backstop'. The first eventually misses a query, and the one it misses is always the newest, least-tested feature. The correct framing is that RLS is the gate and the application-level where clause is just an optimisation — the same reasoning as idempotency being adjudicated by a database unique constraint. Whatever the data layer can enforce should not depend on everyone remembering.
- On resources, give two concrete things: a rate-limit bucket per tenant, and workers sharded by a hash of the tenant id. That is the same sharding mechanism used to preserve per-user ordering, with a different hash input and a different purpose — containing spikes rather than serialising. Noisy neighbours hurt more in agent workloads because a single execution can run thirty seconds, so a thousand queued items from one tenant leaves everyone else waiting.
- Billing is the simplest and the most often forgotten: add a tenant column to the token usage ledger and tag every write. Invoicing, quotas and over-budget degradation all hang off it. Bring the cost figures too — roughly $0.0006 per turn at 2000 in and 500 out, about $900 a month at 10k daily actives and five turns each — because quoting per-tenant economics shows you actually ran the numbers.
- Then the escalation criteria, the second discriminator. Three tiers: shared table with a tenant column, schema per tenant, database per tenant. The trigger is not tenant count, it is whether a single tenant can starve the rest and whether there is a hard compliance requirement. 'Split the database past a hundred tenants' is guesswork: a hundred small tenants share a table happily, while one regulated enterprise customer may require physical separation on its own. State the cost too — a database per tenant looks clean, but migrations, backups, monitoring and connection pools all multiply by tenant count, so operational cost jumps rather than scaling linearly.
- Expect the sharpest follow-up: does the idempotency key change under multi-tenancy? The algorithm does not, but its scope must include the tenant id. Without it, two tenants whose clients independently produce the same string — both using order id order-1024 — collide, and the later request is rejected by the unique constraint as a duplicate. One tenant's write is swallowed by another tenant's history, both logs look perfectly normal, and it is the hardest class of multi-tenant bug to find.
答题要点
- 三层隔离并列,缺一层对应一类事故:数据、资源、计费
- 数据靠行级安全兜底,应用层的 where 只是优化——能在数据层强制的不要靠人记得
- 资源是每租户独立限流桶加按租户标识哈希分片,防的是噪声邻居而不是数据串
- 计费是台账加一列租户标识,账单、配额、超支降级全靠它
- 升级到 schema 级或库级的判据是「单租户能否拖垮别人」与「有没有合规硬要求」,不是租户数量
- 库级隔离的代价是迁移、备份、监控、连接池全部乘以租户数,运维成本陡增
- 幂等键算法不变,但作用域必须带租户标识,否则两个租户的同名键会互相挡掉请求
Key points
- Three parallel layers, each missing one causing its own class of incident: data, resources, billing
- Data isolation is backstopped by row-level security; the application where clause is only an optimisation
- Resource isolation is a per-tenant rate-limit bucket plus sharding workers by tenant hash, aimed at noisy neighbours
- Billing isolation is a tenant column on the usage ledger, powering invoices, quotas and degradation
- Escalate to schema or database isolation based on starvation risk and compliance mandates, not tenant count
- Per-tenant databases multiply migrations, backups, monitoring and connection pools — operational cost jumps
- The idempotency key algorithm stays, but its scope must include the tenant id or identical keys across tenants collide
一个 Agent 系统的模型成本失控了,你会从哪几个层面着手控制?每一层大概能省多少?An agent system's model spend is out of control. Which levers do you pull, in what order, and roughly how much does each save?
国内高频海外高频进阶#system-design#cost#capacity-planning分析过程 · 先想清楚再作答
- 这题最容易脱口而出的答案是「换个便宜模型」,也是最容易被追死的答案——面试官紧跟着就问「你怎么知道换了质量不掉」,答不出离线评估集和对比实验就露馅了。正确的第一句是「先看台账」:按用户、按天、按模型各切一刀,找出是哪一维在涨。先定位再动手,这是工程习惯。
- 第二步是把基准摆到桌上,没有基准的成本讨论全是废话。单轮 2000 输入加 500 输出,输入 2000 除以一百万乘 0.15 等于 0.0003 美元,输出 500 除以一百万乘 0.60 也等于 0.0003 美元,一轮约 0.0006 美元;日活 1 万、人均 5 轮就是 5 万轮,一天约 30 美元、一个月约 900 美元。
- 然后给五层,排序的依据是**你要付出的代价从小到大**,不是省钱多少:缓存与 prompt cache、模型分级路由、上下文压缩、步数与工具预算上限、限流与降级。这个顺序本身就是答案的一部分,因为它同时说明了你的落地顺序。
- 每层配一个从基准推出来的数字。缓存按一成半命中估,900 降到 765 左右。分级路由要诚实说清前提:它是唯一能改数量级的一层,但前提是你的基准用的是旗舰模型;基准已经是最便宜那档时这一层榨不出东西——主动说破这一条,比硬编一个省钱比例可信得多。上下文压缩把输入从 2000 压到 1200,单轮变成 0.00048 美元,一个月 720 美元,降两成。
- 第四层最容易被讲成「省钱」,其实它买的是**可预测**:给每个子任务设 5 次工具调用上限之后,单轮成本才有上界——调满 5 次、每次结果回灌 800 token,输入涨到 6000,单轮 0.0012 美元,正好是基准的两倍;没有上限时这个数字没有上界。这一层的正确说法是「我不是靠它省钱,我是靠它让账单可以被预测」。
- 第五层是限流与降级,代价最大所以放最后:900 美元摊到 1 万日活是每人每月 0.09 美元,给单用户设 1 美元硬顶,正常用户碰不到,挡的是脚本刷接口那种极端户。要点是超预算先降档再拒绝,而不是直接拒绝——它是五层里唯一用户能感觉到的一层。
- 可以预期的追问:这五层里哪一层最先做?答「第一层和第三层」,因为它们只改自己的代码、不动产品承诺、也不需要重新验证质量;而分级路由要配离线评估集,限流要配产品沟通,都不是当天能上的。
How to reason about it · think before answering
- The reflex answer is 'switch to a cheaper model', and it is also the easiest one to get killed on: the follow-up is 'how do you know quality did not drop', and without an offline eval set and a comparison run you are exposed. The right opening is 'look at the ledger first' — slice by user, by day and by model to find which dimension is growing. Locate before you act.
- Second, put the baseline on the table, because cost talk without a baseline is noise. At 2000 input and 500 output tokens per turn, input is 2000 over a million times $0.15 which is $0.0003, output is 500 over a million times $0.60 which is also $0.0003, so about $0.0006 per turn. 10k daily actives at five turns is 50k turns, roughly $30 a day and $900 a month.
- Then give five layers ordered by the cost you pay, not by the savings: caching and prompt caching, tiered model routing, context compression, step and tool budget caps, rate limiting and degradation. The ordering is part of the answer, because it also communicates your rollout sequence.
- Attach a number derived from the baseline to each layer. Caching at a 15% hit rate takes $900 to roughly $765. For tiered routing, state the precondition honestly: it is the only layer that can change the order of magnitude, but only if your baseline runs a flagship model — if you already run the cheapest tier there is nothing left to squeeze. Saying that out loud is far more credible than inventing a savings percentage. Compression takes input from 2000 to 1200 tokens, so $0.00048 per turn, about $720 a month, a 20% cut.
- Layer four is usually mis-sold as savings; what it actually buys is predictability. With a cap of five tool calls per subtask, per-turn cost finally has a ceiling: five calls each feeding back 800 tokens pushes input to 6000, so $0.0012 per turn, exactly double the baseline — and with no cap there is no ceiling at all. The right phrasing is 'this does not save money, it makes the bill predictable'.
- Layer five is rate limiting and degradation, last because it costs the most: $900 across 10k daily actives is about $0.09 per user per month, so a $1 monthly hard cap is invisible to real users and only stops scripted abuse. The nuance is degrade before refusing — this is the only layer users can feel.
- Expect the follow-up: which layer first? Say layers one and three, because they only touch your own code, change no product promise, and need no quality re-validation, whereas tiered routing needs an eval set and rate limiting needs product buy-in.
答题要点
- 第一句不是「换便宜模型」,是「先看台账」:按用户、按天、按模型各切一刀定位是哪一维在涨
- 先立基准:单轮约 0.0006 美元,日活 1 万人均 5 轮约每天 30 美元、每月 900 美元
- 五层按代价从小到大:缓存与 prompt cache、模型分级路由、上下文压缩、步数与工具预算上限、限流与降级
- 分级路由是唯一能改数量级的一层,但前提是基准用的是旗舰模型;基准已经最便宜时要诚实说没得省
- 上下文压缩把输入从 2000 压到 1200,单轮 0.00048 美元、每月 720 美元,降两成
- 工具预算上限买的是可预测:有上限时单轮上界是 0.0012 美元,没上限时没有上界
- 限流降级放最后,因为它是唯一用户能感觉到的一层;超预算先降档再拒绝
Key points
- Open with 'look at the ledger', not 'use a cheaper model': slice by user, by day and by model to locate the growth
- Set a baseline: about $0.0006 per turn, roughly $30/day and $900/month at 10k DAU and five turns
- Five layers ordered by cost to you: caching and prompt cache, tiered routing, context compression, step and tool budget caps, rate limiting and degradation
- Tiered routing is the only order-of-magnitude lever, but only if the baseline is a flagship model — say so when it is not
- Compression from 2000 to 1200 input tokens gives $0.00048 per turn, about $720/month, a 20% cut
- Tool budget caps buy predictability: with a cap the per-turn ceiling is $0.0012, without one there is no ceiling
- Rate limiting comes last because users feel it; degrade before refusing
D27 简历与项目包装:STAR、README、架构图、demo 视频、英文简历
我们点开了你 GitHub 上的项目,你觉得一份好的 README 应该让我看到什么?Suppose I open one of your GitHub projects — what should a good README show me?
国内高频海外高频进阶#behavioral#documentation#portfolio分析过程 · 先想清楚再作答
- 这题表面在问文档规范,实际在考「你有没有读者意识」。答成一串小标题清单(简介、安装、使用、贡献指南)会显得像背模板;面试官想听的是你知道读者是谁、他有多少时间、他在找什么。
- 先把读者说清楚再列结构,这一步就能拉开差距:看 README 的人预算大约三分钟,而且不打算 clone 下来跑。所以第一屏必须解决「这是什么」和「能不能跑」,深入的东西往后放。
- 然后给结构,并且为每一段说出它服务的是哪个读者:一句话定位(筛简历的人)、架构图(想快速建立心智模型的人)、快速开始(想验证能不能跑的人)、关键设计决策(面试官)、已知限制(面试官)、目录导读与许可(真的要读代码的人)。
- 重点落在两段上。「快速开始」的硬指标是三条命令之内跑起来,超了说明有隐性依赖;判据是拿一台没跑过的机器照着敲一遍,而不是在自己机器上试。「关键设计决策」每条要含「放弃了什么」,因为这是唯一无法从模板抄来的部分。
- 「已知限制」值得单独说一句:主动写出边界不是暴露短板,而是同时证明了自我认知和诚信。而且你先说了,对方就很难再拿它当把柄,最多顺着问「上生产你会先补哪个」——那是你准备好的题。
- 可以预期的追问:「你的项目 README 里最花时间的是哪一段?」答「关键设计决策」,然后现场讲一条。这题问的是 README,落点其实是让你讲项目,别错过这个递过来的机会。
How to reason about it · think before answering
- On the surface this is about documentation conventions; underneath it tests reader awareness. Reciting a list of headings sounds like a template. They want to hear that you know who the reader is, how much time he has, and what he is looking for.
- Define the reader before listing sections: someone skimming a README has about three minutes and has no intention of cloning the repo. So the first screen must answer 'what is this' and 'can it run'; everything deep goes below.
- Then give the structure, naming the reader each part serves: one-line positioning (the resume screener), architecture diagram (anyone building a mental model), quick start (anyone verifying it runs), key design decisions (the interviewer), known limitations (the interviewer), directory guide and license (people who will actually read the code).
- Put the weight on two sections. Quick start has a hard bar — running in three commands or fewer; more than that means hidden setup. Verify it on a machine that has never run the project, not on your own. Key design decisions must each state what you gave up, because that is the one part no template can supply.
- Known limitations deserve a sentence of their own: stating boundaries is not exposing weakness, it demonstrates self-awareness and honesty at once. And once you have said it, it is hard to use against you — at most they ask which gap you would close first, which you already prepared.
- Expect the follow-up: 'which section took you the longest?' Answer 'key design decisions' and then walk through one on the spot. The question is nominally about READMEs, but it is an invitation to talk about your project — take it.
答题要点
- 先说读者:三分钟预算、不会 clone 下来跑,所以第一屏解决「是什么」和「能不能跑」
- 七段结构:一句话定位、架构图、快速开始、关键设计决策 3 条、已知限制、目录导读、许可
- 快速开始的硬指标是三条命令之内,且要在一台没跑过的机器上验证
- 关键设计决策每条含「放弃了什么」,这是唯一抄不来的部分,也是面试官挑追问的地方
- 架构图用 Mermaid 而不是截图:GitHub 原生渲染、改动是文本 diff、不会过期
Key points
- Start from the reader: a three-minute budget and no intention of cloning, so the first screen answers what it is and whether it runs
- Seven sections: one-line positioning, architecture diagram, quick start, three key design decisions, known limitations, directory guide, license
- Quick start must work in three commands or fewer, verified on a machine that has never run it
- Every design decision states what was given up — the one part no template provides, and where interviewers pick their follow-up
- Use Mermaid rather than screenshots: native GitHub rendering, text diffs, and it does not go stale
D28 模拟面试日:国内/海外各一套全流程,自评
自我介绍环节最容易出的问题是什么?一段好的自我介绍应该长什么样?What most commonly goes wrong in the self-introduction, and what does a good one look like?
国内高频海外高频进阶#self-presentation#communication分析过程 · 先想清楚再作答
- 先看清面试官在这 3 分钟里做什么:一是看你能不能自己组织一段有结构的表达,二是决定接下来 25 分钟挖你哪个项目。看懂第二件事,答案就不是「讲短一点」这么浅了。
- 最容易出的问题有一个统一的根因——按时间顺序讲。从大学讲起、顺着简历从上往下念,于是 3 分钟到点时你还没讲到最近、最有价值的那段经历,而那恰恰是唯一有人想听的部分。
- 由此推出正确形态:倒序,只留三块——你现在是什么方向的工程师、一到两个带数字的代表作、你为什么来面这个岗位。90 秒讲完主线,把剩下的时间让给对方追问,而不是把 3 分钟填满。留白是主动权,不是浪费。
- 第二个高频问题是通篇形容词、没有一个数字。「我做过一个高性能的 Agent 服务」几乎不携带信息;换成一句带约束和指标的话(在什么约束下、为了什么目标、做了什么、把哪个指标从多少改善到多少),才会让对方接着问下去。
- 第三个问题最隐蔽:自我介绍里埋了自己不想被问的东西。你说出口的每一个技术名词都是一张邀请函,不熟的栈别写也别说;反过来,希望被问的点要主动埋进去,这是全场唯一由你控制议题的机会。
- 可以预期的追问:面试官打断你说「再简单说一下」,说明你已经超时或跑题了。所以要提前排练两个版本,一个 60 秒、一个 90 秒,现场直接切,不要临场压缩——临场压缩的结果通常是把结论也一起删掉了。
How to reason about it · think before answering
- Start from what the interviewer is doing during those three minutes: judging whether you can structure a piece of speech unaided, and deciding which project to spend the next twenty-five minutes on. Once you see the second one, the answer stops being 'keep it short'.
- The usual failures share one root cause: telling it chronologically. Starting at university and reading the resume top-down means the three minutes expire before you reach the recent work, which is the only part anyone wants to hear.
- That gives the correct shape: reverse order, three blocks only — what kind of engineer you are now, one or two signature pieces of work with a number attached, and why this role. Land the main line in ninety seconds and leave room for follow-up rather than filling the slot. The silence is leverage, not waste.
- The second frequent failure is adjectives with no numbers. 'I built a high-performance agent service' carries almost no information; a sentence with a constraint, a goal, an action, and a metric moved from X to Y is what makes someone ask the next question.
- The third is the subtlest: seeding things you do not want to be asked about. Every technology you name is an invitation, so leave unfamiliar stacks out — and conversely, plant the topics you want to be asked about, since this is the only moment in the loop where you set the agenda.
- Expect this follow-up: if the interviewer cuts in with 'just briefly', you have already run long or drifted. Rehearse two versions, sixty and ninety seconds, and switch between them rather than compressing live — live compression usually deletes the conclusion too.
答题要点
- 面试官在这 3 分钟里同时做两件事:判断你的表达结构,决定接下来挖哪个项目
- 最常见的错是按时间顺序讲,时间用完还没讲到最近最有价值的经历;正确做法是倒序
- 结构只留三块:现在的技术方向、一到两个带数字的代表作、为什么来面这个岗位
- 90 秒讲完主线、主动留白给对方追问,比把 3 分钟填满更有利
- 每个说出口的技术名词都是邀请函:不熟的不提,想被问的主动埋进去;提前排练 60 秒和 90 秒两个版本
Key points
- The interviewer is doing two things at once: assessing structure and choosing which project to dig into
- The common failure is chronological order, which burns the clock before reaching recent work; reverse it
- Keep three blocks: current engineering focus, one or two signature results with numbers, and why this role
- Landing the main line in ninety seconds and leaving room for follow-up beats filling all three minutes
- Every technology you name is an invitation: omit unfamiliar stacks, plant the topics you want asked, and rehearse a sixty-second and a ninety-second version
一次模拟面试之后,你怎么做一次客观的自我评估,而不是停在「感觉还行」?After a mock interview, how do you assess yourself objectively instead of settling for 'that felt okay'?
国内高频海外高频进阶#self-assessment#deliberate-practice分析过程 · 先想清楚再作答
- 这题在考你有没有把练习工程化。答「录下来多听几遍」只是及格线,真正的区分度在于有没有可重复的评分口径和事先定好的阈值——没有口径,两次模拟之间就没法比较,也就谈不上进步。
- 客观的前提是有可回放的证据,所以先固定三件事:全程录音或录屏、按时间盒计时、事后对着回放打分而不是结束时凭感觉打。刚讲完的十几分钟里自我评价偏差最大,讲得顺就全盘肯定,卡过一次就全盘否定。
- 然后用固定维度代替整体印象:自我介绍、项目讲解深度、编码、系统设计、沟通与反问,各 1 到 5 分,满分 25。关键是每个维度要写好 1 分、3 分、5 分各长什么样的锚点描述,否则同一个「4 分」在两次之间根本不是同一件事。
- 阈值要在打分之前定好,这是防止事后给自己找理由的唯一办法:任何单项低于 3 分就进弱项清单,总分低于 18 分就隔两天把整套流程重跑一次,而不是硬着头皮往下走。
- 最后一步才是全部价值所在——把低分翻译成四列:现象、根因、最小动作、怎么验证。现象必须是回放里能指着看的事实(带时间、带原话),「讲得不好」不算,「项目讲解到第 8 分钟还没说到我做了什么」才算;最小动作必须一天内做得完;验证必须可观察,最好带数字门槛。
- 可以预期的追问:一个人怎么产生追问?用大模型当面试官,但必须先把追问纪律写进指令——一次只问一个问题、基于我的回答连追三层、全程不评价不夸奖不给答案。不写纪律,它会退化成一个不停鼓励你的助手,那就失去了模拟的意义。
How to reason about it · think before answering
- This asks whether you have engineered your practice. 'Record it and listen again' is the passing floor; the discriminator is a repeatable rubric plus thresholds fixed in advance, because without a rubric two sessions are not comparable and improvement is unmeasurable.
- Objectivity requires reviewable evidence, so fix three things first: record audio or screen throughout, run the real time boxes, and score against the recording afterwards rather than on feeling at the buzzer. Self-assessment is at its most distorted in the minutes right after you finish.
- Then replace overall impression with fixed dimensions: self-introduction, project depth, coding, system design, and communication plus candidate questions, each scored 1 to 5 for a total of 25. What makes it work is writing anchor descriptions for what a 1, a 3, and a 5 look like — otherwise the same '4' means different things in different sessions.
- Set thresholds before scoring, which is the only defense against rationalizing afterwards: any dimension below 3 goes on the weakness list, and a total below 18 means rerunning the whole loop two days later instead of pressing on.
- The final step carries all the value: translate low scores into four columns — observation, root cause, smallest drill for tomorrow, and how to verify. An observation has to be a fact you can point at in the recording, with a timestamp and the actual words: 'explained it badly' does not qualify, 'eight minutes into the project story and still had not said what I personally did' does. The drill must fit in one day, and verification must be observable, ideally with a numeric bar.
- Expect the follow-up: how do you generate follow-up questions alone? Use a model as the interviewer, but write the interrogation rules into the instructions first — one question at a time, three consecutive layers of follow-up grounded in what you just said, and no praise, no evaluation, no supplying the answer. Without those rules it degrades into an encouraging assistant, which defeats the point.
答题要点
- 先有证据再有判断:全程录音或录屏、按时间盒计时、事后对着回放打分,不在结束当场凭感觉打
- 用固定五个维度(自我介绍、项目讲解深度、编码、系统设计、沟通与反问)各 1 到 5 分、满分 25,并给每个维度写 1/3/5 分的锚点描述
- 阈值先定后打:任何单项低于 3 分进弱项清单,总分低于 18 分隔两天重跑整套流程
- 把低分翻译成四列:现象、根因、最小动作、怎么验证;现象必须是回放里能指着看的事实,动作必须一天内做得完
- 一个人练时用大模型当面试官,但要先写死追问纪律:一次一问、连追三层、不评价不夸奖不给答案
Key points
- Evidence before judgment: record throughout, run real time boxes, and score against the replay rather than on feeling at the buzzer
- Use five fixed dimensions (intro, project depth, coding, system design, communication and candidate questions) scored 1 to 5 out of 25, each with anchors for what 1, 3 and 5 look like
- Fix thresholds before scoring: any dimension below 3 goes on the weakness list, a total below 18 means rerunning the loop two days later
- Translate low scores into four columns — observation, root cause, smallest drill, verification — where the observation is a pointable fact and the drill fits in one day
- Practicing alone, use a model as interviewer but write the rules first: one question at a time, three layers of follow-up, no praise, no evaluation, no answers
D29 弱项补强 + 编码热身:rate limiter、LRU、并发控制、流式 JSON 解析
怎么实现一个限制并发数的调度器?为什么不能直接用 Promise.all 或者 asyncio.gather?How would you build a scheduler that caps in-flight async tasks, and why is Promise.all or asyncio.gather not enough?
国内高频海外高频进阶#concurrency#async分析过程 · 先想清楚再作答
- 题眼在后半句。面试官在确认你分不分得清「等待一批任务」和「限制同时运行的任务数」——这两件事在 API 名字上很像,在语义上毫无关系。
- 先说破错误答案为什么错:把 500 个任务全部映射成 Promise 再一起 await,这段代码的并发度是 500。Promise 一被创建,它内部的请求就已经发出去了,await 只是在等结果;gather 和 CompletableFuture.allOf 是同一个坑的另外两种口音。
- 再给正确形状的两条路:固定数量的工人从同一个游标取任务(JS 的惯用法,槽位就是工人本身),或者用信号量挡在任务启动之前(Python 的 asyncio.Semaphore、Java 的 Semaphore)。Swift 要用 TaskGroup 自己开滑动窗口,先塞满 limit 个、每收一个结果补一个。
- 本题真正的失分点是槽位泄漏:acquire 之后必须在 finally 里 release,或者把错误在任务内部收敛成结果值。忘了这一步的代码在 happy path 上完全正常,只有下游开始报错时才会一点点变慢直到彻底卡死——这是最难查的那类 bug,因为症状出现在故障之后而不是之中。
- 落到 Agent 场景说收益:批量 embedding、并行工具调用、多路子任务都靠它。收益不只是「不打爆下游」,还有同时驻留的内存与并发度同阶而不是与任务数同阶。
- 可以预期的追问:如果任务本身还要重试呢?答案是重试要在槽位内部完成(占着槽位退避重试),否则重试风暴会绕过限流;再追一层就是给重试加抖动,避免所有失败任务在同一时刻一起回来。
How to reason about it · think before answering
- The hinge is the second half. They are checking whether you separate 'await a batch' from 'cap how many run at once' — similar API names, unrelated semantics.
- Name the wrong answer first: mapping 500 items to promises and awaiting them together runs at concurrency 500. Creating the promise already fired the request; awaiting only collects results. gather and CompletableFuture.allOf are the same trap in other accents.
- Then give the two correct shapes: a fixed set of workers pulling from a shared cursor (the JS idiom, where a worker is the slot), or a semaphore gating task start (asyncio.Semaphore, java.util.concurrent.Semaphore). Swift needs a manual window over a TaskGroup — fill limit slots, then add one task per result received.
- The real failure mode is slot leakage: release must happen in a finally, or the error must be collapsed into a result value inside the task. Code that misses this looks perfect on the happy path and only degrades once the downstream starts failing, which makes it one of the hardest bugs to trace.
- Tie it to agents: batch embedding, parallel tool calls, fan-out subtasks. The benefit is not only sparing the downstream — peak memory now scales with the concurrency limit instead of the task count.
- Expect the follow-up: what if tasks retry? Retries must happen inside the slot, otherwise a retry storm bypasses the limiter entirely. One level deeper: add jitter so failed tasks do not all come back at the same instant.
答题要点
- Promise.all 与 asyncio.gather 只负责等待,任务在被创建的那一刻就已经启动了,并发度等于任务总数
- 两种正确形状:固定数量的工人从共享游标取任务,或者用信号量挡在任务启动之前
- 槽位必须在任何退出路径上归还:Java 写在 finally 里,Python 用 async with,Swift 把错误收敛成结果值,JS 在循环里 try 与 catch
- 槽位泄漏的症状是「下游一开始报错就越来越慢直到卡死」,happy path 完全看不出来
- 收益是同时驻留的内存与并发度同阶,而不是与任务总数同阶;重试要占着槽位做,并加抖动
Key points
- Promise.all and asyncio.gather only wait; the work started when each promise was created, so concurrency equals the task count
- Two correct shapes: a fixed worker set pulling from a shared cursor, or a semaphore gating task start
- The slot must be returned on every exit path — finally in Java, async with in Python, error-to-value inside a Swift task, try/catch inside the JS loop
- A leaked slot shows up as gradual slowdown to a full stall once the downstream starts erroring, and is invisible on the happy path
- The payoff is peak memory scaling with the concurrency limit rather than the task count; retries must stay inside the slot and carry jitter
D30 总复盘 + 投递启动:题库全量过一遍、知识地图、第二个月投递节奏、网站公开上线
怎么把零散的知识点组织成一张便于复习的知识地图?How do you turn scattered knowledge into a map that is actually useful for review?
国内高频海外高频进阶#knowledge-organization#interview-prep分析过程 · 先想清楚再作答
- 题眼在「便于复习」四个字。绝大多数人答成「按模块分类、画个思维导图」,那产出的是目录不是地图——目录任何一本书的前几页都有,它不能告诉你先补哪里。区分度就在这儿。
- 怎么拆:一张图有两种元素,节点和边。分层(节点怎么分组)是廉价的、几乎人人做得对;真正的信息量在边上。所以先问自己一个问题——我这张图上有几条边,每条边的含义是什么?答不上来就说明画的是目录。
- 给一条可操作的连边判据:只有当「不懂 A 就学不懂 B」时才连 A 指向 B。「A 和 B 都属于消息队列」不算,那是同层并列;「不理解上下文窗口就理解不了为什么要压缩」算。课程的先后顺序也不算——那是日历,不是依赖。
- 结论:地图的用法是把你的弱点涂上去。某个节点讲不清,先看它的上游是不是也红——是的话补上游,一次带亮一串。这就是地图相对清单的唯一优势:清单说哪里错了,地图说该从哪儿开始。
- 生产视角:这套东西在工作里同样有用。排查一个线上问题时,你脑子里那张「谁依赖谁」的图决定了你先看哪个服务的日志;没有这张图的人只能一个个试。面试时把这个类比说出来,会显得你不是为了背题才画图。
- 可预期的追问:那张图应该多大?答案是能在白板上 5 分钟画完——超过这个规模你会开始维护它而不是使用它,节点合并成主题,细节留在题库里。
How to reason about it · think before answering
- The load-bearing phrase is 'useful for review'. Most answers become 'group things by module and draw a mind map', which produces a table of contents, not a map — a contents page cannot tell you what to fix first. That is where candidates separate.
- Break it down: a graph has nodes and edges. Grouping nodes is cheap and almost everyone does it correctly; the information lives in the edges. So ask yourself how many edges your diagram has and what each one means. No answer means you drew a contents page.
- Give an operational rule for edges: draw A to B only when not understanding A blocks understanding B. 'Both are about message queues' does not qualify — that is sibling grouping. 'You cannot understand context compression without the context window' does. Course order does not qualify either; that is a calendar, not a dependency.
- Conclusion: use the map by painting your weak spots onto it. If a node is shaky, check whether its upstream is shaky too — repair upstream and several downstream nodes light up at once. That is the map's one advantage over a checklist: a checklist says what is broken, a map says where to start.
- Production angle: the same habit pays off at work. When debugging an incident, the dependency graph in your head decides whose logs you open first; without it you probe services one by one. Saying this shows the map is a working tool, not an exam prop.
- Expect the follow-up: how big should it be? Small enough to redraw on a whiteboard in five minutes. Past that you start maintaining the map instead of using it — merge nodes into themes and leave the detail in your question bank.
答题要点
- 分层只是分组,任何目录都做得到;地图的信息量全部在边上
- 连边的判据只有一条:不懂 A 就学不懂 B 才连边,同类并列和课程顺序都不算
- 把弱项涂到节点上,红点扎堆时优先补上游节点,一次带亮一串下游
- 跨层的长边最值钱,面试时顺着长边讲能体现体系,孤立节点只能给出零碎答案
- 规模控制在白板 5 分钟能画完,再细的内容留在题库里而不是图上
Key points
- Grouping into layers is what any table of contents does; a map's information is in its edges
- One rule for edges: draw one only when A is a genuine prerequisite for B — sibling topics and course order do not count
- Paint your weak spots on the nodes and fix upstream first when reds cluster; one fix lights up several downstream nodes
- Long cross-layer edges are the valuable ones — following them in an interview shows a system, isolated nodes only produce fragments
- Keep it redrawable on a whiteboard in five minutes; finer detail belongs in the question bank, not the map