提示词是代码的一部分
新手写 AI 应用最常见的做法是用 f-string 拼提示词:
prompt = f"请把下面的文字翻译成英文:{text}"
能跑,但很快会出问题:提示词散落在代码各处、多轮对话的 system 消息没法复用、用户输入里带的花括号会炸、改一句话要翻遍全项目。
ChatPromptTemplate 就是把提示词变成一个可以复用、可以传参、可以测试的对象。
两种写法
from langchain_core.prompts import ChatPromptTemplate
# 单条用户消息
p1 = ChatPromptTemplate.from_template("把下面内容翻译成{lang}:\n{text}")
# 带 system 的完整对话结构(更常用)
p2 = ChatPromptTemplate.from_messages([
("system", "你是一个专业翻译,只输出译文,不要任何解释和引号。"),
("user", "把下面内容翻译成{lang}:\n{text}"),
])
print(p2.invoke({"lang": "英文", "text": "今天天气不错"}))
先单独 invoke 一下模板,把填好的提示词打印出来看一眼——这是调试 AI 应用最有效的一招。很多"模型不听话"其实是提示词根本没填对。
写好 system 提示的五条
- 给角色:"你是一个有十年经验的 Python 面试官",比"你是助手"约束力强得多。
- 给规则,用否定式:"不要解释"、"不要输出 markdown 代码块"。模型很容易加一堆客套话,必须明确禁止。
- 给格式:想要什么形状,就把形状写出来。要 JSON 就把字段列清楚。
- 给例子:一两个示例(few-shot)比十句描述都管用,这是性价比最高的一招。
- 给边界:"如果资料里没有答案,就回答'我不知道',不要编造。"
few-shot:用例子教它
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
examples = [
{"input": "我要退货", "output": "售后"},
{"input": "怎么开发票", "output": "财务"},
{"input": "App 打不开", "output": "技术"},
]
example_prompt = ChatPromptTemplate.from_messages([
("user", "{input}"),
("assistant", "{output}"),
])
few_shot = FewShotChatMessagePromptTemplate(
examples=examples,
example_prompt=example_prompt,
)
final = ChatPromptTemplate.from_messages([
("system", "你是工单分类器。只输出分类名,不要其它任何字。"),
few_shot,
("user", "{input}"),
])
chain = final | llm | StrOutputParser()
print(chain.invoke({"input": "付款失败了"})) # 财务
它的实际效果,等价于把示例伪装成"之前的几轮对话"塞进上下文。模型看到自己"曾经"这么回答过,就会照着格式继续。
分类、抽取、格式转换这三类任务,加 few-shot 几乎总是能显著提升稳定性。
花括号的坑
模板里 {} 是占位符。如果你的提示词本身要出现花括号(比如要求输出 JSON),必须双写转义:
ChatPromptTemplate.from_template(
'按这个格式输出:{{"name": "...", "age": 0}}\n内容:{text}'
)
单写会报 KeyError: '"name"'。这是写 JSON 输出提示时百分百会踩的坑。
把提示词单独存放
项目大了之后,提示词应该从代码里拆出来:
# prompts.py
CLASSIFY_SYSTEM = """你是工单分类器。
可选分类:售后 / 财务 / 技术 / 其它。
只输出分类名本身,不要标点,不要解释。
无法判断时输出:其它"""
好处是改提示词不用碰业务逻辑,也方便做版本对比。提示词要像代码一样管理起来:改动要有理由,效果要能对比。
10 秒自测
想让模型输出 {"ok": true} 这样的字面内容,模板里该怎么写?
双写花括号:{{"ok": true}}。单写会被当成变量占位符。