010、LangChain之Prompt Templates:模板化你的提示词
上周调一个客服对话系统,发现同样的用户问题,模型回答质量忽高忽低。排查了半天,问题出在提示词上——每次拼接用户输入时,格式不一致,有的带标点,有的没上下文,模型根本不知道自己在扮演什么角色。这种“手写提示词”的痛,做过LLM应用的人应该都懂。
为什么需要模板化
先看一个反面教材。假设你要做一个翻译助手,最原始的写法可能是这样:
prompt=f"请将以下英文翻译成中文:{user_input}"看起来没问题?但当你需要加入角色设定、输出格式、few-shot示例时,代码会迅速膨胀成面条式拼接:
prompt=f"你是一个专业的翻译助手。请将以下英文翻译成中文。要求:1. 保持原意 2. 符合中文表达习惯。\n英文:{user_input}\n中文:"这里踩过坑:一旦需求变更,比如要支持多语言、要加入历史对话,你就要在所有调用处改字符串拼接逻辑。更可怕的是,不同开发者的拼接风格不同,有人加换行,有人不加,模型行为完全不可控。
LangChain的PromptTemplate
LangChain的PromptTemplate就是来解决这个问题的。它把提示词拆成“固定部分”和“变量部分”,类似模板引擎的思想。
基础用法
fromlangchain.promptsimportPromptTemplate# 别这样写:直接在字符串里写死格式# prompt = f"翻译:{text}"# 正确姿势:定义模板template="请将以下{source_lang}翻译成{target_lang}:\n{text}\n{target_lang}:"prompt_template=PromptTemplate(input_variables=["source_lang","target_lang","text"],template=template)# 使用时传入变量formatted_prompt=prompt_template.format(source_lang="英文",target_lang="中文",text="Hello, world!")print(formatted_prompt)# 输出:# 请将以下英文翻译成中文:# Hello, world!# 中文:注意这里有个坑:input_variables必须和模板中的变量名完全一致,少一个或多一个都会报错。我刚开始写的时候经常漏掉,后来养成习惯:先写模板字符串,再对照着写变量列表。
带few-shot的模板
实际项目中,我们经常要给模型看几个例子。用PromptTemplate可以优雅地组织:
template="""你是一个{role}。请根据以下示例回答问题。 示例1: 问题:{example1_question} 回答:{example1_answer} 示例2: 问题:{example2_question} 回答:{example2_answer} 现在请回答: 问题:{question} 回答:"""prompt_template=PromptTemplate(input_variables=["role","example1_question","example1_answer","example2_question","example2_answer","question"],template=template)这里踩过坑:few-shot示例的数量如果太多,模板会变得非常长。建议把示例做成列表,用循环生成,而不是硬编码在模板里。后面会讲到更高级的FewShotPromptTemplate。
ChatPromptTemplate:对话场景专用
如果你用的是Chat模型(比如GPT-4、Claude),普通的PromptTemplate就不够用了。对话模型需要区分System、Human、AI消息。
fromlangchain.promptsimportChatPromptTemplate,HumanMessagePromptTemplate,SystemMessagePromptTemplate# 别这样写:把所有内容塞进一个HumanMessage# messages = [HumanMessage(content=f"你是一个助手。用户说:{user_input}")]# 正确姿势:分角色定义system_template="你是一个{role},擅长{skill}。请用{style}的风格回答。"human_template="{user_input}"chat_prompt=ChatPromptTemplate.from_messages([SystemMessagePromptTemplate.from_template(system_template),HumanMessagePromptTemplate.from_template(human_template)])# 使用时传入变量messages=chat_prompt.format_messages(role="技术顾问",skill="Python编程",style="简洁专业",user_input="如何优化列表推导式?")这里有个经验:System消息里放角色设定和全局约束,Human消息里放用户输入。AI消息通常由模型生成,不需要模板化。如果你需要给模型看历史对话,可以加入AIMessagePromptTemplate。
模板中的高级技巧
部分变量(Partial Variables)
有时候你有一些固定不变的变量,比如系统版本号、当前日期。每次都传一遍很烦人:
fromdatetimeimportdatetime template="当前时间:{date}\n用户问题:{question}"prompt_template=PromptTemplate(template=template,input_variables=["question"],partial_variables={"date":datetime.now().strftime("%Y-%m-%d")})# 调用时只需要传questionresult=prompt_template.format(question="今天天气怎么样?")注意:partial_variables里的变量不能出现在input_variables中,否则会冲突。这个坑我踩过两次才记住。
模板验证(Template Validation)
LangChain默认会检查模板中的变量是否都在input_variables里声明了。但如果你用了from_template方法,它会自动解析变量名:
# 自动解析,不需要手动写input_variablesprompt_template=PromptTemplate.from_template("你好,{name}!今天{weather}。")print(prompt_template.input_variables)# ['name', 'weather']这个特性很方便,但有个隐患:如果你的模板里不小心写了拼写错误的变量名(比如{nmae}),它不会报错,只是当成普通文本。建议写完模板后手动检查一下。
实战:构建一个可复用的问答模板
假设你要做一个技术问答系统,需要支持多轮对话和上下文记忆。这里给出一个我实际项目中的模板设计:
fromlangchain.promptsimportChatPromptTemplate,MessagesPlaceholder# MessagesPlaceholder用于插入历史消息列表chat_prompt=ChatPromptTemplate.from_messages([("system","你是一个{domain}专家。请基于以下知识库回答用户问题:\n{knowledge_base}"),MessagesPlaceholder(variable_name="history"),("human","{input}")])# 使用时fromlangchain.schemaimportHumanMessage,AIMessage messages=chat_prompt.format_messages(domain="嵌入式开发",knowledge_base="STM32F4系列主频168MHz,有1MB Flash...",history=[HumanMessage(content="什么是DMA?"),AIMessage(content="DMA是直接内存访问...")],input="DMA和中断有什么区别?")这里踩过坑:MessagesPlaceholder的variable_name必须和传入的变量名一致。而且历史消息必须是BaseMessage对象列表,不能是字符串列表。很多新手在这里翻车。
个人经验建议
模板和逻辑分离:把模板定义放在单独的配置文件或YAML文件里,不要硬编码在Python代码中。这样产品经理改提示词时,不需要动代码。
版本控制模板:每次修改模板都记录版本号,因为模型对提示词极其敏感,改一个标点都可能影响输出质量。我习惯在模板开头加注释:
# v1.2 2024-03-15 增加了输出格式约束测试模板边界:写单元测试验证模板在各种输入下的表现。比如空字符串、超长文本、特殊字符。我遇到过用户输入包含
{}导致模板解析失败的情况。不要过度模板化:如果模板变量超过10个,说明你的提示词设计可能有问题。考虑拆分成多个子模板,或者用更结构化的方式(比如JSON格式)组织信息。
留意模板长度:模板本身也会消耗token。把固定部分(比如角色设定、few-shot示例)缓存起来,不要每次都重新计算。LangChain的
partial_variables可以帮你做这件事。
最后说一句:PromptTemplate看起来简单,但它是整个LLM应用的基石。模板设计得好,模型表现稳定;模板设计得烂,后面所有优化都是白费力气。花时间把模板打磨好,比调什么超参数都管用。