权限申请通常同时包含自然语言理由、目标环境和权限范围。模型可以帮助我们理解这些信息并提出审核建议,但不应该直接修改权限系统。本篇把结构化输出、结果校验和安全边界组合起来,构建一个最小的访问申请审核器:模型生成候选意见,Python 执行确定性规则,最终结果写入审核文件,等待人工确认。
先定义审核边界
示例程序只审核申请,不调用权限系统,也不发送通知。输入包括申请人、环境、权限范围和申请理由;输出包括建议结论、风险原因、允许范围和给审核人的备注。结论只有 approve、review、reject 三种值。
程序还要保留一条硬规则:生产环境申请或包含写权限的申请,不能因为模型返回了 approve 就自动通过。未知权限也不能被模型“解释成”已知权限。模型适合阅读理由和发现风险,程序负责执行不可绕过的策略。
一次审核可以拆成五步:
- 程序读取申请,并检查环境、权限名称和必填字段。
- 程序把申请作为输入交给模型,要求模型只生成审核建议。
- SDK 把响应解析成固定的
Review 对象。
- 普通 Python 代码检查结果是否引用了不存在的权限,并执行高风险拦截。
- 程序保存建议文件,后续由人工或授权系统完成最终决定。
这条链路故意没有“模型调用授权接口”这一步。即使模型判断失误,影响也只会停留在待复核的建议中。真实接入时,授权动作仍需检查身份、资源范围和审核状态,不能只相信 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。