逐日AI

面试题库

共 328 题,当前筛选 11 题。

标签
还有 126 个标签
#api-design2#chunking2#cost-tradeoff2#debugging2#distributed-systems2#error-handling2#hybrid-search2#llm-as-judge2#multi-agent2#multi-hop2#oauth2#operations2#pipeline-design2#prompt-caching2#rag2#recall2#retrospective2#retry2#scheduling2#sse2#tool-permissions2#trade-offs2#access-control1#agentic-rag1#agents-sdk1#analytics1#architecture-review1#behavioral1#budget-control1#caching1#cancellation1#checkpointing1#circuit-breaker1#citation-verification1#client1#client-integration1#coding-agent1#compaction1#compliance1#concurrency1#confused-deputy1#context-engineering1#contextual-retrieval1#copyright1#correctness1#cost-control1#customer-support1#data-quality1#database1#deployment1#distribution1#embedding-migration1#error-propagation1#escalation1#evidence1#faithfulness1#fallback1#feedback-loop1#fencing-token1#filter-pushdown1#filtering1#framework-design1#graph-rag1#guardrails1#handoff1#image-generation1#integration1#iterative-scan1#json-parsing1#labeling1#latency1#latency-budget1#least-privilege1#long-context1#long-session1#long-term-memory1#mcp1#message-bus1#methodology1#model-migration1#multi-tenancy1#notifications1#ocr1#offline-testing1#project-storytelling1#protocol-versions1#quality1#query-rewriting1#rate-limiting1#reconnect1#refusal1#replay1#reproducibility1#rerank1#resume1#retrieval1#risk-assessment1#rollout1#routing1#runtime1#safety1#scaling1#self-introduction1#split-brain1#state-management1#state-persistence1#statelessness1#stdio-transport1#storytelling1#subagent1#subagents1#subscriptions1#test-strategy1#thresholds1#timezone1#token-accounting1#tool-design1#tool-schema1#tools1#trust-boundary1#ux1#verification1#versioning1#workflow-design1#workflow-engine1#zero-downtime1

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

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

  • 用户切到后台或者直接关掉网页,回来后怎么恢复那条还在生成的回复?The user backgrounds the app or closes the tab. How do you restore a reply that was still being generated?
    国内高频海外高频深入#streaming#reliability#architecture

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

    1. 先识别这题和「网络断了」不是同一个问题:客户端已经不存在了,任何写在前端的重试逻辑都不会执行。
    2. 由此推出唯一出路:生成过程必须能脱离这个客户端独立存活,也就是把流本身放到服务端持久化。
    3. 落到具体架构:发起请求时给这轮生成分配一个流 id,服务端一边把 token 推给当前连接,一边把同样的内容写进 Redis 之类的存储;会话记录里保存这个 activeStreamId。
    4. 恢复路径是另开一个 GET 端点:客户端带着会话 id 请求,服务端按 activeStreamId 找到那条流并接着推;找不到活跃流就返回 204,让前端知道没有需要恢复的东西。
    5. 说清代价,别只说方案:多了一份存储、一套过期清理、以及「同一条流可能被多个连接消费」的并发问题。
    6. 延伸:这套结构和普通聊天产品的「消息已持久化,重进会话直接读库」不同——区别在于回复还在生成中,需要的是可续的流而不是一条静态记录。

    How to reason about it · think before answering

    1. First separate this from a dropped connection: the client is gone, so no client-side retry will ever run.
    2. That leaves one option — the generation must outlive the client, which means persisting the stream server-side.
    3. Concretely: assign a stream id per generation; the server pushes tokens to the live connection while also writing them to storage such as Redis, and the chat record stores that activeStreamId.
    4. Recovery is a separate GET endpoint: the client asks with the chat id, the server locates the stream by activeStreamId and resumes; with no active stream it returns 204.
    5. Name the costs, not just the design: extra storage, expiry/cleanup, and concurrency when several connections consume the same stream.
    6. Extension: this differs from ordinary message persistence because the reply is still being produced — you need a resumable stream, not a static row.

    答题要点

    • 客户端已经不在了,前端重试无从谈起,必须让生成过程在服务端独立存活
    • 发起生成时分配流 id,服务端边推送边把内容写进 Redis,会话里记录 activeStreamId
    • 恢复走单独的 GET 端点:按会话 id 找到活跃流接着推,没有活跃流就返回 204
    • 代价:额外存储、过期清理,以及同一条流被多个连接消费的并发处理
    • 与「消息持久化后重新读库」的区别在于回复仍在生成中,需要的是可续的流

    Key points

    • The client is gone, so recovery must live server-side: the generation has to outlive the connection
    • Assign a stream id at start; the server writes tokens to Redis while streaming, and the chat stores activeStreamId
    • Resume through a dedicated GET endpoint that replays the active stream, returning 204 when there is none
    • Costs: extra storage, expiry and cleanup, and concurrent consumers of one stream
    • It differs from plain message persistence because the reply is still in flight, so you need a resumable stream
  • 断线重试之后,怎么保证不重复计费、也不重复执行已经做过的工具调用?After a retry, how do you avoid double billing and re-executing tool calls that already ran?
    国内高频海外高频深入#reliability#tools#idempotency

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

    1. 先把问题拆成两半:计费是「记录问题」,工具副作用是「执行问题」,两者的解法不同,混在一起答会含糊。
    2. 计费侧:用量应该在服务端按实际收到的 token 记账,而不是按「请求次数」。断在中途已经产生的 token 是真实成本,要照记;重试产生的是新成本,也要照记——关键是别把同一批 token 记两遍。
    3. 为此需要一个稳定的标识:给每轮生成一个 run id,用量记录以 run id + 序号去重,重放同一段不会重复入账。
    4. 工具侧:真正危险的是有副作用的工具(转账、发消息、下单)。解法是幂等键——由调用参数派生一个稳定的 key,执行前先查这个 key 是否已有结果,有就直接返回旧结果。
    5. 补一层状态机视角:把每次工具调用记为「待执行 / 执行中 / 已完成」,重试时只重放未完成的部分,已完成的直接取结果,这也是恢复中断任务的通用做法。
    6. 常见追问:幂等键该谁生成?应由客户端或调度侧生成并随请求传递,服务端自己生成就没法跨重试保持一致。

    How to reason about it · think before answering

    1. Split it in two: billing is a bookkeeping problem, tool side effects are an execution problem, and they have different fixes.
    2. Billing: meter server-side by tokens actually produced, not by request count. Tokens produced before the break are real cost; so are retry tokens. The point is not to count the same batch twice.
    3. That needs a stable identifier: give each generation a run id and dedupe usage records by run id plus sequence.
    4. Tools: the danger is side-effecting tools — transfers, messages, orders. The fix is an idempotency key derived from the call arguments, checked before execution.
    5. Add the state-machine view: record each call as pending / running / done and replay only what is unfinished.
    6. Follow-up: who generates the idempotency key? The caller must, and pass it along — a server-generated key cannot stay stable across retries.

    答题要点

    • 拆成两个问题:计费是记账问题,工具副作用是执行问题,解法不同
    • 计费按服务端实际产生的 token 记,用 run id 加序号去重,避免同一批 token 重复入账
    • 有副作用的工具用幂等键:由调用参数派生稳定 key,执行前先查是否已有结果
    • 把每次工具调用记成待执行/执行中/已完成的状态机,重试只重放未完成的部分
    • 幂等键要由调用方生成并随请求传递,服务端自行生成无法跨重试保持一致

    Key points

    • Separate billing (bookkeeping) from tool side effects (execution); they need different mechanisms
    • Meter by tokens actually produced, deduped by run id plus sequence so one batch is never counted twice
    • Guard side-effecting tools with an idempotency key derived from the call arguments
    • Model each tool call as pending / running / done and replay only unfinished work
    • The caller must generate and pass the idempotency key so it stays stable across retries
  • 用户主动点「停止生成」和网络意外断开,在服务端看起来都是连接没了,怎么区分处理?A user pressing stop and a dropped connection both look like a closed connection server-side. How do you tell them apart?
    国内高频海外高频深入#streaming#reliability#ux

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

    1. 先点破为什么要区分:主动停止是「用户不想要了」,应当立即释放算力并结束这轮;意外断开是「用户还想要」,理应保留结果供恢复。处理反了,用户要么白花钱,要么回来发现内容没了。
    2. 所以不能只靠 TCP 连接状态判断——它对两种情况的表现是一样的。必须有一个显式信号。
    3. 做法是给「停止」单独一个接口:前端点停止时先调这个接口,带上 run id,服务端据此把该轮标记为「用户取消」,再中止上游模型调用。
    4. 而单纯的连接关闭一律按「意外断开」处理:继续把已生成内容落盘、保留可恢复的流,等客户端回来续。
    5. 补一个现实约束:停止请求本身也可能因为断网而发不出去。所以服务端还需要兜底——比如流没有任何消费者超过一定时间就自行结束,避免算力空转。
    6. 延伸到计费:两种情况都要为已经产生的 token 计费,因为上游厂商已经收了钱;区别只在于要不要保留结果和是否继续生成。

    How to reason about it · think before answering

    1. Say why it matters: stop means the user no longer wants the output, so free compute and end the run; a drop means they still want it, so preserve the result for resumption.
    2. Connection state alone cannot distinguish them — it looks identical — so you need an explicit signal.
    3. Give stop its own endpoint: the client calls it with the run id before closing, and the server marks the run as user-cancelled and aborts the upstream call.
    4. Treat a bare connection close as an unexpected drop: keep persisting output and hold the stream for resumption.
    5. Add the real-world caveat: the stop request itself may fail to send when the network is down, so the server needs a fallback — end a stream with no consumer after a timeout.
    6. Extend to billing: both cases still owe for tokens already produced, since the upstream provider has charged; they differ only in whether output is retained.

    答题要点

    • 两者语义相反:主动停止要立即释放算力并结束,意外断开要保留结果等待恢复
    • TCP 连接状态无法区分,必须有显式信号:给停止单独一个接口,带 run id 标记为用户取消
    • 只收到连接关闭一律按意外断开处理,继续落盘并保留可恢复的流
    • 兜底:停止请求本身也可能发不出去,服务端需对长时间无消费者的流自行结束
    • 计费上两者都要为已产生的 token 记账,区别只在于是否保留结果、是否继续生成

    Key points

    • The semantics are opposite: stop frees compute immediately, a drop preserves output for resumption
    • Connection state cannot distinguish them, so add an explicit stop endpoint carrying the run id
    • Treat a bare close as an unexpected drop: keep persisting and hold the stream for resume
    • Fallback: the stop call may itself fail to send, so end streams with no consumer after a timeout
    • Both still bill for tokens already produced; they differ only in retention and whether generation continues

D2 工具调用原理:JSON Schema、tool_use 循环;不用框架手写 Agent Loop

  • 怎么防止 Agent 循环停不下来?只加一个最大步数够吗?How do you keep an agent loop from running forever — is a max-step counter enough?
    国内高频海外高频深入#agent-loop#reliability#cost

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

    1. 后半句是明摆着的陷阱。只答「加一个计数器」是及格线,面试官真正想听的是你知道计数器拦不住什么。
    2. 先解释它为什么会停不下来:停止原因一直是 tool_calls,通常是因为工具返回的东西没帮模型前进——结果为空、字段答非所问、错误文案没说清该怎么改,于是它换个参数一试再试。所以第一层其实不是护栏,是把工具的返回值和错误文案写得有信息量。
    3. 再给硬护栏,三条互补:步数上限最直接;token 与成本预算拦的是「步数不多但每步都很贵」;单轮的墙上时钟超时拦的是「一步就卡了两分钟」。只有步数上限的系统,照样会被一次超长上下文的调用打爆预算。
    4. 语义层面再加一条:检测重复调用。同一个工具、同一份参数连续出现两次以上,几乎可以断定它在原地打转,直接截断并把「你已经用完全相同的参数调过这个工具了,换个思路或者告诉用户你做不到」回传给模型,往往比等步数耗尽更快收敛。
    5. 触顶之后必须有交代:不能静默返回空字符串,要给用户一句能理解的话;同时把触顶记成一个指标,触顶率上升通常意味着某个工具的描述或返回值该改了,而不是把上限调大。
    6. 可以预期的追问:上限设多少?没有普适值。聊天类任务 5 到 10 步通常够,需要多轮检索的任务可以更高。正确做法是看线上的步数分布,取 p99 再留一点余量,而不是拍脑袋——上限设得越死,你的系统就越靠近固定流程那一端,越不像一个 Agent。

    How to reason about it · think before answering

    1. The second half is an open trap. 'Add a counter' is the passing grade; what they want is whether you know what a counter cannot catch.
    2. Explain why it runs away first: the finish reason stays tool_calls because the tool results are not moving the model forward — empty results, fields that do not answer the question, error text that never says what correct looks like. So the first line of defense is not a guard rail at all; it is writing tool results and error messages that carry information.
    3. Then three complementary hard limits: a step cap is the obvious one; a token and cost budget catches 'few steps, all of them expensive'; a per-step wall-clock timeout catches 'one call hung for two minutes'. A system with only a step cap can still blow its budget on a single enormous context.
    4. Add a semantic guard: detect repeats. The same tool with identical arguments twice in a row is almost always spinning. Cut it short and tell the model so — 'you already called this tool with exactly these arguments' — which usually converges faster than waiting for the counter to run out.
    5. Hitting the cap needs an honest ending: never return an empty string, give the user a sentence they can act on, and record cap hits as a metric. A rising cap-hit rate usually means a tool's description or return value needs fixing, not that the cap should be raised.
    6. Expect: what number do you pick? There is no universal one. Chat-style tasks usually fit in five to ten steps; retrieval-heavy tasks need more. Read the production distribution, take p99 plus headroom, and remember that the tighter the cap, the closer your system sits to a fixed workflow rather than an agent.

    答题要点

    • 根因通常是工具返回值或错误文案没信息量,模型无法前进只能反复重试,先把这层写好
    • 三条硬护栏互补:最大步数、token 与成本预算、单步墙上时钟超时,只有步数上限并不够
    • 语义护栏:同一工具加同一份参数连续重复调用即判定原地打转,截断并把这个事实回传给模型
    • 触顶要给用户一句交代,不能静默返回空;同时把触顶率当指标,上升说明工具该改而不是把上限调大
    • 上限值按线上步数分布取 p99 加余量;上限越死越接近固定流程,越不像 Agent

    Key points

    • The root cause is usually uninformative tool results or error text, so fix that layer before adding guards
    • Three complementary hard limits: max steps, a token and cost budget, and a per-step wall-clock timeout
    • Add a semantic guard: identical tool plus identical arguments twice in a row means it is spinning — cut it and tell the model
    • Give the user an honest message when the cap is hit, and track the cap-hit rate as a signal that a tool needs fixing
    • Size the cap from the production step distribution, not intuition; a tighter cap makes the system a workflow rather than an agent

D7 封装成服务:Fastify + SSE + Docker(dg P07);W1 复盘

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

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

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

    How to reason about it · think before answering

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

    答题要点

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

    Key points

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

D8 为什么 Gateway/Worker 分离;Postgres 表设计(sessions/runs/messages)+ Drizzle

  • 消息总线是至少一次投递,同一条消息被重复投递时,怎么保证不会产生两条 run?With at-least-once delivery, how do you guarantee a redelivered message does not create two runs?
    国内高频海外高频深入#idempotency#database#reliability

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

    1. 这题在考幂等的落点在哪一层。凡是答「在代码里先查一下有没有,没有再插入」的,基本当场结束——因为那正是这题想筛掉的答案。
    2. 先把前提摊开:重复不是意外。总线是至少一次语义、客户端会超时重发、用户会手抖双击,同一句话到达两次是必然事件。所以设计目标不是「避免重复到达」,而是「重复到达时结果相同」。
    3. 然后给推导:幂等需要一个由请求内容决定的键。随机 UUID 每次都不同,等于没有幂等;正确取法是把会话 id、客户端消息 id、消息内容拼起来做哈希,客户端没有消息 id 时退用内容加一个粗粒度时间窗。
    4. 结论落在存储层:在 runs 表的这一列上加唯一约束,插入写成「冲突就什么都不做」,返回零行时回查那条已有的 run,把同一个 runId 返回给用户。两次请求、一条 run、一个 runId。
    5. 解释为什么「先查后插」不行,这是本题的分水岭:两个 Gateway 实例可以同时查、同时发现没有、同时插入,这两步之间有一个应用层拦不住的时间窗;它窄到压测复现不出来,上线后每天漏几条。**幂等的最终裁判必须是数据库的唯一约束**,应用层的判断只是为了少一次插入尝试。
    6. 可以预期的追问:那消费侧的重复执行呢?答:唯一约束保证了只有一条 run,但 Worker 可能重复拿到同一条 run,所以状态迁移也要带条件更新(只有当前状态是 pending 时才能改成 running),并且用一个显式的迁移白名单挡住「已完成的 run 被推回运行中」这种会覆盖用户已收到回复的情况。

    How to reason about it · think before answering

    1. This question is about which layer idempotency lives in. Anyone who answers 'check whether it exists, then insert' has usually just failed it — that is exactly the answer being screened out.
    2. State the premise: duplicates are not accidents. The bus is at-least-once, clients retry on timeout, users double-click. The same message arriving twice is certain, so the goal is not to prevent duplicates but to make duplicates produce the same result.
    3. Then derive the key: idempotency needs a key derived from request content. A random UUID differs every time and buys nothing; hash the session id, the client message id and the message body together, falling back to content plus a coarse time bucket when the client has no id.
    4. Land it in storage: put a unique constraint on that column in the runs table, write the insert as on-conflict-do-nothing, and when it returns zero rows read back the existing run and return the same run id. Two requests, one run, one id.
    5. Explain why check-then-insert fails, which is the whole point: two gateway instances can query, both see nothing, and both insert. The window between the two statements cannot be closed in application code, it is too narrow to reproduce under load tests, and it leaks a few bad rows every day in production. The database's unique constraint has to be the final arbiter; the application-level check only saves a wasted insert.
    6. Expect the follow-up: what about duplicate execution on the consumer side? The unique constraint gives you one run, but a worker can still receive it twice, so status changes need conditional updates (move to running only if the current status is pending) plus an explicit transition whitelist that blocks a finished run from being pushed back to running and overwriting a reply the user already saw.

    答题要点

    • 重复投递是必然事件,设计目标是「重复到达时结果相同」,不是「避免重复」
    • 幂等键必须由请求内容决定:会话 id 加客户端消息 id 加内容做哈希,随机 UUID 等于没有幂等
    • 在 runs 的幂等键列上建唯一约束,插入用「冲突就什么都不做」,零行时回查已有 run 返回同一个 runId
    • 先查后插在并发下必然出双份,两条语句之间的时间窗应用层拦不住,幂等的最终裁判是数据库唯一约束
    • 消费侧还要用条件更新加状态迁移白名单,避免同一条 run 被重复执行或把已完成的回复覆盖掉

    Key points

    • Redelivery is certain, so the goal is identical outcomes on duplicates, not preventing duplicates
    • The idempotency key must be derived from request content — session id plus client message id plus body, hashed; a random UUID buys nothing
    • Put a unique constraint on that column, insert with on-conflict-do-nothing, and read back the existing run when zero rows return
    • Check-then-insert races under concurrency; the window between the statements cannot be closed in application code, so the unique constraint must be the final arbiter
    • On the consumer side add conditional status updates and a transition whitelist so a finished run is never re-run or overwritten

D9 Redis Streams 消息总线:XADD/XREADGROUP/XACK/XAUTOCLAIM、consumer group、毒消息

  • 什么是 at-least-once?既然消息会被重复投递,业务上到底要怎么保证幂等?What is at-least-once delivery, and given that messages get redelivered, how do you actually make the business side idempotent?
    国内高频海外高频深入#message-bus#idempotency#reliability

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

    1. 这题是本章最容易被追到底的一道。绝大多数人能说出「至少一次,所以业务要幂等」,然后就没有下文了——面试官等的恰恰是下文:幂等具体落在哪一行代码上。答不出具体落点,前半句就是背的。
    2. 先解释为什么消费不掉这个重复:写业务和销号是两个系统的两次写(比如 Postgres 加 Redis),处理完成到 XACK 之间必然存在一个可以崩溃的窗口,崩在那里消息就会被重投。这个窗口只能变小,不能消失,所以 exactly-once 不是总线给你的语义。
    3. 由此得到一句可以直接说出口的结论:exactly-once 是消费端幂等做出来的**效果**,不是中间件提供的**能力**。Kafka 的事务能在「读 Kafka 写 Kafka」的闭环里做到,一旦下游是数据库或第三方 API 就又退回至少一次。
    4. 然后给具体落点,两道闸门要分清各自挡什么:第一道是 runs 表 idempotency_key 上的唯一约束,配 insert on conflict do nothing,挡的是**客户端重复提交**——冲突时接入层直接返回已有的 runId,连总线都不投第二遍;第二道是 messages 表的 unique(run_id, seq),同样 on conflict do nothing,挡的是**同一条总线消息被执行两遍**,就算两个消费者真的同时跑完,用户也只会看到一条回复。中间还可以加一道便宜的短路:捞到消息先看 run 是不是已经 done,是就直接补一个 XACK 走人——但那是省钱的优化,正确性靠的是那两个唯一约束。
    5. 接着讲最容易做错的一步:幂等键怎么取。它必须能从「同一个意图」稳定推出来。客户端每次重试都新生成一个 uuid 是最常见的错法,那每次都是新意图,唯一约束一次都命中不了,闸门形同虚设。正确做法是客户端生成一次、重试复用同一个值,服务端兜底可以用「会话 id 加消息内容哈希加秒级时间戳」。
    6. 可以预期的追问:不可逆的副作用怎么办,比如发一次退款?答:把外部调用也变成带幂等键的(大多数支付网关都支持 idempotency key 头),并且先在本地库里落一条「已发起」记录再调用,用同一个键去重;实在不支持的接口就只能靠本地状态机加人工对账,这时要主动说出「这类操作我会把它挪出重试路径」。

    How to reason about it · think before answering

    1. This is the question that gets probed hardest. Most candidates say 'at-least-once, so make the business idempotent' and stop — but the follow-up is exactly what matters: which line of code enforces it.
    2. Explain why the duplicate cannot be removed: committing the business write and acking are two writes to two systems (say Postgres and Redis), so there is always a crash window between finishing the work and XACK. The window can shrink but not disappear, which is why exactly-once is not something the bus gives you.
    3. That yields a sentence worth saying out loud: exactly-once is an effect produced by consumer-side idempotency, not a capability provided by the broker. Kafka transactions achieve it inside a read-Kafka-write-Kafka loop, but the moment the sink is a database or third-party API you are back to at-least-once.
    4. Now name the concrete guards and what each one blocks. First, a unique constraint on runs.idempotency_key with insert ... on conflict do nothing, which blocks duplicate submissions: on conflict the gateway returns the existing run id and never publishes a second bus message. Second, unique(run_id, seq) on the messages table, also on conflict do nothing, which blocks duplicate execution: even if two consumers finish the same run simultaneously the user sees one reply. A cheap short-circuit can sit in between — read the run first and just re-ack if it is already done — but that saves money; correctness comes from the two constraints.
    5. Then the step people get wrong: deriving the key. It must be reproducible from the same intent. Generating a fresh uuid on every retry is the classic mistake, because every retry becomes a new intent and the constraint never fires. The client should mint the key once and reuse it across retries; a server-side fallback can hash session id plus message body plus a second-resolution timestamp.
    6. Expect: what about irreversible side effects such as issuing a refund? Push the idempotency key into the external call (most payment gateways accept an idempotency key header), and record an 'initiated' row locally before calling so the same key deduplicates. For APIs with no such support, fall back to a local state machine plus reconciliation, and say plainly that you would move such operations off the automatic retry path.

    答题要点

    • at-least-once:消息至少被处理一次、可能多次,因为业务提交和 XACK 是两个系统的两次写,中间的崩溃窗口消不掉
    • exactly-once 是消费端幂等做出来的效果,不是中间件的能力;下游只要是数据库或第三方 API 就退回至少一次
    • 闸门一:runs.idempotency_key 唯一约束 + on conflict do nothing,挡客户端重复提交,冲突时不再投递总线消息
    • 闸门二:messages 表 unique(run_id, seq) + on conflict do nothing,挡同一条消息被执行两遍,用户只会看到一条回复
    • 幂等键必须从同一个意图稳定推导,客户端重试要复用同一个值;每次重试新生成 uuid 等于没有幂等
    • 不可逆副作用要把幂等键透传给外部接口,并先落一条本地记录再调用

    Key points

    • At-least-once means a message is processed one or more times, because the business commit and the XACK are two writes to two systems with an unavoidable crash window
    • Exactly-once is an effect of consumer-side idempotency, not a broker feature; any database or third-party sink puts you back at at-least-once
    • Guard one: a unique constraint on runs.idempotency_key with on conflict do nothing blocks duplicate submissions and skips publishing a second bus message
    • Guard two: unique(run_id, seq) on messages with on conflict do nothing blocks duplicate execution, so the user sees exactly one reply
    • The idempotency key must be derivable from the same intent and reused across retries; minting a new uuid per retry defeats the whole mechanism
    • For irreversible side effects, pass the idempotency key through to the external API and record an initiated row locally before calling

D10 分片与租约:userId 哈希→shard、SET NX + TTL + Lua 续约、同用户顺序、handoff

  • 两个 worker 同时认为自己持有同一个 shard 的租约(脑裂)会造成什么后果,怎么规避?What happens when two workers both believe they hold the same shard lease (split brain), and how do you mitigate it?
    国内高频海外高频深入#split-brain#fencing-token#reliability

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

    1. 这题的判分点非常明确:答案里有没有出现「单靠 Redis 租约做不到绝对互斥」。说「用了 SET NX 加 TTL 就安全了」的人,会被追问到答不上来。
    2. 先讲脑裂是怎么发生的,而且要举那个最常见的场景——不是进程崩溃,是持有者只卡了 5 秒:一次 full GC、宿主机 CPU 被邻居打满、容器被 cgroup 限流。它醒过来时内存里还写着「我持有 shard 68」,继续处理手上那条消息、继续写库,而 Redis 里的租约早已到期并被别人抢走。再补一层:Redis 主从复制是异步的,切主时可能丢掉最后几毫秒的写入,于是两个 worker 都能 SET NX 成功。
    3. 然后讲后果,而且要落到业务上而不是停在「数据不一致」:同一个用户的两条消息被两个进程并发处理,回复乱序、上下文错乱、messages 表的 unique(run_id, seq) 撞约束导致落库失败;最严重的是有副作用的工具被重排或重复执行——「取消订单」和「改配送日期」顺序反了,结果是取消了一个用户本来想留下的订单。
    4. 关键的认知转折:既然无法在 Redis 一侧排除这条时间线,正确的思路就不是「让脑裂不发生」,而是「让第二个人的写入落不了地」——把冲突的检测与拒绝推到真正产生副作用的那一层。
    5. 三条手段按性价比给出。一是 worker 自己的自杀规则:连续两次续约失败、或距上次成功续约超过 TTL 的三分之二,立刻停止处理并清空持有集合——最便宜,把「我以为我还持有」的窗口从无限压到两个续约周期。二是 fencing token:抢租约时从一个单调递增计数器取号(Redis 的 INCR)写进租约值,之后所有有副作用的操作都带上它,下游只接受不比见过的最大号小的写入,落到数据库上就是一句条件更新;醒过来的前任拿的是旧号,写入直接被拒。三是每次写之前重新校验租约,并把校验与写入放进同一段脚本或同一个事务——这只缩小窗口,不消除。
    6. 可预期的追问:fencing 的局限在哪?答「它需要下游配合」。数据库能做条件更新所以好使,但下游是第三方接口(发短信、扣款)时你没法让对方帮你比号,这时只能退回幂等键,把重复执行变成无害,而不是让它不发生。真要绝对互斥就得换到有共识协议的系统(etcd、ZooKeeper 的会话租约),代价是写入延迟和运维复杂度。

    How to reason about it · think before answering

    1. The scoring criterion here is explicit: does your answer contain the sentence 'a Redis lease alone cannot give absolute mutual exclusion'. Anyone who says SET NX plus a TTL makes it safe gets probed until they run out of answers.
    2. Start with how split brain arises, and use the common case: not a crash, but a holder that merely froze for five seconds — a full GC, a noisy neighbour saturating the host CPU, cgroup throttling. It wakes up still believing it holds shard 68, keeps processing the in-flight message and keeps writing, while the lease expired and was taken. Add the second layer: Redis replication is asynchronous, so a failover can lose the last few milliseconds of writes and let two workers both win SET NX.
    3. Then the consequences, expressed in business terms rather than 'inconsistent data': two messages from one user processed concurrently means out-of-order replies, a corrupted context window, and unique(run_id, seq) violations that silently drop a message. Worst is reordered or duplicated side effects — swap 'cancel the order' with 'move the delivery date' and you cancel an order the user wanted to keep.
    4. The key shift: since you cannot rule out that timeline on the Redis side, the goal is not to prevent split brain but to make the second writer's writes fail — push conflict detection and rejection down to the layer that actually causes side effects.
    5. Give three mitigations by value. First, a self-kill rule in the worker: after two consecutive renewal failures, or when the last success is older than two thirds of the TTL, stop processing and clear the held set — cheapest, and it bounds the 'I think I still hold it' window to two renewal periods. Second, fencing tokens: take a monotonically increasing number (Redis INCR) when acquiring, store it in the lease value, attach it to every side-effecting operation, and have the downstream accept only numbers not lower than the highest it has seen — in a database that is one conditional update. The revived predecessor carries a stale number and is rejected. Third, re-validate the lease immediately before each write inside the same script or transaction, which shrinks the window without closing it.
    6. Expect the follow-up: where does fencing break down? It needs downstream cooperation. Databases do conditional updates, but a third-party endpoint (SMS, payments) will not compare your token, so you fall back to idempotency keys that make duplicate execution harmless rather than impossible. True mutual exclusion means moving to a consensus-backed system such as etcd or ZooKeeper session leases, paying in write latency and operational complexity.

    答题要点

    • 单靠 Redis 租约做不到绝对互斥:持有者被冻结再醒来、以及主从异步复制丢写,这两条时间线排除不掉
    • 后果要落到业务:同用户回复乱序、上下文错乱、唯一约束冲突丢消息,最严重是有副作用的工具被重排或重复执行
    • 思路是「让第二个人的写入落不了地」,把冲突检测推到产生副作用的那一层,而不是指望脑裂不发生
    • 三条手段:worker 自杀规则(续约连续失败就放手)、fencing token(写入时带单调号做条件更新)、写前重新校验租约
    • fencing 需要下游配合;下游是第三方接口时只能退回幂等键,要绝对互斥就得换 etcd / ZooKeeper 这类有共识协议的系统

    Key points

    • A Redis lease alone cannot guarantee mutual exclusion: a frozen holder that revives, and asynchronous replication losing writes on failover, are both unavoidable
    • State consequences in business terms: out-of-order replies, corrupted context, unique-constraint violations dropping messages, and reordered or duplicated side effects
    • The goal is to make the second writer's writes fail — push conflict detection to the side-effecting layer instead of hoping split brain never happens
    • Three mitigations: a worker self-kill rule on repeated renewal failure, fencing tokens enforced as conditional updates, and re-validating the lease immediately before writing
    • Fencing needs downstream cooperation; against third-party endpoints fall back to idempotency keys, and true mutual exclusion means a consensus system like etcd or ZooKeeper

D16 Supervisor 动态路由:structured output 路由、override、routingReason

  • 路由不确定或者路由错误时,系统应该怎么兜底?阈值该怎么定?How should the system handle an uncertain or wrong routing decision, and how do you pick the threshold?
    国内高频海外高频深入#routing#fallback#reliability

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

    1. 题眼在「不确定」和「错误」是两件事。多数人只答重试或人工接管,那是把两个问题揉成一个。区分度在于你能不能先给出一条价值判断,再给策略。
    2. 先立论:**路由到错的子 Agent,比路由失败糟糕得多**。失败你至少知道自己失败了,可以追问一句;错了,接手的子 Agent 完全不知道自己接错了活,会用笃定的语气给出一个格式完整的错误答案,用户不会怀疑,会照着去操作。一个自信的错误答案比一句「我没听清」贵一百倍。
    3. 再给可执行的策略,数字要具体:模型给的置信度低于 0.6,或者路由名不在合法名单里,一律落到兜底的 smalltalk,并在 routingReason 里打上 fallback 前缀加原因码(低置信度、未知路由、结构非法各一种)。兜底那位的人设是「信息不足先追问一句缺的关键信息,不要猜」——兜底的本质是把不确定性还给用户。
    4. 阈值怎么定这一问是重点,别背数字:**取决于两类错误哪一类更贵**。客服场景里多问一句只是用户小小的不耐烦,派错可能变成一条错误的退款承诺,所以宁可保守取 0.6;内部工具型 Agent 里多问一句反而更烦人,阈值就该放低。再补一句可落地的定法:拿标准样本集扫一遍,画出不同阈值下的误派率与追问率,选拐点。
    5. 必须点破的一个坑:**置信度是模型自己报的,它不是概率**。模型说 0.9 不代表有九成对。它只是同一模型、同一提示词下相对可用的排序信号,只能当闸门用,不能拿去算期望值。真正的准确率要靠离线评估去量。
    6. 可以预期的追问:兜底会不会把问题掩盖掉?答案是不会,前提是你记了原因码——把一周内落进兜底的请求按原因分组,能直接看出分诊提示词缺了哪一类描述。兜底是止血,原因码才是治本的输入。

    How to reason about it · think before answering

    1. The hinge is that uncertain and wrong are two different failures. Most candidates answer retry or escalate to a human, collapsing both into one. The discriminator is stating a value judgement before giving a policy.
    2. The claim first: routing to the wrong sub-agent is far worse than failing to route. A failure announces itself and lets you ask a clarifying question. A wrong route does not — the receiving agent has no idea it got the wrong job and will produce a confident, well-formatted, wrong answer that the user will act on. A confident wrong answer costs a hundred times more than I did not catch that.
    3. Then give a concrete policy with real numbers: if the model's confidence is below 0.6, or the route name is not in the allowed list, fall back to the small-talk agent and stamp the reason with a fallback prefix plus a cause code (low confidence, unknown route, invalid shape). The fallback agent's job is to ask for the one missing detail rather than guess — falling back means handing the uncertainty back to the user.
    4. The threshold question is the real test, so do not recite a number: it depends on which error is more expensive. In customer support one extra question costs mild annoyance while a misroute can become a wrong refund promise, so stay conservative. For an internal tool the extra question is the bigger cost, so lower it. Then give a method: sweep thresholds over a golden set, plot misroute rate against clarification rate, and pick the knee.
    5. Name the trap: the confidence number is self-reported and is not a probability. Nine tenths does not mean nine in ten are right. It is a usable ranking signal within one model and one prompt — good as a gate, useless for expected-value math. Real accuracy comes from offline evaluation.
    6. Expect: does falling back just hide the problem? Not if you record cause codes. Group a week of fallbacks by cause and you can see exactly which intent the routing prompt fails to describe. The fallback stops the bleeding; the cause code is what fixes it.

    答题要点

    • 先分清两件事:路由失败可以追问,路由错误会让子 Agent 自信地给出错误答案,后者贵得多
    • 策略:置信度低于 0.6 或路由名不在名单里,一律落兜底的 smalltalk,并在 routingReason 打上 fallback 前缀加原因码
    • 兜底不是随便找个人接,而是把不确定性还给用户——兜底那位应当追问缺失的关键信息而不是猜
    • 阈值取决于两类错误哪一类更贵:客服场景多问一句便宜、派错很贵,所以保守;定法是拿标准样本集扫阈值找拐点
    • 置信度是模型自报的,不是概率,只能当闸门用;真正的准确率要靠离线评估量
    • 落兜底时记原因码,按原因分组就能看出分诊提示词缺了哪一类描述

    Key points

    • Separate the two: a failed route can ask a clarifying question, a wrong route produces a confident wrong answer, and the second is far costlier
    • Policy: confidence below 0.6 or a route outside the allowed list falls back to small talk, stamped with a fallback prefix and a cause code
    • Falling back is not picking someone at random — the fallback agent asks for the missing detail instead of guessing
    • The threshold depends on which error costs more; sweep it over a golden set and pick the knee between misroutes and clarifications
    • Self-reported confidence is not a probability — use it as a gate only, and measure real accuracy offline
    • Record cause codes on every fallback; grouping them shows which intent the routing prompt fails to describe

D18 历史保真与摘要、多模态占位、checkpointer 持久化

  • 从 checkpoint 恢复执行(replay)需要注意什么?说几个真实会踩的坑。What do you need to watch out for when replaying execution from a checkpoint? Give failure modes you would actually hit.
    国内高频海外高频深入#checkpointing#replay#reliability

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

    1. 这题最容易答成「读出来接着跑就行」。区分度在于你能不能说出**这些坑几乎全是静默的**——不抛异常、日志干净、结果看起来也对,只有对比数据时才发现不对。能说出这一点,答案就已经赢了一半。
    2. 先给一条推导链:检查点里存的是「当时那个版本的代码眼里的状态形状」,恢复就是把它塞回今天这个版本的代码里。所以所有坑都来自**两端不一致**:数据的形状、执行的入口、和那些不该被重放的东西。
    3. 坑一,恢复时又把输入喂了一遍。恢复的入口是不带输入地调用,状态已经在检查点里;带着原来那句话再调一次,框架会把它当成一次新的状态更新叠在中断点上,历史变成两份。它不报错。
    4. 坑二,分叉忘了带检查点 id。只给会话 id 拿到的是这条线最新的状态,于是「从第 2 步重来」变成了「在最后一步后面接着写」。同样不报错,只有对比子任务列表才看得出来。
    5. 坑三,版本兼容。改一个字段名、加一个必填字段,库里的老检查点就和新代码对不上;而缺字段读出来是 undefined,拼进文案就是字符串「undefined」,参与算术就是 NaN——比如工具预算的上限判断,一旦变成 NaN 比较,恒为假,预算上限在恢复出来的那条线上彻底失效。正确做法是在读的那一侧迁移,迁移函数只补默认值和改名、不做业务判断,绝不能失败。
    6. 坑四,不该被重放的东西进了状态。一次性的人工干预(比如人工改派)如果写进图状态,就会被检查点持久化并在每次恢复时重放一遍。判断口径:这条信息说的是「这一次执行怎么跑」还是「这个会话是什么」,前者进运行时配置,后者才进状态。
    7. 可以预期的追问:待执行的并行子任务存不存?答存——检查点里除了状态快照还有一份「还没跑的那几步,连参数一起」,所以恢复不用重跑规划节点;但它存在框架的内部通道里,自研存储层只实现「存状态」而漏掉这一半,恢复出来的图会看起来跑完了、其实一件活都没派出去。

    How to reason about it · think before answering

    1. The easy failure is answering just load it and keep going. The discriminator is recognising that almost every replay bug is silent — no exception, clean logs, plausible output, and you only notice when you diff the data. Saying that up front wins half the question.
    2. Give a chain first: a checkpoint stores the state shape as the code of that moment understood it, and replay pushes it back into today's code. So every failure comes from a mismatch across those two ends — the shape of the data, the entry point of execution, and things that should never have been replayed at all.
    3. Trap one: feeding the input again on resume. Resume takes no input; the state is already in the checkpoint. Passing the original message once more makes the framework treat it as a fresh update stacked on the interrupt point, and the history quietly doubles. Nothing throws.
    4. Trap two: forking without a checkpoint id. With only the thread id you get that thread's latest state, so start over from step 2 silently becomes append after the last step. Again nothing throws; you only see it by diffing the task list.
    5. Trap three: version drift. Rename a field or add a required one and every old checkpoint stops matching the new code. A missing field reads as undefined, which renders as the literal string undefined in user-facing text and as NaN in arithmetic — a tool-budget ceiling compared against NaN is always false, so the budget silently stops existing on resumed threads. Migrate on read, and keep the migration to defaults and renames only: it must never fail.
    6. Trap four: replayable data that should not be replayed. A one-off human override written into graph state gets checkpointed and re-applied on every resume. The test: does this describe how this run executes, or what this conversation is? The former belongs in runtime config, only the latter in state.
    7. Expect the follow-up: are pending parallel tasks preserved? Yes — a checkpoint holds not just the state snapshot but the steps not yet run, arguments included, so the planner does not re-run. But they live in a framework-internal channel, so a hand-rolled store that persists state and forgets that half will resume into a graph that looks finished while no work was ever dispatched.

    答题要点

    • 先点破共性:replay 的坑几乎全是静默的,不报错、日志干净、结果看着也对
    • 恢复不要带输入,带了就是在中断点上又追加一次,历史变成两份
    • 分叉必须带检查点 id,只给会话 id 会落在最新状态上,「从第 2 步重来」变成「接着往后写」
    • 版本兼容:缺字段读出来是 undefined 或 NaN,会让预算上限之类的比较恒为假;在读的那一侧迁移,迁移只补默认值和改名且不能失败
    • 一次性的人工干预不要进图状态,否则会被持久化并在每次恢复时重放;「这次怎么跑」进配置,「这个会话是什么」才进状态
    • 待执行的并行子任务连参数一起存在检查点里,所以恢复不重跑规划;自研存储层漏掉这一半,恢复出来的图会一件活都不派

    Key points

    • Lead with the pattern: replay bugs are almost all silent — no exception, clean logs, plausible output
    • Resume takes no input; passing one appends another update at the interrupt point and doubles the history
    • Forking requires the checkpoint id — thread id alone lands on the latest state, turning start over from step 2 into append after the end
    • Version drift: missing fields read as undefined or NaN, so comparisons like a tool-budget ceiling become permanently false. Migrate on read, restricted to defaults and renames, and never let it fail
    • Keep one-off human overrides out of graph state or they get persisted and re-applied on every resume — how this run executes belongs in config, what this conversation is belongs in state
    • Pending parallel tasks are stored with their arguments, so the planner does not re-run; a hand-rolled store that skips that half resumes into a graph that dispatches nothing

D21 评估与可观测:golden set、LLM-as-judge、tracing、失败率/成本面板;Pi vs LangGraph 总结;W3 复盘

  • 用大模型给大模型的输出打分(LLM-as-judge),有哪些不可靠的地方?怎么办?What makes LLM-as-judge unreliable, and what do you do about it?
    国内高频海外高频深入#evaluation#llm-as-judge#reliability

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

    1. 这题筛的是「你是真用过,还是听说过」。用过的人能报出具体的失效形态和量级,没用过的人只会说「可能不准」。
    2. 第一种,**同源偏差**:judge 和被评估的 Agent 用同一个模型时,它偏向认可自己的输出——同一个模型对「什么算好答案」的偏好是一致的,让它复核自己刚写的东西,它当然觉得没问题。实测数量级:同一批被改坏的产出,同源 judge 给 14/15,换个模型只给 12/15,被多放过去的正是最该抓的边缘产出。这个坑不止在 judge,**凡是「模型评模型」的位置都有**,Critic 节点是同一个问题。
    3. 第二种,**长度偏好**:judge 倾向给篇幅大的答案更高分。实测:一条 33 字的正确回复灌上 141 字无关客套话,凭印象打分的提示词就从 2 分涨到 4 分,内容一个字没变。
    4. 第三种,**评分提示词漂移**:judge 的评分随提示词微调整体移动。同一份产出,两套评分提示词一套给 2 分一套给 5 分。所以有条硬纪律——**分数只在同一套 judge 提示词内部可比**,跨版本比较是没有意义的。
    5. 解药要一一对应,别笼统说「多测几次」:固定 judge 提示词并版本化(每条评分记录带上 rubric 版本与 judge 模型,那是它的坐标;面板发现混了两套口径应当直接拒绝聚合,而不是算出一个没含义的平均分);默认用不同的模型当 judge,而且这该是默认值不是可选项;留一小批人工标注做校准集,每次改评分提示词拿它对一遍,**比的是结论(过或不过)而不是分数差**——差 1 分无所谓,结论翻了就是事故。
    6. 还有一条更根本的:**把评分标准从主观印象换成可核对的清单**,它同时解掉长度偏好——照清单逐条数,灌水加不了分。实测那条灌水回复在清单口径下前后都是 5 分,纹丝不动。
    7. 可以预期的追问:judge 便宜还是人工便宜?答:judge 的成本和被评估的系统本身一个量级,所以「跑一次全量评估多少钱」是你决定每次提交都跑还是每天跑一次的依据;而人工的成本不在钱在延迟——它给不了你改一次提示词就想看一次结果的反馈速度,所以人工只该用在校准集上。

    How to reason about it · think before answering

    1. This screens for whether you have actually used it. People who have can name specific failure shapes with magnitudes; people who have not just say it might be inaccurate.
    2. First, self-preference: when the judge and the evaluated agent share a model, it favours its own output — the same model has a consistent notion of what a good answer looks like, so asking it to review what it just wrote gets an approving verdict. Measured: on the same batch of deliberately degraded outputs, a same-model judge gave 14/15 while a different model gave 12/15, and the extra passes were exactly the borderline cases worth catching. This is not confined to judges — every model-grading-model position has it, and a Critic node is the same problem.
    3. Second, length bias: judges reward longer answers. Measured: padding a correct 33-character reply with 141 characters of irrelevant pleasantries moved an impression-based rubric from 2 to 4 without changing a word of substance.
    4. Third, rubric drift: scores shift wholesale when the judge prompt is tweaked. The same output scored 2 under one rubric and 5 under another. Hence the hard rule: scores are comparable only within one judge prompt, and cross-version comparison is meaningless.
    5. Match each remedy to its failure rather than saying run it a few more times. Freeze and version the judge prompt — every score record carries its rubric version and judge model, which are its coordinates, and a dashboard that finds two rubrics mixed should refuse to aggregate rather than emit a meaningless average. Default to a different model as judge, as a default and not an option. Keep a small human-labelled calibration set and re-run it whenever the rubric changes, comparing verdicts (pass or fail) rather than score deltas — one point of drift is fine, a flipped verdict is an incident.
    6. And one deeper fix: replace impressionistic criteria with a checkable list, which also dissolves length bias — counting items off a list gives padding nothing to earn. Measured, that padded reply scored 5 both before and after under the checklist rubric.
    7. Expect: is a judge cheaper than humans? The judge's cost is the same order as the system being evaluated, so what a full evaluation run costs decides whether you run it per commit or nightly. Human cost is not money but latency — it cannot give you feedback at the speed of one prompt edit, which is why humans belong on the calibration set only.

    答题要点

    • 同源偏差:judge 与被评估 Agent 同模型会虚高(实测 14/15 vs 异源 12/15),且凡「模型评模型」的位置都有,Critic 同理
    • 长度偏好:灌水 141 字能让印象分从 2 涨到 4,内容一字未变
    • 评分提示词漂移:同一产出两套 rubric 一个 2 分一个 5 分,所以分数只在同一套提示词内部可比
    • 解药一一对应:rubric 版本化并随记录存坐标、面板发现混口径直接拒绝聚合、默认换模型当 judge
    • 留人工标注校准集,比结论(过/不过)而不是比分数差——差 1 分无所谓,结论翻了是事故
    • 更根本的是把主观印象换成可核对的清单,同时解掉长度偏好(清单口径下灌水前后都是 5 分)

    Key points

    • Self-preference: a same-model judge inflates scores (14/15 vs 12/15 cross-model), and it applies to every model-grading-model spot including Critic
    • Length bias: 141 characters of padding moved an impression score from 2 to 4 with no substantive change
    • Rubric drift: the same output scored 2 and 5 under two rubrics, so scores compare only within one judge prompt
    • Remedies map one-to-one: version the rubric and store it alongside each record, refuse to aggregate mixed rubrics, default to a different judge model
    • Keep a human-labelled calibration set and compare verdicts, not score deltas — a point of drift is fine, a flipped verdict is an incident
    • The deeper fix is a checkable list instead of impressions, which also removes length bias (the padded reply scored 5 both ways)