Prompt 不是即兴发挥:结构化设计与版本化评测的工程化路径
一、Prompt 作为生产工具的失控
工程师每天都在写 Prompt。调试一次模型,拼一段 prompt。换个场景,又重写一段。存在聊天记录里,下次找不到。
Prompt 是生产工具,但被当一次性消耗品。改一行,效果变了,没人知道为什么变好变坏。线上挂的 Prompt,没人敢动,怕回归。新旧 Prompt 谁更好,靠"试几次感觉"判断。
这种状态不可持续。LLM 应用越往生产走,Prompt 越要可管理。结构化、版本化、可评测、可复用。和代码一样进仓库,进 CI。把 Prompt 当代码,才有质量基线可言。
二、Prompt 的结构化与评测机制
工程化的 Prompt,不是一坨文本。是一组带语义的片段组合。
角色。定义 LLM 扮演什么身份。角色影响输出风格与专业深度。
上下文。提供任务背景与领域知识。上下文质量决定回答的针对性。
约束。限定输出范围、长度、风格。约束不写,LLM 容易跑题或啰嗦。
示例。Few-shot 示例锚定输出格式。示例要选有代表性的边界样本。
输出格式。结构化输出,便于程序解析。JSON Schema 或固定字段。这五要素组合成 Prompt 模板。
模板带版本号,进版本库。每个版本对应一组评测用例。A/B 测试基于评测集跑评分,对比版本优劣。评测用例本身也需评审,避免"测试集与 Prompt 同源"。下面是 Prompt 工程化的数据流:
flowchart TD A[业务需求] --> B[拆解: 角色/上下文/约束/示例/输出格式] B --> C[模板 v1 落库] C --> D[评测用例集] D --> E[批量调用 LLM] E --> F[评分: 准确率/格式合规/延迟/成本] F --> G{对比基线} G -->|优于基线| H[发布为新基线] G -->|不及基线| I[回退 + 分析] style H fill:#e8f5e9 style I fill:#ffebee关键在"评分"那一步。没有评分,就没有 A/B。评分要可量化、可复现、可对比。而非"我感觉这版好"。
三、生产级实现
下面用代码描述 Prompt 模板管理与评测工具。带版本管理、变量注入、批量评测。含错误处理与超时隔离。
import json import time from dataclasses import dataclass, field from typing import Callable, Any @dataclass class PromptTemplate: """Prompt 模板:五要素结构化。 版本号必须递增,便于回归追溯。 没有版本号的模板不允许上线。""" name: str version: str role: str context: str constraints: list[str] examples: list[dict] output_format: dict created_at: float = field(default_factory=time.time) class PromptRegistry: """模板注册中心:按 name 索引所有版本。 设计为内存字典,真实系统接数据库或文件仓库。 每次注册做版本冲突检测,避免覆盖历史。""" def __init__(self) -> None: self._store: dict[str, dict[str, PromptTemplate]] = {} def register(self, tpl: PromptTemplate) -> None: versions = self._store.setdefault(tpl.name, {}) if tpl.version in versions: # 版本冲突直接抛出,禁止静默覆盖 raise ValueError( f"模板 {tpl.name} v{tpl.version} 已存在" ) versions[tpl.version] = tpl def get(self, name: str, version: str = "latest") -> PromptTemplate: if name not in self._store: raise KeyError(f"模板 {name} 不存在") versions = self._store[name] if version == "latest": # 按 semver 排序取最新 latest = sorted(versions.keys())[-1] return versions[latest] return versions[version] def render(tpl: PromptTemplate, variables: dict) -> str: """把模板与变量拼成最终 Prompt。 变量缺失时抛错,避免悄悄产出残缺 prompt。 设计宁可失败,不吞错误。""" try: ctx = tpl.context.format(**variables) except KeyError as e: raise KeyError(f"缺少变量: {e}") from e examples_str = json.dumps(tpl.examples, ensure_ascii=False) constraints_str = "\n".join(f"- {c}" for c in tpl.constraints) # 拼装顺序固定:角色 → 上下文 → 约束 → 示例 → 输出格式 return ( f"# 角色\n{tpl.role}\n\n" f"# 上下文\n{ctx}\n\n" f"# 约束\n{constraints_str}\n\n" f"# 示例\n{examples_str}\n\n" f"# 输出格式\n{json.dumps(tpl.output_format, ensure_ascii=False)}" ) @dataclass class EvalCase: """评测用例:输入 + 期望输出 + 判定函数。 判定函数由业务自定义,避免工具绑死标准。""" inputs: dict expected: Any judge: Callable[[Any, Any], bool] @dataclass class EvalReport: """评测报告:通过率 + 失败用例。 失败用例必须保留,用于回归分析。""" total: int = 0 passed: int = 0 failures: list[dict] = field(default_factory=list) avg_latency: float = 0.0 def evaluate( tpl: PromptTemplate, cases: list[EvalCase], llm_call: Callable[[str], tuple[Any, float]], timeout: float = 30.0, ) -> EvalReport: """批量评测:对每个用例渲染 + 调用 + 判定。 LLM 调用是慢且易失败的外部依赖,必须隔离。 单用例失败不中断整体评测。""" report = EvalReport(total=len(cases)) latencies: list[float] = [] for i, case in enumerate(cases): try: prompt = render(tpl, case.inputs) output, latency = llm_call(prompt) latencies.append(latency) if case.judge(output, case.expected): report.passed += 1 else: report.failures.append({ "case_index": i, "inputs": case.inputs, "expected": case.expected, "actual": output, }) except Exception as e: # 异常用例单独标记,便于区分"失败"与"报错" report.failures.append({ "case_index": i, "error": str(e), }) report.avg_latency = ( sum(latencies) / len(latencies) if latencies else 0.0 ) return report def ab_compare( registry: PromptRegistry, name: str, v_old: str, v_new: str, cases: list[EvalCase], llm_call: Callable[[str], tuple[Any, float]], ) -> dict: """A/B 对比:旧版 vs 新版同评测集跑分。 只有新版显著优于旧版才允许替换基线。 "显著"由业务定义阈值,工具只提供数据。""" old = evaluate(registry.get(name, v_old), cases, llm_call) new = evaluate(registry.get(name, v_new), cases, llm_call) old_rate = old.passed / max(old.total, 1) new_rate = new.passed / max(new.total, 1) return { "old_pass_rate": old_rate, "new_pass_rate": new_rate, "delta": new_rate - old_rate, "old_latency": old.avg_latency, "new_latency": new.avg_latency, } if __name__ == "__main__": registry = PromptRegistry() tpl_v1 = PromptTemplate( name="summarize", version="1.0.0", role="你是技术摘要助手", context="对以下文章做摘要:{article}", constraints=["不超过 100 字", "保留关键技术点"], examples=[{"input": "文章A", "output": "摘要A"}], output_format={"summary": "string"}, ) registry.register(tpl_v1) print(render(registry.get("summarize"), {"article": "测试文章"}))真实系统会接 LLM 网关与评测仓库。评分结果落库,按时间序列回归对比。并把 Prompt 变更纳入 PR 评审,禁止直接改线上。
四、Prompt 不是即兴发挥的代价与边界
Prompt 工程化有用,但局限明显。
Prompt 脆弱性。改一个字,效果可能反转。版本管理能追溯,但不能消除脆弱。必须配回归评测集兜底。
评测集偏差。评测用例本身可能不代表真实分布。评测高分,上线翻车。评测集要持续更新,引入线上样本。
版本爆炸。微调一次出个版本,很快堆满仓库。要有淘汰机制,旧版本定期归档。只保留有意义的版本序列。
可迁移性下降。Prompt 强绑定某模型,换模型就废。跨模型评测要单独跑,不能直接套用。模型升级时,Prompt 也要重新评测。
评测成本。全量评测耗 token,跑一遍可能上百元。应做抽样评测,关键路径全量。并缓存历史结果,避免重复跑。
Prompt 工程化的几个被忽视的实践要点:第一,评测用例的"期望输出"本身要有评审流程,否则用例错了,再准的 Prompt 也是按错误标准对齐。第二,A/B 评测要做统计显著性判断,而不是看一两个百分点的差异就下结论,小样本下波动很大。第三,Prompt 模板里要显式标注"模型适配版本",因为同一个 Prompt 在不同模型上效果差很多,模板与模型要绑定版本管理。最后,线上 Prompt 的变更要走灰度而非全量替换,先放量 10% 观察指标,再决定是否全推,避免一次糟糕的 Prompt 直接影响全部用户。
五、总结
Prompt 是 LLM 应用的核心生产工具,必须工程化。机制上以五要素结构化、版本化注册、评测集打分、A/B 对比。工程上靠注册中心与评测报告形成闭环,禁止线上手改。落地路线:先做模板结构化与版本注册;建立评测用例集;实现批量评测与 A/B 对比;接 CI 门禁强制评审;最后做线上灰度发布。Prompt 当代码管,LLM 应用才有可能上生产。