免费模型端点可能在返回200成功的同时悄然改变路由、量化或注入系统提示词,导致输出与审查时不一致。解决方案是用确定性单token提示词生成基准哈希值并纳入CI,漂移时自动失败。
提示词没变。输出变了。
这就是普通 CI 任务永远捕获不到的失败模式。你固定了一个免费模型端点,它返回 200,构建变绿,而背后的模型悄然发生了变化。
问题:伪装成固定依赖的运动靶心
就绪检查回答一个问题:端点能否响应?Token 看门狗任务回答另一个问题:运行是否保持便宜?两者都有用。两者都检测不到改变了路由、解码参数、量化或注入系统提示词、却仍然快速响应的模型。
硬故障是喧闹的。静默漂移不是。端点可以持续返回有效的 JSON 和有效的文本,而你所依赖的行为已经转移了。
你需要捕获的失败不是模型宕机。而是模型不再是上周审查的那个模型。
一个指纹,而不是另一个监控
这个任务不问"它还活着吗?",而是问"它是否产生了与已提交基线相同的确定性输出?"
你发送一小套无聊的、单一 token 的提示词。你对归一化后的响应做哈希。你把这些哈希提交到仓库。每次 CI 运行,任务会比较哈希值,并在发生变化时 fail closed。
指纹不是基准测试。它是一个绊线。
有意保持提示词无聊。
单一 token 或接近单一 token 的回答
无日期、时间、网络查询或私人数据
在端点支持的情况下使用 temperature 0
足够的提示词来捕获解码或系统提示词的变化,但不足以变成一个评估工具
下面是一个发出 JSON 指纹的小 Python 脚本。
#!/usr/bin/env python3
'''Compute a deterministic fingerprint for a model endpoint.'''
import hashlib
import json
import os
import urllib.request
PROMPTS = [
'Reply with only the word: blue',
'Reply with only the next prime after 7:',
'Reply with only the French word for yes:',
'Reply with only the opposite of north:',
]
def call(endpoint, prompt, temperature=0):
payload = json.dumps({
'prompt': prompt,
'temperature': temperature,
'max_tokens': 8,
}).encode()
req = urllib.request.Request(
endpoint,
data=payload,
headers={'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def normalize(text):
return ' '.join(text.strip().lower().split())
def main():
endpoint = os.environ['MODEL_ENDPOINT']
out = {}
for prompt in PROMPTS:
raw = call(endpoint, prompt)
# Change this path to wherever your endpoint puts generated text.
text = raw.get('choices', [{}])[0].get('text', '')
norm = normalize(text)
out[prompt] = {
'normalized': norm,
'sha256': hashlib.sha256(norm.encode()).hexdigest(),
}
print(json.dumps(out, indent=2, sort_keys=True))
if __name__ == '__main__':
main()
请求体故意做得很小。有些端点期望 chat-messages 数组,有些期望 max_new_tokens 而不是 max_tokens,还有些忽略 temperature。运行前调整 raw.get() 中的路径。这不绑定特定模型或供应商。
将实时输出与已提交基线比较
在人工审查后生成一次基线:
python fingerprint.py > baseline.json
然后在 CI 中运行比较。
#!/usr/bin/env python3
'''Fail closed when a live fingerprint no longer matches baseline.'''
import json
import sys
baseline = json.load(open('baseline.json'))
actual = json.load(sys.stdin)
failed = False
for prompt, record in baseline.items():
expected = record['sha256']
found = actual.get(prompt, {}).get('sha256')
if found != expected:
failed = True
print(f'DRIFT: {prompt}')
print(f' expected {expected}')
print(f' found {found}')
if failed:
sys.exit(1)
print('fingerprint stable')
这个任务很小。它发送几个 token 并在指纹发生变化时错误退出。
stages:
- check
model-fingerprint:
stage: check
image: python:3-slim
variables:
MODEL_ENDPOINT: $MODEL_ENDPOINT
script:
- python fingerprint.py > actual.json
- python compare.py baseline.json < actual.json
artifacts:
when: always
paths:
- actual.json
expire_in: 1 day
让失败有用
一个会报的指纹比没有指纹更糟糕,如果它很吵的话。
将 actual.json 保存为产物,以便值班人员可以检查原始输出。
打印第一个发生漂移的具体提示词。
只能通过显式提交来更改基线,绝不要在 CI 中静默修改。
在哈希之前比较归一化后的文本。单条标点差异应该在日志中可见。
MonkeyCode 的位置
披露:本文作为 MonkeyCode 产品推广的一部分而准备。
MonkeyCode 的免费模型访问很重要,因为这个工作流需要一个可以监控的端点,而无需额外计费。免费服务器选项很重要,因为这个任务足够小,可以作为定时检查来运行,而不是作为付费 runner 任务。我保持脚本通用,因为绊线不应该绑定到一种请求格式。我这里不假设特定的配额、模型名称或硬件保证;那些在依赖之前需要验证。
精确哈希值故意设置得很窄。
精确匹配是脆弱的。即使新行为更好,一个标点修正也可能触发任务。
指纹检测的是变化,不是质量。它不能说新输出是否更差,只能说它不同。
如果启用了采样,哈希值会无缘无故地变化。锁定 temperature 或使用确定性模式。
基线必须经过审查。否则漂移变成噪音,任务会被禁用。
这不是安全或隐私边界。提示词必须安全地支持第三方处理。
当工作负载是长文本、创造性或改写风格输出时,不要使用精确指纹。误报率会太高。
如果没有人员维护基线,就跳过它。没人重置的绊线只是一盏人们学会忽略的红灯。
如果你需要语义等价而不是精确措辞,也跳过它。这种方法关注字面响应,而不是含义。
从四个无聊的提示词开始
运行指纹一次。检查归一化字段。只有在答案看起来正确时才提交基线。然后让下一次运行在任何变化时 fail closed。
目的不是阻止模型更新。是让它们在到达生产环境之前可见。