程序校验优先
LLM 决策 → 程序校验 → 权限检查 → 人工审批 → 执行。
一句话理解:让 LLM 负责“提议”,让程序负责“把关”——模型输出的每一个动作都先变成结构化提案,经过程序校验、权限检查、必要的人工审批,最后才由程序去执行。
Agent 最迷人的地方是它能根据情况做判断;最危险的地方也是它能根据情况做判断。模型可能算错金额、看错订单状态、被一封邮件带偏,也可能在长任务里忘了一开始说好的限制。
解决办法不是让模型“更小心”,而是 改变谁有最终决定权。在一个设计良好的 Agent 系统里,模型从来不直接操作真实世界,它只是提交一份提案;真正决定执行与否的,是一串不会疲劳、不会被说服的程序检查。
五步管道
一个写操作从提议到生效,要依次经过五步:
每一步的职责不同:
| 步骤 | 回答的问题 | 由谁执行 | 失败时 |
|---|---|---|---|
| LLM 决策 | 应该做什么?为什么? | 模型 | — |
| 程序校验 | 这个提案本身合法吗?符合业务规则吗? | 代码 | 返回具体错误,让 Agent 修正 |
| 权限检查 | 当前用户、当前 Agent,有权做这件事吗? | 代码 + 权限系统 | 直接拒绝,记录事件 |
| 人工审批 | 这件事值得冒这个风险吗? | 人 | 驳回、修改或转交 |
| 执行与验证 | 真的执行成功了吗?结果和预期一致吗? | 代码 | 进入错误恢复流程 |
顺序很重要:程序能判断的,不要留给人判断。审批人的注意力非常宝贵,不应该浪费在“金额是不是负数”“订单是否存在”这种问题上。
第一步:把决策变成结构化提案
模型不应该输出“我决定给这位客户退 380 元”这样的自然语言让下游去猜,而应该输出一份结构化提案。结构化输出的方法见 结构化输出。
{
"action": "refund",
"order_id": "SO-20260903-88231",
"amount": 380.00,
"currency": "CNY",
"reason_code": "DUPLICATE_CHARGE",
"evidence": ["pay_7781023", "pay_7781188"],
"customer_message_draft": "您好,经核查您 9 月 3 日的订单被重复扣款……"
}
几个设计细节:
- 用枚举代替自由文本:
reason_code只能是预定义的几种原因,程序才能针对每种原因写规则。 - 证据用 ID 引用:
evidence放的是支付流水号,程序可以回查,而不是模型复述的“我看到有两笔扣款”。 - 不让模型填它不该决定的字段:操作人、租户、审批人这些信息由程序从会话上下文注入,而不是由模型生成,见 安全的工具设计。
提案里的字段各有来源,校验时一律以系统数据为准:
第二步:程序校验
校验分为两类:格式校验和业务规则校验。
def validate_refund(p, db):
errors = []
# 格式校验
if p.amount <= 0:
errors.append("金额必须大于 0")
if p.reason_code not in REFUND_REASONS:
errors.append(f"未知的退款原因:{p.reason_code}")
# 业务规则校验:一切以系统数据为准,不信任提案里的描述
order = db.get_order(p.order_id)
if order is None:
return errors + ["订单不存在"]
if order.status not in ("PAID", "DELIVERED", "EXCEPTION"):
errors.append(f"订单状态为 {order.status},不允许退款")
if p.amount + order.refunded_amount > order.paid_amount:
errors.append("累计退款金额将超过实付金额")
if p.reason_code == "DUPLICATE_CHARGE":
charges = [db.get_payment(e) for e in p.evidence]
if len(charges) < 2 or any(c is None or c.order_id != p.order_id for c in charges):
errors.append("重复扣款的证据不足:需要同一订单的至少两条支付记录")
if db.has_pending_refund(p.order_id):
errors.append("该订单已有处理中的退款,疑似重复提交")
return errors
注意两点:
- 校验依据的是系统数据,而不是提案里的说法。模型说“有两笔扣款”,程序就去查这两笔扣款是否真实存在、是否属于这个订单。
- 错误信息要具体,并回传给 Agent。“累计退款金额将超过实付金额”能让 Agent 修正提案,而一个笼统的“校验失败”只会让它反复重试。
第三步:权限检查
校验回答“这件事合不合理”,权限回答“你有没有资格做”。两者必须分开:
- 当前发起任务的用户,是否有权对这个客户、这个订单执行退款?(例如华东区客服不能处理华南区订单)
- 这个 Agent 实例的令牌,作用域是否包含退款?单笔上限是多少?
- 这个订单属于哪个租户?是否与当前会话的租户一致?
权限检查必须在 服务端、执行动作的系统里 做,而不是在 Agent 的提示词里做。OWASP 在“过度代理”风险的防护建议中也强调:授权应在下游系统中执行,而不是依赖 LLM 判断是否允许。详见 最小权限与凭据管理。
第四、五步:审批与执行
通过校验和权限检查的提案,再根据 操作分级 决定是否需要人工审批。审批通过后,执行环节也有讲究:
- 幂等执行:为每个提案生成唯一的幂等键,网络重试或 Agent 重复提交时不会退两次款。
- 执行的是被批准的那一份:审批人看到的提案和最终执行的参数必须一致,中间不允许 Agent 再修改。
- 执行后回读:退款接口返回成功后,回查退款单状态和订单的累计退款金额,确认真的生效,见 各类动作的验证方式。
为什么“告诉模型规则”不够
一个真实而有启发的例子来自 Claude Code 的官方文档。它的 auto 模式会把用户在对话中说的边界(比如“先别推送”“等我审核后再部署”)当作拦截信号。但文档特别提醒:这些边界并不是存储下来的规则,每次检查时从对话记录里重新读取;如果上下文压缩删掉了那条消息,边界就可能丢失。需要硬性保证时,应该添加一条拒绝规则。
这正是本课的核心:写在上下文里的约束是“软”的,写在程序里的规则是“硬”的。重要的约束,一定要有一份硬的版本。
常见误区
- “模型已经很准了,校验是多余的。” 准确率 99% 意味着每一百次资金操作就有一次出错。校验的成本远低于一次错误退款。
- “校验失败就直接报错结束。” 好的校验会返回具体原因,让 Agent 有机会自己修正,很多问题不需要人介入。
- “权限检查放在工具描述里说明就行。” 工具描述是给模型看的,权限检查必须由执行动作的系统强制完成。
设计要点
- Agent 的每个写操作,是否都以结构化提案的形式输出?
- 提案中哪些字段由模型生成,哪些由程序注入?
- 业务规则校验是否基于系统数据回查,而非提案中的描述?
- 校验失败的错误信息是否足够具体,能让 Agent 修正?
- 权限检查是否在执行动作的服务端完成,并且独立于 Agent?
- 执行是否幂等?审批通过的参数与实际执行的参数是否保证一致?
练习
为「销售分析 Agent」的“调整产品折扣”动作设计完整管道,用 60 分钟完成:
- 定义提案的 JSON 结构,标出哪些字段由程序注入。
- 写出至少 6 条校验规则(格式 3 条、业务规则 3 条),例如折扣不能低于成本价。
- 写出权限检查条件和需要人工审批的触发条件。
- 描述执行后如何验证折扣真的生效。
下一步
程序校验防得住“模型犯错”,但还有一种更隐蔽的威胁:模型被外部内容“说服”去做坏事。下一课「Prompt 注入」专门讲这种攻击。