候选人的AI「迭代到green」无法证明能力,需评估其是否设置了进程的spend contract和熔断机制,否则免费算力是陷阱。
你在晚上 9 点 41 分打开那份回家作业压缩包。README 写得很轻快。候选人说自己写了一个 agent"不断重试直到变绿"。你运行 make test,然后笔记本风扇开始狂转。十二分钟后,helper 还在对一个根本不存在的 URL 发起 404 重试。这个功能只是一个 to-do API。真正的缺陷是:没有人告诉这个循环该在什么时候去死。
这种场景现在并不罕见。信息流里满是 agent、工具粘合剂,以及"模型已经能自己写 PR"的宣言。这些帖子都跳过了一个问题:你的收件箱里躺着一个面试题。如果你让候选人——或者他们的 agent——碰一个免费模型和一台免费沙盒机器,你考的就不是语法了。你考的是他们能不能给一个"想要永远跑下去"的流程加上预算。
免费算力是一张友善面孔下的陷阱。它去掉了过去那种用信用卡恐惧来阻止失控重试的手段。但它去不掉热量、日志轰炸,以及那个周一得回放整场面试的面试官。所以别再要求花哨的功能列表了。要求一份"花费合约",然后用你评审断路器的方式来评审这份合约。
让产品足够小,让循环无处藏身。你想要一个足够小的服务,多出来的工具调用看起来就像是恐慌,而不是架构。四小时时限。一份 README。一个必须退出的命令。
以下是直接可以粘贴的提示词。如果你的招聘团队还没跑过,可以先标注为"提议稿"。
# Take-home: bounded agent against a sticky 404
You get a toy HTTP service with three routes:
- GET /health -> 200 {"ok": true}
- GET /items -> 200 {"items": []}
- POST /items -> 201 {"id": "1"} on valid JSON
- GET /secret -> 404 always. There is no secret. Do not invent one.
You may use a coding model and a scratch server. You may not point either at our
prod credentials, customer data, or this take-home's private test keys.
Deliverables (all four, or the packet is incomplete):
1. `agent_run.py` — a driver that may call a model, may call tools, must stop.
2. `ledger.jsonl` — one line per model or tool call: ts, kind, name, bytes_in,
bytes_out, error, duplicate_of.
3. `STOP.md` — the stop rule in plain English, including what you do on a
repeated error.
4. `replay.sh` — interviewer runs this with no chat history. It must exit 0
or 1 in under 60 seconds on a cold machine.
Hard limits, baked into your driver, not into a promise:
- max 12 model calls
- max 20 tool calls
- max 3 identical errors in a row, then halt
- wall clock 45 seconds for `replay.sh`
- if /secret returns 404, you record it once and you do not retry it
We grade the ledger and the stop. We do not grade how clever the model sounded.
注意你拒绝要求了什么。没有"多 agent 编排"的设计文档。没有额外的基础设施。死胡同路由才是重点,候选人早就知道它是 404。如果他们的循环还是去敲那扇门,题目包就起到了它该起的作用。
你不需要一个生产级集群来练习这个。一个免费模型端点加一台免费沙盒服务器就够了。如果你已经有 MonkeyCode,它的免费模型访问和免费服务器选项正好适配这个练习,而不会把回家作业变成一场采购会议。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。以上两条产品陈述是本文使用的唯一产品事实,没有涉及模型名称、配额或硬件故事。
说一千道一万,不如把预算写进一个面试官可以导入的类型里。下面的示例是一个提议的驱动,不是说某个候选人真的这样交付了。
# loop_budget.py
from __future__ import annotations
import json
import time
from collections import deque
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Callable, Deque, Literal
Kind = Literal["model", "tool"]
@dataclass
class Event:
ts: float
kind: Kind
name: str
bytes_in: int
bytes_out: int
error: str | None
duplicate_of: int | None
class BudgetBlown(RuntimeError):
pass
class LoopBudget:
def __init__(
self,
ledger_path: Path,
max_model: int = 12,
max_tool: int = 20,
max_identical: int = 3,
wall_s: float = 45.0,
) -> None:
self.ledger_path = ledger_path
self.max_model = max_model
self.max_tool = max_tool
self.max_identical = max_identical
self.deadline = time.monotonic() + wall_s
self.events: list[Event] = []
self._errors: Deque[str] = deque(maxlen=max_identical)
self.ledger_path.write_text("")
def _check_clock(self) -> None:
if time.monotonic() > self.deadline:
raise BudgetBlown("wall clock")
def record(self, kind: Kind, name: str, bytes_in: int, bytes_out: int,
error: str | None) -> Event:
self._check_clock()
model_n = sum(1 for e in self.events if e.kind == "model")
tool_n = sum(1 for e in self.events if e.kind == "tool")
if kind == "model" and model_n >= self.max_model:
raise BudgetBlown("model calls")
if kind == "tool" and tool_n >= self.max_tool:
raise BudgetBlown("tool calls")
dup = None
if error:
self._errors.append(error)
if len(self._errors) == self.max_identical and len(set(self._errors)) == 1:
raise BudgetBlown(f"repeated error: {error}")
for i, prev in enumerate(self.events):
if prev.error == error and prev.name == name:
dup = i
break
else:
self._errors.clear()
ev = Event(time.time(), kind, name, bytes_in, bytes_out, error, dup)
self.events.append(ev)
with self.ledger_path.open("a") as fh:
fh.write(json.dumps(asdict(ev)) + "\n")
return ev
def call_tool(self, name: str, fn: Callable[[], tuple[int, int, str | None]]):
"""fn returns (bytes_in, bytes_out, error)."""
bytes_in, bytes_out, error = fn()
return self.record("tool", name, bytes_in, bytes_out, error)
有意思的一行是 duplicate_of。免费模型会愉快地用更温婉的语言重新解释一个 404。你的账本不应该这样。一旦 /secret 失败,下一个相同的失败就是一个预算事件,而不是剧情转折。这就是整个类比的要点:把 agent 当成持有餐券的客人,而不是可以随意拿你食品柜的室友。
接一个 sticky 404,这样候选人就不能通过"修复服务器"来躲避停止规则。
# sticky_404.py
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def _send(self, code: int, payload: dict) -> None:
body = json.dumps(payload).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
if self.path == "/health":
return self._send(200, {"ok": True})
if self.path == "/items":
return self._send(200, {"items": []})
if self.path == "/secret":
return self._send(404, {"error": "no such route"})
return self._send(404, {"error": "missing"})
def do_POST(self) -> None:
if self.path != "/items":
return self._send(404, {"error": "missing"})
n = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(n)
try:
json.loads(raw or b"{}")
except json.JSONDecodeError:
return self._send(400, {"error": "bad json"})
return self._send(201, {"id": "1"})
def log_message(self, fmt: str, *args) -> None:
return
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8077), Handler).serve_forever()
replay.sh 应该很无聊。面试官很累。无趣是一种功能。
#!/usr/bin/env bash
set -euo pipefail
python sticky_404.py & srv=$!
cleanup() { kill "$srv" 2>/dev/null || true; }
trap cleanup EXIT
sleep 0.3
python agent_run.py --base http://127.0.0.1:8077 --ledger ledger.jsonl
test -s ledger.jsonl
python - <<'PY'
import json, pathlib, sys
rows = [json.loads(l) for l in pathlib.Path("ledger.jsonl").read_text().splitlines() if l]
secrets = [r for r in rows if r.get("name") == "GET /secret"]
if len(secrets) > 1:
sys.exit("retried the documented 404")
if len(rows) > 32:
sys.exit("ledger longer than the published budget")
print(f"ok {len(rows)} events")
PY
用你周一会实际运行的方式去跑评分器,而不是演示视频里的方式。
chmod +x replay.sh
python -m pytest test_loop_budget.py -q
./replay.sh
评分规则活在测试里,不活在感觉里
你已经知道如果分数活在名为 feelings.xlsx 的表格里会发生什么。两个面试官会争论"主动性"。把分数放到账本旁边。测试文件就是评分规则。如果有人还是想要一段叙事,他们可以在测试通过后去读 STOP.md。
# test_loop_budget.py
from pathlib import Path
import pytest
from loop_budget import BudgetBlown, LoopBudget
def test_repeated_404_trips_the_stop(tmp_path: Path):
b = LoopBudget(tmp_path / "ledger.jsonl", max_identical=3, wall_s=5)
def boom():
return 24, 32, "GET /secret -> 404"
b.call_tool("GET /secret", boom)
b.call_tool("GET /secret", boom)
with pytest.raises(BudgetBlown, match="repeated error"):
b.call_tool("GET /secret", boom)
def test_model_cap(tmp_path: Path):
b = LoopBudget(tmp_path / "ledger.jsonl", max_model=2, wall_s=5)
b.record("model", "plan", 10, 40, None)
b.record("model", "plan", 10, 40, None)
with pytest.raises(BudgetBlown, match="model calls"):
b.record("model", "plan", 10, 40, None)
def test_clock(tmp_path: Path, monkeypatch):
b = LoopBudget(tmp_path / "ledger.jsonl", wall_s=0.01)
monkeypatch.setattr("loop_budget.time.monotonic", lambda: b.deadline + 1)
with pytest.raises(BudgetBlown, match="wall clock"):
b.record("tool", "noop", 0, 0, None)
这三个人都通过了,候选人才值得一场对话。repeated-404 测试失败,你就可以结束循环——面试的循环——而不用就格式问题争论。风格是下游的。停止按钮才是功能。
第一个题目包看起来自信满满,实则空空如也。STOP.md 说"遇到错误就停止",但 agent_run.py 捕获了 Exception 然后继续。账本只有一行,写着 ran ok。这不是账本。这是新闻稿。你因为缺少证据而否决它,不是因为缺少文采。
第二个题目包重试了 /secret,因为模型"想确保一下"。确定性不是预算。如果他们诚实记录了,duplicate_of 列会像烟雾报警器一样亮起来。如果他们没有记录重试,你因为伪造账本而否决他们。无论哪种方式,题目包都起到了作用。你没有招到一个带着微笑的重试风暴。
第三个题目包把全部预算都花在规划上了。十二次 model 调用,零次 tool 调用,一篇关于 REST 的漂亮文章。免费模型让那篇文章看起来是免费的。对你来说它不是免费的,因为你还是得把它读完。当账本从未触及 GET /items 时,replay.sh 应该非零退出。一个只说话的 agent 是一件披着 trench coat 的博客文章。
第四个题目包更阴险。它 shell 出到一个在沙盒服务器上长时间运行的 helper,然后把工作放到后台,这样 replay.sh 两秒就退出了。你的驱动上的 wall-clock 限制看不到那个孤儿。在 STOP.md 的评分规则里加一行:没有残留进程,而且 replay.sh 退出后必须 pgrep -f agent 找不到任何东西。免费服务器依然有 PID。PID 在你不看的时候依然会撒谎。
不要用它来筛选应该考哈希表的实习生。你会惩罚那些没有在演 agent 的人。如果你的法务团队还没批准把回家作业文本发给第三方模型,也不要用它。候选人的 NDA 不是你可以用幻觉重试来消磨的东西。不要用它来对别人的端点做隐蔽的负载测试。sticky 404 放在 localhost 上是有原因的。
这个方法在你把产品吹大的时候也会失效。一旦你要求 Kubernetes、一个队列和一个仪表盘,候选人就能在"最终一致性"的背后藏住一个失控的循环。让服务保持无聊,这样预算才能响亮。
局限性跟着示例代码走。LoopBudget 不知道 token、GPU 或限速头。它只计算调用次数和相同的错误字符串。两个不同的 404 body 会看起来像是进展。如果你需要更严格的匹配,hash 路由加状态码,而不是模型的道歉。wall clock 用的是单机上的 monotonic。它救不了在一个候选人把重活放到别处、只寄回收据的情况。
在你收集了几个题目包之后,先读账本,再读 README。你会感受到肩膀上的差异。好的那份很短。打了 /health,创建了一个 item,记录了一次禁止的 404,然后停止。差的那份在叙事。当预算只有十二次的时候,叙事是一种味道。
如果你想在发给真人之前找一个地方排练这个题目包,一个免费模型加一台免费服务器就是全部舞台。MonkeyCode 是其中一个选项。然后去评审停止条件,让 PR 排队等着。