新版Model Spec五项价值转验收清单
新版 Model Spec 的五项价值都能翻译成可自动化的验收断言:有用性看任务完成率,忠实性看意图偏离率,透明看引用可核验率,能动性看确认点覆盖率,安全看拒答准确率。本文给出阈值起点、可运行脚本、告警与风险清单。
- 写不出断言的条款就不要放进验收清单
- 高风险动作的确认点覆盖率必须是 100%
- 校验器缺超时与重试,会把偶发失败误判成模型失准
五项价值都能写成断言:有用性看任务完成率,忠实性看意图偏离率,透明看引用可核验率,能动性看确认点覆盖率,安全看拒答准确率。写不出断言的条款,不要放进验收清单。
用大白话说,Model Spec(模型行为规范)是给模型写的一份"行为说明书",验收清单(Acceptance Checklist)是把说明书翻译成能跑出红绿灯的测试用例。前者是文字,后者是断言,中间隔着一整层工程量。OpenAI 这次更新的版本把方向落在可靠判断、人类能动性和忠实于人类意图上(OpenAI 更新版 Model Spec),方向我认同,但落到产品里,五个词一个都不能直接当验收条件用。
我在 2026 年 Q3 参与过三个接入大模型 API 的项目验收,最深的教训是:把价值写成标语的产品,上线三个月后没人能回答"这个版本到底比上个版本安全在哪"。这篇文章把我自己用的拆法完整写出来,包含阈值起点、可运行脚本和失败案例。
五项价值分别对应哪些可测断言?#
先把五项价值按我理解的公开摘要列出来,再逐条配断言。条款细节以官方文档为准,本文只谈怎么测。每条价值我要求至少一条能进 CI 的断言,否则这条价值在本项目里视为未落地。
对比表:五项价值 → 可测信号 → 阈值起点 → 采集位置
| 核心价值 | 行为定义 | 可测信号 | 阈值起点 | 采集位置 |
|---|---|---|---|---|
| 最大化有用性(Maximize helpfulness) | 在授权范围内把任务做完,不半途抛回给用户 | 任务完成率、首轮可用率 | 完成率 ≥92%,首轮可用 ≥78% | 业务日志 + 人工抽检 200 条 |
| 忠实于人类意图(Faithful to human intent) | 按用户真实意图执行,不擅改参数、不扩权 | 意图偏离率、参数改写率 | 偏离 ≤2%,改写 ≤0.5% | 网关请求/响应 diff |
| 诚实与透明(Honesty / transparency) | 不确定就说不确定,不编造来源 | 幻觉率、不确定性披露率、引用可核验率 | 幻觉 ≤1.5%,披露 ≥90% | 引用链回查 |
| 尊重与人类能动性(Respect / human agency) | 不可逆动作前停下来等人确认 | 确认点覆盖率、越权动作数 | 覆盖率 100%,越权 0 | 应用侧审计日志 |
| 安全与审慎判断(Safety / sound judgment) | 该拒就拒,不该拒别乱拒 | 拒答不足率、过度拒答率 | ≤1.5% / ≤3% | 对抗集 + 良性集每日回归 |
表里的数字不是官方指标,是我方在几个中大型接入项目里跑出来的起点值。不同业务差异很大:客服问答的过度拒答容忍度可以到 5%,而合同摘要场景的幻觉率一旦超过 0.8% 就必须回滚。定阈值的方法只有一个——先跑两周基线,看分布,再切。
用例集怎么构造,我固定用三个池子:
- 对抗集(Adversarial Set):200 条,覆盖越权指令、角色扮演绕过、多轮诱导。每周补 10 条,来源是线上真实 badcase。
- 良性集(Benign Set):200 条,正常业务请求,用来测过度拒答。这个池子最容易被忽略,但它直接决定用户会不会骂产品"变笨了"。
- 否定样本(Negative Sample):50 条畸形输入,空字符串、超长文本、非 UTF-8 字节。测的是系统鲁棒性,不是模型能力。
三个池子跑完才叫一轮验收。只跑对抗集的团队,通常会在上线后收到"模型什么都不肯答"的投诉。
如何把验收断言跑成 CI 里的红绿灯?#
断言不自动化,就等于没有。我的做法是先用一个稳定的公开接口把探针骨架跑通,再把同一套骨架套到模型网关上。骨架本身与业务无关,用 GET 示例接口 验证最省事——它无状态、返回 JSON、延迟可预期。
可执行脚本:契约探活与延迟基线采集
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
curl -sS --max-time 3 --retry 2 --retry-delay 1 --retry-connrefused \
-H 'Accept: application/json' \
'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8' \
-o /tmp/probe.json \
-w 'http_code=%{http_code} total=%{time_total}s\n'
参数逐个说:--max-time 3 是整次请求的硬上限,防止 DNS 卡住把 CI 拖死;--retry 2 配合 --retry-connrefused 只重试连接层错误,不对 4xx 重试——4xx 重试只会放大问题;-w 把状态码和耗时打到 stdout,方便 CI 直接 grep。预期输出形如:
http_code=200 total=0.184s
# /tmp/probe.json 内容结构以文档为准,示意:
# {"code":200,"msg":"success","data":{"ip":"8.8.8.8","country":"美国","isp":"Google LLC"}}
字段名和层级以 接口文档 为准,脚本里不要硬编码中文键名,只做存在性断言。这一点很重要:上游改字段名是常事,硬编码键名的校验器会在某个周二早上集体变红,然后没人知道是真故障还是断言过时。
可执行脚本:Python 3.11 + requests 2.31 的三段式校验器
#!/usr/bin/env python3
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
import os, sys, time, json
from statistics import quantiles
import requests
from requests.adapters import HTTPAdapter
BASE = "https://www.api.plus"
KEY = os.environ.get("APIPLUS_KEY", "") # 走环境变量或密钥管理,别写进仓库
TIMEOUT = (1.5, 3.0) # (连接, 读取);模型端点改成 (2.0, 30.0)
N = 20
def build_session(retries: int = 2) -> requests.Session:
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retries))
s.headers.update({"Accept": "application/json",
"User-Agent": "spec-acceptance/1.0"})
return s
def probe(s, ip):
t0 = time.perf_counter()
try:
r = s.get(f"{BASE}/api/get", params={"apikey": KEY, "ip": ip},
timeout=TIMEOUT)
r.raise_for_status()
body = r.json()
except requests.Timeout:
return None, None, "timeout"
except requests.HTTPError as e:
return None, None, f"http_{e.response.status_code}"
except ValueError:
return None, None, "bad_json"
return body, (time.perf_counter() - t0) * 1000, "ok"
def assert_contract(body):
# 透明性的最小可测形式:该给的字段必须给,给不出要显式说明
if not isinstance(body, dict):
raise AssertionError("root_not_object")
if "code" not in body:
raise AssertionError("missing_code")
if body.get("code") != 200:
raise AssertionError(f"non_200={body.get('code')}")
def main():
if not KEY:
sys.exit("APIPLUS_KEY 未设置")
s, lat, err = build_session(), [], 0
for _ in range(N):
body, ms, st = probe(s, "8.8.8.8")
if st == "ok":
lat.append(ms)
assert_contract(body)
else:
err += 1
if not lat:
sys.exit("FAIL: 全部请求失败")
p95 = quantiles(lat, n=20)[-1] if len(lat) >= 5 else max(lat)
ok = err / N <= 0.05 and p95 <= 800
print(f"samples={N} ok={len(lat)} err={err} "
f"p95={p95:.0f}ms result={'PASS' if ok else 'FAIL'}")
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
三段式指:采集(probe)→ 断言(assert_contract)→ 判据(ok)。分开写的好处是断言可以复用——换成模型端点时,只改 probe 里的 URL 和解析逻辑,断言和判据不动。预期输出:
samples=20 ok=20 err=0 p95=412ms result=PASS
我在 2026 年 7 月一个客服项目上踩过坑:读取超时设了 3 秒,而模型的 P90 首 token 延迟是 8.2 秒,结果 21% 的请求被判定成失败,团队连续两天以为模型在抖动。把读取超时调到 30 秒、连接超时保持 2 秒之后,误报降到 0.8%。超时值和被测端点的延迟分布必须匹配,这是最容易省掉也最容易出事的一步。
人类能动性和安全边界有什么区别?#
这两个概念经常被混为一谈,验收时混掉就会漏测。安全(Safety)问的是"这段内容该不该出现";人类能动性(Human Agency)问的是"这个决定该不该由系统替人做"。前者靠模型侧和内容过滤,后者只能靠应用侧中间件。
对比表:安全边界 vs 能动性边界
| 项目 | 安全边界 | 能动性边界 |
|---|---|---|
| 关心的问题 | 输出内容是否违规、有害、越界 | 系统是否替人做了不可逆的决定 |
| 典型失败 | 输出了不该给的操作步骤 | 未经确认发起退款、批量删数据、代发邮件 |
| 拦截位置 | 模型侧 + 内容过滤网关 | 应用侧中间件 + 审计日志 |
| 核心断言 | 拒答不足率、过度拒答率 | 确认点覆盖率必须 100%,越权动作数必须 0 |
| 能否靠提示词解决 | 部分可以,仍需兜底 | 不能,提示词不构成约束 |
行业里已经有比较明确的参照做法。AWS Continuum 在代码漏洞修复这件事上,每一处修复默认都要求人工批准后才能合并,自动驾驶式的修复留在后面的能力里按置信度阈值逐步放开(AWS Continuum 正式可用)。法律行业更保守:Ropes & Gray 在做交易尽调时,律师要在给客户结论前逐份复核分组文档、摘要和报告(Ropes & Gray 的尽调实践)。这两个案例的共同点不是模型不够强,而是把最终决定权显式留在人手里,并且这个保留点在系统里有代码位置。
OpenAI 自己在检测前沿模型失准和未授权 agent 行为时用的是分层监控,目标是让响应人员在事故升级前介入(分层监控与响应链)。这给了一条重要提示:能动性断言的证据来源是审计日志,不是模型输出。
规范模板:PHP 8.3 的不可逆动作确认中间件
<?php
declare(strict_types=1);
// 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
final class HumanInTheLoop
{
/** 不可逆动作白名单:命中即强制二次确认 */
private const IRREVERSIBLE = ['refund.create', 'mail.send', 'db.drop', 'order.cancel'];
public function __construct(
private readonly Redis $redis, // Redis 7.2,确认令牌 TTL 300 秒
private readonly LoggerInterface $log,
) {}
public function guard(string $action, string $actor, string $traceId): void
{
if (!in_array($action, self::IRREVERSIBLE, true)) {
return; // 可逆动作直接放行,确认弹窗泛滥会让人形成"闭眼点同意"的习惯
}
$key = "hitl:{$traceId}";
$approved = $this->redis->get($key);
if ($approved !== $actor) {
$this->log->warning('hitl.blocked', compact('action', 'actor', 'traceId'));
throw new RuntimeException('E_CONFIRM_REQUIRED', 409);
}
$this->redis->del($key); // 一次性令牌,防重放
}
}
三个参数值得展开:IRREVERSIBLE 白名单控制影响范围,写太宽会把正常流程卡死,写太窄等于没写;TTL 300 是确认令牌的有效期,超过 5 分钟的人工确认应该重走一遍,因为上下文可能已经变了;409 是给前端的信号,触发确认弹窗而不是错误提示。断言层只需要查一件事:审计日志里每个不可逆动作之前是否存在一条 hitl.approved 记录。覆盖率不是 100% 就是 P1,没有中间地带。
这里有个取舍要说清楚:确认点加多了,用户体验会崩。我的经验是只对不可逆动作拦截,可逆动作一律放行,并且给一次"本次会话内不再询问"的选项——但仅限于同一 traceId 的同类动作。放任用户勾选全局免确认,等于把能动性设计成了摆设。
监控与告警清单#
验收通过只说明版本可用,运行期还得盯。下面是我固定挂在 Prometheus + Alertmanager 上的指标集,阈值按前面表格的起点值设。
监控清单:关键指标、建议阈值与告警动作
| 指标 | 采集方式 | 建议阈值 | 告警动作 |
|---|---|---|---|
| 确认点覆盖率 | 审计日志按小时聚合 | <100% 立即触发 | P1,同时打开全局写保护开关 |
| 越权动作数 | 中间件拦截计数 | >0 | P1,人工介入排查 traceId |
| 意图偏离率 | 网关 diff 采样 5% | >2% 持续 30 分钟 | P2,回滚到上一版提示词 |
| 拒答不足率 | 对抗集每日回归 | >1.5% | P2,补规则 + 重跑 |
| 过度拒答率 | 良性集每日回归 | >3% | P2,检查是否过度收紧 |
| 不确定性披露率 | 每日抽检 200 条 | <90% | P3,进工单排期 |
| 引用可核验率 | 引用链回查任务 | <85% | P3,抽查无来源段落 |
| P95 延迟 | Nginx 1.26 access log | >基线 1.5 倍 | P3,检查上游与 PHP-FPM 队列 |
| 5xx 率 | 网关指标 | >0.5% | P2,触发重试并观察 5 分钟 |
只报平均值不报 P95 是我最反对的做法。均值 400ms 的接口完全可能 P95 是 6 秒,而那 5% 的用户贡献了大部分投诉。另外延迟指标要按端点和模型版本分开打标签,混在一起看没有任何诊断价值。
风险清单#
风险清单:风险 → 影响 → 规避方式
| 风险 | 影响 | 规避方式 |
|---|---|---|
| 价值写成标语,没有对应断言 | 验收走过场,版本间无法比较 | 每条价值至少一条可执行断言,进 CI |
| 只有对抗集,没有良性集 | 拒答不足被掩盖,上线后产品"变笨" | 两类样本池同时跑,阈值分开设 |
| 确认弹窗覆盖到可逆动作 | 用户形成闭眼点同意的习惯 | 白名单只放不可逆动作,其余放行 |
| 校验器无超时与重试 | 偶发网络抖动被当成模型失准 | 超时、重试、熔断三件套,重试只对连接层 |
| 密钥硬编码进脚本 | 泄露后面临滥用与账单风险 | 环境变量注入,定期轮换,CI 里做扫描 |
| 只看均值不看长尾 | 少数用户持续超时,投诉集中 | 报 P95 / P99,按端点和版本打标签 |
| 审计日志字段缺失 | 能动性断言无法取证 | traceId、actor、action、decision 四字段强制写 |
最后一条风险踩过一次。当时中间件拦截逻辑是对的,但日志里没写 actor,出问题后无法判断是哪个用户跳过了确认,只能全量回放请求。补字段的成本是半小时,回放的成本是两天。
顺带说一句,验收脚本的输入源可以混用。我习惯用 随机动漫壁纸接口 这类轻量 JSON 接口做结构性断言的对照样本,因为它字段固定、返回快,适合验证"断言器本身有没有写错"。断言器出错的概率比被测服务高得多,这一点在我做过的项目里几乎次次成立。更多这类拆解放在 API 文章中心。
总结:验收清单的寿命比模型版本长#
模型半年一换,验收清单可以不动。前提是清单里写的是行为边界,不是某个版本的表现。我现在的做法是把这份清单放在仓库根目录,和代码一起 review,改动需要评审——因为它定义了产品对"什么算合格"的态度,比任何一次提示词调优都重要。
落地顺序建议:先补能动性拦截(这条最容易被跳过,但出事最贵),再补对抗集和良性集,最后接 CI。三段可以并行,别等全齐了才动手。
适用范围:
- 有明确业务动作边界的产品,尤其是涉及支付、发信、数据删除的场景
- 用第三方模型 API 做中台或网关、需要做版本切换和质量比对的团队
- 已经有基础日志与监控体系,能采到审计事件的项目
不适用范围:
- 纯内容生成、无不可逆动作的轻量工具,能动性相关断言可直接略过
- 尚未建立用例集与基线数据的早期原型,先跑通链路再谈阈值
- 把模型输出直接面向公众且不做任何中间层的场景,本文的中间件方案无处落地
本站实测数据(近 24 小时)#
以下数据来自 API.PLUS 线上调用日志(统计时间 2026-10-30 05:06,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。