逐日AI

面试题库

共 328 题,当前筛选 15 题。

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

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

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

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

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

    How to reason about it · think before answering

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

    答题要点

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

    Key points

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

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

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

    How to reason about it · think before answering

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

    答题要点

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

    Key points

    • Separate the two SSE modes: native EventSource auto-reconnects with Last-Event-ID but is GET-only; LLM APIs use POST and cannot rely on it
    • The client must therefore detect the break, retry itself, and keep whatever text already arrived
    • Continuation: send the received prefix as context so the model resumes rather than restarting the turn
    • Limits: tool_use and thinking blocks cannot be partially recovered; resume from the last complete text block
    • The server must make retries safe: resumable responses, idempotent tool side effects, correct billing for tokens already produced
  • 移动端 App 里的对话,网络频繁抖动,你会怎么设计重连策略?On mobile, connectivity is flaky. How would you design the reconnection strategy for a chat feature?
    国内高频海外高频进阶#reliability#mobile#streaming

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

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

    How to reason about it · think before answering

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

    答题要点

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

    Key points

    • Mobile differs: network handoffs, OS suspension, limited background time — do not copy the web strategy
    • Exponential backoff with jitter, where jitter prevents a reconnect storm when an outage clears
    • Cap attempts and interval, then hand the user an explicit reload instead of retrying forever
    • Listen to OS connectivity events: stop while offline, reconnect on restore, which saves battery over polling
    • Resume replies via server-side persistence by chat id; queue outgoing messages with idempotency keys

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

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

    1. 这题最容易答成名词解释。区分度在于你能不能指出 ReAct 和那个 while 循环是同一个东西,而不是两套并列的技术——把它们说成两样,面试官会认为你只读过博客没写过代码。
    2. 先给历史脉络:ReAct 出现时模型接口还没有工具字段,做法是在提示词里跟模型约定一套纯文本格式,让它交替吐出 Thought、Action、Action Input,你用正则把动作抠出来执行,再把 Observation 拼回提示词里继续。
    3. 再做映射,这是拿分的一步:今天的 function calling 把这套口头约定固化成了协议——Thought 对应 message.content,Action 对应结构化的 tool_calls,Observation 对应你追加回去的那条 role 为 tool 的消息。所以 ReAct 是那个循环的名字,不是另一种实现。
    4. 把取舍说出来:文本版脆在解析,模型少写一个换行、把参数写成 JSON、把 Action 和 Thought 换个顺序,正则就崩;结构化版把这个包袱交给了服务端,是今天的默认选择。但文本版没死——本地小模型、老接口不支持 tools 字段时,回退到「提示词约定 + 正则」仍是唯一可行的兜底,代价是解析失败率自己扛。
    5. 可以预期的追问:要不要让模型显式写出 Thought?它多花 token,但复杂任务的准确率通常更好,日志也终于可读。这是一个可调旋钮,不是必选项,按任务复杂度决定。
    6. 第二个追问:ReAct 和先规划后执行(Plan-and-Execute)有什么区别?ReAct 每一步都重新决策,边走边看,适合环境会变、信息要边查边补的任务;先规划后执行一次性出完整计划,步数和成本更可控,但对中途出现的意外不敏感。真实系统常常混用:先出一个粗计划,每一步内部再走 ReAct。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 题眼在后半句。只答「catch 住、打日志、返回错误」是普通后端思维,答不出「在 Agent 里错误是给模型的反馈」就拿不到区分度分。
    2. 先分类,判据是一句话:这个错误模型改得动吗?参数格式不对、缺了必填项、值不在枚举里、单位没去掉——模型改得动,回传,并且要把「正确的样子」写进错误文案,否则它只会换个花样再错一次。反过来,数据库连不上、下游服务 500、凭证过期,模型改一万遍参数也没用,这类该由代码决定重试还是终止,回传只是让它空转烧钱。
    3. 结论落到形式上:值得回传的错误要变成一条正常的 role 为 tool 的消息,tool_call_id 照样对上,而不是抛异常终止循环。抛了用户看到 500;回传了模型往往下一轮就自己改对,这是 Agent 稳定性最便宜的一份来源。
    4. 接着答「不该传的」:绝不回传原始异常堆栈。堆栈里有文件路径、内部服务名,有时还有连接串,它会原封不动进入下一次请求,也可能被模型复述给用户;而且动辄上千 token,每一轮都跟着历史重发。回给模型的必须是你自己写的一句话,原始堆栈只进日志。
    5. 可以预期的追问:模型一直改不对怎么办?错误也要计入步数,撞上步数上限就终止并给用户一句交代;再进一步,同一个工具连续失败若干次可以直接把它从这一轮的可用工具里摘掉,逼模型换条路。
    6. 第二个追问:这和模型层的 fallback 是一回事吗?是同一套判断的两侧——那边问「换一家 provider 有没有可能变好」,这边问「让模型改一改有没有可能变好」,都是先分类再决定重试,一刀切重试在两边都是错的。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 题眼在「权衡」。答「框架更快」或者「手写更可控」都只说了一半,面试官想听的是你有没有一条能当场执行的判据,而不是立场。
    2. 给判据:问自己「我需不需要看见并改动这段循环里的每一步」。需要就手写——学习调试阶段、合规审计要求每次模型调用和工具调用都可拦截可留痕、或者场景本身只有一两个工具两三轮循环,那点代码量的收益抵不过一整棵依赖树。
    3. 不需要就用框架,判断标准是这几件事你是不是迟早都要做:工具数量上去、要把每一步实时推给前端、会话要能重启后继续、上下文满了要压缩、要随时换模型。这些凑齐了就是一个小型框架,自己写等于重新发明一个没人帮你测的版本。
    4. 然后主动说出框架的三笔代价,这是区分度所在:一是排障栈变深,工具没被调用可能是描述、schema、钩子拦截三种完全不同的原因;二是你继承了一堆没写过的默认值,模型、系统提示词、内置工具都是别人替你选的;三是升级会改变你没测过的行为,代码一行没动线上表现却变了,这类问题最难定位。
    5. 结论要给出可落地的折中:先手写一遍把循环吃透,再上框架;上了框架也要显式覆盖掉默认值,并把框架版本锁死。这样既拿到了开发速度,也没把行为的控制权整个交出去。
    6. 可以预期的追问:那你怎么评估一个框架好不好?答看它的分层能不能让你「只要一半」——只要模型调用层、循环自己写行不行;必须整包吞下的框架,迟早要为用不上的那一半付代价。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 题眼在「权衡」两个字——面试官不要你背一个 for 循环,他要看你知不知道 fallback 是有代价的。
    2. 先拆出第一个关键判断:不是所有错误都该 fallback,而分类的依据不是状态码的首位数字,是「换一家有没有可能变好」。400 请求体不合法、403 被安全策略拦截,换谁都一样,重试只是把同一个 bug 再犯一遍、白花两倍的钱和时间;408 超时、429 限流、5xx 服务端故障是对方的问题,换一家大概率能成。最容易答错的是 402 余额不足和 404 模型被下线或改名——它们同属 4xx、长得像「你的问题」,其实换一家完全可能成功;401 则要看凭证怎么管,三家共用一把网关 key 时换了也没用,各有各的 key 时 A 被吊销切到 B 完全能救。分类是 fallback 的第一步,不是重试。
    3. 再说成本:切换意味着同一段 prompt 你付了两次钱,三家链路最坏是三倍成本。这条一定要主动说出来,它区分了「写过」和「上过线」。
    4. 然后是延迟:串行 fallback 的总耗时是各家超时值的累加。如果每家给 15 秒、三家串下来用户要等 45 秒,那还不如早点失败。所以超时值必须按 provider 分别设,且要设总预算上限。
    5. 最后是雪崩,这是最容易被追问的点:主 provider 限流时你把全部流量瞬间压到备用上,很可能把备用也压垮。所以要加熔断——连续失败 N 次就暂时摘掉该 provider,过一段时间放少量流量试探。
    6. 可以预期的追问:怎么知道该摘多久?答案是指数退避 + 半开状态试探,和数据库连接池的熔断是同一套思路。

    How to reason about it · think before answering

    1. The word 'trade-offs' is the hinge: they are not asking for a for-loop, they want to know you understand fallback has costs.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 前半句是送分题,后半句才是区分度所在——很多人答得出 system prompt 是干什么的,答不出「默认值有什么坑」。
    2. 先说作用:它是唯一一段在整段对话里权重稳定、不会被后续几十轮稀释的指令,用来设定身份、能力边界和输出格式。
    3. 再答「为什么不能用默认的」,要给出三条具体后果而不是泛泛说「不够定制」:一是它不知道你的业务边界,用户问业务外的问题它会热情地答;二是它不约束输出格式,前端样式会被冷不丁冒出的 Markdown 标题打乱;三是最要命的——它会随框架升级而变化,你测好的所有行为建立在一段看不见的文本上,出 bug 时你的代码一行没动,极难排查。
    4. 结论落到工程做法:生产环境的 system prompt 是拼出来的模板,结构是「人设 + 能力边界 + 输出要求 + 动态上下文」,最后一块每次请求现拼。
    5. 可以预期的追问:动态上下文里最容易漏什么?答「当前时间」——模型没有时钟,不告诉它今天几号,它算不出「三天前下的单」是哪天。这个细节很能体现有没有真做过。
    6. 第二个追问:prompt 怎么测试?答案是把它当配置而不是代码——存库、加版本号、支持按比例灰度,因为你没法写单元测试断言「模型语气变友好了」。

    How to reason about it · think before answering

    1. The first half is a warm-up; the discriminating half is why the default is dangerous.
    2. State the role: it is the one instruction block whose weight stays stable across dozens of turns, setting identity, capability boundaries and output format.
    3. 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.
    4. 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.
    5. 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'.
    6. 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

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

    1. 这题考的是「你有没有真的在花自己的钱」。答「用最好的模型」是最差的答案,答「按需选择」太空,要给出可执行的分档维度。
    2. 先建立核心事实:不同模型的价格能差 50 倍以上,而你的任务里很大一部分根本不需要最强的模型。用旗舰模型做意图识别,等于开跑车去楼下取快递。
    3. 然后给出三个可操作的路由维度:任务类型(分类抽取走便宜模型,长文推理走强模型)、延迟要求(前台用户在等就走低延迟,后台批处理可以慢而便宜)、输入长度(超长上下文只有部分模型支持且价格陡增)。
    4. 结论要落到数字上才有说服力:1 万轮对话每轮 2000 token,全走旗舰约 300 元一天;把六成粗活改走小模型后降到 125 元左右,一年省六万多,用户感知不到差别。
    5. 还要主动说出实现上的取舍:先按任务类型静态分档,不要一上来就做「让模型判断该用哪个模型」的动态路由——那个方案本身又要多一次模型调用,延迟和成本可能得不偿失,等有真实数据再优化。
    6. 可以预期的追问:怎么验证降档没有损失质量?答案是准备 golden set,对同一批输入跑两档模型,用人工或 LLM-as-judge 比对准确率,把降档决策建立在数据上而不是感觉上。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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).
    4. 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.
    5. 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.
    6. 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

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

    1. 这题在考你有没有真的做过工具循环。只答「把错误告诉模型」是及格线,区分度在两个地方:错误信息长什么样,以及你有没有给它设刹车。
    2. 先给机制,一句话就能说清:错误不是异常,是数据。工具成功时你把结果包成一条 tool 消息追加进 messages 再继续循环,失败时走同一条路,只是内容换成错误描述。异常一路抛出、终止这一轮,是最常见的错误做法。
    3. 再给判据:好错误信息有三个要素——错在哪个字段、期望是什么、一个合法示例。对比三档就很清楚:「工具执行失败」模型只能原样重试或放弃;「order_id 格式不正确」它知道错在哪却不知道对的长什么样,可能试出一个新错法;「参数 order_id 需要 SO 开头加 8 位数字,例如 SO20260901,你传的是数字 12345」基本一次改对。另外校验要一次报全部错误,报了第一条就返回会让模型多跑好几轮。
    4. 然后主动说代价,这是面试官等的:一次自纠错等于多两条消息加一次完整的模型调用,延迟和 token 都翻倍;更凶的是死循环——错误信息含糊时模型会以近乎相同的方式反复重试。所以必须设三道闸:单工具连续失败 2 次就停手转人工、整轮工具调用总次数上限、整轮 token 预算,哪个先到都终止。
    5. 最后划一条边界,它和 D4 的 fallback 判据同源:判断依据是「模型改参数有没有可能变好」。校验失败、订单不存在、日期超范围——回传。数据库连不上、下游 503、密钥过期——模型改一百遍参数也没用,应该直接失败并告警,回传只会让它朝错误方向瞎试。
    6. 可以预期的追问:回传的错误信息里能放什么?只能放字段名、期望格式和示例;栈信息、SQL、内部路径、真实表名一律不能进,因为模型会把它复述给用户。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 这题看起来是背清单,实际考的是你有没有做过带界面的 Agent。只报事件名不解释用途,会被判成看过文档但没接过前端。
    2. 先说动机:一次带工具的循环短则几秒长则几分钟,中间要调模型、调工具、可能还失败重试,而返回值只有最后一句话。对调用方来说中间全是黑盒——不知道它在干什么,也不知道该不该再等。
    3. 再报清单并各配一句用途:run:start / run:end / run:error 是一轮的开始与两种结束;model:delta 是模型吐出的文本片段,前端拿它做打字机效果;tool:proposed 是模型决定要调工具但还没执行,权限确认就挂在这个事件上;tool:start / tool:end / tool:error 是工具执行的三个出口,tool:end 带耗时;approval:required 让界面弹确认框。
    4. 然后说出这套设计真正的价值:同一条事件流同时喂三个消费者——界面渲染进度、日志系统做链路追踪、计量系统拿 run:end 的 token 数算成本。不为三件事写三套埋点,这是架构判断而不是 API 罗列。
    5. 补两条实现纪律,能显著拉开差距:事件必须带 runId 和自增序号,因为跨进程传输后顺序不保证;监听器里不写业务逻辑,且监听器抛错不能炸掉主循环——事件是旁路不是主干。
    6. 可以预期的追问:model:delta 一个 token 一条事件会不会太多?会,所以要按时间窗合批,攒 50 毫秒推一次,用户感知不到差别而消息量掉一个数量级。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 这题的区分度不在「用摘要」三个字上,几乎人人都答得出。区分度在你有没有说出触发时机和保留清单——只答「让模型总结一下前面的对话」的,面试官会判定你没在长对话上线过。
    2. 先把问题拆成三问再逐个答:什么时候压、压掉什么、保留什么。这个拆法本身就是加分项,因为它说明你把压缩当成一个策略而不是一个函数。
    3. 触发用阈值不用定时器,也不能等报错。给一个具体数字并解释它:历史占用到预算的七成就动手,因为摘要本身是一次模型调用,有延迟也可能失败,卡到九成再压,一旦摘要超时下一轮就直接撞窗口上限了——七成是留给自己的抢救时间。
    4. 压掉的是过程性内容:中间推理、已经被消费完的工具原始返回值、用户后来推翻的需求。它们的共同点是价值已经沉淀进后面的结论里。保留的是系统提示词(它不属于历史)、最近若干条原文、以及用户明确声明过的约束和事实——后者写错了模型会当场失忆。
    5. 再补一条别人不会说的:切口必须对齐到一轮的开头。切在 assistant 的 tool_calls 和对应的 tool 结果中间,下一次请求就有了悬空调用,多数厂商的 API 直接返回 400。这一条最能证明你真的调过。
    6. 可以预期的追问有两个。一是摘要该用哪个模型:用便宜的小模型就行,摘要是抽取任务不是推理任务,这也接上了 D4 的分层路由。二是摘要调用失败了怎么办:降级到不摘要的滑动窗口(直接丢最早的几轮),保证请求发得出去,别让压缩失败连带整轮对话失败。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

    1. 题干把三件事并列,考的其实是你能不能分清它们各自的动机——很多人会把三个都答成「存下来」,那就丢掉了全部区分度。
    2. 先一句话各给一个动机:持久化解决「进程重启和跨机器请求」,恢复解决「加载回来还能接着聊」,分叉解决「同一段历史要走出两条不同的后续」。动机不同,所以数据结构的要求也不同。
    3. 持久化的关键选择是只追加还是快照。答只追加并给理由:写入不受历史长度影响、能回放到任意一步、有审计轨迹;快照只是读加速手段,工程上常见的是「只追加为准 + 定期快照」。这一条直接决定了分叉能不能做。
    4. 恢复的两个坑要主动说。一是把当时的系统提示词一起存进了历史,里面有「现在时间」这类动态上下文,三天后读出来模型的日期判断全错——系统提示词不进持久化历史,每次现拼。二是存档存在了工具调用中途,最后一条是没有配对结果的 tool_calls,直接发出去就是 400,加载后必须做完整性校验,补一条「执行被中断」的结果或丢弃这条尾巴。
    5. 分叉最容易被忽视的是「父会话只读」这条语义。分叉不是回滚:回滚砍掉历史继续用,是破坏性的;分叉复制前 k 条长出新枝,两边都能继续。实现上要深拷贝,直接引用父会话的消息对象会让两条分支互相污染。
    6. 可以预期的追问:分叉多了存储怎么办?答按父引用加偏移存、读时拼接,代价是读路径变复杂;再顺手补一句成本要能顺着 parentId 聚合成一棵树,否则账算不清是哪个用户的哪次重试花的钱。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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

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

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

    How to reason about it · think before answering

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

    答题要点

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

    Key points

    • A script's four assumptions all break in a service: single user, serial execution, trusted input, and a process that dies with the session
    • Session state must be keyed by session id, and in-process storage means data is lost on restart and blocks horizontal scaling
    • Four interface decisions: response shape, session identity, auth and rate limiting including a per-call token ceiling, and error semantics
    • A streaming endpoint cannot report errors by status code after the first byte, so define an in-stream error event and move all validation ahead of it
    • Expose a health endpoint, or orchestrators cannot tell whether the instance is ready for traffic
  • 把一个 Node 服务打包成 Docker 镜像,Dockerfile 里有哪些关键决定?What are the key decisions in a Dockerfile that packages a Node service?
    国内高频海外高频进阶#docker#deployment#nodejs

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

    1. 这题看着是背步骤,其实考的是「你有没有为构建速度和安全性做过取舍」。把 FROM、COPY、RUN、CMD 顺着念一遍是最没有区分度的答法。
    2. 第一个决定是指令顺序,也是唯一能立刻量化收益的:镜像是逐层叠出来的,某层的输入没变就复用缓存。所以先只拷 package.json 和 lockfile、装完依赖再拷源码——改一行业务代码只让最后两层失效,依赖那层照旧命中;反过来一上来就 COPY 全部,改一个字都要重装依赖。
    3. 第二个是基础镜像钉版本。写 latest 等于让镜像在某天悄悄升到下一个大版本,可复现性当场归零,而可复现正是用容器的全部理由。
    4. 第三个是运行时配置:容器里必须监听 0.0.0.0,只听 127.0.0.1 的话它只在容器内部可达,宿主机做了端口映射也连不上——这个坑在本机跑的时候完全正常,所以特别常见。另外 EXPOSE 只是声明意图,真正开端口的是 docker run 的 -p。
    5. 第四个是安全:用非 root 用户跑业务进程(容器和宿主机共用内核,逃逸后 root 的破坏面大得多),.dockerignore 排除 node_modules(宿主机的二进制在 Linux 容器里跑不起来,还会让构建上下文暴涨)和 .env(密钥打进镜像等于发给每个能拉到镜像的人,运行时用 --env-file 传)。
    6. 可以预期的追问,也是长连接服务最该主动说的一条:CMD 要用数组形式直接起 node,让它当 PID 1。写成 pnpm start 的话 PID 1 是包管理器,docker stop 的 SIGTERM 未必传得到 node,优雅退出代码永远不执行,只能等十秒超时被 SIGKILL——对 SSE 服务,那意味着所有在途的流被硬切。

    How to reason about it · think before answering

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.
    6. 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