逐日AI
第 2 周 · D13约 4 小时

先问后做:结构化提问工具、只读探索模式与计划审批

让 Agent 在动手前先把话说清楚:实现一个让模型结构化反问的工具,加一个只放行只读工具的探索模式,最后让它产出一份可审批的计划,用户批准后才切回可写状态执行。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能实现一个结构化提问工具,并说清它比在正文里反问强在哪
  2. 能实现只读探索模式,并解释它与权限规则的关系
  3. 能设计计划的产出格式与审批流程,让批准后的执行可核对

昨天解决的是「桌子放不下了怎么办」,今天解决的是一开始就少往桌上放东西。读完回到页面顶部把三条目标勾掉。

小白版讲解

先说清楚要怎么做,再动手

那位新人现在什么都会了。于是你交给他一件事:「让 divide 在除数为 0 的时候更安全一点。」半小时后他回来说改好了。你打开一看:他把测试改了。

他没做错任何一件事——「更安全」本身就有两种读法:抛错是一种,返回 null 让调用方自己判断是另一种。而在他那个读法里,改哪边都能让测试变绿。

返工的成本不在改代码,在于你要重新读一遍他改了什么,再把他推回去。 而这半小时本来只需要一句话就能省掉:「你是想让它抛错,还是返回 null?」

所以今天做的事只有一件:让它在动手之前把话说清楚。 拆成三段:

  1. 该问就问。 给它一个反问的工具,而且逼它把「有哪几种做法」先想清楚再问。
  2. 先只读地探一遍。 探索阶段一个字都不许改,写工具与命令全挡在门外。
  3. 交一份可审批的计划。 写清改哪些文件、怎么验证、有什么风险,你批准了它才动手;批准之后还要逐条核对它有没有跑偏。

第三段最容易被做成摆设——很多人做到「模型输出一份计划、用户按回车确认」就停了。真正让这套东西有意义的是最后半句:批准之后的执行必须能被核对。 一份没人回头对过的计划,和没有计划是一样的。

结构化提问:为什么反问要做成一个工具

最省事的做法是让模型在正文里问一句:「你是想让它抛错还是返回 null?」看起来完全够用。三条理由说明它不够用。

一、正文里的问句没有边界。 模型说完那句话之后,循环拿到的是 finish_reason: stop——它和「我答完了」长得一模一样。程序分不出「这一轮结束了」和「这一轮在等人」,而用户会以为 Agent 卡住了。

二、答案回不到正确的位置。 用户下一句话是一条新的用户消息,问题却在上一条助手消息里,中间隔着一整轮,绑定只能靠模型自己回忆。做成工具之后,答案是一条 toolCallId 指回那次提问的 tool 消息——绑定是结构上的

三、也是收益最大的一条:自由文本问不清楚。 正文里的问题永远长成「你想怎么做?」这种没法回答的形状。而工具有参数表:

src/plan/ask.ts
parameters: {
  type: 'object',
  properties: {
    question: { type: 'string', description: '一句话的问题,不要一次问两件事' },
    options: {
      type: 'array',
      description: '可选项,每项是 value(回灌给你的短值)与 label(给人看的说明)',
      items: { type: 'object', required: ['value', 'label'] },
    },
    default: { type: 'string', description: '直接回车时选的 value' },
    multi: { type: 'boolean', description: '允许多选' },
  },
  required: ['question'],
  additionalProperties: false,
},
readOnly: true,

要填 options,它就得先把「有哪几种做法」想清楚才问得出来。参数表就是提问的质量下限,而这个下限是 schema 白送的,一行提示词都不用写。

readOnly 为真也值得说一句:提问不改任何文件,所以第五天那道门直接放它过去。把提问也做成要审批的操作,会出现「为了问一句话先问一句话」的荒唐链路。

回答的解析有三个坑,都是被「用户实际会怎么回」逼出来的。一是空回答不等于没答案:他最常做的动作就是直接回车,想的是「按你说的默认那个来」;有 default 就取 default,没有才算跳过,而跳过也要明确回灌「用户没有回答」,不能装作他选了第一个。二是序号与选项名都要收:人最自然的输入是「2」,而模型认的是它自己给的 value。三是不认识的输入要原样当答案回灌回去——选项是模型列的,它常常列不全,把「都不是,我要返回 NaN」传回去比逼用户在三个错选项里挑一个有用得多。

至于什么时候该问——判据只有一条:两种合理读法会导致完全不同的改动时才问,能自己读代码查明的事不许问。两端的代价都真实:问太多,用户很快开始一路回车(于是等于没问);不问,你付的是返工。

只读探索模式:它是权限规则的一个预设

第二段是「先只读地探一遍」。这里最容易走错路,所以先说结论:

只读探索模式不是一套新的权限系统,它是第五天那张规则表的一个预设。

具体说,进入探索模式只做一件事——把规则表整体换成两条:

TextText
{ decision: 'deny' }                          所有工具一律不许
{ tool: 'submit_plan', decision: 'ask' }      除了「交一份计划」这一件事

而只读工具根本走不到规则表:第五天那个 decide() 的第一行就是「readOnly 为真直接放行」。于是 glob、grep、read_file、ask_user 照常可用。「探索模式」这四个字在代码里只对应一次规则表替换。

反过来做会怎样:加一个 mode 字段,然后在循环里判一次、门里判一次、每个写工具里再判一次。你会得到三处可能不一致的判断,而它们不一致的那天,表现是「计划模式下它偷偷改了文件」。权限的判定入口全课只能有一个,这条比今天任何一个功能都重要。

换规则表还有一个必须踩对的细节:要整体替换,不能追加。

顺着这条再定一个口径:计划模式不给任何命令开口子,连 node --test 也挡。探索阶段跑测试确实无害,但「哪条命令算只读」是个无法穷举的判断——跑测试会不会写快照文件?会不会碰缓存目录?而这个模式的全部价值就在于边界简单到能一句话说清:只读工具全过,其余全挡。 想跑测试,先出计划。

被挡回来的调用长什么样,第五天已经定过:门返回一条 ok 为 false 的工具结果,写清被拒的是什么、为什么、可以换哪条路。所以拒绝理由要写成模型能改道的样子——本实验里它是这样接的:

TextText
⚙ run_command({"command":"node --test"})
✘ run_command 回灌 149 字符 · 这次 run_command 调用没有被执行:现在是只读探索模式:可以读文件、搜内容、问用户,但不能改文件也不能跑命令。
现在是只读探索模式,跑命令被挡回来了——那我用只读工具看代码。
⚙ read_file({"path":"src/calc.js"})

第三行是模型看完那条拒绝理由之后自己说的。它换了一条路,而不是原地重试同一个调用——第五天那条「拒绝文本要写成它能改道的样子」在今天兑现了。

计划长什么样:可核对是唯一的判据

第三段是计划,而它的格式只有一条判据:可核对。

自然语言的计划读起来最顺:「优化一下除法的边界处理,并补充相应的测试。」问题是这句话改一个文件和改五个文件都算符合。所以计划是结构化的四个字段,每个都要能拿实际发生的事去对:

字段内容事后拿它对什么
goal一句话说清要达成什么这份计划答的是不是我问的那个问题
steps每步改哪几个文件、做什么实际改了哪些文件,也是批准的授权范围
verify怎么证明做成了那条命令到底跑过没有
risks有什么可能出错给人一个「不批」的理由

最容易被省掉的是 verify:一份没有验证方式的计划,执行完只能靠人看;写清了「跑 node --test,四个用例全绿」,执行阶段就有一条硬判据。最容易被糊弄的是 steps 里的 files——整套机制全靠它撑着,没有它后面的核对全都做不了。

不合格的计划怎么办?整份退回,不要就地补全。

还有一个顺序问题:门排在工具执行之前,所以用户看到计划的时候,程序还没校验过它。格式不合格的计划不该拿去打扰用户,所以校验要提前到提问那一步——解析不过就当「用户拒绝」,把原因回灌给模型让它重发。这是一次预检,判定用的还是同一个校验函数。

审批之后:批准的是这份计划,不是无限授权

现在看今天最漂亮的一段——计划审批没有引入任何新机制。

submit_plan 的全部机关只有一处:它不是只读的。于是第五天那道门自动接住它。规则表里它判 ask,门就调审批回调去问一句;答 y,门返回 null,工具照常执行(也就是真的切回可写模式);答 n,门返回一条 ok 为 false 的工具结果,模型看到「没有被执行」自己改道。

AgentEvent 一个事件类型都没加,渲染层一行没改,循环一行没改。加的只是一个工具和一条规则。 这是第五天那句「审批是循环里的一次暂停,不是一次异常」的第二次分红。

需要改的只有一处,而且它本来就该在那里:摆给用户看的内容。 终端里问「需要确认:submit_plan」是句废话,要摆的是整份计划——这是渲染层的职责,所以它就写在渲染层:

TextText
计划:给 divide 加上除零保护,让 divide by zero 那个用例变绿
  1. 在 divide 开头判断除数为 0,为 0 时抛错  →  src/calc.js
  2. 跑一遍测试确认四个用例全绿  →  (只读,不改文件)
  验证:跑 node --test,四个用例全绿
  风险:原来依赖 divide(1, 0) 返回 Infinity 的调用方会开始收到异常
批准这份计划吗?y 批准并开始执行 / n 不批(说说哪里要改)

注意这里只给了 y 与 n 两个选项。第五天那个审批有四个选项,其中 a(本会话都可以)与 w(写进配置)的含义是「以后同样的操作别问了」——而「以后所有计划都自动批准」正好把计划模式的意义整个抹掉。

批准之后授权多少,是本节的题眼。两个极端都错:什么都不授权,则计划里每个文件在执行时还要被单独问一遍——用户刚批准了一份写明文件的计划,再问一遍是纯粹的骚扰,而他很快学会一路按 y,门就虚了;全开,则批准这份计划和批准「随便改」没有区别,计划里那几个文件成了摆设。

正确的粒度就是计划本身:files 逐个文件放行,一个不多。

src/plan/mode.ts
for (const file of plannedFiles(plan)) {
  // 放行之前先用原规则表判一次:本来就 deny 的不许放行
  const verdict = decide(writeTool, { tool: writeTool.name, path: file }, permissions)
  if (verdict.decision === 'deny') {
    refused.push(file)
    continue
  }
  permissions.rules.push({
    tool: writeTool.name,
    path: file,
    decision: 'allow',
    why: '在用户已经批准的那份计划里',
  })
  granted.push(file)
}

那个 decide 判断是今天最值得警惕的一行。按计划放行不能越过 deny。 规则表里有一条硬规则「不许改测试目录」(理由是不能靠改测试让它变绿)。批准时若无脑给每个文件加一条 allow,模型只要把 test/calc.test.js 写进计划,就能借一次审批绕开那条硬规则——而用户批准时未必读得出这层含义。

所以放行前先判一次:本来判 ask 的才放行,本来就 deny 的保持 deny,并明确回灌「批准这份计划不会解除那条禁止」。自检把这条钉死了:

TextText
✔ 批准后只放行 src/calc.js;test/calc.test.js 本来就 deny,批准不解除它;计划外的文件仍然要单独问

执行时的一致性核对:偏离要说得出来

最后一段,也是最多人做漏的一段。

批准一份计划之后最容易出的事不是「它不干」,而是它干了别的:计划里说改一个文件,实际改了三个;计划里说要跑测试验证,它改完就宣布成功。

这两种偏离的共同点是——它们都不是错误。工具全部成功、循环正常结束、终端上一片对勾。所以偏离必须被主动核对出来,和昨天那条压缩探针同理:静默的失败只能靠主动检查暴露。

采集用的是全课已经用过三次的形状:一层透明包装(第七天的落盘、第十二天的用量取样都是它)——事件原样传下去、路过时记一笔,于是循环、渲染层、工具都不知道有核对这件事。只记 ok 为真的写入:被门挡下的、执行失败的都不算「实际改动」。

核对表三件事,每一件都要写清「所以呢」,不能只标一个叉:

TextText
计划核对:计划改 1 个文件,实际改 2 个
  ✔ src/calc.js(计划内)
  ⚠ src/notes.md 不在计划里——请让它说明为什么改了这个文件
  ⚠ 计划里写了验证方式,但没有跑过对应的命令——「改完了」不等于「做成了」

上面这几行来自本实验故意跑偏的那条对照支线。同一份计划,走批准并按计划执行那条支线,三列全是勾:

TextText
计划核对:计划改 1 个文件,实际改 1 个
  ✔ src/calc.js(计划内)
  ✔ 计划里的验证方式真的跑过了

两条支线的差别只来自「实际做了什么」,计划是同一份——这个对照才是「计划审批是一套机制」而不是「一次确认弹窗」的证据。

最后划两条边界。一、核对只看这一次执行做了什么,不看代码写得对不对:「改动是否正确」由计划里的 verify 负责(跑测试),不是核对的活儿。二、没动过东西的那几轮不要打核对表,否则每一轮都会刷一屏「计划里的文件没被改动」,而那句警告在真正需要它的时候就没人看了。

源码导读

动手实验

🧪 D13 实验:一个提问工具、一个只读探索模式与一套计划审批后执行的流程

代码位置:labs/my-coding-agent-21days/day-13-plan-mode

今天挖了五个练习点,五个全是「看着更简单、实际更糟」的陷阱:把两条预设追加在规则表后面(于是 node --test 照跑)、空回答不取默认值也不认序号、缺 verify 就替它补一条、批准就把写权限全开、核对表只数「计划内的动了几个」。起点代码原样跑是十二项里过六项,全部离线。

  1. 实现回答解析:空回答取默认值、序号与选项名都收、不认识的原样当自由回答。
  2. 把探索模式改成整体替换规则表,确认 edit_filenode --test 都判 deny,而只读工具直通。
  3. parsePlan 在缺 verify、一步都没写 files 时整份退回,并把原因回灌给模型。
  4. 实现按计划逐个文件放行,并在放行前用 decide 判一次——本来 deny 的不许放行。
  5. 补齐核对表的三列(计划外、漏做、验证跑没跑),跑 MOCK=1 SELFTEST=1 pnpm start 看到 12/12 通过,再用 README 里那三条管道命令看批准、拒绝、跑偏三种现象。

验收看五条勾:自检 12/12 通过;探索模式那一项打出「规则表从 4 条整体换成 2 条预设」;端到端那一项打出六次工具调用的顺序,其中第二次 run_commandblockedBy=approval;批准那一项打出「只放行 src/calc.js,test/calc.test.js 本来就 deny」;核对表两条支线一条全绿、一条抓到两处偏离。

面试题

今天三道题,考的是「什么时候把人放回回路」以及这件事怎么实现,不是「Agent 要不要人工确认」:

  1. 什么时候 Agent 该停下来反问用户?问得太多和不问各有什么代价?
  2. 计划模式怎么实现?它和权限系统是什么关系?
  3. 批准了一份计划之后,怎么保证执行没有偏离?

完整题干、分析过程与答题要点见本课面试题库的第十三天。第二题最有区分度——多数人会答「加一个 mode 字段然后到处判一下」,能说清「它只是权限规则的一个预设、判定入口只能有一个」的人很少。

检查清单与明日预告

  • 能说出反问要做成工具而不是写在正文里的三条理由,以及第三条为什么收益最大
  • 能说清提问渠道为什么靠闭包注入,而不是往 ToolContext 加字段
  • 能说出回答解析的两个坑:空回答要取默认值、序号与选项名都要收
  • 能一句话说清只读探索模式与权限规则的关系,以及为什么判定入口只能有一个
  • 能解释为什么换规则表要整体替换而不能追加(更具体的规则会赢)
  • 能背出计划的四个字段,并说清不合格时为什么要整份退回而不是就地补全
  • 能说清「批准的是这份计划,不是无限授权」怎么落地,以及为什么放行不能越过 deny
  • 能说出计划核对的三列,以及为什么「没跑验证就宣布成功」也算偏离

明天是 D14《checkpoint 与 rewind:文件快照、对话回退,以及两者为什么必须独立》。今天做的是动手之前的那道闸,明天做动手之后的那条退路。两者是一对:今天那份计划让你事前知道它要动什么,明天那套快照让你事后能把它动过的东西还原回去。明天也是第二周最后一天,正文最后会把 D8 到 D14 这七层串一遍。

面试题库

  • 什么时候 Agent 该停下来反问用户?问得太多和不问各有什么代价?When should an agent stop and ask the user a question? What does asking too much cost, and what does never asking cost?
    国内高频海外高频基础#clarification#human-in-the-loop

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

    1. 这题看着是产品题,其实在考「你有没有一条能写进代码的判据」。答「不确定的时候就问」等于没答——模型对什么都不确定。
    2. 怎么拆:先给判据。**只有当两种合理读法会导致完全不同的改动时才问**,能自己读代码查明的事不许问。「让 divide 更安全一点」是前者(抛错和返回 null 改出来的代码不一样,而且测试期望的是哪一种也不确定);「divide 在哪个文件里」是后者,grep 一下就有。这条判据的好处是它可判定,能直接写进工具的 description 里约束模型。
    3. 再说两端的代价,它们不对称但都真实。问太多:用户很快学会一路回车,于是那些提问全部失效——**一个被无脑通过的问题比不问更糟**,因为你以为自己确认过了。不问:你付的是返工,而返工的成本不在改代码,在于你要重新读一遍它改了什么再把它推回去。
    4. 结论:宁可少问,但每一问都要是真的岔路口;而且**提问必须便宜**——给选项、给默认值、支持直接回车,把回答的成本压到一次按键。默认值这一项最容易被忽略:用户在终端里最常做的动作就是直接回车,他心里想的是「按你说的默认那个来」。
    5. 可预期的追问:无人值守(CI、别的 Agent 调你)时怎么办?这时候没有提问渠道,**不许假装问过、也不许自动选第一个**。正确做法是回灌一条明确的结果:「当前环境无法向用户提问,请按最稳妥的一种做法继续,并在回答里说明你替用户做了哪个假设。」这条和审批门的口径一致——无人值守时不许自动同意。

    How to reason about it · think before answering

    1. This looks like a product question but really tests whether you have a criterion you could put in code. Answering ask when you are unsure says nothing — the model is unsure about everything.
    2. How to break it down: give the criterion. Ask only when two reasonable readings would lead to genuinely different edits; anything the agent can determine by reading the code is off limits. Make divide safer is the first kind — throwing versus returning null produce different code, and which one the test expects is also unsettled. Where does divide live is the second kind; a grep answers it. The value of this criterion is that it is decidable, so it can go straight into the tool description.
    3. Then the two costs, asymmetric but both real. Asking too much: users quickly learn to hit enter on everything, which voids every question — a question that gets rubber-stamped is worse than no question, because you believe you confirmed something. Never asking: you pay in rework, and the cost of rework is not the edit, it is re-reading what it changed and pushing it back.
    4. Conclusion: ask rarely, but only at real forks — and make asking cheap. Offer options, offer a default, accept a bare enter, so answering costs one keystroke. The default is the piece people forget: hitting enter is the most common thing a user does in a terminal, and what they mean is use the default you suggested.
    5. Likely follow-up: what about unattended runs, in CI or when another agent drives you? There is no channel to ask, and you must neither pretend to have asked nor silently pick the first option. Feed back an explicit result: this environment cannot ask the user, so proceed with the safest option and state which assumption you made on their behalf. That matches the approval gate's rule — never auto-approve when nobody is watching.

    答题要点

    • 判据是「两种合理读法会导致完全不同的改动」,能自己查明的事不许问
    • 问太多的代价是用户一路回车,于是提问全部失效——比不问更糟,因为你以为确认过了
    • 不问的代价是返工,而返工贵在你要重读它改了什么并把它推回去
    • 提问必须便宜:给选项、给默认值、支持直接回车,把回答压到一次按键
    • 无人值守时不许假装问过也不许自动选第一个,要明确回灌「这里问不了人,请说明你的假设」

    Key points

    • The criterion is two reasonable readings leading to different edits; never ask what code reading can settle
    • Asking too much makes users rubber-stamp everything, voiding the questions — worse than not asking, because you think you confirmed
    • Never asking costs rework, and rework is expensive because you must re-read the changes and push back
    • Asking must be cheap: options, a default, and a bare enter, so answering is one keystroke
    • Unattended, never fake an answer or auto-pick; feed back that nobody can be asked and require the assumption be stated
  • 计划模式怎么实现?它和权限系统是什么关系?How would you implement plan mode, and what is its relationship to the permission system?
    国内高频海外高频进阶#plan-mode#permissions

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

    1. 这题区分度最高,因为多数人会答「加一个 mode 字段,然后在该判的地方判一下」——而「该判的地方」有多少处,正是这题真正在问的。
    2. 怎么拆:先说结论。**计划模式不是一套新的权限系统,它是既有权限规则表的一个预设。** 进入模式只做一件事:把规则表整体换成两条——所有工具一律 deny,外加「交计划」这一个工具判 ask。只读工具根本走不到规则表(判定函数第一行就是「只读直接放行」),所以读文件、搜内容、反问用户全部照常可用,一个新分支都不用写。
    3. 为什么不能加 mode 字段到处判:你会在循环里判一次、在审批门里判一次、在每个写工具里再判一次,得到三处可能不一致的判断。它们不一致的那天,表现是「计划模式下它偷偷改了文件」——这种 bug 极难在测试里覆盖,因为它取决于哪条路径先命中。**权限的判定入口只能有一个**,这是比任何功能都硬的一条。
    4. 一个能显出你真写过的细节:**换规则表要整体替换,不能追加。** 如果匹配规则是「更具体的规则赢」(一个好的规则表都该这样),那么原表里那条「跑测试允许」带工具名带命令模式,比无条件的 deny 具体得多;把 deny 追加上去,计划模式下测试照跑,而且没有任何提示。原表存起来、退出时放回去即可。
    5. 顺着这条还有一个口径要表态:**计划模式该不该给「无害的命令」开口子。** 我的答案是不开——「哪条命令算只读」无法穷举(跑测试会不会写快照?会不会碰缓存?),而这个模式的全部价值就在于边界一句话说得清:只读工具全过,其余全挡。
    6. 可预期的追问:审批那一步要不要给协议加事件类型(比如一个 awaiting_approval 事件)?不要。把「交计划」做成一个**非只读工具**,既有的审批门就会自动拦住它去问用户;批准就是工具照常执行,拒绝就是回灌一条「没有被执行」的工具结果让模型改道。事件类型、循环、渲染层一个字都不用动,只有「摆给用户看的内容」要特判——而那本来就是渲染层的职责。

    How to reason about it · think before answering

    1. This has the most signal, because most people answer add a mode flag and check it where needed — and how many places need it is exactly what the question is probing.
    2. How to break it down: lead with the conclusion. Plan mode is not a new permission system; it is a preset of the existing rule table. Entering the mode does one thing: swap the table for two rules — deny every tool, plus ask for the single submit-a-plan tool. Read-only tools never reach the table at all, because the decision function's first line passes anything read-only, so reading files, searching, and asking the user all keep working with no new branch.
    3. Why not a mode flag checked everywhere: you end up checking it in the loop, in the approval gate, and inside every write tool — three judgments that can disagree. The day they do, the symptom is it quietly edited a file in plan mode, and that bug is nearly impossible to cover in tests because it depends on which path hits first. There must be exactly one place where permission is decided.
    4. A detail that shows you built it: swapping the table must replace, not append. If the matcher is most specific rule wins — as any good rule table should be — then an existing rule like allow running the test command carries a tool name and a command pattern and is far more specific than an unconditional deny. Append the deny and tests still run in plan mode, silently. Save the old table and restore it on exit.
    5. That leads to a stance worth stating: should plan mode carve out exceptions for harmless commands? I say no. Which command counts as read-only cannot be enumerated — does running tests write snapshots, or touch a cache? — and this mode's entire value is a boundary you can state in one sentence: read-only tools pass, everything else is blocked.
    6. Likely follow-up: does approval need a new protocol event, like awaiting_approval? No. Make submit-a-plan a non-read-only tool and the existing gate stops it to ask the user. Approval means the tool simply runs; rejection means feeding back a was-not-executed tool result so the model reroutes. Event types, loop and renderer stay untouched; only what is shown to the user needs a special case, and that was always the renderer's job.

    答题要点

    • 计划模式是既有权限规则表的一个预设,不是新的权限系统
    • 进入模式只做一件事:规则表整体换成「全部 deny + 交计划判 ask」两条
    • 只读工具走不到规则表(判定第一行就放行),所以探索能力是白拿的
    • 换表要整体替换不能追加,否则原表里更具体的规则会打败无条件的 deny
    • 权限判定入口只能有一处;审批走既有的门,不给事件协议加类型

    Key points

    • Plan mode is a preset of the existing permission rule table, not a separate system
    • Entering it does one thing: swap the table for deny-everything plus ask on the submit-plan tool
    • Read-only tools never reach the table, so exploration comes for free
    • Replace the table rather than appending, or a more specific existing rule beats the unconditional deny
    • There must be one permission decision point; approval reuses the existing gate with no new event types
  • 批准了一份计划之后,怎么保证执行没有偏离?After a plan is approved, how do you ensure the execution did not drift from it?
    国内高频海外高频深入#plan-verification#drift-detection

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

    1. 题眼在「保证」。多数人答到「让模型按计划执行、最后让它自己汇报」就停了,而那是让被检查的人写检查报告。这题要的是一个不依赖模型自觉的机制。
    2. 怎么拆:先说清偏离长什么样,以及为什么它抓不住。两种典型偏离是「多改了计划外的文件」和「没跑验证就宣布成功」,而它们的共同点是**都不是错误**:工具全部成功、循环正常结束、终端上一片对勾。静默的失败只能靠主动检查暴露。
    3. 所以第一步在**计划的格式**上:计划必须可核对,否则后面无从对起。四个字段——目标、每步改哪几个文件、怎么验证、有什么风险——其中「改哪几个文件」是核对的抓手,「怎么验证」是判断「做成了」的硬判据。自然语言计划(「优化一下边界处理」)改一个文件和改五个文件都算符合,等于没有计划。而且不合格的计划要**整份退回、不要就地补全**:我们替它补的验证方式是我们编的,用户批准之后没人对它负责。
    4. 第二步是**授权范围**:批准之后按计划里那几个文件逐个放行,一个不多。全开等于批准「随便改」;什么都不放行,则每个文件执行时还要再问一遍,用户很快学会一路按同意。**批准的是这份计划,不是无限授权。** 于是计划外的写入天然会撞回审批门——这是第一层防线,而且它是拦住的,不是事后发现的。
    5. 还有一条容易被忽略的红线:**按计划放行不能越过 deny。** 规则表里若有硬规则(例如不许改测试目录,因为不能靠改测试让它变绿),批准时无脑给计划里每个文件加放行,模型只要把测试文件写进计划就能借一次审批绕开硬规则。所以放行前要用原规则表判一次,本来 deny 的保持 deny 并明确告知模型。
    6. 第三步是**事后核对**:用一层透明包装(把事件原样传下去、路过时记一笔)采集这一轮真的成功写过哪些文件、真的跑过哪些命令,然后与计划对三列——计划内改了哪些、有哪些计划外的、有哪些计划里写了却没动的,再加一条「验证方式跑过没有」。只记成功的写入:被门挡下的、执行失败的不算实际改动。
    7. 可预期的追问:怎么证明这套核对不是摆设?做对照。同一份计划走两条支线,一条按计划执行,另一条故意多改一个文件且不跑验证,看核对表能不能抓到那两处。抓不到的核对表就是个装饰。另外要划清边界:核对只回答「这一次执行做了什么」,不回答「改动对不对」——后者是验证方式(跑测试)的职责。

    How to reason about it · think before answering

    1. The hinge is ensure. Most answers stop at have the model follow the plan and report back, which is asking the inspected party to write the inspection report. This question wants a mechanism that does not rely on the model's good faith.
    2. How to break it down: describe what drift looks like and why it hides. The two typical forms are edited files outside the plan and declared success without running the verification, and what they share is that neither is an error: every tool succeeded, the loop ended normally, the terminal is full of checkmarks. Silent failures only surface through an active check.
    3. So step one is the plan format: a plan must be checkable or there is nothing to check against. Four fields — goal, which files each step touches, how to verify, what the risks are — where the file list is the handle for reconciliation and the verification is the hard criterion for done. A prose plan like improve the boundary handling is satisfied by editing one file or five, which is the same as having no plan. And an unqualified plan must be returned whole rather than patched up: a verification step we invented is ours, and after approval nobody owns it.
    4. Step two is the authorization scope: on approval, grant exactly the files the plan lists, no more. Granting everything means approving arbitrary edits; granting nothing means every file is asked about again during execution, and the user learns to approve reflexively. What was approved is this plan, not unlimited authority. Writes outside the plan then hit the approval gate naturally — a first line of defense that blocks rather than merely reports.
    5. One easily missed red line: a plan-based grant must not override a deny. If the table has a hard rule — say, never edit the test directory, because you cannot make tests pass by editing tests — then blanket-granting every planned file lets the model smuggle a test file into the plan and launder it through one approval. Re-evaluate each file against the original table first, keep the denials, and tell the model explicitly.
    6. Step three is reconciliation after the fact: a transparent wrapper that passes events through while recording which files were actually written and which commands actually ran, compared against the plan on three axes — planned and touched, touched but unplanned, planned but untouched — plus whether the verification ran. Only successful writes count; gate-blocked and failed calls are not real changes.
    7. Likely follow-up: how do you prove the reconciliation is not decorative? Build a control. Run the same plan down two branches, one following it and one deliberately touching an extra file and skipping verification, and see whether the table catches both. A reconciliation that catches nothing is an ornament. Also draw the boundary: reconciliation answers what this execution did, not whether the change is correct — the latter is the verification step's job.

    答题要点

    • 偏离不是错误(工具全成功、终端一片对勾),静默失败只能靠主动检查暴露
    • 计划必须可核对:四个字段里「改哪些文件」是抓手、「怎么验证」是硬判据;不合格整份退回不要补全
    • 批准之后按计划里的文件逐个放行,一个不多——批准的是这份计划,不是无限授权
    • 按计划放行不能越过 deny,否则模型能把硬规则里的文件写进计划来洗白
    • 事后用透明包装采集实际写入与实际跑过的命令,核对计划内 / 计划外 / 漏做三列加一条验证跑没跑,并用对照支线证明它真能抓到偏离

    Key points

    • Drift is not an error — tools succeed and the terminal looks clean — so it only surfaces via an active check
    • The plan must be checkable: the file list is the handle, the verification is the hard criterion; return unqualified plans whole instead of patching them
    • On approval grant exactly the planned files — what was approved is this plan, not unlimited authority
    • A plan-based grant must never override a deny, or hard rules can be laundered through one approval
    • Afterwards, collect actual writes and commands with a transparent wrapper and reconcile planned, unplanned and untouched plus whether verification ran, proving it with a control branch

评论