迁移模型前,用契约测试替代凭感觉
案例为依赖漏洞摘要任务搭建三文件测试框架,把相同 npm audit 输入交给两个模型后端并比较结果。任何输出契约失败都会阻断 CI,同时用每份合格摘要消耗的词元数衡量效率。
AI 深度解读
把定时任务从付费模型迁到免费层,最容易漏掉的成本,是输出格式偶尔出错后谁来收拾。作者给一个依赖漏洞摘要任务做了三文件测试工具:让两个后端处理相同输入,用校验结果决定能否迁移,再比较每条合格摘要消耗的 token。
这套做法值得借鉴,但文章没有公布实测成绩,而且示例校验器没有完全兑现提示词里的要求。读它最有用的角度,是学习如何建立迁移门槛,同时看清这个门槛还漏了什么。
换模型之前,先定义什么输出能交付
作者的任务每 30 分钟扫描几个仓库,把 npm audit --json 转成发布负责人能快速阅读的漏洞摘要,再汇总发到聊天频道。虽然文章称它为夜间任务,实际调度频率是每半小时一次。
这个任务已经用付费凭证运行了几周。作者想确认免费模型层能否接手,但判断依据不能只是试几条输入、看起来正常。
他们把目标收得很窄:只验证指定后端能否稳定完成这一项任务,不做通用模型排名。输出必须是一个 JSON 对象,包含 repo、package、severity、summary、action 五个字段;严重程度只能取 low、moderate、high、critical;摘要最多 30 个词,行动建议最多 15 个词且使用祈使句。如果 fixAvailable 为 false 或缺失,行动建议必须以 no fix available 开头。
这些要求就是输出契约。它回答的是:程序拿到结果后,能不能直接往下处理。
模型迁移的第一道门槛,应当是输出能否通过业务校验。
作者准备了约 12 条经过裁剪的固定输入,覆盖有修复方案、没有修复方案、较长的 via 链,以及包名大小写异常等情况。示例包括 left-pad 的 high 漏洞、CVSS 7.5、修复版本 1.3.1,以及没有修复方案的 moderate 漏洞 EigenDependency。
固定样例入库,测试时不抓实时数据,才能让前后两次结果具有可比性。不过,这些测试数据只覆盖作者选中的形态,并不代表真实漏洞的全部情况。
三个文件,怎样把迁移变成一道门槛
工具由 adapter.py、cases.json、parity.py 组成,HTTP 客户端只使用 Python 标准库,方便在没有额外依赖的 CI 容器里运行。
adapter.py 用 Summarizer 协议统一调用接口,ChatCompletions 对接 OpenAI-compatible 的 /chat/completions。请求设置 temperature: 0,默认通过 response_format: {'type': 'json_object'} 请求 JSON 输出;后端不支持时,可以用对应的 _JSON_MODE=0 关闭。
这里必须分清:请求 JSON 模式,只是在请求端表达期望,不能替代收到结果后的校验。
客户端默认超时为 45 秒,最多重试 2 次,仅针对 HTTP 429、500、502、503,等待时间按 min(2 ** retries, 8) 计算。每次调用记录输入和输出 token、延迟及重试次数。重试增加可以提示服务承压,但示例报告并没有把重试次数打印出来。
下面这段负责判断输出是否合格,直接影响 CI 是否放行:
REQUIRED = ('repo', 'package', 'severity', 'summary', 'action')
SEVERITIES = {'low', 'moderate', 'high', 'critical'}
def validate(text: str) -> list[str]:
'''Return contract violations. An empty list means the output is usable.'''
try:
obj = json.loads(text)
except json.JSONDecodeError:
return ['not valid json']
if not isinstance(obj, dict):
return ['not a json object']
problems = [f'missing key {k}' for k in REQUIRED if k not in obj]
if obj.get('severity') not in SEVERITIES:
problems.append(f'bad severity {obj.get("severity")!r}')
if len(str(obj.get('summary', '')).split()) > 30:
problems.append('summary too long')
return problems
要重点看构建 problems 的那一行:它只检查必需字段是否缺失,没有禁止额外字段。后面的检查也没有覆盖字段类型、action 的长度与前缀,以及输出与原始漏洞是否一致。split() 按空白计词,对中文长度也不能直接套用。
运行方式很简单,凭证、模型 ID 和地址全部放在环境变量中:
export PAID_BASE_URL=... PAID_MODEL=... PAID_API_KEY=...
export MC_BASE_URL=... MC_MODEL=... MC_API_KEY=...
python3 parity.py cases.json
echo "exit=$?"
需要注意 echo "exit=$?" 展示的是前一条命令的退出状态。若把这几行直接作为 CI 脚本,必须确保流水线确实传播 Python 的失败状态,不能只打印数字后继续成功结束。
运行器还有一个漏洞:缺少某组环境变量时会跳过该后端,两组都缺失也可能返回成功。正式迁移前,应把缺配置当作失败,否则绿色检查并不代表两个后端都测过。
免费层省下的成本,要按合格结果重新算
作者最看重的指标是:
每条合格摘要的 token = 全部调用报告的 token 总量 ÷ 合格摘要数量
它把不合格输出消耗的 token 也计入成本。模型即使每次回答很短,只要经常需要重跑,就未必省事。原文举的例子是:某后端 token 用量减半,却每五条失败一条。这是解释指标的假设,不是公布的实测结果,也不足以直接证明它比另一后端更贵。
这个指标仍有边界。不同模型的 token 单价可能不同,免费额度和限流也不相同;示例代码没有统计失败 HTTP 请求可能产生的计费消耗。后端不返回 usage 时,它还会把 token 默认为 0,得到过分好看的报告。
作者认为,免费模型访问最适合提示词迭代,例如围绕 12 条固定样例,一个下午重跑 20 次;免费服务器则适合放置运行 cron 的小任务。文章将 MonkeyCode 作为候选,也披露了内容属于其产品推广。免费模型和免费服务器的可用性是运营方当时提供的信息,文章没有给出可核验的额度、容量或性能成绩。
部署部分用 */30 * * * * 每半小时触发,通过 flock -n .run.lock 防止重叠运行,并把日志追加到 logs/run.log。不过,示例调度的是测试门槛,完整的摘要汇总和聊天发送程序并未展示。
我的判断
这篇最有价值的建议,是把提示词和输出校验一起纳入回归测试。相比人工看几条回答,固定输入、自动判错、失败阻断 CI,更容易发现模型切换带来的格式回退。
但示例目前只能证明「部分格式要求通过」,不能证明摘要内容正确。它没有检查模型是否编造修复建议,也没有检查包名和严重程度是否与输入对应;提示词要求 repo,示例传入的 audit 又没有提供仓库信息。字段齐全,仍可能装着错误答案。
另外,两个后端实际上是分别接受同一校验器检查,代码没有实现逐项结果差异比较。原文提到决策表和阈值,却没有展示具体表格,也没有报告通过率、延迟或 token 实测值。因此,免费层是否适合这个任务,仍然是经验判断,不能据此给 MonkeyCode 或其他后端背书。
作者希望测试在一分钟内完成,但调用按样例、按后端串行执行,配合 45 秒超时和重试,并无一分钟内结束的保证。对延迟敏感、要求固定模型版本或容量保障的任务,作者也明确建议跳过这条路线。
能拿来做什么
- 给前端或 Node 项目的依赖摘要任务建立固定样例集,把无修复方案、长
via链和异常包名纳入回归;每次修改提示词都运行同一组输入。 - 把业务要求逐条写进校验器,补齐字段类型、额外字段、行动建议前缀,以及包名和严重程度与输入的一致性检查,并为
repo提供明确来源。 - 在 CI 中让缺配置、缺 token 统计和校验失败得到明确处理,保留失败样例及重试信息,避免一次绿色结果掩盖未执行的测试。
- 迁移时同时比较合格率、每条合格摘要的 token 和延迟,再核对免费额度、数据处理条款及容量要求;后端或提示词变更后重新跑门槛。