核心摘要

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 不变量,却不能估计细微产品效应;大规模随机样本可以估计平均值,却可能完全漏掉低频高影响安全路径。数据是否足够,取决于覆盖范围、基线率或方差、最小可检测效应、统计功效、分配方式、重复调用,以及必须保护的风险切片。

Agent 变更还必须明确证据单位。Anthropic 的 Agent 评测指南 区分 Task、每次随机 Trial、执行 Transcript、最终环境 Outcome,以及评测 Harness 与被测 Agent Harness。看起来成功的 Transcript 不等于 Outcome 成功,一次幸运 Trial 也不能证明版本稳定。比较 Release Bundle 前,应把这四类身份完整写入结果集。

第三层:重复随机运行

每个 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 不是其判定正确的概率。若发布策略确实需要校准概率,应在独立人工标注集上估计,并持续监测漂移。

实现配对回归门禁

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

go
package main

import (
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"os"
	"sort"
)

var requiredManifestPaths = [][]string{
	{"prompt", "sha256"},
	{"model", "revision"},
	{"tools", "schemaSha256"},
	{"tools", "policySha256"},
	{"retrieval", "indexBuild"},
	{"output", "schemaSha256"},
	{"evaluation", "datasetRevision"},
	{"evaluation", "runnerRevision"},
}

type EvalResult struct {
	CaseID  string             `json:"caseId"`
	Repeat  int                `json:"repeat"`
	Slice   string             `json:"slice"`
	Metrics map[string]float64 `json:"metrics"`
}

type Rule struct {
	Metric       string   `json:"metric"`
	Slice        string   `json:"slice"`
	Direction    string   `json:"direction"`
	MinimumDelta *float64 `json:"minimumDelta,omitempty"`
	MaximumDelta *float64 `json:"maximumDelta,omitempty"`
}

type Policy struct {
	Confidence       float64 `json:"confidence"`
	BootstrapSamples int     `json:"bootstrapSamples"`
	Seed             int64   `json:"seed"`
	Rules            []Rule  `json:"rules"`
}

type Check struct {
	Metric    string     `json:"metric"`
	Slice     string     `json:"slice"`
	Pairs     int        `json:"pairs"`
	MeanDelta float64    `json:"meanDelta"`
	Interval  [2]float64 `json:"interval"`
	Passed    bool       `json:"passed"`
}

type Report struct {
	ReleaseDigest string  `json:"releaseDigest"`
	Passed        bool    `json:"passed"`
	Checks        []Check `json:"checks"`
}

type sampleKey struct {
	CaseID string
	Repeat int
}

func loadJSON(path string, target any) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()
	decoder := json.NewDecoder(file)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		return errors.New("expected one JSON value")
	}
	return nil
}

func nestedValue(document map[string]any, path []string) (any, error) {
	var value any = document
	for _, key := range path {
		object, ok := value.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("missing manifest field: %v", path)
		}
		value, ok = object[key]
		if !ok {
			return nil, fmt.Errorf("missing manifest field: %v", path)
		}
	}
	if text, ok := value.(string); ok && text == "" {
		return nil, fmt.Errorf("empty manifest field: %v", path)
	}
	return value, nil
}

func releaseDigest(manifest map[string]any) (string, error) {
	for _, path := range requiredManifestPaths {
		if _, err := nestedValue(manifest, path); err != nil {
			return "", err
		}
	}
	payload, err := json.Marshal(manifest)
	if err != nil {
		return "", err
	}
	digest := sha256.Sum256(payload)
	return fmt.Sprintf("sha256:%x", digest), nil
}

func selectResults(
	rows []EvalResult,
	metric string,
	slice string,
) (map[sampleKey]float64, error) {
	selected := make(map[sampleKey]float64)
	for _, row := range rows {
		if row.Slice != slice {
			continue
		}
		if row.CaseID == "" || row.Repeat < 0 {
			return nil, errors.New("caseId must be non-empty and repeat nonnegative")
		}
		value, ok := row.Metrics[metric]
		if !ok {
			return nil, fmt.Errorf("missing metric %q for case %q", metric, row.CaseID)
		}
		key := sampleKey{CaseID: row.CaseID, Repeat: row.Repeat}
		if _, exists := selected[key]; exists {
			return nil, fmt.Errorf("duplicate result for %q repeat %d", row.CaseID, row.Repeat)
		}
		selected[key] = value
	}
	return selected, nil
}

func pairedDifferences(
	baseline []EvalResult,
	candidate []EvalResult,
	metric string,
	slice string,
) ([]float64, error) {
	left, err := selectResults(baseline, metric, slice)
	if err != nil {
		return nil, err
	}
	right, err := selectResults(candidate, metric, slice)
	if err != nil {
		return nil, err
	}
	if len(left) == 0 || len(left) != len(right) {
		return nil, fmt.Errorf("unpaired or empty results for %s/%s", metric, slice)
	}
	keys := make([]sampleKey, 0, len(left))
	for key := range left {
		if _, exists := right[key]; !exists {
			return nil, fmt.Errorf("candidate is missing %+v", key)
		}
		keys = append(keys, key)
	}
	sort.Slice(keys, func(i, j int) bool {
		if keys[i].CaseID == keys[j].CaseID {
			return keys[i].Repeat < keys[j].Repeat
		}
		return keys[i].CaseID < keys[j].CaseID
	})
	differences := make([]float64, 0, len(keys))
	for _, key := range keys {
		differences = append(differences, right[key]-left[key])
	}
	return differences, nil
}

func mean(values []float64) float64 {
	total := 0.0
	for _, value := range values {
		total += value
	}
	return total / float64(len(values))
}

func bootstrapInterval(
	values []float64,
	confidence float64,
	samples int,
	seed int64,
) [2]float64 {
	random := rand.New(rand.NewSource(seed))
	estimates := make([]float64, samples)
	for sample := range estimates {
		draw := make([]float64, len(values))
		for index := range draw {
			draw[index] = values[random.Intn(len(values))]
		}
		estimates[sample] = mean(draw)
	}
	sort.Float64s(estimates)
	tail := (1 - confidence) / 2
	low := int(tail * float64(samples-1))
	high := int((1 - tail) * float64(samples-1))
	return [2]float64{estimates[low], estimates[high]}
}

func evaluateGate(
	baseline []EvalResult,
	candidate []EvalResult,
	policy Policy,
) ([]Check, bool, error) {
	if policy.Confidence <= 0 || policy.Confidence >= 1 {
		return nil, false, errors.New("confidence must be between zero and one")
	}
	if policy.BootstrapSamples < 100 || len(policy.Rules) == 0 {
		return nil, false, errors.New("bootstrapSamples must be at least 100 and rules cannot be empty")
	}
	checks := make([]Check, 0, len(policy.Rules))
	for index, rule := range policy.Rules {
		differences, err := pairedDifferences(
			baseline,
			candidate,
			rule.Metric,
			rule.Slice,
		)
		if err != nil {
			return nil, false, err
		}
		interval := bootstrapInterval(
			differences,
			policy.Confidence,
			policy.BootstrapSamples,
			policy.Seed+int64(index),
		)
		passed := false
		switch rule.Direction {
		case "higher":
			if rule.MinimumDelta == nil {
				return nil, false, errors.New("higher rule requires minimumDelta")
			}
			passed = interval[0] >= *rule.MinimumDelta
		case "lower":
			if rule.MaximumDelta == nil {
				return nil, false, errors.New("lower rule requires maximumDelta")
			}
			passed = interval[1] <= *rule.MaximumDelta
		default:
			return nil, false, fmt.Errorf("unknown direction %q", rule.Direction)
		}
		checks = append(checks, Check{
			Metric:    rule.Metric,
			Slice:     rule.Slice,
			Pairs:     len(differences),
			MeanDelta: mean(differences),
			Interval:  interval,
			Passed:    passed,
		})
	}
	for _, check := range checks {
		if !check.Passed {
			return checks, false, nil
		}
	}
	return checks, true, nil
}

func main() {
	var manifest map[string]any
	var baseline, candidate []EvalResult
	var policy Policy
	for path, target := range map[string]any{
		"release-manifest.json":  &manifest,
		"baseline-results.json":  &baseline,
		"candidate-results.json": &candidate,
		"release-policy.json":    &policy,
	} {
		if err := loadJSON(path, target); err != nil {
			fmt.Fprintln(os.Stderr, path+":", err)
			os.Exit(1)
		}
	}
	digest, err := releaseDigest(manifest)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	checks, passed, err := evaluateGate(baseline, candidate, policy)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	payload, err := json.MarshalIndent(
		Report{ReleaseDigest: digest, Passed: passed, Checks: checks},
		"",
		"  ",
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	if err := os.WriteFile("gate-report.json", append(payload, '\n'), 0o644); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	if !passed {
		fmt.Fprintln(os.Stderr, "release gate failed; inspect gate-report.json")
		os.Exit(1)
	}
}

每条结果使用 caseId、repeat、slice 与数值型 metrics 对象,例如 {"caseId":"refund-1","repeat":0,"slice":"all","metrics":{"taskScore":0.82}}。对每个受门禁保护的切片,基线与候选文件都必须包含完全相同的 (caseId, repeat) 键。

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

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.go
          go run scripts/validate-release.go --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 调用、重试、存储和人工复核。先跑廉价确定性检查,按变更影响选择评测套件,只缓存不可变输入输出,把昂贵评测留给通过前置门禁的候选。不要对外承诺统一单次成本。

参考资料