1. Prompt 模板与提示词工程概述
在大语言模型(LLM)应用开发中,Prompt(提示词)是用户与模型之间沟通的桥梁。而提示词工程(Prompt Engineering)则是通过精心设计 Prompt 来引导模型生成高质量输出的实践方法论。随着 LangChain 等框架的普及,Prompt 模板成为工程化构建 Prompt 的核心工具——它让我们从手工拼接字符串中解放出来,实现可复用、可维护的提示词管理。
同样重要的还有结构化输出:当我们需要模型返回 JSON、特定字段或固定格式的数据时,Output Parser就派上了用场。它负责将模型生成的原始文本解析为程序可直接消费的结构化对象,打通"模型输出 → 业务代码"的最后一公里。
本文将从 PromptTemplate 的基本使用讲起,逐步深入到 ChatPromptTemplate、Prompt 编写技巧、实战案例,再到结构化输出与 Output Parser 的完整实践,帮助你系统掌握这两个关键技能。
2. 什么是 Prompt?为什么需要 Prompt 模板?
Prompt就是你发给大语言模型的输入文本。它可以是一句话、一段指令,也可以是一组对话消息。Prompt 的质量直接决定了模型输出的质量——同样的模型,不同 Prompt 得到的回答可能天差地别。
但在实际项目中,我们往往需要动态构建Prompt:根据用户输入、业务上下文动态填充某些变量。比如电商场景中,商品名称、卖点、风格要求都是变化的。如果每次都用 f-string 拼接:
prompt = f"请为{product}生成一段{style}风格的营销文案,突出{feature}卖点"这样做有几个明显的问题:
- 难以维护:Prompt 模板散落在各处,修改时需要全局搜索
- 容易出错:变量多了以后,引号、换行、缩进很容易搞混
- 无法复用:每个场景都要重写拼接逻辑
- 缺乏验证:没法自动检查 Prompt 中是否遗漏了必填变量
因此,LangChain 提供了PromptTemplate和ChatPromptTemplate这两个核心类,让我们用声明式的方式管理 Prompt。
3. PromptTemplate 基本使用
PromptTemplate是最基础的 Prompt 模板类,适用于纯文本补全模型或只需要一个字符串作为输入的场景。它的使用非常简单:
from langchain_core.prompts import PromptTemplate 定义模板,用 {变量名} 占位 template = "请用{language}语言写一段代码,实现{functionality}功能" prompt = PromptTemplate( template=template, input_variables=["language", "functionality"] ) 填充变量,得到最终 Prompt final_prompt = prompt.format( language="Python", functionality="快速排序" ) print(final_prompt) 输出:请用Python语言写一段代码,实现快速排序功能核心要点:
- 使用{变量名}作为占位符
- input_variables声明需要哪些变量,LangChain 会做校验
- 调用.format()即可得到最终 Prompt 字符串
- 如果变量缺失,会直接报错,避免静默失败
4. ChatPromptTemplate 基本使用
现代大模型(如 GPT-4、Claude)大多是对话模型,输入输出以"消息列表"的形式组织。每条消息包含role(角色)和content(内容),常见的角色有:
- system:系统级指令,设定模型的行为和角色
- human:用户输入
- ai:模型回复(用于多轮对话的历史记录)
ChatPromptTemplate就是为这种消息列表结构设计的:
from langchain_core.prompts import ChatPromptTemplate chat_prompt = ChatPromptTemplate.from_messages([ ("system", "你是一位经验丰富的{role}工程师,擅长{skill}。"), ("human", "请解释一下{concept}的核心原理,用通俗易懂的方式。"), ]) messages = chat_prompt.format_messages( role="后端", skill="系统架构设计", concept="消息队列" ) for msg in messages: print(f"[{msg.type}] {msg.content}") [system] 你是一位经验丰富的后端工程师,擅长系统架构设计。 [human] 请解释一下消息队列的核心原理,用通俗易懂的方式。5. 核心本质差异
很多初学者容易混淆 PromptTemplate 和 ChatPromptTemplate,这里用一张对比表说清楚:
# PromptTemplate:只生成一个字符串 prompt = PromptTemplate(template="你好,{name}!", input_variables=["name"]) result = prompt.format(name="小明") # 返回 str: "你好,小明!" ChatPromptTemplate:生成消息列表 chat_prompt = ChatPromptTemplate.from_messages([ ("system", "你是助手"), ("human", "你好,{name}!"), ]) result = chat_prompt.format_messages(name="小明") # 返回 List[BaseMessage]| 维度 | PromptTemplate | ChatPromptTemplate |
|---|---|---|
| 输出类型 | 字符串(str) | 消息列表(List[BaseMessage]) |
| 适用场景 | 旧版补全模型、简单文本 | 对话模型(GPT/Claude 等) |
| 结构 | 单一文本块 | system/human/ai 多角色 |
| 多轮对话支持 | 需手动拼接历史 | 天然支持 MessagesPlaceholder |
| 推荐程度 | 了解即可 | 建议优先使用 |
一句话总结:除非你确定在用一个只接受字符串的老模型,否则直接用 ChatPromptTemplate,它更现代、更灵活,也是 LangChain 官方推荐的方式。
6. Prompt 编写建议
写好 Prompt 是一门手艺活。下面四条建议来自大量实战总结,能显著提升模型输出的质量和稳定性。
6.1 明确角色
给模型设定一个具体、专业的角色,能大幅提升回答的专业度和风格匹配度。
# ❌ 模糊 "帮我写一份简历" ✅ 明确角色 "你是一位有10年经验的资深HR和职业规划师,擅长为技术岗位优化简历。"角色越具体,模型越容易"进入状态"。可以结合领域、经验年限、技能特长来刻画角色。
6.2 明确任务
任务描述要具体、可操作,避免笼统的指令。
# ❌ 笼统 "分析一下这段代码" ✅ 具体 "分析下面这段 Python 代码,从以下三个角度给出建议: 性能瓶颈 潜在 Bug 代码风格改进"用编号列表明确任务维度,既能引导模型思考,也方便你验收结果。
6.3 明确约束
告诉模型不要做什么,往往和告诉它要做什么同样重要。
# 常用的约束示例 "请遵守以下约束: - 回答长度控制在 200 字以内 - 不要使用专业术语,用大白话解释 - 如果问题超出你的知识范围,直接说「我不确定」,不要编造 - 输出格式为 Markdown"6.4 给出输入字段
在模板中明确标注哪些是动态输入,并使用清晰易懂的变量名。
chat_prompt = ChatPromptTemplate.from_messages([ ("system", "你是{company}的{role},风格{style}。"), ("human", "产品名称:{product_name}\n目标人群:{audience}\n核心卖点:{selling_point}\n请生成一段营销文案。"), ])变量名要见名知意,比如用 product_name 而不是 p1,用 audience 而不是 aud。这样不仅你自己好维护,后续团队成员也能快速理解。
7. 实战案例一:商品文案生成器
综合运用上述技巧,我们来做一个完整的商品文案生成器:
from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI 1. 定义 Prompt 模板 prompt = ChatPromptTemplate.from_messages([ ("system", """你是一位资深电商文案策划,有8年品牌营销经验。 你擅长根据产品特点创作富有感染力的营销文案。 风格要求:{style}"""), ("human", """请为以下商品生成营销文案: 商品名称:{product_name} 目标人群:{audience} 核心卖点:{selling_point} 额外要求:{requirements}"""), ]) 2. 填充变量 messages = prompt.format_messages( style="简洁有力,口语化,带emoji", product_name="云感记忆枕", audience="25-40岁久坐办公人群", selling_point="3D分区承托,自适应颈椎曲线,透气凝胶材质", requirements="文案分两段:第一段戳痛点,第二段讲解决方案。总共不超过150字。" ) 3. 调用模型 llm = ChatOpenAI(model="gpt-4o", temperature=0.7) response = llm.invoke(messages) print(response.content)模型输出示例:
每天对着屏幕10小时,脖子僵得像根钢筋?翻来覆去睡不着,早上起来肩膀更酸了?😫
云感记忆枕,3D分区承托你的每一寸颈椎曲线,自适应贴合不悬空。透气凝胶材质整晚清凉不闷汗,让你一觉醒来像做了SPA一样轻松~ ☁️💤
这个案例把角色、任务、约束、输入字段四条建议全部落地了。模板里变量清晰、结构分明,改一个参数就能适配不同商品。
8. 减少重复代码:封装模型初始化
在实际项目中,如果每个功能都要写一遍初始化模型 → 定义模板 → 填充变量 → 调用,代码会非常冗余。最佳实践是封装一个工厂函数:
from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI class LLMChain: """轻量级 Chain 封装,减少样板代码""" def __init__(self, model_name="gpt-4o", temperature=0.7): self.llm = ChatOpenAI(model=model_name, temperature=temperature) def create_chain(self, system_template, human_template): """根据 system 和 human 模板创建可调用的链""" prompt = ChatPromptTemplate.from_messages([ ("system", system_template), ("human", human_template), ]) return prompt | self.llm # LCEL 链式调用 使用示例:一行创建,一行调用 chain = LLMChain().create_chain( system_template="你是{role}专家。", human_template="请解释{concept}。" ) result = chain.invoke({"role": "数据库", "concept": "索引优化"}) print(result.content)使用LCEL(LangChain Expression Language)的管道操作符|,可以把 Prompt 和模型串成一个可调用的链。后续所有案例都可以复用这个封装,代码量减少 70% 以上。
9. 实战案例二:学习计划生成器
这个案例展示如何用更复杂的约束和多维度输入来生成个性化内容:
prompt = ChatPromptTemplate.from_messages([ ("system", """你是一位专业的学习规划师,擅长为不同背景的学习者定制学习路径。 请严格遵循以下规则: 根据学习者当前的{current_level}水平制定计划 总学习周期为{duration}周 每天可用学习时间约{hours_per_day}小时 最终目标:{goal} 输出格式:按周列出学习主题和关键任务,使用 Markdown 格式"""), ("human", "请为我想学习{subject}制定一份详细的学习计划。"), ]) messages = prompt.format_messages( subject="Python 数据分析", current_level="有其他编程语言基础,但 Python 零基础", duration="4", hours_per_day="2", goal="能够独立完成数据清洗、可视化和基础统计分析,做出可交付的数据报告" ) response = llm.invoke(messages)这里的关键是把约束条件模板化。current_level、duration、hours_per_day、goal 这些变量在不同学员之间各不相同,但 Prompt 结构是稳定的。模板化之后,你甚至可以用它来批量生成学习计划。
10. 实战案例三:客服回复生成器
客服场景除了要生成回复,往往还需要情绪判断和上下文理解。这个案例还展示了如何把MessagesPlaceholder融入真实业务:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder customer_service_prompt = ChatPromptTemplate.from_messages([ ("system", """你是{company_name}的客服代表。 回复风格:{style} 公司政策:{policy} 请根据用户问题和历史对话,生成专业、友善的回复。 如果问题无法解决,请引导用户拨打客服热线 400-xxx-xxxx。"""), MessagesPlaceholder(variable_name="history"), # 历史对话 ("human", "{user_query}"), ]) 使用示例 messages = customer_service_prompt.format_messages( company_name="云笔记科技", style="亲切但不啰嗦,每句话不超过30字", policy="7天无理由退货,30天换货,1年保修", history=[], # 第一轮对话,暂时为空 user_query="我昨天买的 Pro 版会员,为什么还是不能用 AI 功能?" ) response = llm.invoke(messages)这个模板在真实场景中可以直接对接对话历史管理系统。每轮对话把历史消息存起来,下一轮丢进 history 变量,模型就能"记住"之前的上下文。
11. MessagesPlaceholder 详解
MessagesPlaceholder是 ChatPromptTemplate 中的一个特殊占位符,用于动态插入一组消息(而不是一个字符串)。它的典型应用场景有:
- 多轮对话历史:把之前的 human/ai 消息列表放进去,让模型记住上下文
- Few-shot 示例:插入若干条示例对话,教模型如何回答
- 动态指令:根据业务条件动态决定是否添加某条 system 消息
Few-shot 示例的用法:
examples = [ ("human", "我的产品有质量问题"), ("ai", "很抱歉给您带来不便。请问具体是什么问题呢?我会尽快为您处理。"), ("human", "收到后屏幕有坏点"), ("ai", "非常抱歉!根据我们的售后政策,您可以申请换货。请提供订单号,我马上为您办理。"), ] few_shot_prompt = ChatPromptTemplate.from_messages([ ("system", "你是专业客服。参考以下示例来回复用户。"), MessagesPlaceholder(variable_name="examples"), # 示例对话 ("human", "{user_query}"), ]) messages = few_shot_prompt.format_messages( examples=examples, user_query="我买的键盘有几个键不灵敏" )注意:MessagesPlaceholder 的 variable_name 必须在 format_messages 时传入一个消息列表(List[tuple] 或 List[BaseMessage]),不能传字符串。
12. 本章重点回顾
在进入结构化输出部分之前,先快速回顾 Prompt 模板的核心要点:
- ChatPromptTemplate 优先:现代对话模型的最佳搭档,支持多角色消息
- 变量用 {变量名} 占位:声明 input_variables 做校验,避免遗漏
- 编写四步法:明确角色 → 明确任务 → 明确约束 → 给出输入字段
- MessagesPlaceholder:动态插入消息列表,支持多轮对话和 Few-shot
- 封装复用:用工厂函数或 LCEL 链减少样板代码
常见问题
Q: Prompt 越长越好吗?
不是。Prompt 越长,模型的注意力越分散,容易"遗忘"中间的指令。最佳实践是简短有力:角色设定 1-2 句,任务描述 2-3 句,约束条件用列表形式,输入字段集中放在末尾。如果确实需要大量上下文,优先使用 RAG 把资料放在检索结果里,而不是全部塞进 Prompt。
Q: 为什么模型没有完全按要求输出?
常见原因有几个:
- 指令不够明确:"写一篇好文章"和"写一篇 800 字的技术教程,包含 3 个代码示例",效果天差地别
- 约束之间冲突:比如同时要求"详细解释"和"在 50 字以内",模型会无所适从
- 模型能力限制:小模型在复杂指令上的遵循度天然低于大模型,需要调整期望
- 输出格式未显式要求:如果你需要 JSON,务必在 Prompt 里说"请返回 JSON 格式",并通过 Output Parser 约束——这正是下一章要讲的内容
13. 结构化输出:为什么要用 Output Parser?
到目前为止,我们的模型输出都是自由文本——一段话、一篇文章。但在实际业务中,我们往往需要模型返回结构化数据,比如:
- 从简历中抽取姓名、电话、工作经历(字典)
- 对商品评论输出情感、评分、关键词(JSON)
- 从合同文本中提取甲方、乙方、金额、日期(Pydantic 模型)
如果直接让模型"返回 JSON",结果可能是:
# 模型输出的原始文本 "好的,这是抽取结果:\n{\n "name": "张三",\n "phone": "138xxxx"\n}\n希望对你有所帮助!"这种包裹了额外文字的 JSON 无法直接用 json.loads() 解析。这就是Output Parser要解决的问题——把模型的原始输出清洗、提取、校验、转换为程序可消费的数据结构。
14. StrOutputParser:最简单的解析器
StrOutputParser是最基础的解析器,它做的事情很简单:把模型返回的 AIMessage 对象提取出纯文本内容。
from langchain_core.output_parsers import StrOutputParser 构建链:Prompt → 模型 → 解析器 chain = prompt | llm | StrOutputParser() 直接得到字符串,不需要 .content result = chain.invoke({"text": "人工智能的未来发展趋势"}) print(type(result)) # <class 'str'> print(result) # 直接是文本内容在 LCEL 链中,StrOutputParser 通常作为最后一个节点,使得下游代码直接拿到字符串,无需关心消息对象的内部结构。
案例:文本总结
summarize_prompt = ChatPromptTemplate.from_messages([ ("system", "请用一句话总结以下文本的核心内容。"), ("human", "{text}"), ]) summarize_chain = summarize_prompt | llm | StrOutputParser() summary = summarize_chain.invoke({ "text": "LangChain 是一个用于构建 LLM 应用的开源框架,它提供了 Prompt 管理、Chain 编排、Agent 调度等核心能力,大幅降低了开发门槛。" }) print(summary) LangChain是一个简化LLM应用开发的开源框架,提供Prompt管理、Chain编排和Agent调度等核心功能。15. 使用 Pydantic 定义输出结构
当输出结构比较复杂时,用Pydantic定义数据模型是最佳选择。Pydantic 提供类型校验、默认值、字段描述,LangChain 能自动把字段描述注入 Prompt,引导模型按格式输出。
from pydantic import BaseModel, Field from typing import List, Optional class ResumeInfo(BaseModel): """简历信息结构""" name: str = Field(description="求职者姓名") phone: str = Field(description="手机号码") email: Optional[str] = Field(default=None, description="电子邮箱") education: List[str] = Field(description="教育经历列表,每项包含学校、专业、学位") skills: List[str] = Field(description="技能标签列表") work_experience: List[str] = Field(description="工作经历列表")Field 中的description会被自动取出来告诉模型,这是结构化输出的关键——让模型理解每个字段的含义和期望的值类型。
16. PydanticOutputParser 完整案例:简历信息抽取
from langchain_core.output_parsers import PydanticOutputParser from langchain_core.prompts import ChatPromptTemplate from pydantic import BaseModel, Field from typing import List class ResumeInfo(BaseModel): name: str = Field(description="求职者姓名") phone: str = Field(description="手机号码") skills: List[str] = Field(description="技能列表") 1. 创建解析器 parser = PydanticOutputParser(pydantic_object=ResumeInfo) 2. 获取格式化指令(自动生成!) format_instructions = parser.get_format_instructions() print(format_instructions) 输出类似: The output should be formatted as a JSON instance that conforms to the JSON schema below. {"properties": {"name": {"description": "求职者姓名", "type": "string"}, ...}} 3. 把格式化指令注入 Prompt prompt = ChatPromptTemplate.from_messages([ ("system", "你是简历解析助手。\n{format_instructions}"), ("human", "请从以下简历文本中抽取信息:\n{resume_text}"), ]) 4. 构建链 chain = prompt | llm | parser # parser 会把 JSON 解析为 ResumeInfo 对象 5. 调用 result = chain.invoke({ "format_instructions": format_instructions, "resume_text": """ 张三,手机13812345678,熟练掌握Python、LangChain、FastAPI, 有3年后端开发经验。擅长系统架构设计和性能优化。 """ }) print(type(result)) # <class 'ResumeInfo'> print(result.name) # 张三 print(result.phone) # 13812345678 print(result.skills) # ['Python', 'LangChain', 'FastAPI', '系统架构设计', '性能优化']关键步骤:创建 Pydantic 模型 → 创建 PydanticOutputParser → 获取格式化指令 → 注入 Prompt → 链式调用。parser.get_format_instructions() 会自动生成 JSON Schema 描述,省去了手动编写"请返回以下 JSON 格式"的麻烦。
17. with_structured_output:更简洁的方式
LangChain 还提供了with_structured_output方法,它把"要求模型按指定结构输出"的指令直接嵌入模型调用层,语法更简洁:
# 方式一:直接用 Pydantic 模型 structured_llm = ChatOpenAI(model="gpt-4o").with_structured_output(ResumeInfo) 不需要手动写 Prompt 模板,不需要 get_format_instructions result = structured_llm.invoke(""" 李四,电话13987654321,精通Java、Spring Boot、MySQL, 5年电商系统开发经验,曾主导双11大促系统架构。 """) print(type(result)) # <class 'ResumeInfo'> print(result.name) # 李四你甚至可以把 Prompt 模板和结构化输出组合使用:
structured_llm = ChatOpenAI(model="gpt-4o").with_structured_output(ResumeInfo) prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个简历解析助手,请从以下文本中提取信息。"), ("human", "{resume_text}"), ]) chain = prompt | structured_llm # 链末端是结构化 LLM result = chain.invoke({"resume_text": "王五,电话18800001111,擅长React、TypeScript..."})18. 实战案例三:商品评论分析
这个案例综合使用 PydanticOutputParser,对商品评论进行情感分析 + 结构化提取:
from pydantic import BaseModel, Field from typing import List, Literal from langchain_core.output_parsers import PydanticOutputParser from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate 1. 定义输出结构 class ReviewAnalysis(BaseModel): sentiment: Literal["positive", "negative", "neutral"] = Field( description="评论情感倾向:positive 正面,negative 负面,neutral 中性" ) score: int = Field(description="评分 1-5 分", ge=1, le=5) keywords: List[str] = Field(description="从评论中提取的关键词列表") summary: str = Field(description="一句话总结评论核心观点") actionable: bool = Field(description="是否需要客服跟进处理") 2. 创建解析器和链 parser = PydanticOutputParser(pydantic_object=ReviewAnalysis) llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_messages([ ("system", "你是电商评论分析助手。\n{format_instructions}"), ("human", "请分析以下商品评论:\n{review}"), ]) chain = prompt | llm | parser 3. 分析 result = chain.invoke({ "format_instructions": parser.get_format_instructions(), "review": "用了两周了,续航确实不错,屏幕也清晰。就是充电器发热有点厉害,有点担心安全问题。总体来说还行吧。", }) print(f"情感: {result.sentiment}") print(f"评分: {result.score}") print(f"关键词: {result.keywords}") print(f"总结: {result.summary}") print(f"需跟进: {result.actionable}")模型输出结果:
# 情感: neutral # 评分: 3 # 关键词: ['续航', '屏幕', '充电器', '发热', '安全'] # 总结: 续航和屏幕表现好,但充电器发热存在安全隐患,整体满意但有顾虑。 # 需跟进: True19. 两种结构化方式怎么选
| 维度 | PydanticOutputParser | with_structured_output |
|---|---|---|
| 实现方式 | 通过 Prompt 注入 JSON Schema 指令 | 利用模型原生 Function Calling 能力 |
| 依赖 | 任何模型,只依赖文本生成能力 | 需要模型支持 Tool Calling / JSON Mode |
| 可靠性 | 中等,模型可能输出不合规 JSON | 高,模型直接返回结构化数据 |
| 灵活性 | 高,可自定义格式指令和解析逻辑 | 中,依赖模型厂商实现 |
| 进度提示 | 需要在 Prompt 中手动描述格式 | 自动处理,代码更简洁 |
| 推荐场景 | 小众模型、需要精细控制格式时 | GPT-4/Claude 等主流模型,首选 |
建议:如果你用的是 GPT-4、Claude 等支持结构化输出的模型,优先使用with_structured_output,代码更少、可靠性更高。如果模型不支持或者你需要非常定制的输出格式,再使用 PydanticOutputParser。
20. 处理解析错误
使用 PydanticOutputParser 时,模型偶尔会输出不合规的 JSON(比如多了前后文、少了引号),导致解析失败。需要用OutputFixingParser来自动修复:
from langchain.output_parsers import OutputFixingParser 创建修复解析器:包装原始 parser + 一个 LLM 用于修复 fixing_parser = OutputFixingParser.from_llm( llm=ChatOpenAI(model="gpt-4o", temperature=0), parser=parser, # 原始的 PydanticOutputParser ) 使用修复解析器替代原始解析器 chain = prompt | llm | fixing_parser 即使模型输出格式有小问题,fixing_parser 也会用 LLM 自动修复后重新解析它的工作原理是:如果第一次解析失败,就把原始输出 + 错误信息 + 期望的格式一起发给 LLM,让 LLM 修复格式后重新解析。这在生产环境中非常实用,能显著降低解析失败率。
21. 总结
本文从 Prompt 模板和结构化输出两条主线出发,覆盖了 LangChain 提示词工程的核心知识点:
- PromptTemplate vs ChatPromptTemplate:前者返回字符串,后者返回消息列表,优先用后者
- 编写四步法:明确角色、明确任务、明确约束、给出输入字段
- MessagesPlaceholder:动态插入消息列表,支持多轮对话和 Few-shot
- 封装复用:用 LLMChain 封装或 LCEL 管道减少样板代码
- 结构化输出:用 Pydantic 定义数据模型,Output Parser 做解析校验
- 两种方式对比:主流模型优先用 with_structured_output,需精细控制或用小众模型时用 PydanticOutputParser
- 错误处理:OutputFixingParser 自动修复解析失败,提升鲁棒性
掌握这些技能后,你就能工程化地构建 LLM 应用——Prompt 模板化管理,输出结构化消费,让模型真正融入业务流水线。