
最早我管理提示词是硬编码派的。所有模板直接写在业务代码里用的时候拼字符串改完靠发版上生产。直到有一回一个助手类产品在线上稳定跑了两个月业务突然说要调整开场白语气。我心想不就改一句话吗结果走了一遍完整的发布流程等审核等灰度前后折腾了大半天。就在那天我决定把所有提示词全部从代码里拆出来改用 YAML 统一管理。说实话LangChain 的 YAML 配置能力很早就有了但 v1.0 里它的加载机制更清晰、序列化格式更规范值得完整梳理一遍。这篇文章我会从最基础的 YAML 文件长什么样讲起到 load_prompt 的底层解析链路再到多环境、CI 校验这类生产级玩法最后把我在实操中踩过的坑整理成排查手册。适合已经用 LangChain 做过一些项目、但提示词还散落在代码各处的朋友也适合刚接触 LangChain、想一开始就走规范化路线的初学者。1. 为什么我放弃硬编码改用 YAML 管理提示词模板1.1 硬编码提示词的三座大山硬编码的麻烦不是一两天显现的是项目一复杂就全面爆发。第一个问题是提示词和业务逻辑缠绕在一起。一个稍微像样的 Agent 系统提示词往往分布在路由函数、工具调用、后处理逻辑各个位置。代码读起来满屏都是字符串拼接。想搞清楚某个环节到底给模型发了什么你得顺着代码逻辑走一遍把分散的片段拼起来才看得明白。这种状态持续时间越长维护成本越高。第二个问题是迭代成本。业务方改提示词的需求一天能来三回。每次都是“把这段开场白调整一下”“这个角色的语气再活泼一点”。硬编码模式下每一次微调都意味着一次代码变更、一次测试、一次发布。为了一个字符串的修改走完整套流程听起来很荒谬但确实是很多团队每天都在经历的事。第三个问题是多版本多环境的混乱。开发环境、测试环境、生产环境往往需要不同的提示词策略。代码里写死了就得靠 if 分支来切换环境散落各处的 if-else 一旦多起来维护就成了灾难。1.2 YAML 配置的四个实际收益用 YAML 管理提示词之后最直观的变化是提示词不再是“代码”而是一份可以被阅读和讨论的“文件”。这背后的本质是声明式配置取代了命令式拼接。打个比方硬编码就像让一个厨师边看客人的要求边临场发挥每一步都得手工处理用 YAML 则像是把菜谱写成文档想调整口味直接改文档后端厨师严格按照文档执行。菜谱和厨师是两套角色互不干扰。收益主要体现在四个方面可读性大幅提升。YAML 本身就是一种强调可读性的格式缩进清晰、字段名规范。业务同事即使不写代码也能看懂template字段里写的提示词长什么样甚至可以直接上手改。这在团队协作里价值极高。Diff 非常友好。改成文件管理后每次提示词改动在 Git 里就是一段明明白白的 diff。评审代码的人不需要去理解上下文一眼就能看出哪句话改了、是加长了还是变短了责任归属和变更历史都清清楚楚。彻底解耦。提示词的修改不再依赖开发发版。运维或业务侧把配置目录纳入更新流程后调整模板和重启服务的成本大大降低。模型切换、prompt 微调都变成了配置层面的操作。方便校验和复用。YAML 文件是结构化数据可以写脚本批量校验格式、检查变量是否齐全甚至可以在 CI 里自动验证所有模板可正常渲染。同一套模板还能在不同子系统中复用不需要复制粘贴代码。1.3 边界不是所有场景都适合 YAML当然也不是所有项目都适合立刻上 YAML。如果一个 Demo 只有一两个提示词直接写在代码里完全没问题强行上配置文件反而增加不必要的层级。另外如果提示词本身就是完全运行时动态拼装的比如根据用户输入实时生成不同结构的指令那些高度动态的部分不太适合放进静态配置更适合留在代码里通过逻辑生成。我的判断标准很简单如果一个提示词超过三行或者预计会被多次修改或者会在多个模块复用就放进 YAML。否则硬编码也无妨。规范不是目的可控才是。2. 加载核心与底层原理LangChain v1.0 如何解析 YAML2.1 load_prompt 的解析链路LangChain v1.0 中加载 YAML 提示词的入口是load_prompt函数。我通常这样写from langchain_core.prompts import load_prompt prompt load_prompt(prompts/generate_title.yaml) print(prompt.format(topicAI 应用的提示词管理))如果你用的还是聚合包from langchain.prompts import load_prompt在老版本里也能跑但 v1.0 之后我更推荐直接依赖langchain_core职责更清晰也避免引入一堆用不上的集成模块。这个函数内部做的事情可以拆成三步读取文件内容交给yaml.safe_load解析成 Python 字典。这一步是纯 YAML 层面的解析文件本身不是合法 YAML 时会在这一层报错。从字典里取_type字段这个字段决定了后续用哪个加载器来构造具体的提示词对象。根据_type找到对应的 Loader 类把字典里的其他字段作为参数传给构造函数返回一个BasePromptTemplate子类的实例。这个设计就是典型的工厂模式。在 LangChain 核心库里每一种可序列化的对象都注册了对应的类型标识和加载器。你在 YAML 里写_type: prompt它就知道该构造PromptTemplate写_type: chat它就去构造ChatPromptTemplate。需要注意一点load_prompt加载完返回的是模板对象不是已经渲染好的字符串。真正要把变量填充进去还得调用.format()或.format_prompt()。很多初学者在这一步容易混淆以为加载完就能直接拿去调模型了其实加载只是拿到了“带占位符的模板”。2.2 prompt 与 chat两种模板的序列化差异YAML 配置中最常用、也最容易混淆的两种类型是prompt和chat。_type: prompt对应的是PromptTemplate适用于单段文本模板一般往里塞一个template字符串和一组input_variables。这类模板适合简单的文本生成场景比如标题生成、摘要抽取。_type: chat对应的是ChatPromptTemplate适用于多轮对话消息结构。它内部由messages字段组成一个消息数组每条消息都有一个_type标识自己的角色。LangChain 里常见的消息类型包括_type值对应类含义systemSystemMessagePromptTemplate系统指令humanHumanMessagePromptTemplate用户消息aiAIMessagePromptTemplate模型回复placeholderMessagesPlaceholder运行时动态插入消息列表为什么要有系统消息和用户消息的区分因为现在的模型接口普遍区分角色。YAML 里把角色显式写出来加载后 LangChain 才能正确构造消息对象数组在调用模型时按角色序列化。选择prompt还是chat不是风格偏好问题而是由模型接口决定的。如果你调用的接口是 Chat 风格绝大多数主流模型都是那_type: chat才是正解如果你用的是纯文本补全接口才用_type: prompt。有时候用prompt也能跑但实际发给模型的消息结构和你预期的不一样调试起来很困惑。2.3 v1.0 版本迁移中的导入与兼容提醒v1.0 最大的变化之一是把核心抽象搬到langchain_core包langchain包则专注于第三方集成。所以load_prompt这类基础工具现在更推荐从langchain_core导入。我在迁移老项目时遇到过一种情况代码里from langchain.prompts import load_prompt跑得好好的升到 v1.0 之后直接 ImportError。原因就是安装包的拆分逻辑变了。如果你也遇到类似的导入问题最快的排查办法是在 Python 里打印一下当前环境的包版本确认langchain-core和langchain分别是什么版本再决定用哪条导入路径。另外一个兼容性提醒是老版本里有些 Loader 类在 v1.0 中换了位置。比如FewShotPromptTemplate的加载路径就调整过。碰到这种情况不要硬记路径直接看报错信息里的提示或者打开包目录找一下prompts/loading.py文件一目了然。还有一个小坑v1.0 之后部分模板类的构造参数增加了校验逻辑。以前随便写字段能过现在某些字段类型不对会直接抛 ValidationError。这就意味着 YAML 里的字段写得越规范兼容性越好。3. YAML 提示词模板语法完全解构3.1 基础字符串模板的字段说明先看一个最基础的单段模板_type: prompt name: product-description description: 生成电商商品描述要求突出卖点和适用场景 input_variables: - product_name - category template: 你是一个资深的电商文案编辑。 请为【{category}】类目的商品「{product_name}」撰写一段商品描述 要求字数不超过 120 字突出核心卖点语气自然不生硬。在这个文件里有几个关键字段_type是必填的决定加载器类型等同于告诉 LangChain 该用哪个类来解析。name是模板名主要用于标识不参与渲染。如果你在代码里对模板做元信息管理这个字段很有用。description看起来可有可无但我强烈建议一定写。v1.0 的许多工具链会读取 description 来理解模板用途尤其在模板数量多起来的项目里一份带注释的模板库维护成本低得多。input_variables声明模板中出现的所有占位变量。变量名必须和template里的{xxx}对应。template是模板正文支持多行写法。这里有必要解释一下input_variables的作用。虽然在较新版本里即使不声明变量LangChain 也可能自动从模板里推断出来但显式声明能带来两个保障一是后续校验脚本可以据此检查模板是否被误改二是模板变量较多时自动推断容易漏掉某些出现在特殊位置的变量。使用或|来写多行文本是有讲究的。会保留段落结构但把连续行合并为一行再换行|则保留换行。具体选哪个取决于你希望模板渲染后有没有多余空行。我在实践中更多用因为模板最终发给模型的是一段连续文本多行缩进只是为了 YAML 里更好看。3.2 Chat 多消息模板怎么组织真实的对话应用很少只发一段文本绝大多数都包含系统指令、用户输入甚至历史消息。用 YAML 组织 Chat 模板格式是这样_type: chat name: customer-service-agent description: 客服助手的完整对话模板 input_variables: - user_name - order_id - chat_history messages: - _type: system template: 你是「某电商平台」的客服助手。 你的名字叫小安语气亲切专业。 用户叫 {user_name}请用「{user_name}」称呼对方。 如果用户询问订单进度请结合订单号 {order_id} 进行查询和答复。 - _type: placeholder variable_name: chat_history - _type: human template: {user_input}这个例子展示了三个关键点。第一系统消息里可以同时出现多个变量LangChain 渲染时会一次性替换。这比拆成多个模板再拼接干净得多。第二placeholder类型的消息不渲染任何内容它只是声明“这里运行时插入一段消息列表”。当你需要把聊天历史传进模型时MessagesPlaceholder是标准做法。在 YAML 里写 placeholder 消息记得字段是variable_name不是template这个很容易写错。第三human消息的template可以写成一个变量占位符也可以写成固定文本再加变量。按需组合即可。加载这个模板之后代码里可以这样使用from langchain_core.prompts import load_prompt prompt load_prompt(prompts/customer_service.yaml) messages prompt.format_prompt( user_nameLily, order_id20250101001, user_input我的订单什么时候发货, chat_history[...] # 传入历史消息 ).to_messages()这里format_prompt和format的区别在于format_prompt返回一个PromptValue对象可以调用.to_messages()得到结构化的消息列表更适合聊天接口format则直接返回字符串适合纯文本场景。3.3 变量的四种玩法占位变量远不只{variable}这一种写法实际项目里我经常用到四种不同的玩法。第一种是普通插值也就是{var}适用于 f-string 模板格式。第二种是列表变量。某些提示词需要把一组内容列举出来比如让模型根据一组关键词生成文章。如果变量值本身是 Python 列表模板里可以用模板引擎的语法展开不过前提是把template_format切换成jinja2。比如_type: prompt name: article-from-keywords input_variables: - keywords template_format: jinja2 template: | 请基于以下关键词生成一篇 800 字的文章 {% for kw in keywords %} - {{ kw }} {% endfor %}Jinja2 模板的切换在我们日常写提示词时非常实用它支持循环、条件、过滤器表达能力比单纯的 f-string 高出不少。缺点是渲染速度略慢心智负担稍高但对提示词这么短的文本来说性能差异完全可以忽略。第三种是 partial 局部变量。有时候模板里有些变量在文件加载阶段就确定了每次调用都要传一遍很烦。LangChain 提供了partial_variables机制。在 YAML 里可以直接声明_type: prompt name: weekly-report partial_variables: company_name: 某科技公司 input_variables: - week_content template: 你是 {company_name} 的运营助理请根据以下本周工作内容生成周报 {week_content}这样调用.format(week_content...)时就不用再传company_name了。不过需要说明的是如果局部变量的值本身是个动态函数结果比如“当前日期”那就没法在 YAML 里写死了更适合在代码里调用prompt.partial(todaytoday_str)来注入。第四种是条件分支。配合 Jinja2可以在模板里根据变量是否存在或等于某个值输出不同的指令片段。例如{% if language zh %} 请用中文回答。 {% elif language en %} Please answer in English. {% else %} 请使用中文或英文回答。 {% endif %}这种写法在处理多语言 Agent 时非常顺手。但要注意一点模板里条件分支越多模板的可读性越差而且对模型的理解负担也越大。建议把差异特别大的部分拆成不同模板而不是在一个模板里堆满条件。3.4 Few-shot 示例的两种组织方式提示词里的 Few-shot 示例常见有两种组织方式。第一种是直接把示例文本写死在模板里这也是我早期最常用的方式_type: prompt name: sentiment-classify input_variables: - text template: | 请判断以下评论的情感倾向输出「正面」或「负面」。 示例1 商品质量很好物流也快。 正面 示例2 等了三天还没发货客服也不回消息。 负面 现在请判断 {text}这种方式简单直接渲染结果可控适合示例数量少且固定不变的场景。缺点是不好维护示例一多或者需要按条件切换时模板会变得臃肿。第二种方式是使用FewShotPromptTemplate的结构化配置。在 YAML 里可以放examples字段和example_prompt字段_type: few_shot name: intent-detect input_variables: - user_query examples: - input: 我要退掉昨天买的那个耳机 output: 退货退款 - input: 这个商品还能便宜点吗 output: 价格咨询 example_prompt: _type: prompt input_variables: - input - output template: 用户说{input}\n意图{output} template: | 请识别用户意图输出简洁的意图标签。 {examples} 用户说{user_query} 意图这种方式适合示例数量多、需要动态增删的场景。LangChain 加载 few_shot 模板后会在渲染时自动用example_prompt把每条例子格式化再统一嵌入{examples}占位处。不过说实话我踩过一个小坑examples字段在 YAML 里写起来比较繁琐尤其是示例变多时文件很长。后来我改变了一下策略把示例单独放到 JSON 文件里代码里读取后调用example_selector动态选择。这样配置更清晰也能根据用户输入动态筛选最相关的示例效果比每次把所有示例都塞给模型好得多。4. 进阶玩法目录组织、多环境切换与 CI 校验4.1 多模块目录的组织与命名规范提示词一多单目录平铺就撑不住了。我现在的做法是按业务模块分目录一个模块一个子目录每个模板单独一个 YAML 文件prompts/ ├── customer_service/ │ ├── greet.yaml │ ├── refund.yaml │ └── complaint.yaml ├── content_engine/ │ ├── title.yaml │ ├── summary.yaml │ └── rewrite.yaml └── common/ ├── guardrails.yaml └── output_format.yaml命名规范上我坚持全部小写、用连字符分隔比如refund_policy.yaml这种风格。文件命名和里面的name字段保持一致这样后续加载的时候从文件名就能定位到模板日志打印出来也容易排查。模块多了之后每次都要写全路径很繁琐。我封装了一个简单的加载函数带缓存避免重复解析 YAML 文件多次from functools import lru_cache from pathlib import Path from langchain_core.prompts import load_prompt lru_cache(maxsize128) def get_prompt(module: str, name: str): path Path(prompts) / module / f{name}.yaml return load_prompt(str(path))这样调用时只需要get_prompt(customer_service, refund)清晰好记而且同路径模板只会解析一次。提示词项目有什么特殊之处就是缓存收益很大因为提示词通常不会在运行期变化重复解析纯属浪费。4.2 模板引用与组合的两种实现提示词之间经常有公共片段比如“你是某个平台的客服助手语气专业”这段系统设定几乎所有客服相关模板里都要出现。复制粘贴当然可以但维护时改一处忘一处迟早出事。两种做法可以解决这个问题。第一种是在 Python 代码里组合。加载公共模板再把公共模板和业务模板拼接成新的ChatPromptTemplatebase_prompt get_prompt(common, assistant_base) custom_prompt get_prompt(customer_service, refund) # 合并消息 messages base_prompt.messages custom_prompt.messages combined ChatPromptTemplate(messagesmessages)这种方式灵活能利用 Python 的逻辑进行动态组装适合组合逻辑复杂的场景。缺点是把模板的依赖关系从 YAML 中抽离到了代码里看 YAML 文件看不到它依赖了谁。第二种是我更喜欢的做法在 YAML 顶层自定义一个extends字段在加载后做递归合并。比如_type: chat name: refund-assistant extends: common/assistant_base input_variables: - order_id messages: - _type: human template: 用户想退款订单号是 {order_id}配套加载函数里判断一下有没有extends字段有的话先加载被引用的模板再把当前文件的消息追加到后面。这种方式让 YAML 文件自描述依赖关系一目了然业务同学改动时也不容易破坏公共模板。需要提醒的是extends是自定义字段LangChain 原生并不认识必须由你自己的加载函数处理。如果你直接调用原生load_prompt加载带 extends 的文件它会因为这个未知字段报错或忽略具体取决于版本。所以这类自定义字段一定要记得在加载前剥离或者自行解析。4.3 多环境切换的一点经验不同环境通常需要不同配置尤其模型参数和部分提示词策略。我的做法是目录结构固定通过环境变量决定加载路径前缀。# 各环境通过配置注入 # PROMPT_ENVprod 时加载 prompts/prod/customer_service/refund.yaml # PROMPT_ENVdev 时加载 prompts/dev/customer_service/refund.yaml代码里用os.getenv(PROMPT_ENV, dev)拼接出完整路径。这样切环境只需要改环境变量模板本身不动。另一个更细颗粒度的做法是把环境相关的变量统一注入到 partial。比如生产环境需要隐藏内部术语开发环境可以放开。把这些差异全部抽成变量在模板里通过 Jinja2 条件判断加载时根据环境变量注入不同的值。实务上我建议优先用路径隔离因为不同环境的模板差异往往不止是几个变量而是整个话术都有区别路径隔离最简单也不容易把环境搞串。4.4 CI 里跑一个模板校验脚本提示词配置化的一个额外好处就是可以自动化校验。我在项目里写了一个很朴素的校验脚本每次提交都跑一遍基本杜绝了“模板变量写错线上渲染报错”这种低级事故。校验脚本的思路很简单遍历prompts目录下所有 YAML 文件。用load_prompt加载每个文件确认格式没问题、类型能解析。检查input_variables和模板中实际出现的占位符是否一致。构造一组测试变量实际调用format()渲染确保没有运行时错误。核心代码大致长这样import re from pathlib import Path from langchain_core.prompts import load_prompt def extract_variables(template: str) - set[str]: return set(re.findall(r\{(\w)\}, template)) errors [] for path in Path(prompts).rglob(*.yaml): try: prompt load_prompt(str(path)) except Exception as e: errors.append(f{path}: 加载失败: {e}) continue declared set(prompt.input_variables) if prompt.input_variables else set() declared.update(prompt.partial_variables.keys() if prompt.partial_variables else []) actual set() for message in getattr(prompt, messages, [getattr(prompt, template, )]): if hasattr(message, template): actual | extract_variables(message.template) elif isinstance(message, dict): actual | extract_variables(message.get(template, )) if declared ! actual: errors.append(f{path}: 声明的变量 {declared} 与模板中的变量 {actual} 不一致) if errors: raise SystemExit(\n.join(errors))注意上面这版是针对prompt和chat混用的情况写的简化逻辑不同版本的消息对象结构略有差异。真实项目中你大概率需要按_type分情况处理。但核心思想是一致的把变量一致性校验做进 CI让问题在 merge 之前就暴露。校验表里我通常会检查这几类问题检查项说明YAML 语法是否合法文件能否被yaml.safe_load正常解析_type是否合法是否为prompt、chat、few_shot等已知类型变量声明是否完整input_variables与模板中占位符是否一一对应模板能否实际渲染用模拟变量调用format是否抛出运行时错误是否引用了不存在的 extends自定义extends指向的模板是否存在这套校验跑起来非常快几十个模板也就是几秒钟的事却能让线上节省大量排错时间。5. 常见问题与排查实录5.1 五个高频报错与应对我用 YAML 管理提示词的这几年遇到过不少报错下面这几个出现频率最高可以当速查表用。现象原因解决方式加载时报InvalidPromptConfigError_type字段缺失或拼错打开 YAML 确认_type在首行值应为prompt/chat/few_shot渲染时KeyError: xxxformat调用漏传了模板中声明的变量核对input_variables声明检查调用处传参是否齐全提示词里出现双大括号{{ }}f-string 模板需要转义花括号需要用字面花括号时在 f-string 中写成{{和}}中文乱码或缩进错误文件编码问题统一使用 UTF-8 编码编辑器和 CI 保持一致升级 v1.0 后ImportErrorload_prompt的包路径变化优先从langchain_core.prompts导入检查包版本第一个报错最常见。_type是整个序列化机制的钥匙拼错一个字母LangChain 就不知道拿哪个类来解析。比如把chat写成chatt直接报错。这种问题靠编辑器自带的高亮就能发现关键是一开始就养成写_type的习惯。第二个报错本质上是变量管理的疏漏。我整理过一个经验模板里每个变量都要想清楚它到底是调用方传入的还是用 partial 注入的。别混着写混着写必然漏网之鱼。第三个花括号问题值得一提。你在 YAML 里写 JSON 示例时特别容易踩到。比如模板里有{key: value}在 f-string 模板里这串花括号会被解析成变量直接报错或变成空值。解决方法是换成 Jinja2 格式或者把示例里的花括号加倍转义。更省心的做法是像我在 3.4 节提到的那样把复杂示例挪到外部 JSON 文件从根源上避开花括号冲突。5.2 我的排查流程与调试技巧聊到最后分享一下我实际的排查流程这套流程在多次救场之后基本固定下来了。第一步先验证 YAML 本身的合法性。我会在命令行里单独跑一句python -c import yaml, sys; yaml.safe_load(open(sys.argv[1])); print(YAML OK) prompts/xxx.yaml这是最原始但最有效的一层隔离。如果这一步都过不了问题跟 LangChain 完全无关是文件格式本身的问题常见于缩进不一致、Tab 和空格混用、特殊字符非法。第二步单独加载模板不调模型。写个小脚本调用load_prompt再format一把打印出渲染后的完整文本逐字检查发给模型的内容到底是什么。这一步能发现大多数问题变量没替换、换行不对、模板被截断、消息顺序错乱全都能直观看到。第三步排查变量来源。打印prompt.input_variables和prompt.partial_variables确认模板对象里实际持有的变量集合和调用方传入的集合是否完全匹配。很多时候报错只是表象根源是某个变量被上游逻辑漏掉了。把变量打印出来基本可以断言问题出在哪个环节。最后分享两个我在项目里反复受益的小习惯算是压箱底的东西。第一个习惯是每次改完 YAML 模板都跑一遍本地渲染脚本把渲染结果复制出来看一下再上线。不要迷信“就改了几个字”不会有问题。模板一变模型输出可能天差地别先看渲染结果是最低成本的回归测试。第二个习惯是给模板文件写一行固定的注释头标明创建日期和最后修改人。虽然 Git 里有记录但直接在文件里看到“这版是谁改的”能省很多沟通成本。团队一多这个习惯尤其值钱。用 YAML 管理提示词本质上是把提示词当成和代码同等重要的资产去对待。它不应该只是段落在代码里的字符串它值得有自己的文件、自己的版本历史、自己的 CI 校验。这套流程跑顺之后我只后悔一件事——为什么没有早点把事情做对。