构建生产级大模型五阶段拒绝网关
- 作者

- 姓名
- Nino
- 职业
- Senior Tech Editor
想象一下周二早上的客服队列。某个客服代表阅读了一张工单并起草了一份退款承诺,但该工单却缺失了订单状态。直到客户回复之前,没有人发现这一点。
那份草稿是在证据缺失的情况下做出的决定。决策的所有者是客服主管,而后果则由客户承担。可逆转的节点正是发送按钮。
因此,我不再去质问模型是否值得信赖,而是开始思考拒绝机制应该放在哪里。设立一个网关的成本远低于事后道歉。以下是构建该网关的五阶段测试框架,每个阶段都有其专属的检查机制。
第一阶段:定义停止条件
写在维基百科里的规则只会被阅读一次,而写在文件里的规则才会被执行。请将你的规则放在 stop_conditions.json 中。
[
{
"id": "refund-promise",
"trigger": "draft contains a refund commitment",
"required_evidence": ["customer_id", "order_state", "refund_policy_id"],
"owner": "support-lead",
"allowed_action": "draft_only",
"recovery": "ask_customer_for_order_id"
}
]
四个字段至关重要:触发器、证据、所有者以及恢复路径。如果你无法为之指定唯一的所有者,那你得到的只是一项政策而非规则。
验证方法: 运行 python -m json.tool stop_conditions.json > /dev/null && echo "schema ok" 应当打印 schema ok 并且退出码为零。如果该文件为空,你的网关形同虚设。
第二阶段:搭建测试框架环境
我通常会保留一台临时的测试服务器来进行网关开发。这样做的目的是拥有一台即使被破坏也不会影响生产环境的机器。
在测试高性能工作流时,利用像 n1n.ai 这样可靠的 LLM API 聚合器,可以确保当你的网关允许请求通过时,它能够以低延迟访问稳健的端点。
ssh you@your-free-server
python3 -V # 需要 3.9 或更高版本以支持下文的标准库调用
git clone <your-fork-url> stop-gate && cd stop-gate
python3 -m venv .venv && . .venv/bin/activate
export N1N_API_KEY="<从控制台获取>"
export MODEL_BASE_URL="<从控制台获取>"
export MODEL_ID="<选择控制台列出的模型>"
在此我不会列举具体的模型名称,因为模型列表经常变动,硬编码会导致代码迅速过时。通过将流量路由到诸如 n1n.ai 的聚合服务,你可以在不重写验证框架的前提下灵活切换底层模型。
验证方法: 运行 python -c "import os; print(bool(os.environ['MODEL_BASE_URL']))" 应当打印 True。对于整个框架而言,一次成功的传输调用就足够了。
第三阶段:实现零Token消耗的网关
网关会在调用任何模型之前检查证据。因此,一次拒绝操作的 Token 消耗为零。
# gate.py—参考实现,仅依赖标准库
import json, os, urllib.request
STOP = json.load(open("stop_conditions.json"))
def transport(prompt, base, key, model):
body = json.dumps({"model": model, "input": prompt}).encode()
req = urllib.request.Request(
base, data=body,
headers={"Authorization": f"Bearer {key}",
"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def coverage(case, rule):
have = set(f for f in rule["required_evidence"] if case.get(f))
return have, set(rule["required_evidence"]) - have
def decide(case, call_model=None):
for rule in STOP:
_, missing = coverage(case, rule)
if missing:
return {"decision": "REFUSE", "rule": rule["id"],
"missing": sorted(missing), "owner": rule["owner"],
"recovery": rule["recovery"], "model_called": False}
text = call_model(case) if call_model else None
return {"decision": "DRAFT", "rule": None, "missing": [],
"output": text, "model_called": call_model is not None}
第四阶段:证明拒绝机制有效
接下来是人们经常跳过的一个环节:证明拒绝请求永远不会触及模型。
# test_gate.py
from gate import decide
def boom(_):
raise AssertionError("拒绝请求时绝不能调用模型")
case = {"customer_id": "c-88", "draft": "we will refund you"}
out = decide(case, call_model=boom)
assert out["decision"] == "REFUSE", out
assert out["missing"] == ["order_state", "refund_policy_id"], out
print("refusal gate ok:", out["owner"], out["recovery"])
验证方法: 运行 python test_gate.py 应当打印 refusal gate ok: support-lead ask_customer_for_order_id。如果触发拒绝时仍然调用了模型,说明你建的不是网关,而是一段注释。
第五阶段:记录并分析拦截原因
拒绝是一条数据,而不是无声的阻断。我们需要记录缺少了什么、谁拥有该权限,以及人类该如何恢复。
def record(path, case_id, rule, missing, decision, owner, recovery, ts):
line = {"case_id": case_id, "rule": rule, "missing": sorted(missing),
"decision": decision, "owner": owner,
"recovery": recovery, "ts": ts}
with open(path, "a") as fh:
fh.write(json.dumps(line) + "\n")
验证方法: 运行 jq -r 'select(.decision=="REFUSE") | .missing[]' decisions.jsonl | sort | uniq -c | sort -rn 可以按数量对拦截原因进行分组统计:
14 order_state
9 refund_policy_id
十四次拒绝指向同一个字段。此时应该去修缮录入表单,而不是修改提示词。这就是把日志记录放在最后一步的全部意义所在。
人工审查协议
审查卡片应当包含以下四行且顺序固定:代理想要执行的操作、缺失的证据、谁可以解除阻塞、以及强行批准的后果。阅读焦点的顺序必须与该阅读顺序相匹配,严禁使用纯颜色信号,必须使用普通词汇且不包含系统名称。
我的审查协议精简且可重复。由两名审查员大声朗读一张卡片,其中一人使用屏幕阅读器。我会记录他们的犹豫次数,并计算每个人说出所有者和恢复路径所需的时间。
- 成功标准: 每个拒绝卡片都能在十秒内明确指定唯一的所有者和恢复路径。
- 停止标准: 在必填字段为空的情况下任何批准流程都不得推进。一旦发生这种情况,必须立即叫停发布并修复规则。绝不能放宽测试夹具。
这并不证明模型是绝对安全的,只能证明某种特定类别的操作会严格等待证据到位。
何时跳过此架构
如果监管机构要求将合同指定的模型版本绑定到文档中,请跳过此架构。对于亚秒级交互式流,由于网关需要写入记录,也请跳过此架构。如果你的团队无法指定一个明确的决策所有者,同样应当跳过。匿名的拒绝无法教会任何人任何东西。
那么,本周究竟是哪项缺失的证据应该阻断你的审批?写下那条规则,运行测试夹具,然后让网关为你执行拒绝。
如果你需要一个可靠的基础设施层来支撑你的自动化测试工作流,探索诸如 n1n.ai 的服务将有效简化你的多模型集成方案。
在 n1n.ai 免费获取 API 密钥