3. 提示(Prompts)工程:从模板到序列化

提示工程,说白了就是跟大模型打交道的艺术。你给它的提示好不好,直接决定它回你的东西靠不靠谱。我在项目里见过太多人,模型选得挺好,结果提示写得稀烂,最后怪模型不行——其实问题出在自己身上。

LangChain 把提示工程做成了标准化工具。你不用每次都手写提示,也不用担心格式不一致。今天我们就把它拆开揉碎了讲清楚。

3.1 提示模板(PromptTemplate)

先看最基础的。PromptTemplate 就是给提示留几个变量位置,运行时往里填值。举个例子:

from langchain.prompts import PromptTemplate

template = "请用{style}风格,写一段关于{topic}的简介。"
prompt = PromptTemplate(
    input_variables=["style", "topic"],
    template=template
)

result = prompt.format(style="幽默", topic="机器学习")
print(result)
# 输出:请用幽默风格,写一段关于机器学习的简介。

嗯,这里要注意:input_variables 必须跟模板里的变量名完全一致。我刚开始用的时候拼错过一次,查了半天才发现是变量名写了个空格。

我的习惯:模板变量统一用下划线命名,比如 user_name、product_desc。这样不容易跟 Python 关键字冲突,也方便后期维护。

3.2 聊天提示模板(ChatPromptTemplate)

如果你用的是聊天模型(比如 GPT-4、Claude),那 ChatPromptTemplate 更合适。它支持多轮对话结构:

from langchain.prompts import ChatPromptTemplate
from langchain.prompts.chat import SystemMessagePromptTemplate, HumanMessagePromptTemplate

system_template = "你是一个{role}专家,回答要简洁专业。"
human_template = "请解释一下{concept}"

chat_prompt = ChatPromptTemplate.from_messages([
    SystemMessagePromptTemplate.from_template(system_template),
    HumanMessagePromptTemplate.from_template(human_template)
])

messages = chat_prompt.format_messages(
    role="网络安全",
    concept="零信任架构"
)
# 返回两个 Message 对象:system 和 human

我在做客服机器人项目时,就靠这个区分了系统指令和用户输入。系统消息里写规则,用户消息里写问题,模型理解得特别清楚。

消息类型 用途 示例
SystemMessage 设定角色、行为规则 "你是一个耐心的老师"
HumanMessage 用户输入 "什么是梯度下降?"
AIMessage 模型回复 "梯度下降是一种优化算法..."

3.3 示例选择器(ExampleSelector)

少样本提示里,选哪些示例很关键。ExampleSelector 就是帮你做这件事的。它有好几种策略:

  • 固定选择:每次都取同样的示例
  • 随机选择:从池子里随机抽
  • 语义相似度选择:根据输入找最像的示例

我个人最常用的是语义相似度选择器。它会把你的输入跟示例库里的每条记录算相似度,挑最相关的几条。效果比随机选好太多了。

from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS

examples = [
    {"input": "苹果", "output": "水果,红色或绿色"},
    {"input": "香蕉", "output": "水果,黄色,长条形"},
    {"input": "汽车", "output": "交通工具,四个轮子"},
]

selector = SemanticSimilarityExampleSelector.from_examples(
    examples,
    OpenAIEmbeddings(),
    FAISS,
    k=2
)

selected = selector.select_examples({"input": "草莓"})
# 会返回"苹果"和"香蕉"这两个示例,因为它们跟"草莓"语义更近
我曾经踩过的坑:示例库太大时,每次计算相似度会很慢。建议给示例库加个缓存,或者限制 k 值不超过 5。否则用户等得心焦。

3.4 少样本提示(FewShotPromptTemplate)

FewShotPromptTemplate 把示例选择器和提示模板组合在一起。你给它几个例子,它就能让模型照着例子来回答。

from langchain.prompts import FewShotPromptTemplate, PromptTemplate

example_prompt = PromptTemplate(
    input_variables=["input", "output"],
    template="输入: {input}\n输出: {output}"
)

few_shot_prompt = FewShotPromptTemplate(
    examples=examples,  # 就是上面那个示例列表
    example_prompt=example_prompt,
    prefix="请根据以下示例,为新的输入生成输出:",
    suffix="输入: {new_input}\n输出:",
    input_variables=["new_input"]
)

result = few_shot_prompt.format(new_input="西瓜")
print(result)
# 输出:
# 请根据以下示例,为新的输入生成输出:
# 输入: 苹果
# 输出: 水果,红色或绿色
# 输入: 香蕉
# 输出: 水果,黄色,长条形
# 输入: 西瓜
# 输出:

你看,它自动把示例拼到了提示里。模型看到前面两个例子,就知道该输出什么格式了。这比直接问"西瓜是什么"要稳定得多。

核心要点:少样本提示不是越多越好。3-5 个高质量示例,比 20 个凑数的效果好。我一般先试 3 个,效果不够再加。

3.5 提示序列化与加载

项目做大了,提示模板不能都写在代码里。得存成文件,方便管理和复用。LangChain 支持序列化成 JSON 或 YAML:

# 保存为 JSON
prompt.save("my_prompt.json")

# 从 JSON 加载
from langchain.prompts import load_prompt
loaded_prompt = load_prompt("my_prompt.json")

# 保存为 YAML
with open("my_prompt.yaml", "w") as f:
    f.write(prompt.to_yaml())

我习惯把提示模板按功能分目录存:

  • prompts/summary/ — 摘要类提示
  • prompts/qa/ — 问答类提示
  • prompts/code/ — 代码生成类提示

这样团队协作时,谁改了什么一目了然。而且不同环境(开发、测试、生产)可以加载不同版本的提示文件。

小技巧:序列化时记得把版本号写进文件名,比如 summary_v2.json。回滚的时候特别方便。

知识结构总览

下面这张图把本章的知识点串起来了。你可以看到提示工程在 LangChain 里的完整脉络:

LangChain 提示工程知识结构 提示工程核心 PromptTemplate ChatPromptTemplate FewShotPromptTemplate 变量替换 多轮消息结构 示例选择器 语义相似度选择 序列化与加载 提示工程 = 模板 + 示例 + 序列化,三者缺一不可

从图上你能看到,提示工程不是孤立的知识点。模板负责结构,示例负责引导,序列化负责管理。三块配合好了,你的提示质量才能稳定。