🐎 第7章 无为而治:Headless 模式与 CI/CD 集成
📄 一句话概括

无人值守时没有人在旁边看着,约束只能靠参数表达。 四个维度:输出格式、成本护栏、工具权限、执行控制。 最容易踩的两个坑:「语法对、语义荒谬」的值不报错,以及同一项既允许又禁止——禁用优先, 你以为开了,其实没开。

📝 本章和前面几章的关系

上一节说了模式怎么变,这一节说变了之后靠什么约束它。 这一节有真跑:写了一个参数解析器,六组参数逐个跑,看哪一组会被拒、哪一组悄悄变形。

一、四个维度各管什么

输出格式:给人看还是给程序看。text、json、stream-json 三种,决定了能不能接管道。 成本护栏:最多跑几轮就停。它防的不是「跑错」,是「跑不完还一直跑」。 工具权限:哪些工具能碰。这是最小权限原则唯一能落地的地方。

执行控制:用哪个模型、系统提示写什么、输出要什么形状。 四个维度的顺序有讲究:先定输出格式(决定下游怎么接),再定权限(决定它能做什么),最后才是护栏。

二、六组参数真跑一遍

演示里六组:只有输出格式、加成本护栏、护栏设成零、允许与禁止同一项、拼错的参数名、要结构化输出。 只有拼错的那组报错,退出码 2。其余五组全部「成功」——但其中两组的结果不是你要的。 护栏设成零那组:参数合法,含义是「一轮都不许跑」。它不会报错,只会让任务什么都没干就结束。

允许与禁止同一项那组:解析出来的可用列表里只剩 Read,Edit 被悄悄拿掉了。

三、为什么「合法但荒谬」最危险

因为它不报错。你在日志里看到退出码零、看到一段输出,会以为它干完了。 而实际上它一轮都没跑,或者一个工具都没调。 这类值有一个共同特征:语法上完全合法,语义上和你的意图相反。

防法不是靠参数校验,是靠「跑完之后核对结果」——比如检查它到底改了几个文件。

四、输出格式决定能不能接管道

text:人读的段落,不能接管道。只能在本地看一眼。 json:一整块结果,跑完才有。能取结论、能取用量。 stream-json:一行一个事件,能边跑边读。长任务和 CI 日志用它。

选了 text 又想用 jq 取字段,就只能回去改参数——这是流水线里最常见的返工。 所以第一版配置就把格式定成 json,哪怕现在只是本地跑。

五、权限参数的坑

允许列表和禁止列表同时给的时候,禁用优先。这是安全上正确的选择,但很反直觉。 你写了允许 Edit,又在禁止里顺手写了 Edit,结果是「没有 Edit」,而且不报错。 做法是只写一边:要么列允许(白名单),要么列禁止(黑名单)。两个都写就要自己对一遍。

这个坑和 6.8 节那条「权限要收窄」是一路的:收窄的规则越简单,越不会被自己绕进去。

六、动手 Demo:四组参数,哪些互斥、哪些会被悄悄盖掉

headless 模式下没有人在旁边看着,所以「约束」只能靠参数表达。 四条维度的参数,六组组合逐个真跑一遍。

fmt, maxturn, allow, deny = u"text", None, [], []

eff = [t for t in allow if t not in deny]      # 禁用优先
print(json.dumps({"fmt": fmt, "maxturn": maxturn, "eff": eff}))

实跑输出:

--------------------------------------------------------------------
四组参数,哪些互斥、哪些会被悄悄盖掉
--------------------------------------------------------------------

  headless 模式下没有人在旁边看着,所以「约束」只能靠参数表达。
  四条维度的参数:输出格式、成本护栏、工具权限、执行控制。

  ① 四个维度各管什么
  --------------------------------------------------------------------
| 维度 | 代表参数 | 管什么 | 常见误用 |
|---|---|---|---|
| 输出格式 | --output-format | 给人看还是给程序看 | 流水线里用了 text |
| 成本护栏 | --max-turns | 最多几轮就停 | 不设,跑到超时 |
| 工具权限 | --allowed-tools | 哪些工具能碰 | 给了一堆用不上的 |
| 执行控制 | --model、schema | 用哪个模型、输出什么形状 | schema 和 text 同时要 |


  ② 六组参数真跑一遍
  --------------------------------------------------------------------
| 这组参数 | 退出码 | 解析要点 |
|---|---|:---:|
| 只有输出格式 | 0 | fmt=json turns=None eff=无 |
| 加成本护栏 | 0 | fmt=json turns=8 eff=无 |
| 护栏设成零 | 0 | fmt=text turns=0 eff=无 |
| 允许与禁止同一项 | 0 | fmt=text turns=None eff=Read |
| 拼错的参数名 | 2 | unknown argument: --output-formats |
| 要结构化输出 | 0 | fmt=json turns=None eff=无 |

  第三组最值得看:轮数上限设成零,参数本身合法,但它意味着「一轮都不许跑」。
  这类「语法对、语义荒谬」的值不会报错,只会让任务什么都没干就结束。
  第四组是权限的坑:同一项既允许又禁止。真实实现里禁用优先——所以你以为开了 Edit,其实没开。

  ③ 输出格式决定能不能接管道
  --------------------------------------------------------------------
| 格式 | 长什么样 | 能流式读吗 | 什么时候用 |
|---|---|---|---|
| text | 人读的段落 | 不能 | 本地看一眼 |
| json | 一整块结果 | 能,跑完才有 | 取结论、取用量 |
| stream-json | 一行一个事件 | 能,边跑边有 | 长任务、CI 日志 |

  选了 text 又想用 jq 取字段,就只能回去改参数——这是流水线里最常见的返工。

  ④ 成本护栏该设在哪
  --------------------------------------------------------------------
  不要设「一次跑完」的上限,要设「跑到这儿就停」的上限:轮数、时间、花费。
  因为无人值守的失败模式不是「跑错」,是「跑不完还一直跑」。
  护栏的另一个作用是把失败变早:八轮就停,比三十分钟超时好查得多。
  ! 参数名与取值以各版本实际帮助为准,这里演示的是它们的分工,不是逐字背诵的命令。

①四个维度:输出格式(给人看还是给程序看)、成本护栏(最多几轮)、工具权限(哪些能碰)、 执行控制(模型与输出形状)。 ②六组里只有拼错参数名那组报错,退出码 2。其余五组全部「成功」,但其中两组的结果不是你要的。 ③护栏设成零那组最值得看:参数合法,含义是「一轮都不许跑」。它不报错,只会让任务什么都没干就结束。 这类「合法但荒谬」的值有一个共同特征:语法上完全合法,语义上和你的意图相反。 防法不是参数校验,是跑完核对结果。 ④允许和禁止同时给同一项时,禁用优先。 你写了允许 Edit 又在禁止里写了 Edit,结果是没有 Edit,而且不报错。 做法是只写一边:要么白名单,要么黑名单,两个都写就要自己对一遍。

30 秒实验

  1. 把护栏从八轮改成两轮,重跑一遍。参数照样合法,但任务会怎样结束? 这种情况该看哪个信号判断它「没干完」?
  2. 给输出格式加上一个不存在的值(比如 yaml),重跑一遍。它会当场报错还是静默退回默认? 这说明了什么?
  3. 把禁止列表整个删掉,只留允许列表,重跑第四组。结果变了吗?为什么「只写一边」更不容易出错?

常见误区

  1. 只设成本护栏不设权限。护栏防的是花钱,权限防的是它把不该动的东西动了。
  2. 允许和禁止同时写,还以为是「取交集」以外的语义。禁用优先,而且不报错。
  3. 输出格式留默认的 text,下游却用 jq 解析。第一次改需求就得返工。
  4. 把「语法合法」当成「配置正确」。护栏设成零也能跑,只是它什么都没干。
  5. 以为参数只是开关。在无人值守里,参数是唯一的约束手段,写错就等于没约束。
自测三问(点开对答案) 1. 四个维度分别是什么? —— 输出格式(能不能接管道)、成本护栏(最多跑几轮)、工具权限(能碰什么)、 执行控制(模型与输出形状)。 2. 「合法但荒谬」的参数值为什么危险? —— 因为它不报错。你会看到退出码零和一段输出,以为它干完了,实际上它什么都没干。 只能靠跑完核对结果来发现。 3. 允许列表和禁止列表同时给了同一项,哪个生效? —— 禁止生效。而且不报错——你以为开了,其实没开。所以最好只写一边。

参数会设了,下一节把它接进命令行:一条管道把 agent 当普通命令用。

相关笔记