☰
LangChain提示词模板嵌套:把Prompt变成可组合的流水线
2026/10/17 11:00:44 网站建设 项目流程

用Python写LangChain框架的应用有一段时间后,我最大的体会是:提示词千万别写死在代码里。这一篇是LangChain框架系列的第1-7篇,重点聊提示词模板里一个非常隐蔽但又很关键的点——模板中的模板。简单说,我们不仅可以在一个模板里定义变量,还可以让模板A渲染出来的结果,直接作为模板B的输入变量;一层套一层,组成一条提示词生产流水线。今天把这块拆开讲清楚。

很多人上手LangChain时都用过最朴素的PromptTemplate,觉得它就是一个带变量的字符串,跟Python里f-string差不多。但实际项目跑到一定规模,你会发现提示词并不是“一个”字符串,而是“一组”互相嵌套的片段。角色设定、任务目标、示例文本、输出格式、语气风格,这些东西混在同一个模板里会变得又长又难维护;拆成独立小模板,再通过“模板嵌套”的方式组装,代码的可读性和复用性会好很多。

我们分五个部分来聊:先用一个真实问题说明为什么需要模板,再梳理基础API,接着重点拆解三种嵌套姿势,然后给出一个可以直接抄作业的多级模板工厂,最后把常见报错和排查经验整理成速查表。无论你是刚接触LangChain,还是已经在调prompt调到头秃,这篇文章都能派上用场。

1. 提示词模板到底在解决什么问题

1.1 从写死字符串到模板化的转变

最早我做LLM应用时,提示词都是直接写在代码里的:

query = "请帮我分析一下用户评论:这个手机拍照片太好看,电池也耐用。"

一开始项目小,这样写确实很快。但需求一多就麻烦了:有的场景要分析商品评论,有的场景要分析售后工单,有的场景要判断用户情绪。如果每个场景都写一条完整字符串,代码里会出现大量重复的文案,而且业务方随便改一个措辞,就要去代码里搜索替换,别提多痛苦。

模板化解决的是“把固定结构和可变内容分开”的问题。用LangChain的PromptTemplate,一个典型的模板长这样:

from langchain.prompts import PromptTemplate prompt = PromptTemplate.from_template( "请对下面的用户评论进行情感分析,结果用【积极】【中性】【消极】三选一。\n" "用户评论:{comment}\n" "情感判断:" ) # 渲染模板 formatted = prompt.format(comment="这个手机拍照片太好看,电池也耐用。") print(formatted)

这样,提示词的固定框架只写一份,每次业务变化时,只需要传入不同的comment变量。模板还支持input_variables显式声明变量、output_parser解析模型输出,这些都是f-string做不到的。

1.2 模板确实好,但普通模板不够用

我的第一个转折点出现在需求叠加之后。假设今天要做一个“客服满意度分析”功能,目标是根据用户评价,判断满意还是不满意,并给出建议回复。但不同渠道的用户风格差异很大:电商平台用户语气比较直接,App Store用户喜欢用评星加短评,而企业沙龙用户讲话很委婉。

如果只用一个模板,把所有约束都塞进去,逻辑会挤成一团。更麻烦的是,还有一个角色设定需求:不同行业需要不同的分析专家身份,比如“你是电商客服专家”“你是App体验分析师”。这些角色的描述本身就是一段完整的提示词片段,它不应该每次都重新写一遍。

这就引出了本文的核心概念:模板中的模板。把角色设定做成一个独立模板,把用户评论处理做成另一个独立模板,再让“分析主模板”去引用这两个模板的渲染结果。子模板的输出变成父模板的变量,层层组合,完整的提示词由多个模板分段渲染再拼装,而不是一个人从零开始写完。

2. 基础用法:一个PromptTemplate的前世今生

2.1 从零创建一个模板的常规姿势

在讲嵌套之前,必须先把基础API吃透。LangChain的PromptTemplate有几种创建方式,我日常用最多的两条:

# 方式一:直接声明变量 pt = PromptTemplate( input_variables=["topic", "word_count"], template="请帮我写一篇关于{topic}的文章,字数约{word_count}字。", ) # 方式二:根据模板文本自动推导变量 pt2 = PromptTemplate.from_template( "总结下面这段产品说明,输出不超过{limit}个字。\n产品说明:{text}" )

方式二最省事,它会把模板里的{limit}和{text}自动识别为输入变量。在版本上,早期from langchain.prompts import PromptTemplate是主用路径;新版本也可以从langchain_core.prompts导入,功能基本一致,迁移成本很低。

模板创建后,常用方法就两个:

  • format:直接渲染成字符串,适合配合普通LLM调用。
  • format_prompt:返回一个PromptValue,后续可以继续拼接消息或传给其他组件。

有个容易被忽略的小知识点:变量名尽量不要用特殊符号,最好是小写字母加下划线。{user_comment}比{用户评价}更通用,也避免各种编码问题。模板字符串里的空白符会原样保留,所以多行模板缩进一定要规范,否则模型可能收到一堆奇怪的空格。

2.2 模板校验与部分变量

PromptTemplate默认会做变量校验。你定义一个模板时,如果声明了input_variables,那么{xxx}里的内容必须全部在input_variables中出现,否则会直接报错。这种校验在多层模板组合时尤其重要,能帮你快速发现变量名拼写错误。

还有“部分变量”的概念,它非常关键。partial_variables允许你把模板的一部分变量在定义时就预先填好,而不必在每次渲染时传入。比如某些固定的系统指令、固定的产品名,可以先绑定:

pt = PromptTemplate( input_variables=["feedback"], template=""" 你是售后客服专家。 请针对这条反馈给出处理建议,语气要委婉。 反馈内容:{feedback} """, partial_variables={"expert": "售后客服专家", "tone": "委婉"}, )

如果后续需要修改这部分的默认值,还可以动态调用partial()方法生成新的模板实例,这让模板有了“继承”的感觉。我们的嵌套组合,很多就是从partial_variables开始玩起来的。

3. 模板中的模板:三种嵌套组合姿势

3.1 姿态一:子模板输出作为父模板变量

这种姿势最直观,也最常用。核心思路是:子模板先渲染出一段文本,再把这段文本作为变量传进父模板。举个例子,一个商品分析场景需要两部分内容:商品简介由子模板生成,最终分析提示词使用这段简介作为上下文背景。

from langchain.prompts import PromptTemplate # 子模板:生成商品简介 product_template = PromptTemplate.from_template( "商品名称:{name}\n所属品类:{category}\n核心卖点:{features}" ) # 用子模板渲染出商品简介 product_intro = product_template.format( name="便携榨汁杯", category="生活电器", features="续航强、免洗、静音", ) # 父模板:消费商品简介,输出分析任务 analysis_template = PromptTemplate.from_template( "以下是商品资料:\n{product_intro}\n\n" "请基于这些资料,生成一条30字以内的商品营销文案。" ) final_prompt = analysis_template.format(product_intro=product_intro) print(final_prompt)

这里product_intro就是子模板的输出,它作为一个变量嵌入父模板。好处是,商品资料这一段可以被多个父模板复用:比如同时生成“微博文案版”“详情页卖点版”“客服回复版”,每个父模板只需要引用同一个product_template,资料更新一处,所有父模板的输出同步更新。

我还见过更极致的做法:让子模板的输出不仅是一段文本,而是另一段模板。也就是先渲染出一个带占位符的模板字符串,再由父模板二次渲染。这种方式能用,但调试起来费脑,建议只在确实需要“动态生成提示词结构”的场景才用。

3.2 姿态二:Pipeline式组合,直接声明模板管道

LangChain早期有一个非常契合“模板中的模板”场景的类,叫PipelinePromptTemplate。它的逻辑是:定义多个子模板作为Pipeline阶段,每个子模板的输出会被存入对应的变量名,最终由一个final_prompt统一渲染。这种写法比手动format更清晰,组合关系一目了然。

from langchain.prompts import PromptTemplate from langchain.prompts.pipeline import PipelinePromptTemplate # 最终合成模板 final_template = PromptTemplate.from_template( """{context} --- 任务:{question} 请结合上面的背景信息回答。 """ ) # 子模板1:生成背景 context_template = PromptTemplate.from_template( "公司名称:{company}\n业务范围:{business}\n近期动态:{update}" ) # 子模板2:整理问题 question_template = PromptTemplate.from_template( "用户的问题是:{user_question}\n请提炼核心诉求。" ) pipeline_prompts = [ ("context", context_template), ("question", question_template), ] pipeline = PipelinePromptTemplate( final_prompt=final_template, pipeline_prompts=pipeline_prompts, ) prompt_value = pipeline.format( company="某在线教育公司", business="成人职业培训", update="刚上线AI助教功能", user_question="课程太多,不知道怎么选择适合我的方向。", )

渲染时会先执行context_template和question_template,把输出分别赋给context和question,最后传给final_template。相当于把“模板中的模板”定义成了一条元数据流水线。

如果你使用的LangChain版本里这个类已被移除,或者你想要更灵活的控制,也可以自己写一个类似的组合器:用一个字典存子模板,按顺序渲染,最后填充父模板。核心逻辑只有十几行,却可以让代码结构非常清晰。

def render_pipeline(final_template, pipeline_prompts, **kwargs): rendered = {} for name, tmpl in pipeline_prompts: rendered[name] = tmpl.format(**kwargs) return final_template.format(**rendered, **kwargs)

这跟PipelinePromptTemplate的思路是等价的,而且不依赖特定版本。

3.3 姿态三:Few-shot示例面板,示例模板也是一种嵌套

第三种容易被忽略的“模板中的模板”,是Few-shot示例。大家做Few-shot时习惯把示例写死成字符串,但如果示例数量大、需要动态挑选,就要用到FewShotPromptTemplate和示例选择器。

from langchain.prompts import FewShotPromptTemplate, PromptTemplate examples = [ {"text": "这家店上菜太慢", "label": "差评"}, {"text": "整体还不错,偶尔等位", "label": "中评"}, {"text": "装修很有氛围,服务也细心", "label": "好评"}, ] example_prompt = PromptTemplate.from_template( "评论示例:{text}\n情感倾向:{label}" ) few_shot_prompt_temp = FewShotPromptTemplate( examples=examples, example_prompt=example_prompt, prefix="请判断以下评论是好评、中评还是差评:", suffix="评论:{input}\n情感倾向:", input_variables=["input"], ) result = few_shot_prompt_temp.format(input="桌椅虽然旧了,但是整理得很干净。") print(result)

这里很“模板中的模板”的细节在于:example_prompt本身是一个PromptTemplate,它负责把每一条示例数据渲染成标准格式,然后FewShotPromptTemplate再把渲染后的所有示例作为整体塞进上下文。如果要换示例风格,比如从中文换成英文、从整句换成短语,只要改example_prompt即可,不需要动外层模板。

更进一步,示例还可以由选择器动态挑选。每来一个新的用户输入,我们先用相似度计算选出最相关的N条示例,再让这些示例参与模板渲染。这就是典型的“模板运行时动态生成”:

from langchain.prompts.example_selector import SemanticSimilarityExampleSelector selector = SemanticSimilarityExampleSelector.from_examples( examples, embeddings, k=2, input_keys=["input"], ) few_shot_prompt_temp.example_selector = selector selected_result = few_shot_prompt_temp.format(input="服务态度很好,但菜品一般。")

这样,example_prompt作为“模板中的模板”,它的数据源还可以动态变化,整个提示词的组装就更灵活了。

4. 实战:搭一套多级模板复用体系

4.1 设计三层模板结构

嵌套思路落到工程上,我建议所有提示词都分三层:

  • L1 基础原子模板:存放最细粒度的描述,比如产品名模板、语气词模板、格式模板。
  • L2 领域模块模板:组装原子模板,形成完整片段,比如“客服角色设定”“行业背景介绍”“示例面板”。
  • L3 最终任务模板:引用L2模块的输出,加上任务指令,形成交给模型的完整提示词。

以“多平台客服助手”为例。我们要处理来自淘宝、小红书、知乎三种平台的用户投诉,每个平台的语气和回复风格差异很大。平台背景可以用子模板生成,角色设定用另一个子模板生成,最后合入主模板。

4.2 完整代码示例

from langchain.prompts import PromptTemplate # L1 原子模板:平台信息 platform_template = PromptTemplate.from_template( "平台名称:{platform}\n用户特征:{user_character}\n回复风格:{reply_style}" ) # L1 原子模板:用户情绪 emotion_template = PromptTemplate.from_template( "用户情绪:{emotion}\n情绪程度:1-5分,当前{degree}分" ) # L2 模块模板:客服角色设定 role_template = PromptTemplate.from_template( "你是{platform}平台的资深客服,擅长处理用户投诉,{reply_style}是你的特点。" ) # L2 模块模板:背景信息整合 context_template = PromptTemplate.from_template( """【平台背景】 {platform_context} 【用户状态】 {emotion_context} 【用户原话】 {user_message} """ ) # L3 最终模板 final_template = PromptTemplate.from_template( """{role} {context} 请根据以上信息,生成一段回复用户的话术,要求: 1. 先表达理解和歉意; 2. 再给出明确的解决方案; 3. 整体语气符合平台风格。 回复内容:""" )

不直接调final_template.format,而是写一个工厂函数完成组装:

def build_prompt(platform: str, user_message: str, emotion: str, degree: int): # 渲染L1原子模板 platform_context = platform_template.format( platform=platform, user_character=PLATFORM_MAP[platform]["feature"], reply_style=PLATFORM_MAP[platform]["style"], ) emotion_context = emotion_template.format(emotion=emotion, degree=degree) # 渲染L2模块模板 role = role_template.format( platform=platform, reply_style=PLATFORM_MAP[platform]["style"], ) context = context_template.format( platform_context=platform_context, emotion_context=emotion_context, user_message=user_message, ) # 渲染L3最终模板 return final_template.format(role=role, context=context)

代码结构非常清爽。以后要加一个新平台,只需要在PLATFORM_MAP里加一条配置,增加对应的platform_template参数,完全不影响主流程。这就是“模板中的模板”的工程价值。

4.3 模板工厂的维护策略

我见过很多人把所有的PromptTemplate堆在一个文件里,最后几百行模板和代码混在一起,根本不敢改。建议给模板建立独立管理模块:

  • 每个业务域的模板单独建一个Python文件,比如prompts/ecommerce.py、prompts/social.py。
  • 模板命名采用三段式:动词_对象_用途,比如render_platform_context、build_complaint_role。
  • 所有模板写入模块级常量,禁止在渲染函数内部临时定义模板。

另外一个很好用的策略是“快照测试”。开发阶段把渲染后的最终提示词保存成.txt文件,每次改动模板后对比快照。这样可以非常直观地看到模板嵌套后哪里多了一个换行、哪里少了一个标点。

def test_prompt_snapshot(): prompt = build_prompt( platform="小红书", user_message="快递盒子被压扁了,里面东西也碎了", emotion="愤怒", degree=4, ) snapshot_path = Path("snapshots/complaint_prompt.txt") if snapshot_path.exists(): assert snapshot_path.read_text() == prompt, "Prompt快照不一致" else: snapshot_path.write_text(prompt)

这个办法看起来土,但对防止团队协作时模板被改崩特别有效。

5. 常见报错与避坑合集

5.1 变量相关报错

遇到最多的就是变量缺失或变量名不一致。比如报错:

KeyError: 'tone'

这种问题十有八九是模板里写了{tone},但传入参数时写成了tone_style。因为Python字典只认Key,拼写不一致就崩。我的排查习惯是:

  • 先打印模板的input_variables,看看系统认为应该传什么变量。
  • 再检查调用处的参数名,两者必须完全一致。
  • 多层组合时报错更难定位,我就分别打印每个子模板的渲染结果,用二分法找到是哪个环节丢了变量。

5.2 变量内容本身包含花括号

模板嵌套时,如果某个变量值里包含{或},format阶段就会误以为这是模板占位符,直接报IndexError或KeyError。比如你想让模型输出JSON格式:

# 错误示范 template = "输出格式:{format_rule}" format_rule = '{"key": "value"}'

在渲染时,{"key": "value"}里的{会被当成变量开头。解决办法有两个:第一,在模板文本里把字面量花括号转义成双花括号{{}};第二,把反例内容放到变量值里时,先手动替换成占位符,渲染完再还原。

我自己的习惯是:所有会在变量值中出现的花括号,都先转成中文括号或其他占位符;等整个Prompt渲染完成后再替换回来,避免纠缠。

5.3 部分变量与直接传参冲突

如果你在partial_variables里绑定了某个变量,又在format时传入了同名变量,渲染结果可能会让人困惑。很多版本会直接以后者覆盖,但也有版本直接报错。为了避免项目升级后挂掉,我的建议是:partial_variables只用来绑定那些一定不会变的静态内容;凡是可能动态变化的内容,一律不放进partial_variables。

5.4 嵌套层级过多导致上下文膨胀

模板套模板看起来优雅,但每层渲染都会增加一些描述性文字,最终提示词可能会越来越长。三条子模板组合下来,上下文可能多出几百甚至上千token。这里有两个控制技巧:

  • 每条子模板只保留必要的背景,不要带一堆修饰语。
  • 拆分子模板时充分考虑“内聚性”:A模板负责的信息,B模板不要重复描述。

我的经验是,模板嵌套层级控制在两层到三层之间。再多,调试成本和上下文开销都会指数上升,不值当。

5.5 多行模板里的换行与缩进

PromptTemplate.from_template不会自动清理缩进。你写模板时为了美观加的缩进空格,会原样进入提示词。现代LLM对多余的空白容忍度很高,但项目要求严谨时,这会显得很业余。推荐用标准库的textwrap.dedent统一处理:

from textwrap import dedent from langchain.prompts import PromptTemplate template_text = dedent(""" 请回答下面的问题: 问题:{question} """).strip() prompt = PromptTemplate.from_template(template_text)

这样无论你在代码里怎么缩进,渲染出来的提示词都不会有多余前缀空格。

这串嵌套模板的设计思路,我最后再唠叨几句

“模板中的模板”本质上是把提示词工程从“字符串处理”提升到“结构化组合”。刚开始可能觉得多写好几个模板很麻烦,但业务一旦复杂起来,这套思路节省的维护时间远超前期投入。我实际项目里最舒服的用法是:把子模板当成函数来用,把父模板当成配置文件来用,组合出来的提示词既稳定又灵活。

有一点需要特别提醒:提示词最终是给模型看的文本,嵌套再漂亮,最终渲染结果如果不直观,也等于白做。建议每次更改模板结构后,都把最终成品打印出来,站在模型的角度通读一遍。模板逻辑再精巧,都不如“输出质量稳定”重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询