跳到正文

更多文章

影响力日常操作系统:21天习惯养成计划 从技能雇佣者到价值创造者 互惠账户的运营 影响力的三层架构 组织的注意力经济学
数据工程师如何用 Git 和 LangFuse 管理 Prompt 实现可回滚、可测试的 LLM 应用

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

用 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 等全栈领域。了解更多 ->

Elazer (石头)
Elazer (石头)

11 年数据老兵,从分析师到架构专家。用真实经历帮数据人少走弯路。

加入免费社群

和数据从业者一起交流成长

了解详情 →

成为会员

解锁全部内容 + 知识库

查看权益 →
← 上一篇 生产数据分析:制造业数据驱动优化实战指南 下一篇 → 新零售数据分析:线上线下融合的数字化转型实战指南