课程0基础Agent开发课 / Prompt工程 / Prompt模板工程化管理-从硬编码到可维护
— 17 min read

Prompt模板工程化管理-从硬编码到可维护

> **本文适合谁**

Prompt 模板工程化管理:从硬编码到可维护

本文适合谁

AI 应用规模扩大后,需要系统管理十几个 Prompt 的开发者。从散落在代码里的硬编码字符串,到可版本控制、可测试、可复用的 Prompt 模板体系。


随着 LLM 应用规模扩大,Prompt 管理会成为一个真实的工程问题。一个中型 RAG(Retrieval-Augmented Generation,检索增强生成,让 AI 先从知识库中检索相关内容再回答的技术)系统里通常有十几个不同的 Prompt:问题改写、文档摘要、最终回答生成、查询路由……这些 Prompt 如果散落在代码各处,会带来一系列麻烦。

硬编码 Prompt 的问题

阶段 4 版本管理系统
成熟期

• 文件/数据库存储
• Git版本控制
• A/B测试支持

registry.get('qa_v2.1')
.render(ctx)

阶段 3 模板类
工程期

• 与代码解耦
• 支持继承复用
• 缺少版本管理

class PromptTemplate:
def render(self, **kw)

阶段 2 字符串模板
基础期

• 有变量替换
• 仍在代码中
• 版本混乱

tmpl = '你是{role}...'

阶段 1 硬编码
混乱期

• 散落在代码各处
• 修改需要重新部署
• 无法复用

prompt = '你是一个...' + user_input

Prompt 模板管理四阶段演进——从硬编码到版本管理系统,对应不同项目成熟度

先看一个典型的反面案例:

python
# service/qa_service.py
def answer_question(question, context):
    prompt = f"你是一个知识库助手。根据以下内容回答问题:\n\n{context}\n\n问题:{question}"
    return llm.invoke(prompt)

# service/summary_service.py
def summarize_doc(text):
    # 和上面几乎一样,但没人知道
    prompt = f"请总结以下文档:\n\n{text}\n\n要求:简洁,不超过200字。"
    return llm.invoke(prompt)

# api/chat_handler.py
def handle_chat(messages):
    # 一个月后来维护,发现这里还有一个 Prompt
    system = "你是客服助手,不要讨论竞争对手。"
    ...

这种写法的问题:

  1. 散落在代码各处:搜索 Prompt 要全局 grep,改一个措辞要改多个文件。
  2. 无法版本管理:无法回滚 Prompt,不知道上个版本的效果更好还是更差。
  3. 无法复用:两个服务写了几乎一样的 Prompt,但没人知道,也没人维护一致性。
  4. 无法测试:Prompt 嵌在业务逻辑里,不好单独测试不同版本的效果。
  5. 变量拼接脆弱:用 f-string 拼接,一旦变量名改了或者内容有特殊字符,容易出 bug。

PromptTemplate 的设计模式

LangChain 的 PromptTemplate(提示词模板类,用来管理带变量的 Prompt 字符串)解决了变量管理的问题。它用 {变量名} 作为占位符,和 Python 的 f-string(一种字符串格式化方式,如 f"你好,{name}")相比,主要优势在于声明式——模板和变量分离,模板可以序列化(即可以存储到文件中再加载)。

python
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.messages import SystemMessage, HumanMessagePromptTemplate

# 基础用法:单一文本模板
qa_template = PromptTemplate(
    input_variables=["context", "question"],
    template="""你是一个知识库问答助手。请根据提供的上下文回答问题。
如果上下文中没有足够信息,请如实说明,不要编造。

上下文:
{context}

问题:{question}

回答:""",
    # 元数据:记录模板的用途和版本,方便追踪
    metadata={"version": "1.2", "task": "qa", "author": "team-ai"}
)

# 格式化时传入变量,生成最终 Prompt 字符串
prompt_str = qa_template.format(
    context="Redis 是一个开源的内存数据库,支持多种数据结构。",
    question="Redis 是什么?"
)

# 对话模型用 ChatPromptTemplate,区分 system/human/assistant 角色
chat_template = ChatPromptTemplate.from_messages([
    ("system", "你是一名专业的 {role},用简洁专业的语言回答问题。"),
    ("human", "{question}"),
])

# from_messages 接受 (role, template_str) 元组列表,比手写 Message 对象更简洁
messages = chat_template.format_messages(
    role="Python 工程师",
    question="什么是 GIL?"
)

PromptTemplate 还支持偏应用(partial)——提前固定一部分变量,后续只传入剩余变量:

python
# 固定 role,后续不同调用只需传 question
partial_template = chat_template.partial(role="数据库专家")
messages = partial_template.format_messages(question="B+ 树和 B 树有什么区别?")

用 YAML 文件管理 Prompt 模板

变量管理解决了,但 Prompt 内容本身还在代码里。把 Prompt 内容提取到独立的配置文件中,才能真正做到"Prompt 变更不需要改代码"。

YAML 格式是比较好的选择,可读性强,天然支持多行字符串:

yaml
# prompts/qa_answer.yaml
version: "1.3"
task: "knowledge_base_qa"
description: "知识库问答,根据检索到的上下文生成回答"
author: "team-ai"
updated_at: "2024-03-15"

system_template: |
  你是一个专业的知识库问答助手。
  回答规则:
  1. 只根据提供的上下文作答,不要引用外部知识
  2. 如果上下文不足以回答,明确说"文档中没有相关信息"
  3. 回答简洁,避免重复上下文原文

human_template: |
  上下文:
  {context}

  问题:{question}

input_variables:
  - context
  - question

# 测试用例,方便评估 Prompt 效果
test_cases:
  - context: "Python 的 GIL 是全局解释器锁,同一时刻只允许一个线程执行 Python 字节码。"
    question: "什么是 GIL?"
    expected_keywords: ["全局解释器锁", "线程"]

对应的加载代码:

python
import yaml
from pathlib import Path
from langchain_core.prompts import ChatPromptTemplate
from dataclasses import dataclass
from typing import List, Dict, Any

@dataclass
class PromptConfig:
    version: str
    task: str
    description: str
    system_template: str
    human_template: str
    input_variables: List[str]
    metadata: Dict[str, Any]

def load_prompt_from_yaml(yaml_path: str) -> tuple[PromptConfig, ChatPromptTemplate]:
    """从 YAML 文件加载 Prompt 配置和模板对象"""
    path = Path(yaml_path)
    if not path.exists():
        raise FileNotFoundError(f"Prompt 配置文件不存在:{yaml_path}")

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

    config = PromptConfig(
        version=config_dict["version"],
        task=config_dict["task"],
        description=config_dict.get("description", ""),
        system_template=config_dict["system_template"],
        human_template=config_dict["human_template"],
        input_variables=config_dict["input_variables"],
        metadata={k: v for k, v in config_dict.items()
                  if k not in ("system_template", "human_template", "input_variables")}
    )

    # 根据配置构建 ChatPromptTemplate
    template = ChatPromptTemplate.from_messages([
        ("system", config.system_template),
        ("human", config.human_template),
    ])

    return config, template

# 使用示例
config, template = load_prompt_from_yaml("prompts/qa_answer.yaml")
print(f"加载 Prompt:{config.task} v{config.version}")

messages = template.format_messages(
    context="Redis 支持 String、List、Hash、Set、ZSet 五种基本数据类型。",
    question="Redis 有哪些数据类型?"
)

Prompt 版本管理:Git 追踪

Prompt 文件放到 Git 仓库里,就自动获得了完整的版本历史。最佳实践是把所有 Prompt 文件集中在一个目录:

code
prompts/
├── qa/
│   ├── qa_answer.yaml        # 当前版本
│   └── qa_answer_v1.yaml     # 保留旧版本供对比
├── routing/
│   └── query_router.yaml
├── summarization/
│   └── doc_summary.yaml
└── __init__.py               # 统一导出,供其他模块 import

prompts/__init__.py 集中管理所有模板加载:

python
# prompts/__init__.py
from pathlib import Path
from .loader import load_prompt_from_yaml

PROMPTS_DIR = Path(__file__).parent

# 在模块加载时一次性读取所有 Prompt,避免每次调用都读文件
QA_ANSWER = load_prompt_from_yaml(PROMPTS_DIR / "qa" / "qa_answer.yaml")
QUERY_ROUTER = load_prompt_from_yaml(PROMPTS_DIR / "routing" / "query_router.yaml")
DOC_SUMMARY = load_prompt_from_yaml(PROMPTS_DIR / "summarization" / "doc_summary.yaml")

业务代码里直接 import 使用,不再关心 Prompt 的具体内容:

python
# service/qa_service.py
from prompts import QA_ANSWER

config, template = QA_ANSWER

def answer_question(question: str, context: str) -> str:
    messages = template.format_messages(context=context, question=question)
    return llm.invoke(messages).content

Prompt 生命周期管理流程

效果不符合预期

效果达标

需要调整

通过

测试失败

测试通过

效果下降

效果正常

业务需求变化

修改 YAML 文件

本地测试

git commit

Code Review

合并到主分支

CI 自动运行 Prompt 评估测试

回滚或修复

部署生产

监控线上效果

对比 git log 找到变更

继续迭代

A/B 测试不同 Prompt 版本

版本管理的目的之一是支持 A/B 测试——让两个 Prompt 版本同时运行,用数据决定哪个更好:

python
import random
import time
from dataclasses import dataclass, field
from typing import Dict, List
from langchain_openai import ChatOpenAI
import os

@dataclass
class ABTestResult:
    version: str
    latency_ms: float
    response: str
    # 实际项目中还需要记录用户评分、点赞/踩等反馈
    metadata: Dict = field(default_factory=dict)

class PromptABTester:
    def __init__(self, variants: Dict[str, any], weights: List[float] = None):
        """
        variants: {"v1": template_v1, "v2": template_v2}
        weights: 各版本的流量权重,默认均等分配
        """
        self.variants = variants
        self.weights = weights or [1.0 / len(variants)] * len(variants)
        self.results: List[ABTestResult] = []

    def _select_variant(self) -> str:
        """按权重随机选择一个版本"""
        names = list(self.variants.keys())
        return random.choices(names, weights=self.weights, k=1)[0]

    def run(self, llm, **kwargs) -> ABTestResult:
        """运行一次推断,记录结果"""
        version_name = self._select_variant()
        template = self.variants[version_name]

        messages = template.format_messages(**kwargs)

        start = time.time()
        response = llm.invoke(messages)
        latency = (time.time() - start) * 1000

        result = ABTestResult(
            version=version_name,
            latency_ms=round(latency, 2),
            response=response.content,
        )
        self.results.append(result)
        return result

    def report(self) -> Dict:
        """汇总各版本的统计数据"""
        from collections import defaultdict
        stats = defaultdict(lambda: {"count": 0, "total_latency": 0.0})

        for r in self.results:
            stats[r.version]["count"] += 1
            stats[r.version]["total_latency"] += r.latency_ms

        return {
            version: {
                "count": data["count"],
                "avg_latency_ms": round(data["total_latency"] / data["count"], 2)
            }
            for version, data in stats.items()
        }


# 使用示例
llm = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
    temperature=0,
)

# 加载两个版本的 Prompt
_, template_v1 = load_prompt_from_yaml("prompts/qa/qa_answer.yaml")
_, template_v2 = load_prompt_from_yaml("prompts/qa/qa_answer_v2.yaml")

tester = PromptABTester(
    variants={"v1": template_v1, "v2": template_v2},
    weights=[0.5, 0.5],  # 各 50% 流量
)

# 模拟 10 次请求
test_context = "Python 3.12 引入了 per-interpreter GIL,允许多个子解释器并行执行。"
test_question = "Python 3.12 的 GIL 有什么变化?"

for _ in range(10):
    result = tester.run(llm, context=test_context, question=test_question)
    print(f"[{result.version}] {result.latency_ms}ms | {result.response[:50]}...")

print("\n统计报告:")
print(tester.report())

各方案对比

方案 维护性 版本追踪 团队协作 A/B 测试 实现成本
硬编码在代码里 困难 不支持 最低
代码里的常量 一般 依赖 git diff 一般 手动
YAML 文件 + git 完整 支持
LangSmith Prompt Hub 很好 完整 + UI 很好 内置支持 高(需要服务)

对于大多数项目,YAML 文件(一种人类可读的配置文件格式,用缩进表示层级结构,常用于存储配置信息)+ git 是性价比最高的方案。LangSmith Prompt Hub(LangChain 官方提供的 Prompt 管理平台)提供了 Web UI 和在线编辑功能,适合团队规模较大、Prompt 迭代非常频繁的场景,但引入了外部依赖。

落地步骤:把所有 Prompt 提取到 YAML 文件,集中放在 prompts/ 目录;用 PromptTemplate / ChatPromptTemplate 包装,替代字符串拼接;纳入 git 版本管理,Prompt 变更和代码变更同步追踪;建立评估测试集,Prompt 变更后自动跑评估。

下一篇讲思维链进阶:Tree of Thoughts 和 Self-Consistency,解决标准 CoT 单条推理链容易走偏的问题。

本页目录