提示词版本管理:把 Prompt 当代码用 Git 与评测回归守着

提示词早已不是一次性的聊天文案。在真实项目里,一个分类器提示词会被反复微调,一个客服系统提示词会被多个同事接手修改,一个生成摘要的提示词要随模型升级而重写。当提示词开始影响线上业务指标,它就必须像代码一样被管理:可回溯、可对比、可回归、可评审。本文把提示词当作「一等公民」的代码资产,讲清楚用 Git 做版本控制、用评测集做回归守门、以及多环境与评审协作的完整工作流。

为什么提示词需要版本管理

很多团队早期把提示词硬编码在 main.py 的字符串里,改一句就改一处代码、发一次版。这种做法在小 demo 阶段没问题,一旦进入多人协作和生产环境,会立刻暴露三类问题。

第一,提示词即代码。提示词决定了模型的输入分布,直接决定输出质量。一句措辞变化可能让准确率从 92% 掉到 70%,而且这种退化不会报错、不会抛异常,只会在用户投诉里慢慢浮现。既然它和代码一样影响结果,就该享受代码所有的工程保障:版本、diff、评审、回滚。

第二,回归风险。模型对提示词高度敏感,且提示词的效果依赖具体模型版本。今天在 Claude 上跑通的提示词,换成另一家模型或升级到新版本后可能变差;一个「顺手」的小改也可能破坏原本稳定的边界情况。没有回归测试,你就永远不知道某次改动到底是变好还是变坏。

第三,协作冲突。当两个人同时改同一个提示词字符串,纯文本合并冲突还好解决;更麻烦的是「语义冲突」——A 加了格式约束,B 加了示例,二者单独看都对,合在一起却互相打架。提示词需要清晰的归属、变更理由和评审记录,而这些正是版本管理系统擅长的。

用 Git 管理提示词

把提示词放进 Git 仓库,是版本管理的第一步。核心原则只有一条:提示词应该是独立、可读、可 diff 的文本文件,而不是散落在代码里的字符串。

目录约定

建议把提示词集中到一个目录,按用途或业务模块拆分文件,与源码分离:

prompts/
  classifier/
    sentiment.yaml        # 情感分类提示词
    intent.yaml           # 意图识别提示词
  rag/
    answer_generator.yaml # 检索问答提示词
  agent/
    planner.yaml          # 智能体规划提示词
tests/
  test_prompt_regression.py
  dataset/
    sentiment_cases.jsonl # 评测用例

每个提示词文件只负责一个能力,文件即版本单元。这样 git log prompts/classifier/sentiment.yaml 能单独看到这条提示词的演进史,git diff 也能清爽地对比改动。

commit message 规范

提示词的改动也要写清楚「为什么」,而不是只写「改了提示词」:

prompt(classifier): 强化情感分类的边界约束

- 增加中性样本的处理说明,要求模型在不确定时输出「中性」
- 补充 2 个易混淆示例( sarcasm / 反讽)
- 关联评测集 sentiment_cases.jsonl,准确率 88% 到 91%

原因:线上反馈中性评论被误判为负面占比偏高

一句「为什么」比十句「改了什么」更有价值,因为它让半年后的你仍能理解当时的权衡。

保持 diff 可读性

Git 的 diff 对人类友好,前提是提示词是纯文本。避免使用二进制或压缩格式保存提示词;如果提示词里嵌套了长示例,用 YAML 的多行字符串(| 块标量)保留换行,让每一行改动都对应一处可读的语义变化,而不是一大块无法区分的更新。

提示词与代码分离:配置化

把提示词写进配置文件(YAML 或 JSON),代码只负责「读取、渲染、调用」。这样做有三个好处:提示词改了不必改代码、不必重新部署;变量可以外部注入;同一份逻辑能套用不同场景。

name: sentiment-classifier
version: 3
model: claude-opus-4
variables:
  - task
  - examples
template: |
  你是一个严谨的文本情感分析助手。
  任务:{{ task }}。

  请只输出「正面」「负面」或「中性」之一,不要解释。
  参考示例:
  {{ examples }}

渲染时用模板引擎把变量填进去,例如用 Jinja2:

from jinja2 import Template
import yaml

def load_prompt(path):
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

def render_prompt(cfg, **variables):
    return Template(cfg["template"]).render(**variables)

cfg = load_prompt("prompts/classifier/sentiment.yaml")
prompt = render_prompt(
    cfg,
    task="判断用户评论的情感倾向",
    examples="- 评论:物流太快了!\n  情感:正面",
)
print(prompt)

变量占位让提示词成为「模板」,不同业务线只需替换 examplestask,提示词本体保持稳定,回归测试也更容易聚焦。

评测集驱动:构建测试用例

版本管理守的是「变更不退化」,但「好不好」需要评测集来定义。评测集是一组固定输入与期望输出的用例,是提示词质量的基准线。

构建测试用例

用例要覆盖三类:典型样本(日常流量)、边界样本(模糊、反讽、多意图)、回归样本(曾经出过错的 case)。用 JSONL 存储便于程序读取:

{"input": "这家店服务太差了,不会再来了", "expect": "负面"}
{"input": "画面震撼,剧情很打动人", "expect": "正面"}
{"input": "东西还行吧,说不上好也说不上差", "expect": "中性"}
{"input": "退货流程真是一言难尽(反讽)", "expect": "负面"}

用 LLM 当裁判

很多任务没有唯一正确答案(比如「摘要是否通顺」),这时引入 LLM-as-a-judge:用一个判定模型对输出打分。判分模型要和被评测的生成模型区分开,并给出稳定的评分尺度:

def llm_judge(question, answer):
    # 真实实现:调用判定模型,要求返回 JSON {"score": 0到10, "reason": "..."}
    judge_prompt = f"""
    请作为严格评委,对下面的回答质量打分(0到10)。
    问题:{question}
    回答:{answer}
    只返回 JSON:{{"score": int, "reason": str}}
    """
    # return call_llm(judge_prompt)  # 接入真实判定模型
    return {"score": 9, "reason": "覆盖了要点"}  # 离线桩

断言式校验

对能确定答案的任务,直接用断言做硬校验,不依赖主观判分:

def assert_label(output, expect):
    assert output.strip() in {"正面", "负面", "中性"}, f"非法输出: {output}"
    assert output.strip() == expect, f"期望 {expect},实际 {output.strip()}"

pytest 化回归:把评测写成可 CI 跑的测试

评测集的价值在于能自动化、可重复地跑。把提示词评测写成 pytest 用例,每次提交都自动回归,CI 挂了就不能合并——这就是「把 Prompt 当代码守着」的落地形态。

下面是一份可离线跑通的回归测试,call_llm 用桩函数代替真实模型,接入真实 API 时只需替换该函数:

import json
import pytest
import yaml
from jinja2 import Template

def load_prompt(path):
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

def render_prompt(cfg, **variables):
    return Template(cfg["template"]).render(**variables)

def call_llm(system, user):
    # 真实实现改为:requests.post(API_URL, json={...})
    # 此处用桩返回,保证离线可运行
    return "负面" if "太差" in user or "一言难尽" in user else "正面"

def load_cases(path):
    cases = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                cases.append(json.loads(line))
    return cases

@pytest.fixture
def prompt():
    return load_prompt("prompts/classifier/sentiment.yaml")

@pytest.mark.parametrize("case", load_cases("tests/dataset/sentiment_cases.jsonl"))
def test_sentiment_regression(prompt, case):
    system = render_prompt(prompt, task="判断用户评论的情感倾向", examples="")
    output = call_llm(system, case["input"])
    assert output.strip() == case["expect"], (
        f"输入: {case['input']} | 期望: {case['expect']} | 实际: {output.strip()}"
    )

运行 pytest tests/test_prompt_regression.py -q,全部用例通过才算回归达标。当某次提示词改动让用例失败,CI 会直接拦住合并,避免退化流入生产。再配合一个汇总分数的用例,可以持续追踪准确率趋势:

def test_sentiment_accuracy(prompt):
    cases = load_cases("tests/dataset/sentiment_cases.jsonl")
    hit = sum(
        1 for c in cases
        if call_llm(render_prompt(prompt, task="x", examples=""), c["input"]).strip() == c["expect"]
    )
    rate = hit / len(cases)
    assert rate >= 0.9, f"回归准确率 {rate:.2%} 低于阈值 90%"

多环境与灰度:dev、staging、prod

提示词上线不该「一把梭」。借鉴代码的多环境发布,提示词也应分环境推进,并用灰度降低风险。

配置层按环境分离,避免把 prod 提示词写死:

dev:
  model: claude-haiku-4-5
  prompt_path: prompts/classifier/sentiment.yaml
staging:
  model: claude-sonnet-4-5
  prompt_path: prompts/classifier/sentiment.yaml
prod:
  model: claude-opus-4
  prompt_path: prompts/classifier/sentiment.yaml

灰度常用两种方式。其一是 A/B:同一流量同时跑旧版与新版提示词,按业务指标(满意度、转化率)择优;其二是流量切片:先把新版提示词放到 5% 流量,观察评测与线上指标无异常再逐步放量到 100%。下面是按用户 ID 做切片的简单路由:

import hashlib

def route_prompt(user_id, new_prompt, old_prompt, rollout=0.05):
    bucket = int(hashlib.md5(user_id.encode()).hexdigest(), 16) % 100
    return new_prompt if bucket < int(rollout * 100) else old_prompt

提示词管理平台(如 Langfuse)还支持用「标签」部署不同版本到不同环境,无需改代码即可切换线上提示词版本,这进一步把发布和代码解耦。

评审与协作:PR 模板与人工评审清单

提示词改动走 Pull Request,和代码改动同一条评审链路。一份好的 PR 模板能强制作者交代关键信息:

## 提示词变更说明
- 涉及提示词:prompts/classifier/sentiment.yaml
- 变更类型:新增约束 / 调整示例 / 更换模型 / 其他
- 关联评测集:tests/dataset/sentiment_cases.jsonl

## 变更动机
(为什么改?线上问题 / 新需求 / 准确率提升)

## 评测结果
- 回归用例数:
- 通过率:
- 准确率变化:

## 评审清单
- [ ] diff 是否只改了语义相关的部分
- [ ] 新增示例是否覆盖边界 / 反讽场景
- [ ] pytest 回归是否全绿
- [ ] 灰度与回滚方案是否就绪

人工评审时重点看四件事:改动是否最小化、示例是否有代表性、评测是否同步更新、回滚是否一行命令可完成。把这份清单固化进 PR 模板,提示词质量就有了组织层面的保障。

小结

提示词决定模型输入,直接影响业务结果,因此必须像代码一样被管理。用 Git 集中存放独立、可 diff 的提示词文件,并配合清晰的 commit message,让每次改动都可回溯。把提示词配置化、用变量占位与模板渲染,使其与代码解耦、可复用。用固定评测集定义「好」的标准,对确定任务用断言硬校验,对开放任务用 LLM-as-a-judge 打分。把评测写成 pytest 用例接入 CI,做到每次提交自动回归、不达标不合并。上线时分 dev、staging、prod 多环境推进,用 A/B 与流量切片灰度放量。最后用 PR 模板与人工评审清单守住协作质量。整套工作流的核心,是让提示词的「变更、对比、回归、评审、回滚」都有迹可循。

参考与延伸阅读

  • Anthropic 官方提示工程指南:提出清晰直接、补充上下文、使用示例(multishot 3 到 5 个)、用 XML 标签结构化、赋予角色、长上下文处理等原则,是提示词编写与迭代的权威参考。已核验(参考官方文档 docs.anthropic.com 与 Anthropic 工程博客 best-practices-for-prompt-engineering)。
  • LangSmith 官方文档(docs.smith.langchain.com):提供可观测性、评测(evaluation)、提示工程与部署能力,支持用数据集(datasets)系统化测试提示词版本。已核验。
  • Langfuse 官方文档(langfuse.com/docs):开源的 AI 工程平台,提供提示词管理(含版本控制与按标签部署)、评测(LLM-as-a-judge、数据集、实验对比)等能力,可与本文工作流互补。已核验。
  • pytest 官方文档(docs.pytest.org):本文 pytest 化回归测试的基础框架,支持参数化用例与 CI 集成。已核验(通用工具,文档稳定)。
  • Git 官方文档(git-scm.com/doc):版本控制、diff 与分支协作的底层能力来源。已核验(通用工具,文档稳定)。
  • Jinja2 官方文档(jinja.palletsprojects.com):本文提示词模板渲染所用的模板引擎,用于变量占位与配置化。待核实(未逐页访问核验具体版本 API)。
本文累计阅读