🐎 第8章 器以载道:Agent SDK 智能体开发套件
📄 一句话概括

选项可以分三层给:默认值、项目配置、调用时传入。标量好办,列表是麻烦:到底替换还是追加? 实测同一组三层配置,替换得到「只有 Grep」,追加得到「Read、Edit、Grep」。 一个是只给一个,一个是什么都能用。

📝 本章和前面几章的关系

上一节讲消息长什么样,这一节讲怎么让它按你要的样子跑。 这一节的坑很具体:同一个配置项,两种合并语义会得到完全不同的权限。

一、三层各放什么

默认值:SDK 自带的。你不写就在用这一层,而且你不知道它是什么。 项目配置:团队约定。一个项目一套,跟仓库走。 调用时传入:这一次的特殊要求。优先级最高。

分三层的目的是让「默认行为」是安全的:不确定的时候,最省事的那层应该最保守。

二、两种合并语义

标量(模型名、轮数上限)没有歧义:后者覆盖前者。 列表有歧义:替换还是追加?两种实现都见过。 演示里三层叠完:替换得到只有 Grep,追加得到 Read、Edit、Grep。

差别是「只给它一个工具」和「三个工具都能用」。这个差别在你排查安全问题时是决定性的。 所以配选项的第一件事:搞清楚这套实现用的是哪种语义,别猜。

三、权限模式那几档

default:要动手就问。适合交互式,无人值守下等于会卡住。 acceptEdits:改文件不问,跑命令问。半自动,适合「改代码」这类任务。 plan:只读,不许动手。这是最有用也最被忽略的一档。

bypassPermissions:什么都不问。只能在确实没有别的手段时用。 plan 那一档的价值在于「把分析变成独立的一段」:先让它读出结论,人看过再放开权限动手。

四、工具权限匹配的是什么

匹配的是工具名,不是路径。给 Edit 一项,就等于给了所有路径的编辑权。 想按路径限制(比如只许改 src 下),只能在工具实现里判断,或者在钩子里拦。 很多人以为写了工具白名单就安全了——白名单只管「用哪个工具」,不管「对哪个文件用」。

这个区别在 8.8 节那四道防线里还会再出现一次:第三道防线要自己写。

五、一条实践

把「最终生效的选项」当成一条要打日志的东西。 出问题时第一句话往往是「它到底拿到了什么权限」,有这一行日志就不用猜。 这一条和 5.10 节那个「看不见的东西必须有日志」是同一条,只是换了对象。

日志里至少要有三样:模型名、允许的工具、权限模式。

六、动手 Demo:三层选项叠起来,列表是替换还是追加

选项可以分三层给:默认值、项目配置、调用时传入。 标量好办(后者覆盖前者),列表是麻烦:到底替换还是追加?这里两种都算一遍。

for _nm, d in LAYERS:
    for k, v in d.items():
        if isinstance(v, list):
            rep[k] = list(v)                                    # 替换
            app[k] = list(dict.fromkeys(app.get(k, []) + v))     # 追加并去重

实跑输出:

--------------------------------------------------------------------
三层选项叠起来,列表是替换还是追加
--------------------------------------------------------------------

  选项可以分三层给:默认值、项目配置、调用时传入。
  标量好办(后者覆盖前者),列表是麻烦:到底替换还是追加?

  ① 三层各放什么
  --------------------------------------------------------------------
| 层 | 谁写的 | 什么时候生效 |
|---|---|---|
| 默认值 | SDK 自带的 | 不写就在用这一层 |
| 项目配置 | 团队约定 | 一个项目一套 |
| 调用时传入 | 这一次的特殊要求 | 最高优先级 |


  ② 真跑:两种合并语义
  --------------------------------------------------------------------
| 键 | 按替换合并 | 按追加合并 |
|---|---|---|
| allowed_tools | ['Grep'] | ['Read', 'Edit', 'Grep'] |
| max_turns | 20 | 20 |
| model | sonnet | sonnet |
| permission_mode | acceptEdits | acceptEdits |

  allowed_tools 这一行最能说明问题:替换得到 Grep,追加得到 Read、Edit、Grep。
  两种语义都说得通,但结果差很远——一个是「只给它 grep」,一个是「什么都能用」。
  所以配选项的第一件事,是搞清楚这套实现用的是哪种语义,别猜。

  ③ 权限模式那几档
  --------------------------------------------------------------------
| 模式 | 它怎么做 | 适合什么场景 |
|---|---|---|
| default | 要动手就问 | 交互式 |
| acceptEdits | 改文件不问,跑命令问 | 半自动 |
| plan | 只读,不许动手 | 审查、分析 |
| bypassPermissions | 什么都不问 | 只能在没有别的手段时用 |

  第三档最有用也最被忽略:只读模式让「先分析、再动手」变成两段可分开的动作。

  ④ 工具权限匹配的是什么
  --------------------------------------------------------------------
  匹配的是工具名,不是路径。给 Edit 一项,就等于给了所有路径的编辑权。
  想按路径限制(比如只许改 src 下),只能在工具实现里判断,或者在钩子里拦。
  很多人以为写了工具白名单就安全了——白名单只管「用哪个工具」,不管「对哪个文件用」。
  这个区别在 8.8 节那四道防线里还会再出现一次。

  ⑤ 一条实践
  --------------------------------------------------------------------
  把「最终生效的选项」当成一条要打日志的东西。
  出问题时第一句话往往是「它到底拿到了什么权限」,有这一行日志就不用猜。
  ! 选项名与语义以真实版本为准;这里演示的是「分层合并」这件事本身。

①三层:默认值(SDK 自带,你不写就在用)、项目配置(团队约定,跟仓库走)、 调用时传入(这一次的特殊要求,优先级最高)。 ②标量没有歧义:后者覆盖前者。列表有歧义:替换还是追加,两种实现都见过。 ③演示里三层叠完:替换得到只有 Grep,追加得到 Read、Edit、Grep。 差别是「只给它一个工具」和「三个都能用」,排查安全问题时是决定性的。 ④工具权限匹配的是工具名,不是路径。给 Edit 就等于给了所有路径的编辑权; 想限路径只能在工具实现里判断或者用钩子。

30 秒实验

  1. 把「调用时传入」那层的 allowed_tools 删掉,重跑一遍。两种语义的结果还会不同吗?
  2. 给默认层加上 allowed_tools 为空列表,重跑一遍。替换和追加各得到什么? 「空列表」在两种语义下含义一样吗?
  3. 把权限模式从 acceptEdits 改成 plan,重跑一遍。选项本身变了,行为会怎么变? 这个变化能用选项表看出来吗?

常见误区

  1. 以为列表一定是替换。追加也常见,两者结果差很远。
  2. 以为工具白名单能限制路径。它只限制「用哪个工具」。
  3. 无人值守时用 default 模式。它要动手就问,没人回答就卡住。
  4. 配完不看最终生效的选项。三层叠完可能和你以为的完全不一样。
  5. 把 bypassPermissions 当默认。它是例外,不是常态。
自测三问(点开对答案) 1. 选项为什么分三层? —— 让默认行为是安全的:不确定的时候,最省事的那层应该最保守。调用时传入优先级最高。 2. 列表合并的两种语义会带来什么差别? —— 替换只留最后一次给的,追加会累积。同一组配置可能得到「只有一个工具」或「三个工具都能用」。 3. 工具白名单能限制路径吗? —— 不能。它匹配的是工具名。要限路径只能在工具实现里判断或者在钩子里拦。

选项会配了,下一节解决另一个问题:这次跑完,下次怎么接上。

相关笔记