本文来源于数据从业者全栈知识库,更多体系化内容请访问知识库。
用 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 版本控制:
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 hashlibfrom 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 statsimport 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 SentenceTransformerimport 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 dataclassfrom typing import Optionalimport yamlimport jsonfrom pathlib import Pathfrom datetime import datetimeimport hashlib
@dataclassclass 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)
# 获取并使用 Promptcompiled_prompt = pm.compile( "rag-answer", context="Flink 的 Checkpoint 是...", user_question="Checkpoint 和 Savepoint 的区别?")小结
Prompt 工程管理的核心原则:
- Prompt 是代码,用 Git 管:YAML 格式存储,语义化版本号
- 用 LangFuse 实现动态切换:生产环境不靠发布代码来换 Prompt
- A/B 测试,用数据说话:改 Prompt 之前先有假设,改完用数据验证
- 动态 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 等全栈领域。了解更多 ->