← 上一节:02 · 检查该由谁来做 · 章目录 · 下一节:04 · 把三样信息写成类型
03 · 判断写成一个纯函数
上一节留了个问题:decideAgentToolPermission 这个函数要写成什么样。
先给答案,再说为什么。
解法:它只做判断,什么都不做
export function decideAgentToolPermission(
request: AgentPermissionRequest, // 三样信息都在这个对象里
): AgentPermissionDecision { // 返回三选一:允许 / 需要问人 / 拒绝
// 只有 if 和 return,没有别的
}三条规矩:
- 不读文件、不读配置、不发网络请求。 需要什么信息,调用它的人先准备好,作为参数传进来。
- 不弹窗、不打印、不写日志。 它只返回结论,做事情是外面的事。
- 同样的输入,永远返回同样的结果。
满足这三条的函数,叫纯函数。
它拿一个对象进去,吐一个结论出来。像一道计算题:给定条件,算出答案,不碰任何计算之外的东西。
为什么不让它顺手把事做了
写代码的时候,有两个念头几乎必然会冒出来。
念头一:"这里读一下用户配置就能判断了。"
比如判断到一半发现需要知道用户有没有把某个目录加进白名单,配置文件就在那儿,读一下两行代码的事。
念头二:"这里直接弹窗问用户不就完了。"
判断结果是"需要问人",那干脆就地问掉,返回一个"用户同意了"多干脆。
两个念头都能工作,而且当下都比"传参数进来"省事。下面说为什么还是要忍住。
为什么一:这段代码必须有测试,而且要好写
权限判断是安全相关的代码。写错了不会报错,只会安静地放行不该放行的东西。这类代码必须有测试兜着。
那就看写一条测试要付出什么。
如果函数是纯的,一条测试长这样:
const decision = decideAgentToolPermission({
工具是只读的,
路径解析后在项目外,
这次运行只允许项目内,
});
assert.equal(decision.type, 'deny');构造一个对象,看返回值。不启动 agent,不需要 API key,不联网,毫秒级跑完。
如果函数里读了配置文件,测试就得先准备一个配置文件,测完再清理。如果函数里会弹窗,测试根本没法自动跑——它会停在那儿等人点。
规则以后会越来越多。第 13 章加"编辑前必须先读过",第 18 章加 shell 命令分类,第 24 章加沙箱结论。每加一条规则就要能加一条测试,而且加起来不费劲,这套规则才不会烂掉。
为什么二:加规则的时候只动一个地方
规则集中在一个函数里,还有个附带好处:有人问"这个 agent 到底允许干什么",答案就是这个函数,从上往下读一遍就是全部规则。
如果判断散在配置读取、弹窗逻辑、日志之间,这个问题就没有一个能读的答案了。
为什么三:第 19 章的人工审批要靠这条分界
这条最值钱,但要到第 19 章才看得到,这里先说清楚。
第 19 章要做的事是:判断结果是"需要问人"的时候,暂停下来,把问题交给用户,等用户点了同意或拒绝,再继续往下走。
如果"问人"这件事当初写进了判断函数里,那一章要动的就是判断逻辑本身——一个已经被几十条测试盯着的函数,一改就要重新验证所有规则。
而现在,判断函数只负责说"这件事需要问人"。至于怎么问、等多久、用户不理怎么办,全在外面。第 19 章几乎不用碰判断逻辑,只需要在外面把"收到需要问人的结论之后干什么"补上。
判断和执行分开,是为了让它们能各自变化。 这一章分开付出的代价,第 19 章收回来。
代价也说清楚
选纯函数不是没有成本。
- 调用它之前,得先把三样信息准备好、拼成一个对象。这段准备代码要有人写,第 09 节会写它。
- 想加一条"需要读配置才能判断"的规则时,得先把配置读出来传进去,比就地读多绕一步。
- 追一次工具调用的代码路径,要多跳一个文件。
现在只有三个玩具工具,这层结构是亏的。它要到第 18 章加 shell 的时候才开始回本——那时候权限规则会变多、变复杂,而且改错的后果是真的能删掉你文件的。
教程里但凡说"这样更好"却不说代价的,多半是没算过账。这里算过:今天亏,第 18 章回本。
本节小结
- 判断写成纯函数:不读配置、不弹窗、不写日志,只根据传进来的信息返回结论。
- 这样测试才好写:构造一个对象看返回值,不启动 agent,不要 key。
- 规则集中在一个函数里,"这个 agent 允许干什么"才有一个能读的答案。
- 判断和"问人"分开,第 19 章接人工审批时不用动判断逻辑。
- 代价:调用前要先拼参数,读代码多跳一层。今天亏,第 18 章回本。
下一节把这三样信息写成类型,动手从那里开始。
← 上一节:02 · 检查该由谁来做 · 章目录 · 下一节:04 · 把三样信息写成类型