initializer agent:init.sh、进度文件与第一个 commit
无人值守运行的第一分钟决定后面几小时的效率。今天把初始化独立成一个阶段:写一个能把环境跑起来的脚本、一份给后来者看的进度文件、一个干净的初始提交,让任何新窗口三分钟就能上手。
今日目标
- 能说出初始化阶段必须产出的三件东西,并解释每一件分别消灭了哪一种失败模式
- 能解释为什么初始化要独立成一个阶段而不是混在主循环里,并说出混在一起会带来的两个具体问题
- 能用一个可检验的标准判断工作区是否合格:任何新窗口三分钟内能否自己跑起来并知道做到哪了
D2 把进度搬出了窗口。今天搬的是另一类同样会被窗口边界抹掉的东西:环境知识。读完回到页面顶部把三条目标勾掉。
小白版讲解
新人第一天:文档在哪、怎么跑起来、上一个人做到哪了
回想一下你入职新公司的第一天。假设团队什么都没准备,你要自己搞清楚三件事:代码在哪、怎么把它跑起来、上一个人做到哪了。
顺利的话,这三件事花掉你半天。不顺利的话——README 是两年前的、启动命令在某个人的聊天记录里、上一个人已经离职——你可能要花两天。
现在把这个场景改一下:每个新人只待三小时,然后换下一个。
这就是无人值守运行的真实形态。一夜八小时、每个窗口一小时,就是八个「新人第一天」。如果每个新人都要自己摸索半小时,你一夜的算力有一半花在了重复搞清楚同一件事上——而且每次搞清楚的结论都一模一样。
修法在现实里也早就有了:入职文档 + 一条能跑的启动命令 + 交接记录。今天要做的就是让 harness 在开工前把这三样准备好。
四种失败模式,四个机关
D1 那张表该回头看了。Anthropic 的工程博客把长时程 Agent 的失败模式归成四类,每一类配一个机关。今天这一天,四个机关里落地了两个半。
| 失败模式 | 机关 | 落在哪一天 |
|---|---|---|
| 浪费时间摸索怎么把应用跑起来 | init.sh,会话开头先读它 | D3(今天) |
| 在环境里留下缺陷且没有文档 | 初始 git 仓库加进度笔记,每次收尾都提交 | D3(今天) |
| 过早宣布胜利 | feature 清单文件,逐条验证才允许翻成通过 | D4 |
| feature 被标记完成但根本没真测 | 端到端验证通过才允许改状态 | D5 |
注意这张表的用法:失败模式与机关必须成对看。单说「要写个 init.sh」是一句没有理由的规范;说「不写它,每个新窗口都会重新摸索一遍启动方式」,它才是一个有因果的设计。
后面遇到任何一条 harness 的做法,都可以反过来问一句:它挡的是哪种失败? 答不上来的做法通常是抄来的。
三件套:三样能被别人打开的东西
初始化阶段的交付物不是一段说明,是三件能被别人打开的东西。
第一件,init.sh。 它回答「怎么跑起来」。为什么必须是一个可执行脚本而不是 README 里的一段话?因为一段话只能被读懂,脚本可以被验证。harness 能真的去跑它一次,跑不起来就知道工作区还没准备好——这件事在一段话上做不到。
第二件,进度笔记。 它回答「跑到哪了、下一步做什么」。它和 D2 那段上下文摘要数字同源,但读者不同:摘要写给下一个窗口里的模型,结论前置、没有过渡句;笔记要能被一个刚接手的人从头读到尾。
第三件,一个 commit。 把前两样放进版本库。它的意义在今天还看不全——它是给后面所有回滚一个可靠的地面。D4 会讲清楚这件事。
为什么初始化必须是一个独立阶段
这是今天唯一重要的结构决定。理由只有一句:
混在主循环里,它的产物会落在窗口里;独立成阶段,它的产物落在磁盘上。
落在窗口里的东西活不过窗口边界。于是下一个窗口再探一次、再下一个窗口再探一次,而每次探出来的答案完全一样。这正是 D2 那个教训换了一类信息之后的重演:不是没人知道,是没人把它写到窗口之外。
实验里这个差是可测的。同一份代码、同一个离线模型,唯一的差是初始化放在哪:
完成条数 6 → 9
探测次数 3 → 1
上手产物 没有 → init.sh、PROGRESS.md,各自进了版本库
提交历史 1 条 → 5 条混在一起还有第二个问题,它比浪费更隐蔽:初始化和干活的失败语义不一样。 初始化失败意味着整个运行的前提不成立,正确处理是停下来报警;某一条 feature 失败只是这一条没做成,正确处理是记下来换下一条。混在同一个循环里,这两种失败会走同一条错误处理路径——于是「环境根本没起来」会被当成「这条 feature 有点难」,然后 harness 兴高采烈地接着做第二条。早上你会拿到四十条「已尝试」和一个从来没启动过的服务。
三分钟标准:把一句口号变成一条可执行的判据
「工作区要对新窗口友好」是一句正确的废话。要让它有用,必须能被检验。
本课的判据是:任何新会话三分钟之内能上手。 拆成三个可执行的动作:
- 跑一条命令就知道怎么跑起来(不用读源码猜)
- 读一个文件就知道做到哪了、下一步做什么
- 看一眼提交历史就知道刚才发生了什么
而验收的做法是真的去做一遍,不是看着产物点头。lab 的自检里,这条判据长这样:spawn 一个真进程跑 bash init.sh start,poll 端口,发一个真实 HTTP 请求。
源码导读
今天新增一个模块 src/init/initializer.ts,读的时候重点看三个位置。
第一个位置:产物长什么样。 init.sh 只有两个子命令,刻意都很薄。
// 生成的 init.sh 大致长这样(模板里嵌的是探测出来的值,不是写死的常量)
const script = `#!/usr/bin/env bash
# notekeeper 上手脚本 —— 由 nightrun 的 initializer 生成,不要手改。
#
# bash init.sh check 语法自检(任何时候都该过)
# bash init.sh start 起服务(PORT 可覆盖,默认 ${probe.defaultPort})
set -euo pipefail
cd "$(dirname "$0")"
case "\${1:-help}" in
check) node --check ${probe.entry} ;;
start)
export PORT="\${PORT:-${probe.defaultPort}}"
exec ${probe.startCommand}
;;
esac
`
writeFileSync(scriptPath, script, 'utf8')
// 可执行位是产物的一部分:没有它,新会话第一件事就是踩 permission denied
chmodSync(scriptPath, 0o755)# 生成的 init.sh 大致长这样(模板里嵌的是探测出来的值,不是写死的常量)
script = f"""#!/usr/bin/env bash
# notekeeper 上手脚本 —— 由 nightrun 的 initializer 生成,不要手改。
#
# bash init.sh check 语法自检(任何时候都该过)
# bash init.sh start 起服务(PORT 可覆盖,默认 {probe.default_port})
set -euo pipefail
cd "$(dirname "$0")"
case "${{1:-help}}" in
check) node --check {probe.entry} ;;
start)
export PORT="${{PORT:-{probe.default_port}}}"
exec {probe.start_command}
;;
esac
"""
script_path.write_text(script, encoding="utf8")
# 可执行位是产物的一部分:没有它,新会话第一件事就是踩 permission denied
script_path.chmod(0o755)有两个细节请留意。check 这个子命令看起来没什么用,它的价值在于区分两类失败:语法自检过了说明代码本身没坏,起不来是因为功能还没做;语法自检就没过说明有人写坏了文件。少了这条分界,两种完全不同的问题会长成同一个报错。
另一个是 cd "$(dirname "$0")"。它让脚本从任何目录被调起来都能工作。而脚本里绝不调用 git——它会被从任意目录调起来,一旦在里面写 git 命令就等于依赖继承的 cwd,那是本课的红线。harness 自己的 git 调用一律经 runGit(workdir, args)。
第二个位置:初始化接在主循环的哪两个点上。
// 落点一:主循环之外,整个运行里只发生一次
const initResult = initializeWorkspace(repoDir, state, features)
for (let w = 1; w <= config.windows; w += 1) {
transcript.length = 0
// 落点二:每个窗口开场,把上手命令从磁盘上的 init.sh 读进来。
// 顺序是不变的在前、在变的在后:上手一整夜不变,进度每个窗口都不一样
const initSummary = readInitSummary(repoDir)
const withInit = initSummary === null ? base : `${base}\n\n${initSummary}`
const opening = rebuildContext(withInit, loadState(stateDir), features)
// ...跑这个窗口的若干步...
// 落点三:窗口收尾,重写进度笔记并提交。git log 因此本身就是一条进度线
commitProgress(repoDir, state, features, probe, `chore: 窗口 ${w} 收尾`)
}# 落点一:主循环之外,整个运行里只发生一次
init_result = initialize_workspace(repo_dir, state, features)
for w in range(1, config.windows + 1):
transcript.clear()
# 落点二:每个窗口开场,把上手命令从磁盘上的 init.sh 读进来。
# 顺序是不变的在前、在变的在后:上手一整夜不变,进度每个窗口都不一样
init_summary = read_init_summary(repo_dir)
with_init = base if init_summary is None else base + "\n\n" + init_summary
opening = rebuild_context(with_init, load_state(state_dir), features)
# ...跑这个窗口的若干步...
# 落点三:窗口收尾,重写进度笔记并提交。git log 因此本身就是一条进度线
commit_progress(repo_dir, state, features, probe, f"chore: 窗口 {w} 收尾")注意 readInitSummary 读的是磁盘上的文件,不是探测结果的内存副本。理由与 D2 那条一模一样:只有从磁盘读,这条边界才有资格在 D6 变成一次进程重启。自检里有一条会把 init.sh 挪走,确认它当场返回空——读内存副本的实现在那条断言上会绿,所以那条断言区分的正是「从哪里读」。
第三个位置:一句话需求怎么变成几十条可验证的条目。 靶子自带的四十条 feature 清单是这个展开过程的产物,值得看一眼它的组织方式:功能类二十条、校验类十二条、错误处理类八条,而依赖关系写在描述的措辞里(「依赖 F02 的列表」「必须排在 F10 分页之前」),不另开一个依赖字段。
这不是偷懒。描述是给模型看的唯一规格,依赖必须让它读得出来;写进一个只有 harness 认识的字段,模型就看不见了。这条取舍在 D4 讲清单驱动时会再用一次。
动手实验
练习有五处,都在 src/init/initializer.ts:生成 init.sh、生成 PROGRESS.md、跑初始化阶段、窗口收尾提交、给开场读的那段上手。冻结文件与 D2 的状态层原样带过来,一个字都不用改。
- 先读 src/core/loop.ts 里初始化的三个落点,看清楚哪一件事发生在主循环之外。
- 补上 renderInitScript 与 renderProgress,注意用法行要以井号加三个空格开头,进度笔记要恰好五个二级标题。
- 补上 initializeWorkspace,别忘了 chmod 可执行位,git 调用一律经 runGit。
- 补上 commitProgress 与 readInitSummary,前者没有改动时不要提交,后者必须从磁盘读。
- 跑 MOCK=1 pnpm selftest 到四十四项全绿,再跑 MOCK=1 pnpm start 看两种模式的差。
跑完记得看一眼 work/phase/repo 的提交历史:五条,空壳、初始化、三个窗口收尾。这串 log 本身就是一条能读的进度线——它是 D4 那个反转的铺垫。
今天的变异检验挑的是一个静默失效点:把 renderInitScript 里两行用法说明的前缀从井号加三个空格改成井号加减号,其它一个字不动。再跑一次,窗口开场里那一段变成这样。
| —— 上手(读自仓库里的 init.sh,不是回忆出来的)——
| 这些命令一整夜都不会变,不用再去摸索一遍:
| 进度笔记在 PROGRESS.md,提交历史在 git log。两条命令一条都不在了。 段落还在、标题还在、init.sh 文件也完好,只有真正有用的那部分悄悄没了。没有异常、没有警告。
面试题
今天四道题围绕初始化阶段的职责划分、进度文件的读者意识,以及把「好用」变成可检验判据的能力。
第四道值得特别练。「怎么判断一个工作区对 Agent 足够友好」这种问题,答「文档齐全、结构清晰」基本等于没答——面试官等的是一条别人能照着跑一遍的检验方法。这个思路在评估、在验收、在写 SLO 时是同一套。
检查清单与明日预告
- 能说出初始化阶段必须产出的三件东西,并解释每一件分别消灭了哪一种失败模式
- 能解释为什么初始化要独立成一个阶段而不是混在主循环里,并说出混在一起会带来的两个具体问题
- 能用一个可检验的标准判断工作区是否合格:任何新窗口三分钟内能否自己跑起来并知道做到哪了
- 能说清进度笔记为什么必须重新生成而不是增量维护,以及一份过期的进度文件为什么比没有更糟
- 让 MOCK=1 pnpm selftest 四十四项全绿,并亲眼看过两种模式的提交历史差别
- 做过一次「换掉用法行前缀」的变异检验,能说清断言盯容器与盯内容的区别
- 四道面试题不看要点也能答出至少三道
明天是 D4《一次只做一件事:feature 清单与 git 纪律》。它会以一个反转开场:如果你学过手搓 Coding Agent 那门课,会记得那里有一条明确的裁定——不要用 git 提交,不能污染用户的仓库历史。明天的结论正好相反:git commit 就是进度的权威记录,agent 自己提交。 两边都对,因为操作的仓库根本不是同一个:那门课动的是用户的仓库,本课动的是 agent 自己的工作区。分清这个前提,你就能理解为什么同一个问题在两个场景下有两个相反的正解。
面试题库
让一个 Agent 无人值守地开发一个项目,开工前你会先让它做哪几件事?Before letting an agent develop a project unattended overnight, what do you have it do first?
国内高频海外高频进阶#initialization#failure-modes#long-horizon分析过程 · 先想清楚再作答
- 这题考的是有没有把「开工前」当成一个独立的设计对象。多数人会答「写个好提示词」「把工具配齐」——那是在答交互式 Agent 的问题:人在旁边时,环境有什么坑你当场就补上了。人不在场跑一整夜,没补上的每一个坑都会被后面每个窗口重新踩一遍。
- 正确的拆法是**从失败模式倒推**,而不是凭经验列清单。一手资料把长时程 Agent 的失败归成四类,其中两类在开工第一分钟就能被消灭:**浪费时间摸索怎么把应用跑起来**,以及**在环境里留下缺陷且没有文档**。这两类各配一个机关,其余两类(过早宣布胜利、标记完成但没真测)要靠后面的清单与端到端闸门,不属于初始化。
- 对着第一类失败,机关是一个 `init.sh`。这里有个必须讲清楚的取舍:**为什么必须是可执行脚本,而不是 README 里的一段话?** 因为一段话只能被读懂,脚本可以被**验证**——harness 能真的去跑它一次,跑不起来就知道工作区还没准备好。这件事在一段文字上做不到,而无人值守场景里没有人会替你读那段文字。
- 对着第二类失败,机关是**进度笔记加一个初始 commit**,两件事是一半一半。笔记回答「跑到哪了、下一步做什么」;commit 把前两样放进版本库,它的意义在第一天还看不全——它是给后面所有回滚一个**可靠的地面**,没有这个地面,「回到上一个可用点」就无处可回。
- 所以结论是三件套:`init.sh`、进度笔记、一个 commit。而更值得说出口的是它们的共性——**初始化的交付物不是一段说明,是三件能被别人打开的东西**。顺带给出一条可以带走的反问式判据:遇到任何一条 harness 的做法,问一句「它挡的是哪种失败」,答不上来的做法通常是抄来的。
- 可预期的追问是「三件套是不是太单薄了,要不要再让它先跑一遍测试、先读一遍全部源码」。数量不是重点,**能不能被验证**才是:再加十件产物,只要没人跑过它们,它们的可信度都是零。这里有一条纪律必须主动说——初始化产物是**由 agent 生成的**,而 agent 会写出看起来完全正确却跑不起来的脚本,所以 harness 必须自己跑一遍生成物,模型说写好了不算数。
How to reason about it · think before answering
- This tests whether you treat pre-flight as a design object at all. Most people answer with a better prompt or a fuller toolset, which is the answer to an interactive-agent question: when a human is sitting there, environment gaps get patched on the spot. Run unattended overnight and every unpatched gap gets rediscovered by every window that follows.
- The right approach is to work backwards from failure modes rather than listing habits. The primary source groups long-horizon failures into four classes, two of which can be eliminated in the first minute: time wasted figuring out how to run the app, and defects left in the environment with no documentation. Each gets its own mechanism. The other two - declaring victory early, and marking work done without really testing it - belong to the checklist and the end-to-end gate later, not to initialization.
- Against the first failure the mechanism is an `init.sh`. State the trade-off explicitly: why must it be an executable script rather than a paragraph in the README? Because a paragraph can only be read, while a script can be verified - the harness can actually run it once, and if it fails you know the workspace is not ready. You cannot do that to prose, and unattended there is nobody to read the prose anyway.
- Against the second failure the mechanism is a progress note plus an initial commit, two halves of one thing. The note answers where things stand and what comes next. The commit puts both artifacts under version control, and its value is not fully visible on day one - it gives every later rollback a trustworthy floor. Without that floor, rolling back to the last good point has nowhere to land.
- So the answer is three artifacts: `init.sh`, a progress note, a commit. What is worth saying out loud is what they share - the deliverable of initialization is not an explanation, it is three things somebody else can open. Offer a portable test alongside it: for any harness practice, ask which failure it blocks. A practice with no answer was usually copied from somewhere.
- Expected follow-up: is three too thin - should it also run the tests first, or read the whole codebase? Quantity is not the point; verifiability is. Add ten more artifacts and if nobody has run them their credibility is still zero. Volunteer the discipline here: initialization artifacts are generated by an agent, and agents produce scripts that look entirely correct and do not run. So the harness must execute what was generated. The model saying it works does not count.
答题要点
- 三件套:init.sh、进度笔记、一个初始 commit,三件都是能被别人打开的东西。
- 按失败模式倒推:init.sh 挡「浪费时间摸索怎么把应用跑起来」。
- 进度笔记加初始 commit 挡「在环境里留下缺陷且没有文档」。
- 必须是可执行脚本而不是一段话:一段话只能被读懂,脚本可以被验证。
- 初始 commit 的意义是给后面所有回滚一个可靠的地面。
- 通用判据:任何一条 harness 做法都要答得出它挡的是哪种失败。
- 产物是 agent 生成的,harness 必须自己跑一遍——模型说写好了不算数。
Key points
- Three artifacts: init.sh, a progress note, an initial commit - all things someone else can open.
- Derive them from failure modes: init.sh blocks time wasted figuring out how to run the app.
- The progress note plus the initial commit block defects left with no documentation.
- It must be an executable script, not prose: prose can only be read, a script can be verified.
- The initial commit gives every later rollback a trustworthy floor to land on.
- Portable test: every harness practice must name the failure mode it blocks.
- The artifacts are agent-generated, so the harness runs them itself. The model's word does not count.
进度文件应该写什么?写成给人看的日志和给下一个窗口看的摘要有什么区别?What belongs in a progress file, and how does a human-readable log differ from a summary written for the next window?
国内高频海外高频进阶#progress-file#state-management#long-horizon分析过程 · 先想清楚再作答
- 这题考的是**读者意识**。答「记录做了什么」只是在描述日志,拿不到区分度。面试官想听的是:你知道这份文件有两类读者,而同一批事实要按读者换一种组织方式——不是换一套文案,是换结论的位置和引用的方式。
- 先把内容摆出来,它其实很短,五问五答:怎么把它跑起来、现在完成了几条、下一步做哪一条、怎么才算一条做完了、已知的坑有哪些。注意最后两条容易被漏掉:「怎么算做完」定义的是验收口径,没有它,下一个窗口会按自己的理解宣布通过;「已知的坑」承载的是跨窗口才有意义的教训,比如某条已经试过三次仍然没成,别再原样重试。
- 然后是读者的差别。**给下一个窗口看的摘要**:结论前置、用 feature 编号而不是描述文本(编号稳定、描述会随需求改)、并且要明说更早的对话已经不存在——不说这句,模型会以为自己漏读了什么而去翻不存在的历史。**给人看的日志**:要能被一个刚接手的人从头读到尾,可以有过渡句、可以解释背景。**两者数字同源、口吻不同**,这是一句可以直接答出去的总结。
- 真正拉开差距的是下一条结论:**进度文件必须每次重新生成,不能增量维护。** 手工增量维护的文件一定会漂移——没人记得每次都改,改了也没人核对它和真实进度是否一致。重新生成意味着它不可能说谎,因为它的每个数字都来自同一份权威状态。
- 为什么这么严重,要用一句话钉死:**一份内容错误的进度文件比没有进度文件更糟。** 没有文件时,下一个窗口知道自己得去查;有一份过期文件时,它会照着做。前者浪费几分钟,后者产出一整夜的错误工作,而且没有任何东西会报错。
- 落到实现上给一条可验收的做法:每次窗口收尾把这份文件整个重写,然后配一条自检——手改其中一行,再触发一次收尾,那一行必须被覆盖掉。这条断言的好处是它直接钉住「重新生成」这个性质本身,而不是去评判某一次输出写得好不好看;输出的措辞可以随时改,性质不能丢。
- 可预期的追问是「整个重写不就把历史丢了吗」。历史在提交历史里,而且那份历史是 append-only、不会漂移的。让一份会被反复重写的文本同时承担「当前状态」和「历史记录」两个职责,正是它开始说谎的起点——两个职责分给两个介质,各自都能做对。
How to reason about it · think before answering
- This tests audience awareness. Answering 'record what was done' merely describes a log and earns nothing. The interviewer wants to hear that the file has two kinds of reader, and that the same facts get organized differently for each - not different wording, but a different position for the conclusion and a different way of referring to work.
- Lay out the content first; it is short. Five questions, five answers: how to run it, how many items are complete, which item is next, what counts as complete, and which pitfalls are already known. The last two get skipped most often. 'What counts as complete' defines the acceptance standard - without it the next window declares success on its own terms. 'Known pitfalls' carries the lessons that only mean anything across windows, such as an item already attempted three times without success, so stop retrying it unchanged.
- Now the reader difference. The summary for the next window puts conclusions first, refers to work by feature id rather than description (ids are stable, descriptions drift with requirements), and states outright that earlier conversation no longer exists - omit that and the model assumes it missed something and goes hunting for history that is not there. The human log has to be readable start to finish by someone just picking the work up, so it can afford transitions and background. Same numbers, different register - that sentence alone is a good answer.
- The real discriminator is the next conclusion: the progress file must be regenerated every time, never maintained incrementally. A hand-maintained file will drift - nobody remembers to update it every time, and when they do nobody checks it against reality. Regeneration means it cannot lie, because every number in it comes from the same authoritative state.
- Pin down why that matters with one line: a progress file with wrong content is worse than no progress file at all. With no file, the next window knows it has to go look. With a stale file, it acts on what it reads. The first wastes minutes; the second produces a night of wrong work, and nothing anywhere raises an error.
- Give a verifiable implementation: rewrite the whole file at every window close, then write one assertion - hand-edit a line, trigger another close, and that line must be gone. The assertion pins the regeneration property itself rather than judging whether one particular output reads nicely.
- Expected follow-up: does a full rewrite throw away history? History lives in the commit log, which is append-only and does not drift. Making one repeatedly rewritten text carry both current state and historical record is exactly where it starts lying. Split the two responsibilities across two media and each one can be correct.
答题要点
- 五问五答:怎么跑起来、完成几条、下一条做什么、怎么算做完、已知的坑。
- 给下一个窗口的摘要:结论前置、用 feature 编号、明说更早的对话已不存在。
- 给人看的日志:能从头读到尾,可以有过渡与背景。两者数字同源、口吻不同。
- 进度文件必须每次重新生成,增量维护一定漂移。
- 一份内容错误的进度文件比没有更糟:没有时会去查,过期时会照着做。
- 可验收的断言:手改一行再触发收尾,那一行必须被覆盖掉。
- 整个重写不丢历史:历史归提交历史,一份文本别兼两个职责。
Key points
- Five questions: how to run it, how many done, what is next, what counts as done, known pitfalls.
- Summary for the next window: conclusions first, feature ids, and say earlier conversation is gone.
- Human log: readable end to end, transitions allowed. Same numbers, different register.
- The file must be regenerated each time; incremental maintenance always drifts.
- A wrong progress file is worse than none: with none you go look, with a stale one you act on it.
- Verifiable assertion: hand-edit a line, close a window, and that line must be overwritten.
- Rewriting loses nothing: history belongs to the commit log, one text should not carry both jobs.
为什么初始化要独立成一个阶段,而不是让主循环第一轮顺手做掉?Why make initialization its own phase instead of letting the first pass of the main loop handle it?
国内高频海外高频深入#architecture#initialization#failure-handling分析过程 · 先想清楚再作答
- 这题考的是结构判断力,不是知识点,所以答「更清晰」「更模块化」等于没答——那两句对任何拆分都成立,换成把日志抽出来、把配置抽出来同样说得通。要给出的是**这个拆分特有的后果**:不这么拆会具体坏在哪里,而且最好能用一个可测量的差把它顶起来。
- 理由可以压成一句话,值得原样答出去:**混在主循环里,它的产物会落在窗口里;独立成阶段,它的产物落在磁盘上。** 落在窗口里的东西活不过窗口边界,于是下一个窗口再探一次、再下一个窗口再探一次,而每次探出来的答案完全一样。
- 第一个具体问题是**重复摸索**,而且是可测的。同一份代码、同一个离线模型,唯一的差是初始化放在哪:探测次数 3 降到 1,完成条数 6 升到 9。注意这个差的来源——两趟探出来的结论一模一样,差别只在它被写到了哪里。一夜八个窗口,每个窗口花半小时重新搞清楚同一件事,就是一半算力花在了重复上。
- 第二个问题更隐蔽,也是这题真正的分水岭:**初始化失败与 feature 失败的语义根本不同。** 初始化失败意味着整个运行的前提不成立,正确处理是停下来报警;某一条 feature 失败只是这一条没做成,正确处理是记下来换下一条。混在同一个循环里,两者会走同一条错误处理路径——于是「环境根本没起来」被当成「这条 feature 有点难」,harness 兴高采烈地接着做第二条。早上你会拿到四十条「已尝试」和一个从来没启动过的服务。
- 第三个好处是顺带的,但它给了一条能迁移的判据:初始化**只跑一次而且幂等**,所以它可以被单独重试、单独验证、单独缓存;主循环里的一步这三件事一件都做不到。反过来说,一个东西只要满足「只跑一次、失败了要整个重来」,它通常就该是一个独立阶段——这条在 CI、在数据管道、在部署流程里同样成立。
- 可预期的追问是「每个窗口开场都去读一次 init.sh,不也是重复吗」。不是同一种重复:读一个磁盘上的文件是一次确定的、O(1) 的动作,摸索是不确定的多轮试错,代价和结论都不稳定。这里还有一条实现纪律值得主动提——开场那段上手信息必须**从磁盘读**,不能拼内存里的探测结果副本,否则这条边界在后面就没资格变成一次真正的进程重启。
- 另一个追问是「初始化失败了到底怎么办」。按上面的语义就有答案:停机报警,不进主循环。这正是把它独立出来的收益兑现的地方——**独立的阶段才允许有独立的错误处理**,混在一起时你连表达这个区别的位置都没有。
How to reason about it · think before answering
- This probes structural judgment, not recall, so 'cleaner' and 'more modular' are non-answers - they hold for any split whatsoever. You need the consequence specific to this split, ideally backed by a measurable difference.
- The reason compresses to one sentence worth delivering verbatim: mixed into the main loop, its output lands in the window; as its own phase, its output lands on disk. What lands in a window does not survive the window boundary, so the next window probes again, and the one after that probes again, and every probe returns exactly the same answer.
- The first concrete problem is repeated probing, and it is measurable. Same code, same offline model, the only difference being where initialization lives: probe count drops from three to one, completed items rise from six to nine. Note where that gap comes from - both runs discovered identical facts; only the destination differed. Eight windows a night, each spending half an hour re-establishing the same thing, means half your compute went into repetition.
- The second problem is subtler and is where this question actually separates people: initialization failure and feature failure do not mean the same thing. Initialization failing means the premise of the whole run does not hold, and the correct response is to stop and raise an alarm. One feature failing means just that one did not land, and the correct response is to record it and move to the next. Share one loop and both take the same error path, so 'the environment never came up' gets treated as 'this feature is a bit hard' and the harness cheerfully starts the second one. In the morning you have forty attempted items and a service that never started.
- The third benefit comes free but yields a transferable test: initialization runs exactly once and is idempotent, so it can be retried, verified and cached on its own. A step inside the main loop can do none of those three. Inverted: anything that runs once and must be redone wholesale on failure usually deserves to be its own phase - the same call holds in CI, in data pipelines, in deployment.
- Expected follow-up: is reading init.sh at every window opening not also repetition? Not the same kind. Reading a file on disk is one deterministic constant-cost action; probing is open-ended trial and error whose cost and conclusion both vary. Volunteer one implementation discipline here too - that opening section must be read from disk, not assembled from an in-memory copy of the probe result, or the boundary loses its right to become a real process restart later.
- Another follow-up: what actually happens when initialization fails? The semantics above answer it - halt and alarm, never enter the main loop. That is where the split pays for itself: only a separate phase is allowed separate error handling. Merged, you do not even have a place to express the distinction.
答题要点
- 一句话理由:混在主循环里产物落在窗口里,独立成阶段产物落在磁盘上。
- 问题一是重复摸索,可测:探测次数 3 降到 1,完成条数 6 升到 9。
- 两趟探出来的结论完全一样,差别只在它被写到了哪里。
- 问题二是失败语义不同:初始化失败该停机报警,feature 失败只换下一条。
- 混在一条错误处理路径上,环境没起来会被当成这条有点难,早上拿到四十条已尝试。
- 第三个好处:只跑一次且幂等,所以能被单独重试、验证、缓存。
- 可迁移判据:只跑一次、失败要整个重来的事,通常就该独立成阶段。
Key points
- One-line reason: mixed in, output lands in the window; as a phase, output lands on disk.
- Problem one is repeated probing, measurable: probes three to one, completions six to nine.
- Both runs discover identical facts; only the destination differs.
- Problem two is failure semantics: init failure means halt and alarm, feature failure means move on.
- On one error path, a dead environment reads as a hard feature - forty attempts and no running service.
- Third benefit: it runs once and is idempotent, so it can be retried, verified and cached alone.
- Transferable test: run-once, redo-wholesale work usually belongs in its own phase.
怎么判断一个工作区对新来的 Agent 足够友好?给一个可执行的检验方法。How do you judge whether a workspace is friendly enough for a newly arrived agent? Give an executable test.
国内高频海外高频深入#acceptance-criteria#verification#developer-experience分析过程 · 先想清楚再作答
- 这题问的是**把一句正确的废话变成判据**的能力。「文档齐全、结构清晰」基本等于没答——两个形容词,没有一个能被别人拿去跑一遍。面试官等的是一条**别人能照着做、做完有明确结果**的检验方法。这个思路在评估、在验收、在写 SLO 时是同一套。
- 先给判据本身:**任何新会话三分钟之内能上手。** 然后立刻把它拆成三个可执行的动作,否则它还是一句口号。一,跑一条命令就知道怎么把它跑起来,不用读源码猜;二,读一个文件就知道做到哪了、下一步做什么;三,看一眼提交历史就知道刚才发生了什么。三条各对一件产物,缺哪一条就知道该补哪件。
- 接着是这题真正的重点:**验收要真的做一遍,不是看着产物点头。** 具体做法是 spawn 一个真进程跑起手脚本、poll 端口、发一个真实请求,拿到响应才算过。一句话总结可以直接答出去:**产物存在不算数,产物跑得起来才算数。**
- 为什么非得这么狠,理由要说清楚,否则听起来像洁癖:**这些产物是 agent 生成的**,而 agent 会写出看起来完全正确却跑不起来的脚本。你读它读不出问题,因为它在文本层面确实没问题。同理,任何「让模型生成配置、脚本、迁移」的设计里,harness 都必须自己跑一遍生成物——模型说写好了不算数。
- 给一个具体的坑来证明「读」代替不了「跑」,这一步最能体现实战经验:**可执行位**。起手脚本如果用 `bash init.sh` 调,不需要执行位;用 `./init.sh` 调才需要。于是漏掉 chmod 这件事在你自己的机器上**很可能测不出来**——你恰好一直用前一种调法。这类缺陷只有真跑、并且按新会话真实的调用方式跑,才会暴露。
- 还有一层值得主动提:**判据本身也可能是恒真的。** 本课有个现成的例子——最初那条断言查的是「窗口开场里有没有上手这个段落」,把用法行的前缀改掉之后两条命令一条都不剩,而断言照样全绿,因为段落确实还在,只是里面空了。改成逐条查那两条命令真的在上下文里,注入同样的变异才恰好一项转红。写断言时问自己一句:我断言的是那件事的**后果**,还是那件事的**包装**?
- 可预期的追问是「三分钟这个数字怎么定出来的」。老实答:数字本身不重要,也没有实验能定出一个普适的三分钟。它的作用是**逼你把标准翻译成一串能计时的动作**——真正的判据是那三个动作,不是那个数。换成五分钟,三个动作一条都不用改,这恰好说明判据落在动作上而不落在数字上。
How to reason about it · think before answering
- This asks whether you can turn a correct platitude into a criterion. 'Good docs, clear structure' is effectively a non-answer - two adjectives, neither of which anyone can go run. The interviewer wants a test someone else can follow, with an unambiguous result at the end. The same habit applies to evaluation, to acceptance, to writing SLOs.
- State the criterion: any new session can get going within three minutes. Then immediately decompose it into three executable actions, or it stays a slogan. One, run a single command and know how to start the thing, without guessing from source. Two, read a single file and know where the work stands and what is next. Three, glance at the commit history and know what just happened. Each maps to one artifact, so a failure tells you which artifact is missing.
- Now the real point of the question: acceptance means actually doing it, not nodding at the artifacts. Concretely: spawn a real process running the startup script, poll the port, send a real request, and require a response before it passes. The summary line is answerable as is - an artifact existing does not count, an artifact running does.
- Explain why you need to be this strict or it sounds like fastidiousness: these artifacts were generated by an agent, and agents write scripts that look entirely correct and do not run. Reading them finds nothing, because at the text level there is nothing to find. The same holds anywhere a model generates configuration, scripts or migrations - the harness must execute the output itself. The model's claim that it works does not count.
- Give a concrete pitfall proving that reading cannot replace running, which is where field experience shows: the executable bit. Invoke the startup script through the shell explicitly and it needs no execute permission; invoke it as a path and it does. So forgetting chmod may well be untestable on your own machine, because you happen to use the first form. That class of defect surfaces only when you really run it, and run it the way a new session actually will.
- One more layer worth volunteering: the criterion itself can be a tautology. This course has a ready example - the first assertion checked whether the window opening contained the getting-started section. Change the prefix on the usage lines and both commands vanish, yet the assertion stays green, because the section is still there and merely empty. Rewritten to check that each of the two commands is actually in the context, the same mutation turns exactly one item red. Ask yourself while writing assertions: am I asserting the consequence, or the packaging?
- Expected follow-up: where does three minutes come from? Answer honestly - the number itself does not matter and no experiment fixes a universal three minutes. Its job is to force the standard into a sequence of actions you can time. The real criterion is those three actions, not the number. Swap in five minutes and not one action changes, which is precisely the proof that the criterion rests on the actions.
答题要点
- 判据:任何新会话三分钟之内能上手,必须拆成三个可执行动作才有用。
- 三个动作:跑一条命令知道怎么起、读一个文件知道做到哪、看一眼提交历史知道刚发生了什么。
- 验收要真做一遍:spawn 真进程跑起手脚本、poll 端口、发真实请求。
- 产物存在不算数,产物跑得起来才算数——因为产物是 agent 生成的。
- 可执行位是现成的例子:用 bash 调不需要执行位,用路径调才需要,漏掉 chmod 本机测不出来。
- 判据本身也可能恒真:断言盯了段落标题而不是那两条命令,变异之后照样全绿。
- 三分钟这个数不重要,它的作用是逼你把标准翻译成能计时的动作。
Key points
- Criterion: any new session gets going within three minutes, decomposed into three executable actions.
- The three: one command to start it, one file for where things stand, one glance at history for what just happened.
- Acceptance means really doing it: spawn the script, poll the port, send a real request.
- An artifact existing does not count, an artifact running does - because an agent generated it.
- The executable bit is the ready example: shell invocation needs none, path invocation does, so a missing chmod hides locally.
- The criterion can be a tautology too: asserting the section heading stayed green after the commands vanished.
- Three minutes is not the point; it forces the standard into actions you can time.