LangChain提示词工程从入门到实践:模板化、变量管理与Few-shot示例
1. 从第一版提示词到可维护的提示词工程先说我自己的真实经历。最早用大模型写应用提示词就是一个超长的字符串直接怼在model.invoke()里。第一版跑通了挺爽。第二天需求变了要把系统提示里的角色名从“客服助手”换成“资深销售顾问”我全局搜替换的时候心里就在犯嘀咕。真正让我下定决心用模板系统管理的是一次调接口的意外。明明换了模型输出格式却乱得离谱排查半天发现是几个Prompt里有两个空格不一致导致few-shot示例的结构识别错了。那时候我才意识到提示词一旦超过50行字符串拼接的方式就已经失控了它应该被当作代码一样去管理。LangChain的Prompts模块解决的就是这个问题。它不是一个花哨的“魔法提示词生成器”而是一套帮你把提示词从“一次性草稿”变成“可复用、可测试、可版本化的工程资产”的机制。核心就三件事模板化、变量管理、分角色组装。这篇文章我直接分享我在项目里实际用下来的经验包括模板怎么设计不踩坑、ChatPromptTemplate比普通Template好在哪、以及几个让我浪费了一整天的疑难杂症。如果你是刚接触LangChain或者写过一段时间但总觉得提示词管理很别扭的开发这篇文章应该能帮上忙。2. 为什么提示词需要模板而不是字符串拼接2.1 模板化的核心动机变与不变分离先想一个场景。你在做一个“简历优化助手”系统提示基本固定只有用户上传的简历内容和目标岗位是动态的。用传统方式写prompt f 你是一个专业的简历优化助手请根据以下岗位要求优化用户提供的简历内容。 岗位要求 {job_requirement} 用户简历 {resume_text} 请输出优化后的完整简历。 这段代码能跑但有两个隐患。第一变量嵌套顺序完全依赖人肉维护。等你在这个字符串里加了五六个变量谁先谁后、哪里漏了花括号出了问题只能靠肉眼debug。第二模板和业务逻辑耦合。模板一旦需要调整措辞你得在Python代码里改字符串而这段代码可能已经被别的业务逻辑层层包围。我见过有人把提示词模板直接写在了API路由函数里每次改文案都要动业务代码风险极高。模板化的思路就是把“固定不变的部分”和“会变的部分”分离。固定部分是骨架变量是插槽。LangChain里的PromptTemplate干的就是这件事from langchain_core.prompts import PromptTemplate template 你是一个专业的简历优化助手。 请根据以下岗位要求优化用户提供的简历内容。 岗位要求 {job_requirement} 用户简历 {resume_text} 请输出优化后的完整简历。 prompt PromptTemplate.from_template(template)注意from_template这个类方法。它会自动解析模板字符串里的{变量名}生成对应的input_variables。少写了变量或者模板里出现了未定义的变量它会直接报错。这一下就把“变量有没有写对”从运行时排查变成了启动时检查。2.2 变量校验宁可启动时报错不要运行时崩PromptTemplate默认要求模板里的所有变量都必须被填充。如果你构造的时候漏了参数调用时会抛出KeyError。这个设计我一开始觉得烦后来真香。举个例子。你定义了一个模板prompt PromptTemplate.from_template(请帮我写一份关于{topic}的{style}文案字数{word_count}字。)如果你只传了topic和style漏了word_countLangChain不会用“空字符串”静默替代而是直接抛异常。这种“严格模式”在工程上是救命的因为它逼着你在开发阶段就把所有变量补全而不是等到用户真正调用接口时才发现输出里缺了一段。这里有一个细节值得单独提模板中不要出现不必要的花括号。比如你想在提示词里给模型展示JSON示例直接写{key: value}会被模板解析器当成变量定义。第一次踩这个坑的人绝对不少。解决办法有两个一个是把JSON示例里的花括号用{{}}转义另一个是改用ChatPromptTemplate的分消息结构避免在字符串层面处理这类问题。后面我会细讲第二种方案的用法。3. 核心组件拆解PromptTemplate、ChatPromptTemplate与Message3.1 从字符串模板到消息模板PromptTemplate处理的是“单段文本”适合补全、生成这类场景。但真正在做聊天机器人、Agent任务时你面对的是多轮对话结构——系统设定、用户输入、历史消息、模型回复它们是不同角色的消息混在一起反而丢掉了语义信息。这时候就需要ChatPromptTemplate。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是一个严谨的数学辅导老师擅长用逐步推理的方式讲解题目。), (user, 请帮我解这道题{question}), ])这里每个元素是一个元组第一个元素是消息角色第二个是内容模板。角色包括system、user、assistant、human、ai等。用这种方式组织提示词信息层次非常清楚系统设定和用户问题是分离的模型在训练时见过的就是这个格式所以理解起来比一段混合文本更稳定。实际项目里我几乎不直接传字符串模板给ChatPromptTemplate。我习惯把系统提示和用户提示分成两个模板类分别定义在独立的模块里再组合成最终的ChatPromptTemplate。比如from langchain_core.prompts import ChatPromptTemplate system_template 你是一个{role}擅长{skill}。请遵循以下原则{principles} user_template 我的需求是{request}。背景信息{context} prompt ChatPromptTemplate.from_messages([ (system, system_template), (user, user_template), ])这样改系统设定时不需要动用户模板改用户模板时也不会影响系统策略。后来我维护一个多语言版本的对话应用这种拆分方式省了大力气——每种语言只需要替换各自的模板文件组装逻辑完全不变。3.2 Partial变量提前固定延迟注入PromptTemplate和ChatPromptTemplate都支持partial_variables。这个功能在两种场景下特别有用。第一种场景是变量在模板生成前就已经确定但你不希望每次调用时都重复传。比如系统提示里固定要包含当前日期from datetime import datetime def get_current_date(): return datetime.now().strftime(%Y年%m月%d日) prompt PromptTemplate.from_template( 今天是{date}请根据今天的日期帮我安排{task}。 ).partial(dateget_current_date)调用的时候只需要传taskformatted prompt.format(task下周的工作计划)第二种场景更常见——模板组合。你构造了一个子模板作为另一个模板的一部分子模板的变量需要提前固定避免在最终调用时暴露过多参数。比如多智能体系统里每个智能体的角色描述是固定的但任务内容是动态的。你可以把角色描述通过partial注入然后在组装大模板时只暴露任务相关变量。需要注意一个问题partial变量和模板字符串中的变量名必须完全一致否则format时依然会报缺变量。这个错误信息很明确一般不会卡太久。3.3 from_template的边界什么时候不适合用字符串模板from_template很好用但它擅长的是“模板字符串中使用变量”的场景。一旦变量内容本身是结构化数据比如一个对象、一个JSON片段在模板里用{变量}的方式注入经常会出现格式问题。我举个例子。你在做一个数据分析助手需要把数据库返回的记录转成提示词上下文。如果记录是一个列表直接用{records}注入出来的文本可能是Python的repr格式——带引号、带括号根本不是自然语言。模型虽然能硬着头皮读但效果时好时坏。这种情况下我建议在组装模板之前先用代码处理好变量格式把它转成字符串再注入records_text \n.join( f记录{idx}名称{r[name]}数值{r[value]} for idx, r in enumerate(records) ) prompt ChatPromptTemplate.from_messages([ (system, 你是数据分析助手根据用户提供的记录回答问题。), (user, 以下是相关记录\n{records}\n\n请回答{question}), ])这是我在实际项目里踩过坑之后养成的习惯模板只负责组装结构数据预处理必须在外部完成。不要在模板里塞repr、列表推导、条件判断这些逻辑放代码里更清晰、更可测试。4. 新手必看变量、参数与输出解析的深度绑定4.1 模板变量与输出格式的联动设计我先问你一个问题你写提示词时有没有把“输出格式”写进模板里很多初学者写提示词只写任务目标不限定输出格式。模型自由发挥的结果就是同一套逻辑下一会儿输出Markdown列表一会儿输出JSON一会儿又是散文。这在大模型应用里是致命伤——因为后续代码要解析输出格式不稳定等于没法解析。更聪明的做法是把输出格式直接设计成模板的一部分。比如answer_template 你是智能客服请根据用户问题给出简洁回答。 用户问题{question} 请以JSON格式输出包含两个字段 - answer: 回答内容 - confidence: 置信度取值范围0到1 直接输出JSON不要输出解释。 有了这个模板配合StructuredOutputParser使用就能把模型输出直接解析成结构化数据。但这里有个容易被忽略的点模板里约定的格式越具体模型输出就越稳定。只写“输出JSON”和写出完整的字段说明效果差别很大。我实测下来的经验是给出示例值比只写字段名更管用- answer: 请前往门店出示会员码 - confidence: 0.92模型在few-shot示例的引导下输出格式的错误率会明显降低。这一条建议值得直接抄进你自己的模板设计规范里。4.2 OutputParser与模板变量的配合LangChain里输出解析器和提示词模板往往是成对出现的。比较常用的是PydanticOutputParser它用一个Pydantic模型定义输出的结构然后自动生成格式说明并追加到提示词里。from pydantic import BaseModel, Field from langchain_core.output_parsers import PydanticOutputParser class AnswerModel(BaseModel): answer: str Field(description回答内容) confidence: float Field(description置信度0到1之间) parser PydanticOutputParser(pydantic_objectAnswerModel) prompt ChatPromptTemplate.from_messages([ (system, 你是智能客服输出要求如下\n{format_instructions}), (user, 用户问题{question}), ])format_instructions可以通过parser.get_format_instructions()获取然后通过partial_variables注入模板。这样提示词里会自动包含“必须输出合法JSON字段为answer和confidence”的说明你不用手写这段JSON格式提示。这里有个细节值得注意format_instructions生成的内容通常包含了“必须符合给定JSON schema”这类说明加在system消息里比加在user消息里效果好。我一开始放在user消息里模型偶发会把这个说明误认为是用户需求的一部分输出里就出现了多余的废话。放到system消息后这类问题明显少了。原理上system消息对模型行为的约束力更强应该装“规矩”而user消息只放“任务”。4.3 变量过多时如何保持模板可读性当一个提示词模板的变量超过6个可读性会急剧下降。你光看模板字符串根本不知道每个变量会渲染成什么内容更别提调试了。我的习惯是变量超过4个就做一次“模板拆分”。比如一个复杂的文案生成任务我会拆成_template加_context两个部分prompt ChatPromptTemplate.from_messages([ (system, 你是{domain}领域的资深文案专家。), (user, 请基于以下产品信息生成{style}风格的文案。\n\n产品信息\n{product_info}\n\n目标用户\n{target_audience}\n\n重点要求\n{requirements}), ])虽然变量总数还是5个但布局是分层的第一段定义角色第二段是任务和上下文。后续调试时我只需要检查product_info和target_audience这两块核心内容是否正确组装。另外我强烈建议在模板字符串里保留清晰的段落分隔符和字段名。比如“产品信息”“目标用户”这样的显式标签不只是给人看的也是给模型看的。模型在填充时更容易区分每条信息的边界输出更稳定。5. Few-shot示例的高级玩法从示例选择到动态组装5.1 Few-shot模板的基本结构Few-shot是提升模型输出质量的高性价比手段。LangChain里FewShotChatMessagePromptTemplate是专门干这个的。它的基本结构分为示例集、示例提示词模板、主消息模板。from langchain_core.prompts import ( ChatPromptTemplate, FewShotChatMessagePromptTemplate, ) examples [ {input: 这家餐厅的菜真难吃, sentiment: 负面}, {input: 快递小哥服务特别好, sentiment: 正面}, {input: 电影一般般吧, sentiment: 中性}, ] example_prompt ChatPromptTemplate.from_messages([ (user, {input}), (assistant, {sentiment}), ]) few_shot_prompt FewShotChatMessagePromptTemplate( example_promptexample_prompt, examplesexamples, ) final_prompt ChatPromptTemplate.from_messages([ (system, 你是一个情感分析助手请判断用户输入的情感倾向。), few_shot_prompt, (user, {input}), ])这样构造出来的消息序列是system情感分析助手user这家餐厅的菜真难吃assistant负面user快递小哥服务特别好assistant正面user电影一般般吧assistant中性user{input}模型看到的是完整的“问答”模式比你只丢一句“请判断情感倾向”要稳得多。5.2 示例选择的坑不是越多越好我试过把几十个示例全都塞进提示词里结果有两个副作用一是Token消耗暴增二是模型注意力被大量示例稀释反而抓不住核心规律。实际项目中5个左右高质量示例基本就够了关键是示例要覆盖不同的边缘场景。比如情感分析任务如果示例全是负面评价模型对正面判断的准确率会明显偏低。所以我至少保证正面、负面、中性、带讽刺语气、带反问语气各一个示例。覆盖面比数量重要。5.3 按需动态选择示例向量检索 语义相似度固定示例的缺陷在于无法适配所有输入。如果你想根据用户的每个具体问题动态选择最相似的几个示例LangChain里也有对应方案——FewShotPromptTemplate的example_selector。from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import InMemoryVectorStore example_selector SemanticSimilarityExampleSelector.from_examples( examples, OpenAIEmbeddings(), InMemoryVectorStore, k3, )思路很简单把每个示例向量化用户输入来了之后取向量相似度最高的前k个示例动态组装成提示词。这个方案的优势非常明显——对于任务类型复杂的系统不同输入匹配到的示例不同提示词每次都是“量身定制”的。但要注意这个方案有额外的延迟和成本因为它要做一次向量检索。对于高并发的接口我通常会在检索结果上做缓存比如对用户输入做哈希短时间内相同输入直接复用检索结果。缓存过期的策略可以根据业务场景自己调但核心思想是别让每次请求都重复算一遍向量相似度。6. 实践案例一个可维护的“智能客服”提示词工程6.1 从需求分析到模板设计我拿一个实际项目来演示完整流程。需求是做一个电商智能客服能回答订单、物流、退换货相关问题且必须输出JSON格式供前端渲染。第一步确定消息角色分工system身份和规则。user用户当前问题和上下文信息。assistant可选上一轮模型回复用于多轮对话。第二步设计模板。我的做法是拆分模块让每个部分可以独立修改system_base 你是一个电商平台客服助手请根据用户问题和订单信息给出准确回复。 system_rules 回复规则 1. 不得编造订单信息不确定的信息必须说明“需要核实”。 2. 回答要简洁不超过200字。 3. 输出必须为JSON格式包含answer、intent两个字段。 order_context 订单信息参考可能不完整仅供参考 {order_info} 第三步组装prompt ChatPromptTemplate.from_messages([ (system, system_base \n system_rules), (user, order_context \n\n用户问题{user_input}), ])order_info和user_input是核心变量。order_info在调用前由后端查询并格式化模板里不做任何处理。6.2 Few-shot示例如何嵌入客服场景给客服场景加了几个典型示例效果提升非常直接。比如用户问“我的快递到哪了”如果只有纯模板模型可能输出一大段查询建议。但如果你给了一个示例user我的快递到哪了assistant{answer: 您的订单正在运输中预计明天送达。, intent: order_status}模型就会学着用同样简洁、同样JSON的格式回复。这就是示例的力量。如果感觉示例太多太长也可以像前面说的用动态示例选择把示例库放在向量存储里按需提取。在客服场景中我建议把高频问题做成示例库比如订单查询、退款进度、物流异常每个类别放2个示例效果和性价比比较平衡。6.3 LangSmith追踪与可视化调试当你开始接触复杂的Prompt组合肉眼检查已经不够用了。LangSmith可以记录每一次调用的完整链路包括Prompt模板的渲染结果、模型的原始输出、解析后的结构化数据。调试的时候直接看Link里的渲染结果比在本地一遍遍打印要高效得多。实际排查一个“模型输出JSON格式但解析失败”的问题时我在LangSmith里看到了完整渲染后的Prompt发现是order_info那段文本太长把JSON格式说明挤到了模型注意力的边缘。后来我把格式说明在模板里重复了两次一次在system里一次在示例里解析失败率直接降了一半。这个经验不用追踪工具很难发现。7. 常见报错与实战速查表7.1 Missing variables变量缺失与拼写错误报错形式通常是KeyError: Missing variables: xxx解决思路检查模板字符串里{}中的变量名和format/invoke时传的Key是否一致。特别注意带下划线的变量名比如user_input和userinput肉眼很难分辨。我一开始经常卡在这种低级错误上后来习惯是模板里所有的变量名统一用蛇形命名在代码里定义常量杜绝手写字符串。7.2 字面花括号被误解析在模板里写JSON示例时用{{和}}转义template 请输出JSON格式{{answer: {answer}}}如果不转义{answer: ...}会被当成变量定义导致报错。这个坑相当常见尤其是从普通字符串拼接转LangChain模板的第一天。7.3 模型输出包含Markdown代码块即使模板里说了“不要输出解释”模型依然可能输出json {answer: ...}这种输出直接交给我解析十有八九要出问题。解决办法是用StructuredOutputParser配套处理或者在解析前做一次字符串清理。我习惯写一个通用函数把首尾的json标记剥掉再json.loads。注意**不要指望模型100%遵守格式规则输出解析这层防御必须自己做**。 ### 7.4 长提示词截断 模板里的示例太多或者order_info过长容易触发上下文窗口限制。模型表现出的症状不是报错而是输出内容变短、格式乱、甚至直接复述模板里的话。排查时先确认Token用量再考虑精简示例或压缩上下文。 一个实用的压缩方案contextual_compression检索器可以把长文档压缩成与问题最相关的几个句子。但在客服这种强上下文场景里我更多是建议业务侧精简订单信息字段只保留和用户问题相关的部分不要一股脑全塞进去。 ### 7.5 问题速查表 | 症状 | 可能原因 | 快捷排查方法 | | --- | --- | --- | | 模型输出全是模板原话 | 变量没注入成功模板里有未定义变量 | 检查invoke参数与input_variables | | JSON解析报错 | 输出被Markdown包裹或有前缀说明 | 添加输出解析器增加cleaning步骤 | | 输出风格漂移 | 系统规则写得太泛 | 细化system消息加few-shot示例 | | 响应长度不稳定 | 模板里没限制长度或示例长度差异过大 | 在system里显式写“不超过xx字” | | 变量被吞掉 | 模板字符串里空格或换行导致变量拼接异常 | 检查模板渲染结果用LangSmith定位 | ## 8. 模板版本管理和多环境部署心得 ### 8.1 把模板当作代码来管理 我见过很多团队提示词散落在代码、配置文件、数据库甚至某个不为人知的在线文档里。这种管理方式在早期能跑但只要业务一复杂马上失控。 我的做法是**把模板文件单独放一个目录按领域划分**比如 text prompts/ ├── customer_service/ │ ├── order_query.py │ ├── return_refund.py └── content_generation/ ├── ad_copy.py ├── blog_outline.py每个模块里定义模板对象不直接在其他业务代码里写模板字符串。这样模板的修改可以通过代码审查和版本控制来把关不会出现“线上的提示词到底是谁改的”这种问题。8.2 模板内容的灰度调整提示词改动往往影响面很大。我在这方面踩过坑一次性把客服的提示词全换成了新的结果输出风格突变用户反馈“感觉换了个客服”。后来改成灰度策略系统里同时保留新旧两个模板用流量比例控制逐渐切换。LangChain的模板对象是普通的Python对象完全可以用代码来控制路由逻辑不会增加额外成本。8.3 测试用例沉淀建议给每个模板配一个小型测试集包含典型的输入、期望的输出字段、必现的格式规则。测试方法很简单——用固定模型跑几个用例检查输出能否被解析器正常解析。随着模板迭代这些测试用例会变成你最宝贵的回归保障。9. 我最后想强调的几个细节回到开头那个场景。如果你现在还在用字符串拼接写提示词我给的建议很直接花半天时间把现有代码里的Prompt都改成PromptTemplate或ChatPromptTemplate你会感受到两个实实在在的变化。第一变量错误从“运行时才知道”变成了“启动时就被拦截”。第二模板和业务代码分离以后改提示词不再需要动主流程哪怕是非技术人员也能通过修改模板文件参与调优。关于变量命名再多说一句。模板里的变量名要有语义{question}比{q}合适{order_info}比{info}合适。因为模板字符串会被模型读到语义清晰的变量名有助于模型理解任务结构。最后分享一个技巧调试模板时不要直接丢给模型试先把模板渲染结果打出来看看。print(prompt.format_messages(order_info..., user_input...))先确认渲染结果没问题再调用模型。这一步能帮你省掉大量无效排查。自己看一眼渲染后的文本很多问题一眼就能发现。祝你的提示词工程越写越顺。