作者因prompt歧义导致同一commit重复调用免费模型三次,建议将prompt视为合同并做版本化管理。
上周二,我的 pipeline 在同一个 commit 上调用了一个免费模型三次。
每次输出看起来都很自信。
但每次都是错的。
模型并没有失败。是我的 prompt 失败了。
我差点去怪罪模型。然后我查看了 job 日志。同一条模糊的指令一次又一次地发出。每次重试都是在这个同样的错误上消耗新的 token 预算。
这让我问了一个简单的问题:为什么我在检查 prompt 之前就要关注模型输出?
看不见的成本不是模型本身。而是那通坏调用。
免费模型依然有成本。
等待响应的时间。
在一个本不该存在的 job 上消耗的 CI 分钟数。
一个需要人工 review 的错误判定。
以及对于到底是模型还是输入导致了失败的困惑。
MonkeyCode 提供免费模型访问和免费服务器选项。披露:本文是 MonkeyCode 产品推广的一部分。免费访问会让你忽略浪费。你不再按 token 付费,所以你不再注意到有多少调用是重复的。
修复方法不是换一个更好的模型。而是在模型之前加一个更小的门槛。
像源码一样对待 prompt,而不是像胶水一样
大多数团队会版本化管理他们的代码。
大多数团队不会用同样的重视程度来版本化管理他们的 prompt。
Prompt 是你与模型之间的一份契约。如果契约模糊,输出就模糊。如果契约悄悄变更,CI 结果就变得不可预测。
所以我在任何模型调用之前开始 lint prompt 文件。
Prompt 必须有一个 role(角色)。
Prompt 必须陈述 constraints(约束条件)。
Prompt 必须定义 output_format(输出格式)。
Prompt 必须足够长以保证具体性。
Prompt 不能依赖诸如"尽力就好"这类短语。
这不是语义审查。这是一个静态的契约检查。它先捕获那些廉价的错误。
一个可以在 GitLab CI 中运行的 prompt 契约 linter
这是我保存在仓库里的一个小 linter。
#!/usr/bin/env python3
import sys
from pathlib import Path
REQUIRED = ['role', 'constraints', 'output_format']
FORBIDDEN = ['do your best', 'be concise', 'be smart']
MIN_LEN = 200
MAX_LEN = 4000
def lint_prompt(path: Path):
text = path.read_text()
lower = text.lower()
problems = []
for section in REQUIRED:
if section not in lower:
problems.append(f'missing section: {section}')
for phrase in FORBIDDEN:
if phrase in lower:
problems.append(f'ambiguous instruction: {phrase}')
if len(text) < MIN_LEN:
problems.append('prompt is too short to specify a contract')
if len(text) > MAX_LEN:
problems.append('prompt is too long; split into sub-prompts')
return problems
def main(paths):
failed = False
for raw in paths:
path = Path(raw)
problems = lint_prompt(path)
print(f'{path.name}: {len(problems)} problems')
for problem in problems:
print(f' - {problem}')
failed = True
sys.exit(1 if failed else 0)
if __name__ == '__main__':
main(sys.argv[1:])
它运行很快。不需要调用模型。它告诉你这个 prompt 是否值得调用模型。
一个 prompt 文件可能是这样的:
# prompts/diff_review.md
role: Review small diffs for obvious regressions.
constraints:
- Do not invent code.
- Cite the exact file and line.
- Say unsure when the change is too large.
output_format:
verdict: ok | needs-work | unsure
reason: one sentence
Linter 检查的是结构,而不是正确性。这正是关键所在。
把 linter 放在模型调用之前
GitLab CI job 很小。
prompt-contract:
stage: test
image: python:3.12-slim
script:
- python scripts/prompt_contract.py $(find prompts -name '*.md')
rules:
- changes:
- prompts/**/*
如果 prompt 文件变更了,契约 job 就会运行。如果它失败了,pipeline 就会停止。不会发生任何模型调用。
Lint job 是确定性的。
Lint job 不消耗模型配额。
失败信息指向 prompt,而不是模型。
模型 job 只会看到通过了基本契约检查的 prompt。
免费服务器选项适合在哪里
你不需要在每个 runner 里安装 Python。
如果你有免费服务器选项,把 linter 部署成一个微小的 HTTP 端点。然后 CI job 变成一个请求。
prompt-contract-remote:
stage: test
script:
- curl --fail -X POST http://contract-check.internal/check --data-binary @prompts/diff_review.md
rules:
- changes:
- prompts/**/*
契约文件依然存在 repo 里。检查器运行在某个稳定的地方。CI job 保持轻量。
我在小项目里用本地版本。当 runner 镜像变得混乱,或者多个 pipeline 需要同样的检查时,我会用服务器版本。
两个版本都不会让模型变得更聪明。但两个版本都能阻止模型被问一个糟糕的问题。
这种方法的局限性
静态 prompt linting 不是魔法。
它捕获缺失的 section,而不是缺失的判断力。
一个 prompt 可能通过了检查,但依然对特定领域产生错误的答案。
一个哈希门禁能注意到 prompt 的变更,而不是模型行为的变更。
它不能替代人工 review——尤其是对可能影响生产环境的代码。
我不依赖这个 linter 来保证正确性。我依赖它来阻止明显的浪费。
如果你只有一个 prompt 而且很少调用,跳过这一步。
如果你需要在每个 pipeline 里无论如何都要有模型结果,跳过这一步。
如果你的 prompt 是运行时生成的而且从不存储在 repo 里,跳过这一步。
如果你的团队已经在其他工具里 review prompt,跳过这一步。
开销很小,但不是零。repo 里多一个文件依然是多一个需要维护的东西。
这周我改变了什么
我不再把模型调用当作工作单元。
现在的工作单元是 prompt 契约。
先 lint 契约。
然后再决定是否调用模型。
然后根据契约来解读输出。
这个顺序消除了我 CI 运行中大部分重复的失败调用。剩下的模型失败都是真正的问题,而不是模糊的指令。
免费模型不会修复一个糟糕的 prompt。但一个便宜的 lint 门槛可以防止你在午饭前把那个糟糕的 prompt 喂给模型五次。
你会先检查什么:prompt 还是输出?