核心摘要

Prompt CI/CD 是管理大模型应用行为变更的发布纪律。真正的部署单元不是一段提示词,而是由提示词、模型修订、参数、工具、检索依赖、输出 Schema、数据集、评估器和策略组成的不可变 Release Bundle。可靠流水线先验证确定性契约,再比较重复离线运行并校准 Judge 指标,按风险设置发布门禁,最后通过受控在线实验、监控和完整回滚管理剩余风险。

目录

  1. Prompt CI/CD 真正发布什么
  2. 建立不可变 Release Bundle
  3. 设计离线评测门禁
  4. 正确使用 LLM-as-Judge
  5. 实现配对回归门禁
  6. 区分离线 Eval 与在线实验
  7. 保护 CI 信任边界
  8. 监控并回滚完整发布包
  9. 常见失败模式与审查清单
  10. 常见问题
  11. 参考资料

核心要点

  • 发布的是行为包,不是文本文件。 脱离模型、工具、检索、Schema 和运行时策略,提示词文本不能唯一标识行为。
  • 先做确定性检查,再做概率评分。 Schema、工具允许列表、引用要求和授权规则不应交给 Judge 猜测。
  • 使用匹配样本和重复运行。 总平均分可能掩盖安全、语言、租户或任务切片的回归。
  • 所有自动 Judge 都要校准。 论文中的人工一致率不能直接成为另一个任务的准确率承诺。
  • 离线与在线证据分开。 离线回归降低上线前风险,在线实验估计真实流量下的业务影响。
  • 按不可变发布身份回滚。 只恢复提示词,可能继续使用引发事故的模型、索引、Schema 或工具版本。

Prompt CI/CD 真正发布什么

Prompt CI/CD 是对大模型应用行为变更进行持续集成、评测、发布、观测和回滚的工程流程。它把提示词版本管理从文本历史扩展为可审查的发布证据链。

「提示词即代码」是一个有用起点,但不是完整结论。源码 Diff 能解释编辑者改了什么,却不能证明哪套运行时行为被评测和部署。同一段提示词只要遇到以下任一变化,结果就可能明显不同:

行为依赖 变更示例 可能回归
提示词与模板 调整指令顺序或 Few-shot 示例 优先级或输出风格改变
模型修订 供应商别名切换到新快照 拒答或工具行为改变
解码配置 Temperature、Seed、Token 预算 方差增加或输出截断
工具契约 Schema、允许列表、授权策略 参数无效或产生危险副作用
检索系统 切块、Embedding、语料、索引 证据缺失或跨租户泄露
输出契约 JSON Schema 或解析器 下游解析失败
评测系统 数据集、Rubric、Judge、阈值 因测试变化而「看起来更好」
路由策略 语言、租户、Fallback、流量规则 不同用户收到不同发布包

因此,一个发布需要两种身份:

  1. 源码身份记录作者、评审和变更历史,通常由 Git 提供。
  2. 运行时身份记录真正产生并接受评测的完整行为包。

Git 很适合作为源码事实来源,但它本身不是实验注册表、制品仓库、部署控制器,也不能独立证明行为可复现。

建立不可变 Release Bundle

不可变发布清单应使用内容摘要、不可变修订或制品 ID 标识所有会改变行为的依赖。latest、浮动模型名和无版本向量索引可以用于选择候选版本,却不能作为发布身份。

json
{
  "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 用于发布决策,并标注真正影响风险的维度:

text
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 只跑一次,可能把随机波动误判为行为变化。对非确定性路径应:

  1. 让基线与候选使用相同 Case ID;
  2. 按预先声明的采样策略重复运行;
  3. 保留原始输出、延迟、Token、工具轨迹和评估器决策;
  4. 比较配对差值,而不是两个不相关的总平均值;
  5. 报告置信区间与分切片结果;
  6. 高影响变更证据不足时,进入人工裁决而非自动放行。

门禁规则必须在看到结果前确定。根据实验结果临时选择阈值,会把发布门禁变成结果谈判。

正确使用 LLM-as-Judge

LLM-as-Judge 可以扩展开放式输出的 Rubric 评分,但它的分数仍是另一个模型产生的测量值。MT-Bench 论文只在其模型、问题、提示和标注设置中报告较高人工一致率,同时明确记录了位置、冗长、自偏好和推理能力限制,不能把该数字泛化为通用准确率。

可辩护的 Judge 流程包括:

  1. 定义可观察 Rubric。 把「回答质量好」拆成事实支持、指令遵循、完整性、危险动作和引用有效性。
  2. 按任务选评分方式。 细微相对变化可用 Pairwise;存在可靠答案时可用 Reference-guided 评分。
  3. 盲化并随机化。 隐藏版本身份,随机调整答案顺序;交换顺序重跑以测量位置敏感性。
  4. 固定 Judge 身份。 记录模型修订、Judge Prompt、Rubric、解码参数、解析器和重试策略。
  5. 用人工标签校准。 在具有代表性的盲化样本上测量总体与分切片一致性和错误。
  6. 裁决实质分歧。 Judge 与人工冲突、证据不足、高风险 Case 交给有资质的评审者。
  7. 变化后重新校准。 Judge 模型、Rubric、数据分布或产品任务变化后,旧校准结论不再自动成立。

Judge 自报 Confidence 不是其判定正确的概率。若发布策略确实需要校准概率,应在独立人工标注集上估计,并持续监测漂移。

实现配对回归门禁

下面的无第三方依赖 Python 示例会验证不可变 Manifest,按 Case 与 Repeat 对齐基线和候选,计算配对 Bootstrap 置信区间,并按指标与切片执行策略。阈值由应用团队依据风险定义,代码没有内置「统一退化 5%」规则。

python
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()

应用自己的策略可以分别保护总体质量和高风险切片:

json
{
  "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 和在线实验回答不同问题。前者判断候选版本是否满足已知契约,并在受控数据上改善或保持行为;后者估计真实用户、流量、延迟和反馈回路中的影响。

flowchart LR A["不可变发布候选"] --> B["确定性契约"] B --> C["配对离线评测"] C --> D["Judge 校准与人工评审"] D --> E{"风险特定发布门禁"} E -->|"失败"| F["携带证据拒绝"] E -->|"通过"| G["受控灰度或 A/B 实验"] G --> H["SRM、主指标、护栏指标"] H --> I{"统计与实际价值决策"} I -->|"推广"| J["受控扩量"] I -->|"停止"| K["完整发布包回滚"] J --> L["持续监控"] L --> K

预先声明实验契约

用户暴露前应记录:

  • 实验单元和稳定分流键;
  • 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 的不可变引用。

安全流水线应拆分为:

  1. 无特权 PR Job
    • 仓库只读;
    • 不提供模型、生产或部署 Secret;
    • 只跑 Schema、Manifest、静态 Fixture 和确定性测试;
    • 上传经过严格校验的结果制品。
  2. 受批准评测 Job
    • 从受保护基线修订运行可信评估器;
    • 把候选制品当数据读取,绝不执行;
    • 使用短期、最小权限凭证;
    • 产出可证明的报告,不修改仓库正文。
  3. 部署 Job
    • 高影响发布需要受保护 Environment 审批;
    • 只接受已批准的 Release Digest;
    • 部署自动化不能同时修改并批准自己的规则。
yaml
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 调用、重试、存储和人工复核。先跑廉价确定性检查,按变更影响选择评测套件,只缓存不可变输入输出,把昂贵评测留给通过前置门禁的候选。不要对外承诺统一单次成本。

参考资料