权限申请通常同时包含自然语言理由、目标环境和权限范围。模型可以帮助我们理解这些信息并提出审核建议,但不应该直接修改权限系统。本篇把结构化输出、结果校验和安全边界组合起来,构建一个最小的访问申请审核器:模型生成候选意见,Python 执行确定性规则,最终结果写入审核文件,等待人工确认。

先定义审核边界

示例程序只审核申请,不调用权限系统,也不发送通知。输入包括申请人、环境、权限范围和申请理由;输出包括建议结论、风险原因、允许范围和给审核人的备注。结论只有 approve、review、reject 三种值。

程序还要保留一条硬规则:生产环境申请或包含写权限的申请,不能因为模型返回了 approve 就自动通过。未知权限也不能被模型“解释成”已知权限。模型适合阅读理由和发现风险,程序负责执行不可绕过的策略。

一次审核可以拆成五步:

  1. 程序读取申请,并检查环境、权限名称和必填字段。
  2. 程序把申请作为输入交给模型,要求模型只生成审核建议。
  3. SDK 把响应解析成固定的 Review 对象。
  4. 普通 Python 代码检查结果是否引用了不存在的权限,并执行高风险拦截。
  5. 程序保存建议文件,后续由人工或授权系统完成最终决定。

这条链路故意没有“模型调用授权接口”这一步。即使模型判断失误,影响也只会停留在待复核的建议中。真实接入时,授权动作仍需检查身份、资源范围和审核状态,不能只相信 JSON 文件。

设计输出契约

先把模型输出定义成 Pydantic 类型。extra="forbid" 会拒绝契约之外的字段,枚举则限制结论取值。注意,结构化输出只能约束形状和类型,不能证明模型的判断正确,所以还要在模型外部检查权限范围和自动批准条件。

可以把输出理解成一张审核表:decision 是候选结论,risk_reasons 说明风险,allowed_scope 表示原申请中可以继续讨论的范围,reviewer_note 面向人工审核者。它不是授权结果,也不应包含令牌、密码或完整用户资料。

保留 review 状态很重要。很多申请不是明显安全或危险,而是需要确认身份、数据范围或业务负责人。显式记录不确定性,比强迫模型二选一更容易接入后续流程。

编写最小可运行程序

安装当前 OpenAI Python SDK 和 Pydantic,密钥与模型名只从环境变量读取:

1
2
3
python -m pip install -U openai pydantic
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_NAME="替换为你可用的模型名称"

把下面代码保存为 access_reviewer.py。其中 --self-test 不访问网络,只验证本地校验和高风险拦截逻辑;不带参数时才会调用模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import json
import os
import sys
from enum import Enum
from pathlib import Path

from openai import OpenAI
from pydantic import BaseModel, ConfigDict


KNOWN_SCOPES = {"read:orders", "read:reports", "write:orders"}


class Decision(str, Enum):
approve = "approve"
review = "review"
reject = "reject"


class AccessRequest(BaseModel):
model_config = ConfigDict(extra="forbid")

applicant: str
environment: str
scopes: list[str]
reason: str


class Review(BaseModel):
model_config = ConfigDict(extra="forbid")

decision: Decision
risk_reasons: list[str]
allowed_scope: list[str]
reviewer_note: str


def validate_review(review: Review, request: AccessRequest) -> Review:
if not request.applicant.strip() or not request.reason.strip():
raise ValueError("申请人和申请理由不能为空")
if request.environment not in {"development", "staging", "production"}:
raise ValueError("环境不是允许的值")
if not request.scopes or not set(request.scopes) <= KNOWN_SCOPES:
raise ValueError("申请包含空权限或未登记权限")
if not review.risk_reasons:
raise ValueError("必须说明风险原因")
if not set(review.allowed_scope) <= set(request.scopes):
raise ValueError("审核结果放宽了原申请范围")
return review


def enforce_policy(review: Review, request: AccessRequest) -> Review:
high_risk = request.environment == "production" or "write:orders" in request.scopes
if high_risk and review.decision == Decision.approve:
reasons = [*review.risk_reasons, "高风险申请必须经过人工复核"]
return review.model_copy(
update={"decision": Decision.review, "risk_reasons": reasons}
)
return review


def ask_model(request: AccessRequest) -> Review:
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.parse(
model=os.environ["MODEL_NAME"],
instructions=(
"你是权限申请审核助手,只能生成审核建议,不能授予权限。"
"根据申请内容给出 decision、risk_reasons、allowed_scope 和 reviewer_note。"
"decision 只能是 approve、review、reject;allowed_scope 只能来自原申请 scopes。"
"生产环境或写权限申请应建议人工复核,不要补充输入中不存在的事实。"
),
input=json.dumps(request.model_dump(), ensure_ascii=False),
text_format=Review,
)
if response.output_parsed is None:
raise RuntimeError("模型没有返回可解析的审核结果")
review = validate_review(response.output_parsed, request)
return enforce_policy(review, request)


def sample_request() -> AccessRequest:
return AccessRequest(
applicant="li.shi",
environment="staging",
scopes=["read:reports"],
reason="需要检查本周测试环境的报表数据。",
)


def self_test() -> None:
request = sample_request()
safe = Review(
decision=Decision.approve,
risk_reasons=["只申请报表读取权限"],
allowed_scope=["read:reports"],
reviewer_note="范围与测试环境匹配。",
)
assert enforce_policy(validate_review(safe, request), request).decision == Decision.approve

risky_request = request.model_copy(
update={"environment": "production", "scopes": ["write:orders"]}
)
risky = safe.model_copy(update={"allowed_scope": ["write:orders"]})
result = enforce_policy(validate_review(risky, risky_request), risky_request)
assert result.decision == Decision.review
print("self-test passed")


def main() -> None:
if "--self-test" in sys.argv:
self_test()
return

review = ask_model(sample_request())
Path("access-review.json").write_text(
json.dumps(review.model_dump(mode="json"), ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print("已生成审核建议:access-review.json")


if __name__ == "__main__":
main()

先运行离线检查:

1
python access_reviewer.py --self-test

确认本地规则通过后,再设置环境变量运行真实调用:

1
python access_reviewer.py

成功后会生成 access-review.json。这个文件只是建议快照,里面的结论仍应经过审核人确认。若需要支持真实用户输入,可以把 sample_request() 换成读取命令行参数或表单数据,但输入校验应保留在模型调用之前。

关键代码为什么这样写

Review 是模型输出的协议,validate_review 和 enforce_policy 则是模型之外的第二道边界。前者检查申请范围、风险原因和字段关系;后者把生产环境或写权限的自动批准降为人工复核。即使模型给出了格式正确的结果,也不能绕过确定性规则。

client.responses.parse(..., text_format=Review) 使用 SDK 按 Pydantic 类型解析结构化结果。output_parsed 为空时,程序直接失败,不把拒答或不完整响应当成审核意见。官方文档也提醒,结构化输出仍需处理拒答和输出不完整等边界情况。

从数据流看,sample_request() 产生输入,ask_model() 负责远程调用和解析,两个校验函数负责本地规则,main() 负责持久化。职责拆开后,无网络和密钥时也能测试大部分逻辑。

self_test() 构造安全和高风险申请,验证普通结果保留 approve,高风险结果降为 review。这不是伪造模型结果,而是对安全闸门的离线测试。真实调用还应记录模型名称、提示词版本和解析失败原因。

示例把结果写到 access-review.json,而不是调用授权接口。这使审核过程可以回放和人工检查。真实项目还应记录申请编号、规则版本、审核人和时间,并对申请理由中的个人信息做最小化保存;这些字段不要让模型自行伪造。

常见问题

结构化输出通过后,能否直接开通权限? 不能。它主要保证字段和类型符合契约,不能替代身份认证、资源授权和人工审批。高风险动作必须由程序和权限系统共同控制。

结构化解析失败时,是否应该让模型无限重试? 不应该。网络错误可以有限重试,但拒答、字段不完整或业务校验失败应记录原因并停止。无限重试会掩盖输入问题并增加费用。

为什么不让模型自己判断所有规则? 提示词容易被遗漏的上下文或异常输入影响。固定的环境、范围和审批规则应写成普通代码,模型只负责补充语义判断。

allowed_scope 为什么必须是原申请的子集? 如果模型凭空增加权限,即使结论写成 review,后续人工也可能误把它当成建议范围。限制为原申请子集,可以防止审核器在处理过程中扩大请求边界。

模型返回了未知权限怎么办? 输入校验会拒绝未登记权限;程序不应把未知字符串映射到最接近的已知权限。需要新增权限时,应先更新权限目录和测试,再开放给模型使用。

如何扩展到批量申请? 为每条申请生成独立结果并保留编号,再统计失败项和高风险项;不要把一批申请合并成一条无法追溯的结论。批量任务还应设置数量和耗时上限。

小结

这个项目把访问审核拆成三层:模型理解申请并提出建议,Pydantic 保证输出结构,确定性规则阻止高风险结果自动通过。模型可以帮助减少阅读工作,但权限授予仍应由经过验证的程序和人工流程完成。先让审核结果可复核、可回放,再考虑接入真正的授权系统。

官方参考:Structured model outputs;OpenAI Python SDK。