Agent DecisionAuthority

把 LLM 的语义建议、运行时事实与最终业务授权分离,由版本化 deterministic policy 根据结构化 facts 决定 ALLOW、ABSTAIN、ESCALATE 或 BLOCK。

#type / concept #status / growing #tech / ai #tech / architecture #tech / security #resource / agent

[!info] related notes

Agent DecisionAuthority

一句话定义

DecisionAuthority 是系统中真正有权把“模型提出的可能性”变成“业务允许执行的动作”的控制边界。

它通常接受已经验证过的结构化 runtime facts,再通过版本化、可测试、deterministic 的 policy 产生最终授权结果。

[!important] 核心区分 Reasoning quality ≠ Authorization

模型可以比规则更“聪明”,但不代表模型拥有更高的业务权限:intelligence ≠ authority

为什么不能让 LLM 自己拥有最终 authority

LLM 很适合:

  • 从复杂输入中提出候选解释;
  • 识别潜在 EvidenceGap;
  • 解释证据的语义关联;
  • 给出不确定性与候选排序。

但它不适合单独决定:

  • 是否允许 AUTO;
  • 是否越过 critical gap;
  • 是否绕过 red flag;
  • 是否将外部知识当作用户事实;
  • 是否扩大当前 configuration 已批准的安全边界。

如果同一个随机组件既负责“发现事实”、又负责“裁决自己是否可以忽略缺失事实”,系统就很难证明安全边界。

更稳健的权力链是:

LLM Proposal

Runtime Verification

Structured Policy Facts

DecisionAuthority

Authorized Business Transition

从“模型很聪明”到“系统可授权”的推导

学习这一概念时最容易产生的第一反应是:

如果模型能力已经非常强,而且 confidence 很高,为什么还需要一层看起来更“笨”的 deterministic policy?

关键在于这两者回答的是完全不同的问题。

LLM confidence
回答:
“在我的语义模型里,我有多相信这个判断?”

DecisionAuthority
回答:
“在当前业务规则、安全约束、证据状态和版本化政策下,系统有没有权限执行这个动作?”

一个模型可以非常确定某个 Diagnosis 候选最可能,但系统仍然可能缺少一个法律上、业务上、安全上或产品上必须确认的事实

例如:

模型:
C6 radiculopathy,confidence=0.98

Runtime:
progressive_motor_weakness = UNKNOWN
criticalGapCount = 1

这里 0.98 只能说明模型的语义倾向,并不能把 UNKNOWN 变成 FALSE。

如果让 confidence 直接越过缺失事实,实际上就是把:

“我不知道”

偷偷转换成:

“应该没问题”

这正是高风险 Agent 最需要避免的推理跳跃。

所以 DecisionAuthority 的价值不在于比模型更会推理,而在于它拥有不同的权力角色

Policy Facts:先把复杂运行压缩成可审计事实

DecisionAuthority 不应该直接解析一大段自然语言 reasoning,而应消费稳定的结构化 facts,例如:

DecisionFacts
├─ active_safety_review
├─ new_red_flag
├─ governance_verdict
├─ status
├─ candidate_count
└─ unresolved_critical_gap_count

这样可以把两类问题分开:

“世界现在是什么状态?”
→ fact extraction / runtime validation

“在这些事实下允许做什么?”
→ decision policy

例如:

criticalGapCount = 1
policy = diagnosis-decision-policy-v1
→ ABSTAIN

这里不需要重新让 LLM 判断“是不是应该 abstain”。

Fact Extraction 是 Authority 之前的独立信任边界

DecisionAuthority 只有在输入 facts 正确时才有意义。

假设 Python 输出:

{
  "status": "completed",
  "unresolved_critical_gaps": [
    {"gap_id": "G7"}
  ]
}

但 Go 在 DTO / mapper 中错误地得到:

criticalGapCount = 0

那么 policy 即使完全正确,也会做出错误授权。

因此生产链路实际上是:

Raw Agent Output / Runtime Observations

Schema Validation

Fact Extraction

DecisionFacts

DecisionPolicy

Decision

这里有两个不同的 deterministic contract:

  1. Fact contract:同一个经过验证的 runtime state 必须稳定转换成正确 facts;
  2. Authority contract:同一 facts + policy revision 必须得到同一 decision。

这也是为什么 Failure Attribution 中要把 Runtime Fact Failure 和 DecisionAuthority Failure 分成两类。

DecisionAuthority 与 SafetyEnvelope 的区别

SafetyEnvelope 更像边界判断:当前 case 是否仍位于被证明可以普通自动化处理的运行域内?

DecisionAuthority 更像动作选择:如果已经超出普通 AUTO 边界,到底应该:

  • ALLOW_DEGRADED
  • ABSTAIN
  • ESCALATE
  • BLOCK

因此可以理解为:

SafetyEnvelope
= eligibility boundary

DecisionAuthority
= authorized action selection

一个例子:

Case A
critical diagnostic gap unresolved
→ outside normal SafetyEnvelope
→ ABSTAIN

Case B
new red flag detected
→ outside normal SafetyEnvelope
→ ESCALATE

Case C
configuration identity invalid
→ authority input itself不可信
→ BLOCK / fail closed

SafetyEnvelope 只告诉你“不能按普通自动化继续”,但不同原因应该映射到不同业务动作,这就是 DecisionAuthority 的工作。

Deny-Overrides:Hard blocker 不是一个低分项

假设模型给出:

confidence = 0.98
judge_score = 0.96
candidate_quality = high

但同时:

critical gap = unresolved

错误思路是把它们做加权平均:

0.98 + 0.96 + ... > blocker
→ AUTO

正确思路是 Deny-Overrides

if hard_blocker:
    normal_auto = forbidden

[!warning] Policy 不是一个 0~1 的 score Hard constraint 与 confidence 属于不同层级。Confidence 可以成为 policy input,但不能把禁止条件“加权覆盖掉”。

为什么 weighted score 在这里危险

如果所有因素最终都进入一个分数:

score =
  0.4 * confidence
+ 0.3 * judge_score
+ 0.2 * evidence_quality
- 0.1 * safety_penalty

那么理论上足够高的 confidence 可以覆盖任何 safety penalty。

这适合排序问题,不适合权限问题。

权限系统通常更像:

Hard Gates
  ↓ all passed
Soft Ranking / Preference

Authorized Choice

而不是:

Everything → one scalar → threshold

这和安全系统、权限系统、编译器 type checking 的思想更接近:某些约束不是“扣一点分”,而是不满足就不能进入下一阶段

Pure Deterministic Policy 不等于“一坨 if/else”

Policy 内部出现显式 if 本身不是坏事。真正重要的是它尽量保持:

Facts → Pure Function → Decision

而不是在 policy 中混入:

  • DB 查询;
  • 网络请求;
  • 模型调用;
  • 隐式可变全局状态;
  • 无版本的运行时配置。

只要相同 facts + policy revision 总能得到相同授权结果,就具备很强的可测试性与可审计性。

规则数量很少时,显式顺序通常比通用 Rule Engine 更容易审计。只有规则规模、组合复杂度或变化速度明显上升时,才值得把规则提升为一等对象。

一个更接近代码的 Policy 形状

概念上可以写成:

@dataclass(frozen=True)
class DiagnosisDecisionFacts:
    active_safety_review: bool
    new_red_flag: bool
    unresolved_critical_gap_count: int
    governance_verdict: str
    output_status: str


def decide_diagnosis(
    facts: DiagnosisDecisionFacts,
    *,
    policy_revision: str,
) -> DiagnosisDecision:
    if policy_revision != "diagnosis-decision-policy-v1":
        return DiagnosisDecision.block("unknown_policy_revision")

    if facts.new_red_flag or facts.active_safety_review:
        return DiagnosisDecision.escalate("safety_blocker")

    if facts.unresolved_critical_gap_count > 0:
        return DiagnosisDecision.abstain("critical_gap_unresolved")

    if facts.governance_verdict != "accepted":
        return DiagnosisDecision.block("governance_rejected")

    return DiagnosisDecision.allow_normal()

这里最重要的不是具体代码,而是结构:

无 I/O
无模型
无隐藏状态
输入显式
policy revision 显式
reason code 显式

这样 policy 可以:

  • 单元测试;
  • historical replay;
  • counterfactual replay;
  • property-based testing;
  • incident debugging;
  • version comparison。

Decision 不应该只有 allowed: bool

一个布尔值通常太弱:

allowed = false

无法告诉上层究竟应该怎么做。

更成熟的结果应表达动作语义原因语义

Decision
├─ outcome
│   ├─ ALLOW_NORMAL
│   ├─ ALLOW_DEGRADED
│   ├─ ABSTAIN
│   ├─ ESCALATE
│   └─ BLOCK
├─ reason_codes
├─ policy_revision
└─ relevant facts snapshot/hash

这样 API、Persistence、UI 和 Replay 都可以消费同一个结果。

例如:

ABSTAIN
reason = critical_gap_unresolved

和:

BLOCK
reason = configuration_mismatch

虽然都“没有普通输出”,但产品行为完全不同。

Fail Closed

DecisionAuthority 应对未知或矛盾事实保持保守:

unknown policy revision
malformed required facts
unknown enum
contradictory safety state

这些状态不应该被解释成“应该没事”,而应进入 BLOCK / 明确失败路径。

[!tip] 记忆方式 UNKNOWN ≠ probably safe

Fail closed 不是“所有异常都给用户报错”

Fail closed 描述的是权限方向,不是最终 UX。

内部可以:

unknown decision fact
→ normal AUTO forbidden

对用户则可能呈现:

“当前信息不足,需要重新确认”

或:

“这次结果需要人工复核”

也就是说:

security/authority semantics

user-facing copy

不要因为希望 UX 友好,就在 authority 层把 UNKNOWN 当成安全。

Model Proposal 与 Authorized Result 是两个不同产物

模型可能提出:

Candidate C17
confidence = HIGH

但 DecisionAuthority 判断:

critical gap unresolved
→ ABSTAIN

那么真正业务输出可以变成:

status = insufficient_information
candidates = []

这意味着系统至少要区分:

Reasoning Result
= 模型/Agent 原本提出了什么

Authorized Business Result
= 系统最终允许交付什么

如果 policy 只是写一句“建议不要返回”,但 API 仍直接把 raw model output 给用户,那么它并没有真正的 authority。

Authority 必须控制真实 Side Effect,而不是只生成一个建议字段

一个常见伪治理结构是:

{
  "candidate": "C6 radiculopathy",
  "policy_recommendation": "abstain"
}

但调用方仍然:

persist candidate
→ API 返回 candidate
→ UI 展示

这种 policy 只是 metadata,不是 authority。

真正 authority 必须处于side-effect choke point

Raw Proposal

DecisionAuthority

ApplyDecision
    ├─ decide what can be persisted as user-visible/current
    ├─ suppress forbidden candidate delivery
    ├─ create escalation state
    └─ record DecisionTrace

这条原则在 Treatment 更重要:AI 可以 proposal,但只有 acceptance authority 能让某 revision 成为 current Treatment。

Policy Versioning

DecisionAuthority 必须版本化。例如:

diagnosis-decision-policy-v1

未来如果规则从:

critical gap → ABSTAIN

改成:

critical safety gap → ESCALATE
critical diagnostic gap → ABSTAIN

应产生新 revision,而不是偷偷改写 v1。

原因是历史记录必须仍然能回答:

“D100 当时为什么按照 v1 得到 ABSTAIN?”

Policy 变化也是行为变化,因此通常会改变 Agent Configuration Bundle identity,并重新进入 qualification / promotion。

为什么 Policy Revision 应进入 Agent Configuration Identity

有时会误以为:

模型、Prompt 没变
→ Agent configuration 没变

但如果 DecisionPolicy 从 v1 改成 v2,即使模型输出一字不变,最终用户可见结果也可能改变。

因此行为配置更准确是:

AgentConfiguration
=
Model policy
+ Prompt
+ Schema
+ Tool policy
+ Evidence policy
+ Governance policy
+ Decision policy
+ behavior-significant settings

DecisionAuthority 是 Agent 产品行为的一部分,不是 Agent 之外的“后处理小工具”。

BodySense Diagnosis 例子

BodyState R52

Python proposes C6 radiculopathy, confidence HIGH

Runtime facts:
  criticalGapCount = 1
  governance = accepted
  newRedFlag = false

SafetyEnvelope: outside normal AUTO

DecisionPolicy v1

ABSTAIN

Authorized result:
  status = insufficient_information
  candidates = []

这里并没有证明 Candidate 一定“错”。系统只是说:在当前证据与 policy 下,没有权限普通自动交付这个 Candidate。

同一个概念迁移到 Treatment 后如何变化

Diagnosis authority 主要控制:

“这个分析能否作为普通结果交付?”

Treatment authority 增加时间和动作维度:

Generation Authority
→ 现在是否允许生成 proposal?

Acceptance Authority
→ 过了一段时间,这个 proposal 现在还能否成为 current Treatment?

Execution Authority
→ accepted intervention 在当前状态下是否仍可执行?

因此 DecisionAuthority 不是只适用于“一次 Agent response”。

它可以出现在业务 lifecycle 的多个 transition 上,只要每个 transition 都明确:

Facts
+ policy revision
→ authorized transition

与 Consultation Runtime 的连接

Consultation 中 authority 不一定表现成“是否返回 Diagnosis Candidate”,还可能控制:

  • 是否允许某个 tool side effect;
  • 是否允许 resume;
  • 是否允许自动继续而不是要求 HITL;
  • stream 中某个 internal event 是否可以变成 public business event。

这说明 DecisionAuthority 是业务 transition ownership,而不是某个固定类名。

测试 DecisionAuthority 应该覆盖什么

1. Happy Path

all hard facts valid
→ ALLOW_NORMAL

2. 每个 blocker 的单独测试

new red flag
→ ESCALATE

critical gap unresolved
→ ABSTAIN

governance rejected
→ BLOCK

3. Deny-Overrides 组合测试

confidence HIGH
+ judge score HIGH
+ critical gap unresolved
→ 仍不得 ALLOW_NORMAL

4. Unknown / malformed facts

unknown enum
unknown policy revision
contradictory safety state
→ fail closed

5. Policy Revision Regression

固定一组 facts:

v1 → ABSTAIN
v2 → ESCALATE

如果这是预期变化,就应通过 counterfactual replay / qualification 明确记录,而不是让 regression test 无意中变化。

6. ApplyDecision / Side-effect Test

不要只断言:

decision.outcome == ABSTAIN

还要验证:

ordinary candidate not delivered
escalation/insufficient_information state persisted
DecisionTrace persisted

因为 authority 只有真正影响 side effect 才算完成。

生产事故里怎样判断是 Authority Bug 还是上游 Bug

假设用户看到本不该出现的 candidate。

按顺序检查:

1. Python 是否识别了 critical gap?
2. HTTP/DTO 是否完整传递?
3. Runtime Fact criticalGapCount 是否正确?
4. DecisionPolicy 输出是否正确?
5. ApplyDecision 是否真正抑制了结果?
6. DB / read model / frontend 是否重新旁路了 raw proposal?

只有第 4 步首次偏离时,才是纯 DecisionAuthority bug。

如果第 3 步已经错了,修 policy 反而是在错误 facts 上打补丁。

详见 Agent Failure Attribution

自测:能回答这些问题才算真正理解

  1. 为什么 confidence=0.99 不能自动越过 critical EvidenceGap?
  2. SafetyEnvelope 和 DecisionAuthority 分别回答什么问题?
  3. 为什么 hard blocker 不适合跟 confidence 做 weighted average?
  4. 为什么 DecisionPolicy 应尽量是 pure function?
  5. Fact Extraction 错误和 DecisionPolicy 错误为什么必须分开定位?
  6. ABSTAINESCALATEBLOCK 为什么不能都压成 allowed=false
  7. 为什么 policy revision 改变通常要形成新 Agent Configuration identity?
  8. 如果 policy 输出 ABSTAIN,但 API 仍返回 raw candidate,为什么说 authority 实际上没有生效?
  9. Treatment 为什么需要 generation/acceptance 两个不同 authority checkpoint?
  10. Fail closed 为什么不等于必须给用户显示一个生硬的内部错误?

常见误解

  • 模型更强就可以放宽 Policy:更强模型并不会自动扩大 SafetyEnvelope;需要新 eval 证据与新 configuration qualification。
  • DecisionAuthority 就是 Judge:Judge 提供语义质量信号;Authority 执行确定性的业务授权规则。
  • Policy 一旦 deterministic 就不能变化:deterministic 指同版本同 facts 下结果稳定,不代表 policy 永不演化。
  • 最终结果为空就没必要保留模型 proposal:审计时仍需要知道模型原本提出什么,以及为什么被 authority 抑制。
  • 有一个 policy_decision 字段就算治理完成:如果它没有控制持久化、交付或实际 side effect,它只是 metadata。
  • policy rule 越抽象越高级:规则少且安全关键时,显式、版本化、可审计往往比通用 Rule Engine 更可靠。
创建于 2026/8/22 更新于 2026/8/23