本文来源于数据从业者全栈知识库,更多体系化内容请访问知识库。

用 Google Doc 管 Prompt,就像用 Word 文档管代码——改了什么、谁改的、改完效果怎样,全靠记忆和缘分。

Prompt 是 LLM 应用最核心的资产,也是最混乱的工程问题。很多团队在 Prompt 上踩过同一个坑:改了个 Prompt,效果变差了,但不知道是哪里改坏的,也回不去。

目录

  • Prompt 是新的代码

  • Prompt 版本管理

  • LangFuse Prompt Management 实战

  • Prompt A/B 测试

  • Prompt 模板工程

  • 完整工具类实现


Prompt 是新的代码

代码需要:Git 版本控制、Code Review、CI/CD、测试覆盖。

Prompt 同样需要这一切,原因完全相同:

变更会影响生产行为。改一个词,模型的输出风格、格式、准确性可能发生显著变化。

需要回滚能力。新 Prompt 上线发现效果变差,你得能在 5 分钟内切回上一个版本,而不是"啊,我记得之前是这么写的……"

需要协作管理。多人团队里,谁改了 Prompt、为什么改、改完有没有经过评估——这些信息不应该只存在于 Slack 消息里。

团队不做 Prompt 管理的后果

场景:用户反映近几天回答质量下降

问题排查:
  - 后端代码没变动(Git log 确认)
  - 模型版本没变(还是 gpt-4o)
  - 检索结果没变(向量库数据正常)

最后发现:
  - 产品经理上周"优化"了 System Prompt
  - 优化内容存在他的 Notion 里
  - 无法对比修改前后的差异
  - 旧版本 Prompt 已经找不到了

这不是极端案例,这是日常。


Prompt 版本管理

用 Git 管理 Prompt(基础方案)

Prompt 文件用 YAML 格式存储,纳入 Git 版本控制:

# prompts/rag-answer/v2.1.0.yaml
metadata:
  name: rag-answer
  version: "2.1.0"
  author: "张三"
  created_at: "2025-06-15"
  description: "RAG 问答系统的主要回答 Prompt,优化了引用格式"
  tags: ["rag", "production"]
  changelog: "v2.1.0: 增加了引用来源标注要求;v2.0.0: 重写了格式要求部分"

system_prompt: |
  你是一个专业的数据工程助手。请基于提供的参考文档回答用户的问题。

  回答要求:
  1. 只使用参考文档中的信息,不要添加文档以外的内容
  2. 如果文档信息不足以回答问题,明确说明"根据现有资料,无法完整回答此问题"
  3. 在回答末尾标注信息来源(格式:[来源:文档名称])
  4. 使用简洁的中文,避免技术术语的堆砌

few_shots:
  - user: "Flink 的 Checkpoint 和 Savepoint 有什么区别?"
    context: |
      Checkpoint 是 Flink 自动触发的容错机制,用于故障恢复。
      Savepoint 是用户手动触发的状态快照,用于版本升级和迁移。
    assistant: |
      Checkpoint 和 Savepoint 的核心区别在于触发方式和用途:
      - Checkpoint:自动触发,专为故障恢复设计,Flink 管理其生命周期
      - Savepoint:手动触发,用于计划性操作(如版本升级),由用户管理

      [来源:Flink状态管理文档]

variables:
  context: "{{retrieved_documents}}"
  user_question: "{{user_input}}"

Semantic Versioning for Prompts

版本号 含义 示例
MAJOR(主版本) 完全重写,与上一版本语义不兼容 1.x.x → 2.0.0
MINOR(次版本) 增加新要求或格式调整,向后兼容 2.0.x → 2.1.0
PATCH(补丁) 文字修正、拼写错误、微调措辞 2.1.0 → 2.1.1

目录结构

目录/文件 说明
prompts/ Prompt 根目录
rag-answer/ RAG 回答类 Prompt
rag-answer/v1.0.0.yaml 历史版本(不要删除)
rag-answer/v2.0.0.yaml 历史版本
rag-answer/v2.1.0.yaml 历史版本
rag-answer/current 软链接指向 v2.1.0.yaml(当前版本)
intent-classification/ 意图分类类 Prompt
intent-classification/v1.0.0.yaml 历史版本
intent-classification/current 软链接指向 v1.0.0.yaml
summarization/ 摘要生成类 Prompt
summarization/v1.0.0.yaml 当前版本

LangFuse Prompt Management 实战

Git 管理 Prompt 解决了版本追踪问题,但不能动态切换版本、不能做 A/B 测试。LangFuse 的 Prompt Management 补充了这部分能力。

在 LangFuse UI 创建 Prompt

LangFuse 后台 → Prompts → Create Prompt
  - Name: rag-answer
  - Prompt 内容(支持 {{variable}} 变量语法)
  - 标记为 production / development 环境
  - 添加标签

Python SDK 读取 Prompt

from langfuse import Langfuse

langfuse = Langfuse(
    public_key="pk-lf-...",
    secret_key="sk-lf-..."
)

def get_production_prompt(prompt_name: str, variables: dict) -> str:
    """
    从 LangFuse 获取生产版本 Prompt
    LangFuse 会自动缓存,不用担心每次调用都发请求
    """
    # 获取最新 production 版本(不指定 version 时默认取 production 标签版本)
    prompt_template = langfuse.get_prompt(prompt_name, label="production")

    # 编译:将变量填入模板
    compiled_prompt = prompt_template.compile(**variables)

    return compiled_prompt, prompt_template.config

# 使用示例
system_prompt, config = get_production_prompt(
    "rag-answer",
    variables={
        "context": "Flink 的 Checkpoint 是...",
        "user_question": "Checkpoint 和 Savepoint 的区别?"
    }
)

# config 里包含 Prompt 元数据,可以记录到 LangFuse Trace 中

获取特定版本(用于 A/B 测试)

# 获取特定版本号
prompt_v2 = langfuse.get_prompt("rag-answer", version=2)

# 获取 staging 环境版本(新版本先在 staging 测试)
prompt_staging = langfuse.get_prompt("rag-answer", label="staging")

生产与开发 Prompt 隔离

import os

ENVIRONMENT = os.getenv("APP_ENV", "development")

def get_prompt_for_env(prompt_name: str, variables: dict):
    """根据运行环境自动选择 Prompt 版本"""
    label = "production" if ENVIRONMENT == "production" else "staging"

    prompt = langfuse.get_prompt(prompt_name, label=label)
    return prompt.compile(**variables)

Prompt A/B 测试

A/B 测试是验证 Prompt 效果的最可靠方式,也是很多团队跳过的步骤(结果是"感觉"新版本更好)。

实验设计

import hashlib
from typing import Literal

def assign_prompt_variant(
    user_id: str,
    experiment_id: str,
    traffic_split: float = 0.5  # 50% 流量给新版本
) -> Literal["control", "treatment"]:
    """
    基于 user_id 的确定性分组(同一用户每次分到同一组)
    避免用 random(),否则同一用户会看到不一致的体验
    """
    hash_input = f"{user_id}:{experiment_id}"
    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
    normalized = (hash_value % 1000) / 1000.0  # 0.0 ~ 1.0

    return "treatment" if normalized < traffic_split else "control"

def get_prompt_with_ab(user_id: str, variables: dict) -> tuple[str, str]:
    """
    返回:(编译后的 Prompt, 实验组标识)
    """
    variant = assign_prompt_variant(user_id, experiment_id="exp_rag_v2")

    if variant == "treatment":
        # 新 Prompt 版本
        prompt_template = langfuse.get_prompt("rag-answer", version=5)
    else:
        # 对照组(当前 production 版本)
        prompt_template = langfuse.get_prompt("rag-answer", label="production")

    compiled = prompt_template.compile(**variables)
    return compiled, variant

记录实验分组到 LangFuse

from langfuse import Langfuse

def handle_user_query(user_id: str, query: str, context: str):
    langfuse = Langfuse()
    prompt_text, variant = get_prompt_with_ab(user_id, {"context": context, "query": query})

    trace = langfuse.trace(
        name="rag-ab-test",
        user_id=user_id,
        tags=[f"experiment:exp_rag_v2", f"variant:{variant}"],
        metadata={"ab_variant": variant, "experiment_id": "exp_rag_v2"},
    )

    # ... 调用 LLM,记录结果 ...

    return answer, trace.id

评估指标与统计显著性

import scipy.stats as stats
import numpy as np

def analyze_ab_results(
    control_scores: list[float],
    treatment_scores: list[float],
    alpha: float = 0.05
) -> dict:
    """
    对两组评估分数做 t 检验,判断差异是否统计显著
    """
    t_stat, p_value = stats.ttest_ind(control_scores, treatment_scores)

    control_mean = np.mean(control_scores)
    treatment_mean = np.mean(treatment_scores)
    relative_improvement = (treatment_mean - control_mean) / control_mean * 100

    return {
        "control_mean": control_mean,
        "treatment_mean": treatment_mean,
        "relative_improvement_pct": relative_improvement,
        "p_value": p_value,
        "statistically_significant": p_value < alpha,
        "sample_sizes": {
            "control": len(control_scores),
            "treatment": len(treatment_scores),
        },
        "recommendation": (
            "上线新版本" if p_value < alpha and treatment_mean > control_mean
            else "保持现有版本" if p_value < alpha
            else "样本量不足或差异不显著,继续收集数据"
        )
    }

# 使用示例(从 LangFuse 拉取评分数据后调用)
result = analyze_ab_results(
    control_scores=[0.72, 0.68, 0.75, 0.70, 0.73],   # 对照组 LLM-as-Judge 分数
    treatment_scores=[0.81, 0.79, 0.83, 0.80, 0.82],  # 实验组分数
)
print(result)
# {"relative_improvement_pct": 11.1, "p_value": 0.003, "recommendation": "上线新版本"}

Prompt 模板工程

Jinja2 管理动态变量

from jinja2 import Environment, FileSystemLoader, StrictUndefined

# 加载模板目录
env = Environment(
    loader=FileSystemLoader("prompts/templates"),
    undefined=StrictUndefined,  # 未定义变量直接报错,避免静默错误
    trim_blocks=True,           # 去除块标签后的换行
    lstrip_blocks=True,
)

def render_prompt(template_name: str, **variables) -> str:
    template = env.get_template(f"{template_name}.j2")
    return template.render(**variables)
{# prompts/templates/rag-answer.j2 #}
你是一个专业的数据工程助手。

{% if language == "en" %}
Please answer the question based on the provided context.
{% else %}
请基于以下参考文档回答问题。
{% endif %}

参考文档:
{{ context }}

{% if few_shots %}
以下是一些示例,帮助你理解回答格式:
{% for example in few_shots %}
用户:{{ example.user }}
助手:{{ example.assistant }}
---
{% endfor %}
{% endif %}

用户问题:{{ user_question }}

Few-shot 示例的动态选择

静态 Few-shot 对所有问题用同样的示例,动态选择则根据用户问题选最相关的示例:

from sentence_transformers import SentenceTransformer
import numpy as np

class DynamicFewShotSelector:
    def __init__(self, example_pool: list[dict], model_name: str = "all-MiniLM-L6-v2"):
        self.examples = example_pool
        self.model = SentenceTransformer(model_name)

        # 预计算所有示例的向量
        questions = [ex["user"] for ex in example_pool]
        self.embeddings = self.model.encode(questions, normalize_embeddings=True)

    def select(self, query: str, top_k: int = 3) -> list[dict]:
        """选择与当前问题最相关的 K 个示例"""
        query_embedding = self.model.encode([query], normalize_embeddings=True)

        # 余弦相似度(因为向量已归一化,点积即余弦相似度)
        similarities = np.dot(self.embeddings, query_embedding.T).flatten()

        top_indices = np.argsort(similarities)[::-1][:top_k]
        return [self.examples[i] for i in top_indices]

# 示例池(实际项目中从数据库或文件加载)
EXAMPLE_POOL = [
    {
        "user": "Kafka 和 RabbitMQ 有什么区别?",
        "assistant": "Kafka 是分布式日志系统,适合高吞吐量的事件流...",
    },
    {
        "user": "Flink 的水位线是什么?",
        "assistant": "水位线(Watermark)是 Flink 处理乱序事件的机制...",
    },
    # ... 更多示例
]

selector = DynamicFewShotSelector(EXAMPLE_POOL)
relevant_examples = selector.select("Spark Streaming 和 Flink 对比", top_k=2)

System Prompt 的多语言处理

SYSTEM_PROMPTS = {
    "zh": """你是一个专业的数据工程助手。
请使用简洁的中文回答,避免过多技术术语堆砌。
如果需要提及技术术语,请简要解释其含义。""",

    "en": """You are a professional data engineering assistant.
Please answer concisely in English.
For technical terms, provide brief explanations when appropriate.""",
}

def get_system_prompt(language: str = "zh") -> str:
    return SYSTEM_PROMPTS.get(language, SYSTEM_PROMPTS["zh"])

完整工具类实现

一个生产可用的 Prompt 版本化管理工具类:

from dataclasses import dataclass
from typing import Optional
import yaml
import json
from pathlib import Path
from datetime import datetime
import hashlib

@dataclass
class PromptVersion:
    name: str
    version: str
    system_prompt: str
    few_shots: list[dict]
    variables: dict
    metadata: dict

    @property
    def prompt_hash(self) -> str:
        """Prompt 内容的哈希值,用于检测内容变更"""
        content = self.system_prompt + json.dumps(self.few_shots, ensure_ascii=False)
        return hashlib.sha256(content.encode()).hexdigest()[:12]

class PromptManager:
    """
    本地 Git 管理 + LangFuse 远程同步的混合 Prompt 管理器
    """

    def __init__(self, prompts_dir: str = "prompts", langfuse_client=None):
        self.prompts_dir = Path(prompts_dir)
        self.langfuse = langfuse_client
        self._cache: dict[str, PromptVersion] = {}

    def load_from_file(self, name: str, version: Optional[str] = None) -> PromptVersion:
        """从本地文件加载 Prompt"""
        prompt_dir = self.prompts_dir / name

        if version:
            file_path = prompt_dir / f"v{version}.yaml"
        else:
            # 读取 current 软链接指向的版本
            current_link = prompt_dir / "current"
            file_path = current_link.resolve() if current_link.is_symlink() else None
            if not file_path:
                raise FileNotFoundError(f"No current version found for prompt: {name}")

        with open(file_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

        return PromptVersion(
            name=data["metadata"]["name"],
            version=data["metadata"]["version"],
            system_prompt=data["system_prompt"],
            few_shots=data.get("few_shots", []),
            variables=data.get("variables", {}),
            metadata=data["metadata"],
        )

    def get(self, name: str, version: Optional[str] = None, use_cache: bool = True) -> PromptVersion:
        """获取 Prompt,优先从缓存读取"""
        cache_key = f"{name}:{version or 'current'}"

        if use_cache and cache_key in self._cache:
            return self._cache[cache_key]

        # 优先从 LangFuse 获取(如果配置了的话)
        if self.langfuse and version is None:
            try:
                prompt = self._load_from_langfuse(name)
                self._cache[cache_key] = prompt
                return prompt
            except Exception:
                pass  # LangFuse 失败时降级到本地文件

        # 降级到本地文件
        prompt = self.load_from_file(name, version)
        self._cache[cache_key] = prompt
        return prompt

    def _load_from_langfuse(self, name: str) -> PromptVersion:
        """从 LangFuse 加载 production 版本"""
        lf_prompt = self.langfuse.get_prompt(name, label="production")
        # 解析 LangFuse 返回的 Prompt 结构
        config = lf_prompt.config or {}
        return PromptVersion(
            name=name,
            version=str(lf_prompt.version),
            system_prompt=lf_prompt.prompt,
            few_shots=config.get("few_shots", []),
            variables=config.get("variables", {}),
            metadata={"source": "langfuse", "version": lf_prompt.version},
        )

    def compile(self, name: str, **variables) -> str:
        """获取并编译 Prompt(填入变量)"""
        prompt = self.get(name)
        compiled = prompt.system_prompt

        for key, value in variables.items():
            compiled = compiled.replace(f"{{{{{key}}}}}", str(value))

        return compiled

    def audit_log(self, name: str, version: str, user_id: str, action: str):
        """记录 Prompt 使用审计日志"""
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "prompt_name": name,
            "version": version,
            "user_id": user_id,
            "action": action,
        }
        # 写入审计日志文件
        audit_path = self.prompts_dir / "audit.jsonl"
        with open(audit_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")


# 使用示例
from langfuse import Langfuse

langfuse = Langfuse(public_key="pk-lf-...", secret_key="sk-lf-...")
pm = PromptManager(prompts_dir="prompts", langfuse_client=langfuse)

# 获取并使用 Prompt
compiled_prompt = pm.compile(
    "rag-answer",
    context="Flink 的 Checkpoint 是...",
    user_question="Checkpoint 和 Savepoint 的区别?"
)

小结

Prompt 工程管理的核心原则:

  1. Prompt 是代码,用 Git 管:YAML 格式存储,语义化版本号
  2. 用 LangFuse 实现动态切换:生产环境不靠发布代码来换 Prompt
  3. A/B 测试,用数据说话:改 Prompt 之前先有假设,改完用数据验证
  4. 动态 Few-shot:相似度检索选最相关示例,比静态 Few-shot 效果好

系列导航

  • LLMOps体系全景 — 回到全景视图
  • LLM可观测性与监控 — Prompt 效果怎么监控
  • LLM成本控制与优化 — Prompt 压缩控成本
  • LLM评估体系 — 如何评估 Prompt 变更效果

相关文档

  • Prompt Engineering提示工程 — Prompt 编写技术基础
  • MLOps最佳实践 — 传统 ML 版本管理对比
  • RAG检索增强生成实战 — Few-shot 动态选择的实际应用

LLMOps #Prompt管理 #Prompt版本控制 #LangFuse #AB测试


本文节选自数据从业者全栈知识库。知识库包含 2300+ 篇体系化技术文档,覆盖数据分析、数据工程、数据治理、AI 等全栈领域。了解更多 ->

posted on 2026-04-27 23:25  拾穗数据  阅读(66)  评论(0)    收藏  举报