核心摘要
Prompt CI/CD 是管理大模型应用行为变更的发布纪律。真正的部署单元不是一段提示词,而是由提示词、模型修订、参数、工具、检索依赖、输出 Schema、数据集、评估器和策略组成的不可变 Release Bundle。可靠流水线先验证确定性契约,再比较重复离线运行并校准 Judge 指标,按风险设置发布门禁,最后通过受控在线实验、监控和完整回滚管理剩余风险。
目录
- Prompt CI/CD 真正发布什么
- 建立不可变 Release Bundle
- 设计离线评测门禁
- 正确使用 LLM-as-Judge
- 实现配对回归门禁
- 区分离线 Eval 与在线实验
- 保护 CI 信任边界
- 监控并回滚完整发布包
- 常见失败模式与审查清单
- 常见问题
- 参考资料
核心要点
- 发布的是行为包,不是文本文件。 脱离模型、工具、检索、Schema 和运行时策略,提示词文本不能唯一标识行为。
- 先做确定性检查,再做概率评分。 Schema、工具允许列表、引用要求和授权规则不应交给 Judge 猜测。
- 使用匹配样本和重复运行。 总平均分可能掩盖安全、语言、租户或任务切片的回归。
- 所有自动 Judge 都要校准。 论文中的人工一致率不能直接成为另一个任务的准确率承诺。
- 离线与在线证据分开。 离线回归降低上线前风险,在线实验估计真实流量下的业务影响。
- 按不可变发布身份回滚。 只恢复提示词,可能继续使用引发事故的模型、索引、Schema 或工具版本。
Prompt CI/CD 真正发布什么
Prompt CI/CD 是对大模型应用行为变更进行持续集成、评测、发布、观测和回滚的工程流程。它把提示词版本管理从文本历史扩展为可审查的发布证据链。
「提示词即代码」是一个有用起点,但不是完整结论。源码 Diff 能解释编辑者改了什么,却不能证明哪套运行时行为被评测和部署。同一段提示词只要遇到以下任一变化,结果就可能明显不同:
| 行为依赖 | 变更示例 | 可能回归 |
|---|---|---|
| 提示词与模板 | 调整指令顺序或 Few-shot 示例 | 优先级或输出风格改变 |
| 模型修订 | 供应商别名切换到新快照 | 拒答或工具行为改变 |
| 解码配置 | Temperature、Seed、Token 预算 | 方差增加或输出截断 |
| 工具契约 | Schema、允许列表、授权策略 | 参数无效或产生危险副作用 |
| 检索系统 | 切块、Embedding、语料、索引 | 证据缺失或跨租户泄露 |
| 输出契约 | JSON Schema 或解析器 | 下游解析失败 |
| 评测系统 | 数据集、Rubric、Judge、阈值 | 因测试变化而「看起来更好」 |
| 路由策略 | 语言、租户、Fallback、流量规则 | 不同用户收到不同发布包 |
因此,一个发布需要两种身份:
- 源码身份记录作者、评审和变更历史,通常由 Git 提供。
- 运行时身份记录真正产生并接受评测的完整行为包。
Git 很适合作为源码事实来源,但它本身不是实验注册表、制品仓库、部署控制器,也不能独立证明行为可复现。
建立不可变 Release Bundle
不可变发布清单应使用内容摘要、不可变修订或制品 ID 标识所有会改变行为的依赖。latest、浮动模型名和无版本向量索引可以用于选择候选版本,却不能作为发布身份。
{
"releaseId": "support-rag/sha256:4e89c6...",
"sourceCommit": "9f5cb7...",
"prompt": {
"artifact": "prompts/support-answer.json",
"sha256": "c8f4a2..."
},
"model": {
"provider": "provider-a",
"revision": "immutable-model-revision",
"parameters": {
"temperature": 0.2,
"maxOutputTokens": 900
}
},
"tools": {
"schemaSha256": "2a70b1...",
"policySha256": "79945d..."
},
"retrieval": {
"corpusSnapshot": "support-docs-184",
"indexBuild": "index-2026-08-09-03"
},
"output": {
"schemaSha256": "b5cb3a...",
"parserRevision": "parser/7d34fe..."
},
"evaluation": {
"datasetRevision": "support-eval/31",
"runnerRevision": "eval-runner/8c18a0...",
"judgeRevision": "judge-rubric/12"
}
}
清单通过 Schema 校验后,应以规范化序列化方式计算摘要并写入不可变制品库。部署指针由独立控制面指向摘要。回滚时把指针恢复到上一个已批准摘要,而不是临时拼装一个「大致相同」的旧版本。
评审时可以用代码差异对比工具同时检查提示词、策略和 Manifest 变更,并用 JSON 格式化工具阅读生成的 Manifest 或 Gate Report;两者都不能替代 Schema 校验与摘要验证。
先做变更影响盘点
昂贵评测开始前,先判断哪些行为依赖发生变化:
| 变更 | 最小证据范围 |
|---|---|
| 只改提示词文字 | 指令遵循、任务质量、安全和格式切片 |
| 更换模型修订 | 完整行为、延迟、成本、安全和工具调用套件 |
| 修改工具 Schema 或策略 | 参数校验、授权、副作用和拒绝用例 |
| 修改检索快照 | 召回、Grounding、时效、租户隔离和引用 |
| 修改输出 Schema 或解析器 | Schema 语料、兼容性 Fixture、下游消费者测试 |
| 修改评估器或数据集 | 重新建立基线,不能把定义变化前后的分数直接对比 |
变更影响盘点既避免每次文档调整都跑全量昂贵评测,也避免用一组提示词用例批准模型或索引迁移。
设计离线评测门禁
离线门禁应组合确定性不变量、任务指标、风险切片、重复采样和显式发布策略。OpenAI 的评测最佳实践同样强调任务特定数据、接近生产的分布、持续评测和人工校准,而不是凭感觉验收。
第一层:确定性契约
有机器确定答案的问题,应使用普通程序验证:
- 输出能否解析并通过目标 Schema;
- 必填字段和引用是否存在;
- 工具名与参数是否符合允许列表;
- 授权是否由模型之外的代码执行;
- 延迟、Token 和调用预算是否超出策略;
- 日志是否泄露 Secret 或个人信息;
- 已知 Prompt Injection Fixture 能否绕过运行时控制。
这些测试应失败即关闭。Judge 不能用一个高分覆盖缺失的授权检查,也不能放行格式错误的交易载荷。
第二层:任务特定回归集
评测数据应来自接近生产的请求、历史事故、领域专家用例、对抗输入和已知边界条件。保留独立 Holdout 用于发布决策,并标注真正影响风险的维度:
task: refund_explanation
locale: zh-CN
risk: financial
tenant_policy: standard
input_source: production_sample
expected_contract: cited_answer_without_mutation
Golden Dataset 不存在通用正确数量。小集合可以完整覆盖有限的 Schema 不变量,却不能估计细微产品效应;大规模随机样本可以估计平均值,却可能完全漏掉低频高影响安全路径。数据是否足够,取决于覆盖范围、基线率或方差、最小可检测效应、统计功效、分配方式、重复调用,以及必须保护的风险切片。
第三层:重复随机运行
每个 Case 只跑一次,可能把随机波动误判为行为变化。对非确定性路径应:
- 让基线与候选使用相同 Case ID;
- 按预先声明的采样策略重复运行;
- 保留原始输出、延迟、Token、工具轨迹和评估器决策;
- 比较配对差值,而不是两个不相关的总平均值;
- 报告置信区间与分切片结果;
- 高影响变更证据不足时,进入人工裁决而非自动放行。
门禁规则必须在看到结果前确定。根据实验结果临时选择阈值,会把发布门禁变成结果谈判。
正确使用 LLM-as-Judge
LLM-as-Judge 可以扩展开放式输出的 Rubric 评分,但它的分数仍是另一个模型产生的测量值。MT-Bench 论文只在其模型、问题、提示和标注设置中报告较高人工一致率,同时明确记录了位置、冗长、自偏好和推理能力限制,不能把该数字泛化为通用准确率。
可辩护的 Judge 流程包括:
- 定义可观察 Rubric。 把「回答质量好」拆成事实支持、指令遵循、完整性、危险动作和引用有效性。
- 按任务选评分方式。 细微相对变化可用 Pairwise;存在可靠答案时可用 Reference-guided 评分。
- 盲化并随机化。 隐藏版本身份,随机调整答案顺序;交换顺序重跑以测量位置敏感性。
- 固定 Judge 身份。 记录模型修订、Judge Prompt、Rubric、解码参数、解析器和重试策略。
- 用人工标签校准。 在具有代表性的盲化样本上测量总体与分切片一致性和错误。
- 裁决实质分歧。 Judge 与人工冲突、证据不足、高风险 Case 交给有资质的评审者。
- 变化后重新校准。 Judge 模型、Rubric、数据分布或产品任务变化后,旧校准结论不再自动成立。
Judge 自报 Confidence 不是其判定正确的概率。若发布策略确实需要校准概率,应在独立人工标注集上估计,并持续监测漂移。
实现配对回归门禁
下面的无第三方依赖 Python 示例会验证不可变 Manifest,按 Case 与 Repeat 对齐基线和候选,计算配对 Bootstrap 置信区间,并按指标与切片执行策略。阈值由应用团队依据风险定义,代码没有内置「统一退化 5%」规则。
from __future__ import annotations
import hashlib
import json
import random
from pathlib import Path
from statistics import fmean
from typing import Any
REQUIRED_MANIFEST_PATHS = (
("prompt", "sha256"),
("model", "revision"),
("tools", "schemaSha256"),
("tools", "policySha256"),
("retrieval", "indexBuild"),
("output", "schemaSha256"),
("evaluation", "datasetRevision"),
("evaluation", "runnerRevision"),
)
def nested_value(document: dict[str, Any], path: tuple[str, ...]) -> Any:
value: Any = document
for key in path:
if not isinstance(value, dict) or key not in value:
raise ValueError(f"missing manifest field: {'.'.join(path)}")
value = value[key]
return value
def release_digest(manifest: dict[str, Any]) -> str:
for path in REQUIRED_MANIFEST_PATHS:
nested_value(manifest, path)
payload = json.dumps(
manifest, sort_keys=True, separators=(",", ":"), ensure_ascii=False
).encode("utf-8")
return "sha256:" + hashlib.sha256(payload).hexdigest()
def paired_differences(
baseline: list[dict[str, Any]],
candidate: list[dict[str, Any]],
metric: str,
slice_name: str,
) -> list[float]:
def select(rows: list[dict[str, Any]]) -> dict[tuple[str, int], float]:
selected = {}
for row in rows:
if row["slice"] == slice_name:
selected[(row["caseId"], row["repeat"])] = float(row[metric])
return selected
left, right = select(baseline), select(candidate)
if left.keys() != right.keys() or not left:
raise ValueError(f"unpaired or empty results for {metric}/{slice_name}")
return [right[key] - left[key] for key in sorted(left)]
def bootstrap_interval(
values: list[float], confidence: float, samples: int, seed: int
) -> tuple[float, float]:
rng = random.Random(seed)
estimates = sorted(
fmean(rng.choice(values) for _ in values) for _ in range(samples)
)
tail = (1.0 - confidence) / 2.0
low = estimates[int(tail * (samples - 1))]
high = estimates[int((1.0 - tail) * (samples - 1))]
return low, high
def evaluate_gate(
baseline: list[dict[str, Any]],
candidate: list[dict[str, Any]],
policy: dict[str, Any],
) -> dict[str, Any]:
checks = []
for rule in policy["rules"]:
diffs = paired_differences(
baseline, candidate, rule["metric"], rule["slice"]
)
low, high = bootstrap_interval(
diffs,
confidence=policy["confidence"],
samples=policy["bootstrapSamples"],
seed=policy["seed"],
)
passed = (
low >= rule["minimumDelta"]
if rule["direction"] == "higher"
else high <= rule["maximumDelta"]
)
checks.append(
{
"metric": rule["metric"],
"slice": rule["slice"],
"pairs": len(diffs),
"meanDelta": fmean(diffs),
"interval": [low, high],
"passed": passed,
}
)
return {"passed": all(item["passed"] for item in checks), "checks": checks}
def main() -> None:
manifest = json.loads(Path("release-manifest.json").read_text())
baseline = json.loads(Path("baseline-results.json").read_text())
candidate = json.loads(Path("candidate-results.json").read_text())
policy = json.loads(Path("release-policy.json").read_text())
report = {
"releaseDigest": release_digest(manifest),
**evaluate_gate(baseline, candidate, policy),
}
Path("gate-report.json").write_text(
json.dumps(report, indent=2, ensure_ascii=False) + "\n"
)
if not report["passed"]:
raise SystemExit("release gate failed; inspect gate-report.json")
if __name__ == "__main__":
main()
应用自己的策略可以分别保护总体质量和高风险切片:
{
"confidence": 0.95,
"bootstrapSamples": 10000,
"seed": 20260809,
"rules": [
{
"metric": "taskScore",
"slice": "all",
"direction": "higher",
"minimumDelta": -0.01
},
{
"metric": "unsafeActionRate",
"slice": "financial",
"direction": "lower",
"maximumDelta": 0.0
}
]
}
Bootstrap 区间不能修复偏置数据、无效 Judge、重复用户相关性或统计功效不足。门禁报告只是一份证据,不是「发布在所有场景下都安全」的证明。
区分离线 Eval 与在线实验
离线 Eval 和在线实验回答不同问题。前者判断候选版本是否满足已知契约,并在受控数据上改善或保持行为;后者估计真实用户、流量、延迟和反馈回路中的影响。
预先声明实验契约
用户暴露前应记录:
- 实验单元和稳定分流键;
- Control 与 Treatment 的发布摘要;
- 目标人群与排除条件;
- 一个主指标及最小有意义效应;
- 安全、质量、延迟、成本与投诉护栏;
- 样本量或序贯设计假设;
- 暴露周期与停止规则;
- Sample Ratio Mismatch 检查;
- 多重比较处理方式;
- 回滚负责人和 Kill Switch。
p < 0.05 不能独立成为全量规则。决策还需要效应量、置信区间、实际业务意义、随机化完整性、护栏可接受,以及排除新奇效应、季节性、Carryover 或重复用户影响。安全不变量是约束,不是可以拿 Engagement 交换的指标。
高影响安全或授权变更应先进入隔离环境、模拟、红队或 Shadow Traffic。不能为了「看看是否会违反安全契约」而主动把候选版本暴露给真实用户。
保护 CI 信任边界
Prompt CI 会处理大量不可信输入:PR 代码、提示词、检索 Fixture、模型输出、评论、Commit Message 和生成制品。模型凭证与部署权限不能和这些输入共享信任边界。
GitHub 的安全使用指南要求缩小 Token 权限,警告特权 Trigger 签出不可信 PR 代码的风险,并明确指出完整 Commit SHA 才是 Action 的不可变引用。
安全流水线应拆分为:
- 无特权 PR Job
- 仓库只读;
- 不提供模型、生产或部署 Secret;
- 只跑 Schema、Manifest、静态 Fixture 和确定性测试;
- 上传经过严格校验的结果制品。
- 受批准评测 Job
- 从受保护基线修订运行可信评估器;
- 把候选制品当数据读取,绝不执行;
- 使用短期、最小权限凭证;
- 产出可证明的报告,不修改仓库正文。
- 部署 Job
- 高影响发布需要受保护 Environment 审批;
- 只接受已批准的 Release Digest;
- 部署自动化不能同时修改并批准自己的规则。
name: prompt-contracts
on:
pull_request:
paths:
- "prompts/**"
- "evals/**"
- "release-policy.json"
permissions:
contents: read
jobs:
deterministic-contracts:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check trusted repository workspace
run: |
test -f scripts/validate_release.py
python scripts/validate_release.py --no-network
该最小示例故意不使用 Secret,也不授予写权限。若加入 Checkout、Setup、Upload 或第三方 Action,应把每个引用固定到经过核验的完整 Commit SHA。不要把提示词或 PR 元数据直接拼进 Shell 或 $GITHUB_OUTPUT;应通过文件或带引号的环境数据传递,并校验大小和 Schema。
监控并回滚完整发布包
通过 CI 的版本仍可能因为分布漂移、供应商变化、检索时效、工具故障或意外用户行为而失败。生产监控必须保留 Release Digest 和用于区分故障来源的证据。
至少记录:
- 发布与实验分组身份;
- Schema 和工具调用有效率;
- 任务成功与 Grounding 指标;
- 安全与授权违规;
- 延迟、Token、重试和成本;
- Fallback 与拒答率;
- 用户投诉和合格人工复核;
- 按语言、租户、任务和风险切片统计的指标;
- 模型、检索和工具依赖健康度。
回滚是跨依赖事务。推广前应验证上一发布包仍可部署、索引和模型修订仍存在、下游 Schema 兼容、迁移可逆或前向兼容、路由缓存能够收敛。一次真实回滚演练,比文档中写有「支持回滚」更有证明力。
更完整的生产遥测可参考 AI Agent 可观测性指南,运行时路由与 Fallback 隔离可参考 LLM Gateway 架构设计。Prompt Injection 防御仍属于运行时信任模型;回归 Fixture 只能补充,不能替代提示注入攻击与防御指南中的控制。
常见失败模式与审查清单
| 失败模式 | 为什么失败 | 正确控制 |
|---|---|---|
| 只 Hash 提示词 | 其他依赖仍会改变行为 | 对规范化完整 Manifest 计算摘要 |
| 只看总平均分 | 关键切片可能回归 | 配对 Case 并单独门禁风险切片 |
| 套用固定样本数 | 精度与覆盖需求不同 | 从风险、方差、效应和功效设计 |
| 复制论文 Judge 一致率 | 一致率依赖具体设置 | 用代表性人工标签校准 |
看到 p < 0.05 就全量 |
忽略效应量和护栏 | 预先声明完整实验契约 |
| 给 PR Job 模型 Secret | 不可信代码可窃取凭证 | 分离无特权与受批准 Job |
回滚 latest 提示词 |
模型、索引、Schema 可能仍已变化 | 恢复不可变发布包摘要 |
| 失败后重生成基线 | 比较目标被抹除 | 保留已批准基线和原始证据 |
批准发布前,评审者应能回答:
- 被测试的是哪一个精确发布包,能否重建?
- 哪些确定性契约绝不能回归?
- 数据集是否覆盖生产任务与高影响切片?
- 基线和候选是否在相同条件下配对?
- Judge 如何校准、版本化和检查偏差?
- 阈值和在线停止规则是否在看到结果前确定?
- CI 是否可能泄露 Secret 或执行不可信制品?
- 什么观测会触发回滚,完整回滚是否演练过?
常见问题
Prompt CI/CD 到底应该版本化哪些内容?
应版本化所有会改变可观察行为的依赖:提示词与模板、模型修订、解码配置、工具与策略、检索快照、输出 Schema 与解析器、数据集、评估器和路由策略。Git 记录源码历史,规范化 Manifest Digest 标识运行时发布。
提示词回归测试需要多少条样本?
没有固定数字适用于所有任务。确定性契约覆盖、低频安全事件、平均质量估计和在线产品效应需要不同数据。团队要声明目标效应、不确定性、功效、重复次数、切片和停止规则,并单独证明高影响 Case 的覆盖。
LLM-as-Judge 可以直接作为发布门禁吗?
它可以提供门禁证据,但前提是具有明确 Rubric、盲化随机比较、独立人工标签、分切片分歧分析、固定 Judge 修订和人工裁决。Judge 的分数或自报 Confidence 不能作为 Ground Truth。
离线 Eval 能代替 Prompt A/B 测试吗?
不能。离线 Eval 在不影响用户的情况下发现已知回归;在线实验估计真实流量影响。候选必须先满足确定性安全和兼容性门禁。线上胜出不能抵消授权或安全不变量失败。
如何控制 Prompt CI/CD 成本?
成本应按真实套件计算:Case 数、重复次数、输入输出 Token、Judge 调用、重试、存储和人工复核。先跑廉价确定性检查,按变更影响选择评测套件,只缓存不可变输入输出,把昂贵评测留给通过前置门禁的候选。不要对外承诺统一单次成本。