生产级Agent(22):策略即代码与Policy Simulation

文章摘要

前二十一篇已经把生产级 Agent 的运行、权限、审计、回放、SLO 和多租户隔离逐层搭起来。

到了这一阶段,平台已经有越来越多“规则”:

哪些模型能用
哪些Tool能调
哪些租户能访问哪些数据
什么动作需要审批
什么情况下要Fail Closed
哪个Artifact允许外发
哪个Sandbox可以联网
Error Budget烧到多少要冻结发布

如果这些规则散落在:

System Prompt
Java if/else
数据库配置
管理员后台
Kubernetes YAML
审批流程

真正出问题时,团队很难回答:

当时为什么允许?
哪条规则生效?
如果改规则,会影响多少Run?
这次Policy变更会不会把某个租户直接打挂?

所以 Agent Control Plane 下一步必须把“规则”从业务代码里抽出来,变成:

Policy-as-Code

再进一步,在真正生效前运行:

Policy Simulation

也就是拿历史真实 Run、当前配置和候选规则做离线重放,先看“如果昨天就用了新规则,会发生什么”。

这一篇直接把策略模型、版本、决策日志、影子评估、Impact Analysis、Historical Replay、Canary、Break Glass 和发布门禁串成一套。


一、为什么Agent平台特别容易Policy爆炸

一个普通 API 权限可能只是:

Role → Permission

Agent 决策通常同时看:

User
Tenant
Agent
Purpose
Capability
Resource
Data Class
Risk
Approval
Model
Region
Budget
Time

例如:

legal-agent

想调用:

document.export

真正决策可能是:

用户属于Matter吗?
Artifact是不是Privileged?
目标是不是外部邮箱?
当前租户允许外发吗?
是否有审批?
审批绑定的Action Hash一致吗?

如果全部写在一个 Service 里:

if (...) {
    if (...) {
        if (...) {
        }
    }
}

半年后没人敢改。

二、Policy首先必须独立版本化

定义:

public record PolicyBundle(
        String policyId,
        String version,
        String contentHash,
        PolicyStatus status,
        Instant createdAt,
        String createdBy) {
}

状态:

public enum PolicyStatus {
    DRAFT,
    VALIDATED,
    SHADOW,
    CANARY,
    ACTIVE,
    RETIRED
}

不要允许:

编辑ACTIVE Policy并原地保存

每次修改都生成新版本。

三、Policy输入必须结构化

不要把整个 Prompt 丢给另一个 LLM:

“你觉得这次允许吗?”

真正 Policy 输入应该是确定性对象。

public record PolicyInput(
        String tenantId,
        String subjectId,
        String agentId,
        String purpose,
        String capability,
        String resourceId,
        DataClass dataClass,
        RiskLevel risk,
        String modelId,
        String region,
        String approvalId,
        BigDecimal estimatedCost) {
}

模型可以帮助提取 Purpose。

最终 Policy 不直接依赖自然语言。

四、Decision至少不是boolean

public enum PolicyDecision {
    ALLOW,
    DENY,
    REQUIRE_APPROVAL,
    REQUIRE_STRONG_AUTH,
    REQUIRE_HUMAN_REVIEW
}

生产系统里:

允许 / 禁止

通常太粗。

例如:

email.send

可以允许,但需要:

Action-bound Approval

这就不是 DENY。

五、Decision还必须给出Reason Code

public record PolicyResult(
        PolicyDecision decision,
        List reasonCodes,
        String policyId,
        String policyVersion,
        String inputHash) {
}

例如:

{
  "decision": "DENY",
  "reasonCodes": [
    "CROSS_TENANT_RESOURCE",
    "EXTERNAL_EGRESS_BLOCKED"
  ],
  "policyVersion": "v42"
}

以后排障不需要重新猜。

六、Policy定义最好声明输入和输出

例如 YAML:

policy: external-send
version: v42

match:
  capability:
    - email.send
    - messages.send

rules:

  - when:
      data_class: RESTRICTED
      destination: EXTERNAL
    decision: DENY
    reason: restricted_external_egress

  - when:
      risk: HIGH
    decision: REQUIRE_APPROVAL

  - otherwise:
      decision: ALLOW

真实实现可以使用:

OPA/Rego
Cedar
自研DSL

关键不是语言。

关键是:

Policy是数据和代码资产

而不是散落逻辑。

七、Policy-as-Code必须进Git

目录:

policies/
├── model/
├── capability/
├── data-egress/
├── tenant/
├── sandbox/
└── release/

例如:

policies/data-egress/external-send-v42.yaml

每次变更:

PR
→ Review
→ CI
→ Simulation
→ Canary
→ Active

和代码发布一样。

八、Policy CI第一层:Schema Validation

最简单但必须有。

例如:

Decision拼错
Reason缺失
Risk Enum非法

直接在 CI 拒绝。

policy_ci:

  schema_validation: required
  duplicate_rule_check: required
  unreachable_rule_check: required

九、第二层:Static Conflict Detection

两个规则:

Rule A:
HIGH → DENY

Rule B:
HIGH → ALLOW

如果优先级没有明确:

Policy本身就不确定

CI 要发现:

Overlapping Rules
Conflicting Outcome

十、第三层:Golden Policy Tests

每条高风险 Policy 都要有固定测试。

例如:

cases:

  - name: restricted_external_send
    input:
      data_class: RESTRICTED
      destination: EXTERNAL
    expect: DENY

  - name: high_risk_send
    input:
      data_class: INTERNAL
      risk: HIGH
    expect: REQUIRE_APPROVAL

Policy 修改后自动回归。

十一、但Golden Case远远不够

真正危险的是:

规则本身看起来对
但对真实流量影响巨大

例如:

新的Region Policy

可能误伤 30% 客户。

所以需要:

Policy Simulation

十二、Simulation到底模拟什么

候选:

policy-v43

不要立刻 Active。

拿过去 7 天真实 Decision Input:

100万条

重新评估:

Current Policy v42
vs
Candidate v43

然后比较:

Decision Diff

十三、Simulation不需要重跑LLM

Policy 输入已经结构化。

所以只重放:

Decision Input

而不是整个 Agent Run。

成本非常低。

数据模型:

public record PolicySimulationCase(
        String decisionId,
        PolicyInput input,
        PolicyResult baseline,
        PolicyResult candidate) {
}

十四、最核心指标:Decision Flip

ALLOW → DENY
DENY → ALLOW
ALLOW → REQUIRE_APPROVAL
REQUIRE_APPROVAL → ALLOW

这四种变化风险完全不同。

十五、我会给Flip分类

public enum DecisionFlipType {
    MORE_RESTRICTIVE,
    LESS_RESTRICTIVE,
    APPROVAL_ADDED,
    APPROVAL_REMOVED,
    NO_CHANGE
}

尤其:

DENY → ALLOW

必须重点 Review。

因为它在扩大权限面。

十六、Simulation报告至少输出

{
  "baseline": "v42",
  "candidate": "v43",
  "total_cases": 1000000,
  "changed": 18291,
  "more_restrictive": 17200,
  "less_restrictive": 1091,
  "approval_added": 8200,
  "approval_removed": 91
}

单看:

changed=1.8%

不够。

必须知道变化方向。

十七、还要按Slice看

总体只变化 1%。

但:

Legal Tenant:
变化 38%

就很危险。

Slice 至少:

Tenant Tier
Risk
Capability
Data Class
Agent
Region

十八、不要全组合Slice

否则样本爆炸。

只看业务关键:

HIGH_RISK
RESTRICTED_DATA
EXTERNAL_SEND
FINANCE
LEGAL

这些 Slice 单独 Gate。

十九、Simulation要计算影响业务对象

不只是 Decision 数。

还要:

涉及多少用户?
多少Agent?
多少Tenant?
多少Scheduled Task?
多少关键Workflow?

例如:

{
  "tenants_impacted": 39,
  "agents_impacted": 112,
  "critical_workflows_impacted": 4
}

这才方便 Release Review。

二十、Policy Input必须长期可回放

每次生产 Decision 都保存:

input_hash
policy_version
decision
reason

完整 Input 可以加密保存。

例如:

public record PolicyDecisionRecord(
        String decisionId,
        String runId,
        String tenantId,
        String inputRef,
        String inputHash,
        String policyVersion,
        PolicyDecision decision,
        List reasons,
        Instant occurredAt) {
}

没有历史输入,就无法真正 Simulation。

二十一、Privacy要注意

Policy Input 可能包含:

Resource ID
Subject
Tenant
Data Class

不一定要保存原始业务内容。

策略评估应该尽量依赖:

Metadata

而不是完整 Prompt。

这也会让 Simulation 更安全。

二十二、Shadow Policy

Simulation看的是历史。

下一步:

Shadow

Candidate v43 在真实线上请求中同时计算:

但不执行
Active v42:
真正决定

Shadow v43:
只记录结果

二十三、Shadow可以发现历史数据没覆盖的新模式

比如上线后出现:

新Capability
新Tenant类型
新Region

Simulation 数据里根本没有。

Shadow 能看到。

二十四、Shadow结果不能影响用户

即使 Candidate:

DENY

只记录。

不要在 Shadow 阶段偷偷改变:

Latency
Approval
Tool

否则不是真 Shadow。

二十五、Shadow Metrics

policy_shadow_total
policy_shadow_flip_total
policy_shadow_less_restrictive_total
policy_shadow_error_total

再看:

Evaluation Latency

Policy 本身也不能把 Agent Tool Call 拖慢几百毫秒。

二十六、Policy性能也有SLO

例如:

P95 Decision 
Tenant Restriction
>
Enterprise Base
>
Exception

但 Exception 是否能覆盖 Tenant Restriction,要明确。

我一般建议:

Exception只能放宽可放宽的Policy

不能覆盖:

Zero Tolerance
Cross Tenant
Legal Hold

四十八、Policy层级不要动态由模型决定

模型不能说:

“这个情况特殊,我认为Exception优先。”

优先级是确定性系统规则。

四十九、Policy Simulation还可以做“反事实事故分析”

发生事故:

某个Agent错误外发

你准备新 Policy:

v44

可以拿事故 Run:

Replay Input

问:

如果当时是v44,
会不会挡住?

这就是非常直接的 Regression Evidence。

五十、每个事故最终应该沉淀Policy Regression Case

例如:

incident: INC-2026-081

input:
  data_class: CONFIDENTIAL
  destination: EXTERNAL
  capability: email.send

expected:
  decision: DENY

以后任何 Policy Candidate 都跑。

五十一、Policy Dataset不要只来自事故

还要有:

Normal Cases
Edge Cases
Synthetic Cases
Abuse Cases

否则 Policy 会过拟合历史事故。

五十二、Property-based Policy Test

例如不变量:

任何CrossTenant Resource
永远不能ALLOW

随机生成:

Tenant A
Tenant B
Resource
Capability

断言:

assertNotEquals(
    PolicyDecision.ALLOW,
    evaluate(crossTenantInput));

这比只写几个固定样例强。

五十三、Policy Coverage也要统计

有多少生产 Decision:

命中了显式规则

多少落到:

default

如果:

default hit = 60%

说明 Policy 设计过于粗糙。

五十四、我会看Default Decision Rate

default_decision_total
/
all_policy_decisions

目标应该逐步下降。

特别是高风险 Capability:

最好0%

每种情况都有显式规则。

五十五、Unknown Input不能默认Allow

新 Data Class:

TOP_SECRET

旧 Policy 不认识。

最危险:

else → ALLOW

更合理:

Unknown
→ DENY / REVIEW

Policy Schema 演进要采用:

Secure Default

五十六、Policy版本和Run必须绑定

Run 中途 Policy 升级怎么办?

一般有两种:

Snapshot Policy

Run 创建时固定:

v42

整个 Run 使用 v42。

适合:

低风险长任务

Dynamic Policy

每个高风险 Action:

读取当前Active

适合:

安全写操作

两者不能混得不清楚。

五十七、高风险Action我建议Dynamic Re-evaluate

即使 Run 开始时:

允许email.send

半小时后 Security 禁掉。

真正执行前应该重新评估:

current policy

不要因为 Run Snapshot 还允许就继续写。

五十八、但Replay必须知道当时版本

Audit 保存:

Run Policy Snapshot
Action Policy Version

事故重放时才能还原。

五十九、Policy Rollback也要一键

Candidate v43 出现问题:

切回v42

不能重新编辑一份“像v42”的文件。

Active Pointer:

policy_id → version

回滚只切 Pointer。

六十、Rollback前也要注意新产生的数据

如果 v43 已经允许了一批新操作:

回滚Policy

不会自动撤销历史副作用。

所以 Release Report 需要:

affected_runs
side_effects

方便补救。

六十一、Policy Observability

指标:

policy_decision_total{
  policy,
  version,
  decision
}

policy_flip_total{
  type
}

policy_eval_latency_ms

policy_default_total

policy_exception_total

policy_break_glass_total

高基数 Tenant 不一定放 Metric。

细节进 Trace / DB。

六十二、Policy SLO

P95 Decision  slices,
        List criticalCases) {
}

六十九、Release Gate接口

public interface PolicyReleaseGate {

    ReleaseDecision evaluate(
            PolicySimulationReport simulation,
            ShadowReport shadow,
            PolicyChangeRisk risk);
}

Policy 本身也进入 Control Plane。

七十、本篇上线检查清单

□ Policy独立于Prompt和业务代码
□ 每次修改生成新Version
□ Policy输入结构化
□ Decision不是只有Allow/Deny
□ 每次Decision记录Reason Code
□ Policy进入Git和PR
□ CI做Schema和Conflict检查
□ 高风险Policy有Golden Cases
□ Candidate上线前做历史Simulation
□ Simulation区分权限扩大和收紧
□ 关键Tenant/Risk做Slice分析
□ Candidate支持Shadow
□ Shadow不影响真实决策
□ Policy有P95性能SLO
□ 高风险变更走Canary
□ Exception有Owner/Reason/Expiry
□ Break Glass独立治理
□ Policy失败模式提前定义
□ Emergency Deny能绕过普通Cache
□ Decision Trace可解释
□ Incident沉淀Regression Case
□ Unknown Input默认安全
□ 高风险Action执行前重新评估
□ Active Version可一键Rollback

总结

Agent 平台发展到一定规模以后,真正难维护的往往不再是 Prompt。

而是:

“到底哪些事情允许发生。”

如果这些规则散落在几十个 Service、管理员页面和配置文件里,系统越自动化,风险越难预测。

Policy-as-Code 解决的是:

规则可版本、可Review、可审计

Policy Simulation 解决的是:

改规则之前,
先知道它会改变什么

Shadow 和 Canary 再解决:

历史上没见过的新流量怎么办

做到这一层以后,Agent 权限和风险治理才从:

“配置管理”

真正升级成:

可测试的软件工程。

下一篇继续推进:

生产级Agent(23):Agent配置供应链——模型、Prompt、Skill、Tool与Policy如何做签名发布和可信加载。


更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界

https://www.zyentor.com/