LangChain 设计与实现

第6章 提示词模板引擎

作者 杨艺韬 · 12,007 字 · 发布于 · 更新于

本章基于 langchain-core 1.2.26 / langchain-classic 1.0.3 源码分析。核心代码位于 langchain_core/prompts/ 目录。

提示词(Prompt)是与大语言模型交互的"编程语言"。在实际的 LLM 应用开发中,提示词很少是一成不变的静态字符串——它们通常包含需要在运行时填入的变量(如用户输入、检索到的上下文、对话历史等),需要根据场景动态组装多条不同角色的消息,有时还需要根据输入动态选择少样本示例来引导模型的输出格式。

硬编码字符串拼接(如 Python 的 f-string)虽然能满足最简单的场景,但它有诸多局限:无法进行变量验证、难以复用和组合、不支持序列化和版本管理、无法自动推断所需的输入变量。LangChain 的提示词模板引擎将提示词的构建过程从硬编码字符串提升为一个可组合、可验证、可序列化的模板系统。从最基础的字符串模板 PromptTemplate,到支持多角色对话的 ChatPromptTemplate,再到动态选择示例的 FewShotPromptTemplate,这套模板引擎覆盖了从简单到复杂的全部提示词构建场景。更重要的是,所有模板都实现了 Runnable 接口,可以作为 LCEL 链的组成部分参与管道组合。

本章将从 BasePromptTemplate 基类出发,逐层剖析模板体系的层级设计、变量提取与验证机制、消息级别的模板系统,以及模板与文档的格式化集成。

本章要点

  1. BasePromptTemplate 层级:BasePromptTemplate 作为 Runnable 的设计,以及 format、format_prompt、invoke 的三层调用关系
  2. PromptTemplate:字符串模板的三种格式(f-string/mustache/jinja2)、自动变量提取和模板验证
  3. ChatPromptTemplate:多角色对话模板的构建、消息类型的自动推断,以及 from_messages 的格式支持
  4. MessagePromptTemplate 子类:HumanMessagePromptTemplate、AIMessagePromptTemplate、SystemMessagePromptTemplate 和多模态模板
  5. MessagesPlaceholder:动态消息列表占位符及其在对话历史管理中的应用
  6. FewShotPromptTemplate:少样本提示的静态/动态示例选择机制

6.1 BasePromptTemplate:模板体系的抽象基础

6.1.1 类定义与 Runnable 集成

# libs/core/langchain_core/prompts/base.py:40(各字段的中文说明为本专栏所加,源码是英文 docstring)

class BasePromptTemplate(
    RunnableSerializable[dict, PromptValue], ABC, Generic[FormatOutputType]
):
    input_variables: list[str]
    """必需的输入变量名列表"""

    optional_variables: list[str] = Field(default=[])
    """可选变量名列表(由 MessagesPlaceholder 等自动推断)"""

    input_types: builtins.dict[str, Any] = Field(default_factory=dict, exclude=True)
    """变量的类型声明,未声明的变量默认为 str"""

    output_parser: BaseOutputParser | None = None
    """关联的输出解析器"""

    partial_variables: Mapping[str, Any] = Field(default_factory=dict)
    """预设的部分变量"""

    metadata: builtins.dict[str, Any] | None = None
    """追踪元数据"""

    tags: list[str] | None = None
    """追踪标签"""

BasePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:40)同时继承了 RunnableSerializable、ABC(抽象基类)和 Generic[FormatOutputType],这个三重继承的组合非常巧妙。RunnableSerializable[dict, PromptValue] 确定了模板在 LCEL 链中的输入输出类型:接受字典格式的变量映射,产出 PromptValue 对象。ABC 确保了基类不可直接实例化。Generic[FormatOutputType] 为 format 方法的返回类型提供了泛型参数:例如 ImagePromptTemplate 声明为 BasePromptTemplate[ImageURL],format 返回图片 URL 字典;StringPromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/string.py:311)则没有给基类填泛型参数,而是直接把 format 覆写成返回 str,所以这一子树的 format 返回纯字符串;而 format_prompt 方法则始终返回 PromptValue(基类 PromptValue 与子类 StringPromptValue、ChatPromptValue 依次定义在 langchain-core-1.2.26/libs/core/langchain_core/prompt_values.py:24、:54、:80)。

作为 RunnableSerializable 的子类,提示词模板可以直接参与 LCEL 链式调用。这是 LangChain 最常见的使用模式之一:

# 模板作为 LCEL 链的起点
chain = prompt | model | output_parser
result = chain.invoke({"question": "什么是 LangChain?"})

6.1.2 invoke 的调用链路

# libs/core/langchain_core/prompts/base.py:206-230(docstring 从略)
@override
def invoke(
    self, input: dict, config: RunnableConfig | None = None, **kwargs: Any
) -> PromptValue:
    config = ensure_config(config)
    if self.metadata:
        config["metadata"] = {**config["metadata"], **self.metadata}
    if self.tags:
        config["tags"] += self.tags
    return self._call_with_config(
        self._format_prompt_with_error_handling,
        input,
        config,
        run_type="prompt",
        serialized=self._serialized,
    )

invoke 方法(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:207)的实现体现了 LangChain 追踪系统的深度集成。它首先将模板自身的 metadata 和 tags 合并到运行配置中——ensure_config 已把传入 config 的 tags/metadata 复制了一份,所以这些追踪信息只挂在提示词这一次运行上,不会传播到链里的下游步骤(实跑 prompt | model:提示词运行的 tags 为 ['seq:step:1', 'ptag'],模型运行只有 ['seq:step:2'])。然后通过继承自 Runnable 的 _call_with_config 方法执行模板格式化。run_type="prompt" 参数告知 LangSmith 等追踪系统这是一个提示词格式化操作,使得在追踪面板中可以清晰地看到提示词格式化这个独立的步骤及其输入输出。serialized=self._serialized 传入了模板的序列化表示(_serialized 用 @cached_property 缓存,见 langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:125,避免重复序列化),让追踪系统能够记录哪个模板被使用了。

sequenceDiagram
    participant User as 调用者
    participant Invoke as invoke(input)
    participant CWC as _call_with_config()
    participant FP as _format_prompt_with_error_handling()
    participant VI as _validate_input()
    participant Format as format_prompt()
    User->>Invoke: {"question": "..."}
    Invoke->>Invoke: 合并 metadata/tags
    Invoke->>CWC: input, config, run_type="prompt"
    CWC->>FP: input
    FP->>VI: input
    VI->>VI: 检查必需变量
    VI-->>FP: validated_input
    FP->>Format: **validated_input
    Format-->>FP: PromptValue
    FP-->>CWC: PromptValue
    CWC-->>Invoke: PromptValue
    Invoke-->>User: PromptValue

6.1.3 输入验证

# libs/core/langchain_core/prompts/base.py:160-194(create_message 包装从略)
def _validate_input(self, inner_input: Any) -> dict:
    if not isinstance(inner_input, dict):
        if len(self.input_variables) == 1:
            # 单变量模板允许直接传值
            var_name = self.input_variables[0]
            inner_input_ = {var_name: inner_input}
        else:
            raise TypeError(...)
    else:
        inner_input_ = inner_input

    missing = set(self.input_variables).difference(inner_input_)
    if missing:
        msg = (
            f"Input to {self.__class__.__name__} is missing variables {missing}. "
            f" Expected: {self.input_variables}"
            f" Received: {list(inner_input_.keys())}"
        )
        example_key = missing.pop()
        msg += (
            f"\nNote: if you intended {{{example_key}}} to be part of the string"
            " and not a variable, please escape it with double curly braces like: "
            f"'{{{{{example_key}}}}}'."
        )
        raise KeyError(msg)
    return inner_input_

_validate_input 方法(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:160,由同文件 :196 的 _format_prompt_with_error_handling 调用)在每次格式化前被调用,它负责确保用户提供了所有必需的变量。这个方法包含两个精心设计的便利特性:

  1. 单变量快捷传值:如果模板只有一个输入变量,可以直接传入值而非字典。例如唯一变量名为 user_input 时,prompt.invoke("Hello") 等价于 prompt.invoke({"user_input": "Hello"})。

  2. 友好的错误提示:当变量缺失时,错误消息不仅列出缺失的变量,还贴心地提示用户如果想在模板中使用字面花括号,需要用双花括号转义。实跑 PromptTemplate.from_template("Tell me about {topic} in {language}").invoke({"topic": "x"}) 得到:

KeyError: "Input to PromptTemplate is missing variables {'language'}.  Expected: ['language', 'topic'] Received: ['topic']\nNote: if you intended {language} to be part of the string and not a variable, please escape it with double curly braces like: '{{language}}'.\nFor troubleshooting, visit: https://docs.langchain.com/oss/python/langchain/errors/INVALID_PROMPT_INPUT "

多变量模板若传入非字典,则抛 TypeError: Expected mapping type as input to PromptTemplate. Received <class 'str'>.(同样带 troubleshooting 链接)。

6.1.4 partial:部分变量绑定

# libs/core/langchain_core/prompts/base.py:280
def partial(self, **kwargs: str | Callable[[], str]) -> BasePromptTemplate:
    prompt_dict = self.__dict__.copy()
    prompt_dict["input_variables"] = list(
        set(self.input_variables).difference(kwargs)
    )
    prompt_dict["partial_variables"] = {**self.partial_variables, **kwargs}
    return type(self)(**prompt_dict)

partial 方法(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:280)是函数式编程中"偏应用"(partial application)概念在模板系统中的体现。它返回一个新的模板实例(不修改原实例),其中指定的变量已被预填充。被部分绑定的变量会从 input_variables 中移除,添加到 partial_variables 中。这意味着新模板的 invoke 方法不再要求用户提供这些变量。

partial 方法特别强大的一个特性是:部分变量不仅可以是静态值,还可以是 Callable(可调用对象)。当变量值是 callable 时,它不会在 partial 调用时被求值,而是延迟到每次格式化时才被调用。这对于需要动态计算的值(如当前时间、请求 ID 等)非常有用:

from datetime import datetime

prompt = PromptTemplate.from_template("Today is {date}. {question}")
partial_prompt = prompt.partial(date=lambda: datetime.now().strftime("%Y-%m-%d"))
# 每次调用时 date 会动态计算

格式化时,_merge_partial_and_user_variables 方法(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:296)会检测并调用这些 callable:

def _merge_partial_and_user_variables(self, **kwargs: Any) -> dict[str, Any]:
    partial_kwargs = {
        k: v if not callable(v) else v() for k, v in self.partial_variables.items()
    }
    return {**partial_kwargs, **kwargs}

6.1.5 变量名校验

# libs/core/langchain_core/prompts/base.py:79-105(报错文案按源码,create_message 包装从略)
@model_validator(mode="after")
def validate_variable_names(self) -> Self:
    if "stop" in self.input_variables:
        raise ValueError(
            "Cannot have an input variable named 'stop', as it is used internally,"
            " please rename."
        )
    if "stop" in self.partial_variables:
        raise ValueError(
            "Cannot have an partial variable named 'stop', as it is used "
            "internally, please rename."
        )
    overall = set(self.input_variables).intersection(self.partial_variables)
    if overall:
        raise ValueError(f"Found overlapping input and partial variables: {overall}")
    return self

"stop" 是 LangChain 内部保留的参数名(用于传递停止词给模型),不允许作为模板变量名使用;这条校验写在 validate_variable_names 里(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:80)。这个验证在模板创建时就会触发,避免运行时的名称冲突。由于它是 Pydantic 验证器,用户实际看到的是包了一层的 pydantic_core.ValidationError,例如 PromptTemplate.from_template("{stop} x") 实跑报 1 validation error for PromptTemplate 后接 Value error, Cannot have an input variable named 'stop', as it is used internally, please rename.。

6.2 PromptTemplate:字符串模板引擎

6.2.1 三种模板格式

PromptTemplate 是最基础的模板类型,继承自 StringPromptTemplate。它接受一个模板字符串,通过变量替换生成最终的提示词文本。PromptTemplate 支持三种模板格式,通过 template_format 字段切换,每种格式有不同的语法和安全特性:

# langchain_core/prompts/prompt.py

class PromptTemplate(StringPromptTemplate):
    template: str
    template_format: PromptTemplateFormat = "f-string"
    validate_template: bool = False
graph TB
    subgraph "f-string(默认)"
        F1["模板: 'Hello {name}'"]
        F2["格式化: formatter.format('Hello {name}', name='World')"]
        F3["结果: 'Hello World'"]
        F1 --> F2 --> F3
    end
    subgraph "mustache"
        M1["模板: 'Hello {{name}}'"]
        M2["格式化: mustache_formatter('Hello {{name}}', name='World')"]
        M3["结果: 'Hello World'"]
        M1 --> M2 --> M3
    end
    subgraph "jinja2(需谨慎)"
        J1["模板: 'Hello {{ name }}'"]
        J2["格式化: SandboxedEnvironment().render(name='World')"]
        J3["结果: 'Hello World'"]
        J1 --> J2 --> J3
    end

三种格式的具体实现:

# langchain_core/prompts/string.py

# mustache 格式化器
def mustache_formatter(template: str, /, **kwargs: Any) -> str:
    return mustache.render(template, kwargs)

# jinja2 格式化器(沙箱化)
def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
    return SandboxedEnvironment().from_string(template).render(**kwargs)

DEFAULT_FORMATTER_MAPPING: dict[str, Callable[..., str]] = {
    "f-string": formatter.format,   # langchain_core.utils.formatting.StrictFormatter 实例
    "mustache": mustache_formatter,
    "jinja2": jinja2_formatter,
}

f-string 一栏挂的不是自定义函数,而是 langchain_core/utils/formatting.py 里 StrictFormatter 单例的 format 方法。StrictFormatter 继承 string.Formatter,只重写了 vformat:一旦传入位置参数就抛 ValueError,强制所有替换都走关键字参数。

三种格式的选择策略值得讨论。f-string 是默认格式,语法简单直观(底层是 Python 标准库的 string.Formatter),适用于绝大多数场景。mustache 格式的优势在于模板本身是合法的文本(双花括号不容易与其他语法冲突),在需要将模板嵌入 JSON 或 YAML 配置文件时特别有用。jinja2 格式最为强大,支持条件判断、循环、过滤器等高级特性,但也带来了安全风险。

jinja2 格式化器使用 SandboxedEnvironment 来阻止对 dunder 属性(如 __class__、__globals__)的访问,这是防止模板注入攻击的关键安全措施。但是沙箱保护属于"opt-out"(默认允许、选择性禁止)而非"opt-in"(默认禁止、选择性允许)的安全模型,无法完全防御所有攻击向量——langchain-core-1.2.26/libs/core/langchain_core/prompts/string.py:67-71 的注释写明,普通属性访问(如 {{obj.attr}})和方法调用仍然放行(实跑 {{ x.upper() }} 正常输出,{{ x.__class__ }} 渲染为空串)。因此源码中反复强调:即使有沙箱保护,也不应该接受不可信来源的 jinja2 模板。另外,jinja2 是可选依赖,环境里没装时,连 from_template(..., template_format="jinja2") 提取变量这一步都会抛 ImportError: jinja2 not installed, ...。

mustache 在 1.2.26 里也有一道类似的限制:{{person.name}} 这类点号路径只允许逐级进入 dict、list、tuple,遇到其他 Python 对象即放弃解析(langchain-core-1.2.26/libs/core/langchain_core/utils/mustache.py:395-417,注释写明是为了不遍历任意 Python 对象),结果按"找不到"处理,渲染为空串——实跑传入一个带 name 属性的普通对象,Hi {{person.name}} 输出 'Hi '。

6.2.2 自动变量提取

# libs/core/langchain_core/prompts/prompt.py:89
@model_validator(mode="before")
@classmethod
def pre_init_validation(cls, values: dict) -> Any:
    if values.get("template") is None:
        return values

    values.setdefault("template_format", "f-string")
    values.setdefault("partial_variables", {})

    if values.get("validate_template"):
        ...  # :102-114:mustache 直接拒绝验证;须显式给出 input_variables;
             # 再用 check_valid_template 比对(见 §6.8.1)

    if values["template_format"]:
        values["input_variables"] = [
            var for var in get_template_variables(
                values["template"], values["template_format"]
            )
            if var not in values["partial_variables"]
        ]
    return values

pre_init_validation 是一个 @model_validator(mode="before") 验证器,它在 Pydantic 模型初始化(字段赋值)之前运行。这个时机至关重要:它允许在用户不提供 input_variables 参数的情况下,自动从模板字符串中推断变量列表,并将结果设置到 values 字典中供后续的 Pydantic 初始化使用。get_template_variables 函数根据不同的模板格式使用不同的解析策略:

# libs/core/langchain_core/prompts/string.py:256
def get_template_variables(template: str, template_format: str) -> list[str]:
    if template_format == "jinja2":
        input_variables = _get_jinja2_variables_from_template(template)
    elif template_format == "f-string":
        input_variables = {
            v for _, v, _, _ in Formatter().parse(template) if v is not None
        }
    elif template_format == "mustache":
        input_variables = mustache_template_vars(template)
    else:
        msg = f"Unsupported template format: {template_format}"
        raise ValueError(msg)

    # f-string 额外拦截属性访问 / 下标 / 纯数字变量名
    if template_format == "f-string":
        for var in input_variables:
            if "." in var or "[" in var or "]" in var:
                raise ValueError(...)   # 变量名不得含 . 或 []
            if var.isdigit():
                raise ValueError(...)   # 纯数字会被当成位置参数

    return sorted(input_variables)

对于 f-string 格式,LangChain 使用 Python 标准库的 string.Formatter 来解析模板,提取所有花括号包围的字段名,并额外禁止 {obj.attr} / {obj[0]} 这类带属性访问或下标的字段名(防模板注入)以及 {0} 这类会被当作位置参数的纯数字名。对于 jinja2 格式,_get_jinja2_variables_from_template 用 jinja2.meta.find_undeclared_variables 分析模板 AST(抽象语法树),找出所有未声明的变量引用。对于 mustache 格式,mustache_template_vars 走 LangChain 自带的 mustache tokenizer,只收集 section 之外的顶层变量名({{person.name}} 只记 person)。三条路径最后都返回 sorted(...) 的有序列表。

这种自动推断机制意味着用户在创建 PromptTemplate 时通常不需要手动指定 input_variables,框架会自动完成这个工作:

# 不需要手动指定 input_variables
prompt = PromptTemplate.from_template("Tell me about {topic} in {language}")
print(prompt.input_variables)  # ['language', 'topic'](get_template_variables 返回排序后的列表)

6.2.3 from_template 工厂方法

# libs/core/langchain_core/prompts/prompt.py:256-311(docstring 从略;其中
# :267-279 是一条安全警告:优先用 template_format='f-string',绝不要用 jinja2
# 渲染来源不可信的模板,否则等于任意 Python 代码执行)
@classmethod
def from_template(
    cls,
    template: str,
    *,
    template_format: PromptTemplateFormat = "f-string",
    partial_variables: dict[str, Any] | None = None,
    **kwargs: Any,
) -> PromptTemplate:
    input_variables = get_template_variables(template, template_format)
    partial_variables_ = partial_variables or {}
    if partial_variables_:
        input_variables = [var for var in input_variables if var not in partial_variables_]
    return cls(
        input_variables=input_variables,
        template=template,
        template_format=template_format,
        partial_variables=partial_variables_,
        **kwargs,
    )

from_template 是推荐的创建方式。它自动提取变量,排除已部分绑定的变量,然后构造模板对象。直接使用构造器时,pre_init_validation 也会执行同样的变量提取。

6.2.4 format 方法

def format(self, **kwargs: Any) -> str:
    kwargs = self._merge_partial_and_user_variables(**kwargs)
    return DEFAULT_FORMATTER_MAPPING[self.template_format](self.template, **kwargs)

format 方法的实现极其简洁。它首先合并部分变量和用户提供的变量(callable 在此时被调用),然后委托给对应格式的格式化器。

6.2.5 模板拼接

# libs/core/langchain_core/prompts/prompt.py:142
def __add__(self, other: Any) -> PromptTemplate:
    if isinstance(other, PromptTemplate):
        if self.template_format != other.template_format:
            raise ValueError("Cannot add templates of different formats")
        input_variables = list(set(self.input_variables) | set(other.input_variables))
        template = self.template + other.template
        partial_variables = dict(self.partial_variables.items())
        for k, v in other.partial_variables.items():
            if k in partial_variables:
                raise ValueError("Cannot have same variable partialed twice.")
            partial_variables[k] = v
        return PromptTemplate(
            template=template,
            input_variables=input_variables,
            partial_variables=partial_variables,
            template_format=self.template_format,
            validate_template=validate_template,  # 两边都开验证才开(赋值行从略)
        )
    if isinstance(other, str):
        prompt = PromptTemplate.from_template(other, template_format=self.template_format)
        return self + prompt
    raise NotImplementedError(f"Unsupported operand type for +: {type(other)}")

+ 运算符支持两个 PromptTemplate 的拼接,也支持与字符串的拼接。但两个模板必须使用相同的格式(否则 ValueError: Cannot add templates of different formats),且不能有重复的部分变量;其他类型的操作数抛 NotImplementedError。

6.3 ChatPromptTemplate:多角色对话模板

6.3.1 消息模板的构建

ChatPromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:790,基类 BaseChatPromptTemplate 在同文件 :691)是 LangChain 中使用频率最高的模板类型,因为现代大语言模型几乎全部采用基于消息的对话接口。与 PromptTemplate 将所有内容格式化为单一字符串不同,ChatPromptTemplate 将模板格式化为一个有序的消息列表,每条消息都有明确的角色(system/human/ai/tool 等)。它管理一个消息模板的有序列表,每个元素可以是消息模板对象、固定消息实例,或者嵌套的 ChatPromptTemplate:

# langchain_core/prompts/chat.py

class ChatPromptTemplate(BaseChatPromptTemplate):
    messages: list[MessageLike]
    """消息模板列表:可以是 BaseMessagePromptTemplate、BaseMessage 或 BaseChatPromptTemplate"""

    validate_template: bool = False

6.3.2 构造器的格式支持

ChatPromptTemplate 的构造器(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:903)接受多种格式的消息定义:

def __init__(
    self,
    messages: Sequence[MessageLikeRepresentation],
    *,
    template_format: PromptTemplateFormat = "f-string",
    **kwargs: Any,
) -> None:
    messages_ = [
        _convert_to_message_template(message, template_format)
        for message in messages
    ]

    # 自动推断输入变量
    input_vars: set[str] = set()
    optional_variables: set[str] = set()
    partial_vars: dict[str, Any] = {}
    for message in messages_:
        if isinstance(message, MessagesPlaceholder) and message.optional:
            partial_vars[message.variable_name] = []
            optional_variables.add(message.variable_name)
        elif isinstance(message, (BaseChatPromptTemplate, BaseMessagePromptTemplate)):
            input_vars.update(message.input_variables)
    ...

构造器内部通过 _convert_to_message_template 函数(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:1414)将各种格式的输入统一转换为标准的消息模板对象。这个函数是 ChatPromptTemplate 能够支持多种输入格式的关键。以下代码展示了它支持的所有输入格式:

template = ChatPromptTemplate([
    # 格式1: 2-元组 (role, template)
    ("system", "You are a {role}."),

    # 格式2: BaseMessage 实例
    SystemMessage(content="Always be helpful."),

    # 格式3: BaseMessagePromptTemplate 实例
    HumanMessagePromptTemplate.from_template("{question}"),

    # 格式4: MessagesPlaceholder
    ("placeholder", "{history}"),

    # 格式5: 纯字符串 -> HumanMessage
    "{user_input}",
])

元组格式中的角色名到消息类型的映射:

角色名 消息模板类型
"human" / "user" HumanMessagePromptTemplate
"ai" / "assistant" AIMessagePromptTemplate
"system" SystemMessagePromptTemplate
"placeholder" MessagesPlaceholder(字符串写法默认 optional=True)
其他 不支持,_create_template_from_message_type 直接抛 ValueError

这张映射表对应的源码是 langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:1337-:1410;"placeholder" 分支除字符串写法外还接受 [变量名, is_optional] 两元素列表,用来显式关掉 optional。两种写法的变量名都必须用花括号包住,写成 ("placeholder", "history") 会报 ValueError: Invalid placeholder template: history. Expected a variable name surrounded by curly braces.;角色名不在表内(如 ("tool", "x"))则报 ValueError: Unexpected message type: tool. Use one of 'human', 'user', 'ai', 'assistant', or 'system'.(均为实跑原文)。

除上例五种写法外,_convert_to_message_template 还接受两种形态:元组第一项是类而不是字符串,如 (HumanMessage, "{a}")(带 type 字段的消息类按该字段查上表,实跑得到 HumanMessagePromptTemplate)或 (SystemMessagePromptTemplate, "{c}")(直接用该类包一个 PromptTemplate);以及键恰好为 role、content 的字典,如 {"role": "assistant", "content": "{b}"},按 role 查上表,多一个或少一个键都抛 ValueError。

6.3.3 自动变量推断

构造器中的变量推断逻辑特别精巧:

# 可选的 MessagesPlaceholder 自动获得空列表作为默认值
if isinstance(message, MessagesPlaceholder) and message.optional:
    partial_vars[message.variable_name] = []
    optional_variables.add(message.variable_name)

可选的 MessagesPlaceholder 会自动被添加到 partial_variables 中,默认值为空列表。这意味着即使用户不提供该变量,模板也不会报错。

# ChatPromptTemplate.validate_input_variables(model_validator(mode="before"))
# input_types 自动设置为 list[AnyMessage]
if message.variable_name not in input_types:
    input_types[message.variable_name] = list[AnyMessage]

MessagesPlaceholder 的变量会被自动设置类型为 list[AnyMessage],这使得自动生成的 JSON Schema 能够正确反映消息列表的类型。这一段不在 __init__ 里,而在 validate_input_variables 这个 mode="before" 验证器中(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:1049)。

所以绕过 __init__ 构造出来的模板也能拿到正确的 input_types。例如 ChatPromptTemplate.model_validate({"messages": [MessagesPlaceholder("h")]}) 这种写法,实跑 input_types 里同样有 h。

6.3.4 format_messages 的实现

def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
    kwargs = self._merge_partial_and_user_variables(**kwargs)
    result = []
    for message_template in self.messages:
        if isinstance(message_template, BaseMessage):
            result.extend([message_template])
        elif isinstance(
            message_template, (BaseMessagePromptTemplate, BaseChatPromptTemplate)
        ):
            message = message_template.format_messages(**kwargs)
            result.extend(message)
        else:
            msg = f"Unexpected input: {message_template}"
            raise ValueError(msg)
    return result

format_messages 方法(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:1170)是 ChatPromptTemplate 的核心引擎。它遍历所有消息模板,根据模板类型执行不同的处理策略。对于 BaseMessage 实例,直接把它放进结果列表(固定消息不需要格式化)。对于 BaseMessagePromptTemplate(包括 MessagesPlaceholder)和嵌套的 BaseChatPromptTemplate,则统一把合并后的全部 kwargs 传下去,由子模板自己挑用得上的变量。注意这里一律使用 extend 而非 append,因为 MessagesPlaceholder 和嵌套模板都可能展开为多条消息。任何其他类型都会触发 ValueError。

早期版本(langchain-core 0.1.3 及之前)曾按 message_template.input_variables 先切出变量子集再传参,PR #15186(2023-12-26,随 0.1.4 发布)把这段裁掉了,PR 说明是"让 invoke 把所有变量一路传下去";同一 PR 还让可选的 MessagesPlaceholder 不再计入 input_variables。经 invoke 调用时,缺失变量仍由外层 _validate_input 统一报出;直接调用 format_messages 则跳过这层检查,缺失变量会在子模板的格式化调用里以裸 KeyError(实跑为 KeyError: 'a')抛出。

flowchart TB
    Start["ChatPromptTemplate.format_messages(**kwargs)"]
    Merge["合并 partial_variables 和 kwargs"]
    Loop["遍历 self.messages"]
    Check{"消息类型?"}
    BM["BaseMessage<br/>直接添加"]
    MPT["BaseMessagePromptTemplate<br/>传入全部 kwargs<br/>调用 format_messages"]
    CPT["BaseChatPromptTemplate<br/>传入全部 kwargs<br/>调用 format_messages"]
    Extend["将结果 extend 到列表"]
    Result["返回 list[BaseMessage]"]
    Start --> Merge --> Loop
    Loop --> Check
    Check -->|BaseMessage| BM
    Check -->|BaseMessagePromptTemplate| MPT
    Check -->|BaseChatPromptTemplate| CPT
    BM --> Extend
    MPT --> Extend
    CPT --> Extend
    Extend --> Loop
    Loop -->|遍历完成| Result

6.3.5 + 运算符拼接

def __add__(self, other: Any) -> ChatPromptTemplate:
    partials = {**self.partial_variables}
    if hasattr(other, "partial_variables") and other.partial_variables:
        partials.update(other.partial_variables)

    if isinstance(other, ChatPromptTemplate):
        return ChatPromptTemplate(messages=self.messages + other.messages).partial(**partials)
    if isinstance(other, (BaseMessagePromptTemplate, BaseMessage, BaseChatPromptTemplate)):
        return ChatPromptTemplate(messages=[*self.messages, other]).partial(**partials)
    if isinstance(other, (list, tuple)):
        other_ = ChatPromptTemplate.from_messages(other)
        return ChatPromptTemplate(messages=self.messages + other_.messages).partial(**partials)
    if isinstance(other, str):
        prompt = HumanMessagePromptTemplate.from_template(other)
        return ChatPromptTemplate(messages=[*self.messages, prompt]).partial(**partials)
    raise NotImplementedError(f"Unsupported operand type for +: {type(other)}")

ChatPromptTemplate 的 + 运算符(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:1007)支持与多种类型拼接,使得模板的动态组合非常灵活:

system = ChatPromptTemplate([("system", "You are helpful.")])
user = ChatPromptTemplate([("human", "{question}")])
combined = system + user  # 两个 ChatPromptTemplate 合并

注意,BaseMessage.__add__(langchain-core-1.2.26/libs/core/langchain_core/messages/base.py:294)也返回 ChatPromptTemplate,这意味着消息对象也可以参与拼接:

from langchain_core.messages import SystemMessage
prompt = SystemMessage(content="Hello") + ("human", "{input}")
# 得到一个 ChatPromptTemplate

6.4 MessagePromptTemplate 子类体系

6.4.1 BaseMessagePromptTemplate:消息模板基类

# langchain_core/prompts/message.py

class BaseMessagePromptTemplate(Serializable, ABC):
    @abstractmethod
    def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
        """将参数格式化为消息列表"""

    @property
    @abstractmethod
    def input_variables(self) -> list[str]:
        """此模板需要的输入变量"""

BaseMessagePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/message.py:16——注意它单独住在 message.py,而不是 chat.py)定义了消息模板的最小接口:format_messages 和 input_variables。注意 format_messages 返回的是列表而非单条消息,这为 MessagesPlaceholder 等需要展开为多条消息的模板留出了空间。

6.4.2 BaseStringMessagePromptTemplate

class BaseStringMessagePromptTemplate(BaseMessagePromptTemplate, ABC):
    prompt: StringPromptTemplate
    """内部的字符串模板"""

    additional_kwargs: dict = Field(default_factory=dict)
    """传递给消息的附加参数"""

    @classmethod
    def from_template(
        cls, template: str, template_format="f-string", partial_variables=None, **kwargs
    ) -> Self:
        prompt = PromptTemplate.from_template(
            template, template_format=template_format, partial_variables=partial_variables
        )
        return cls(prompt=prompt, **kwargs)

    def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
        return [self.format(**kwargs)]

    @property
    def input_variables(self) -> list[str]:
        return self.prompt.input_variables

BaseStringMessagePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:226)通过组合一个 StringPromptTemplate 来实现消息模板功能。它的 input_variables 直接代理给内部的 prompt。

6.4.3 具体消息模板类

class HumanMessagePromptTemplate(_StringImageMessagePromptTemplate):
    _msg_class: type[BaseMessage] = HumanMessage

class AIMessagePromptTemplate(_StringImageMessagePromptTemplate):
    _msg_class: type[BaseMessage] = AIMessage

class SystemMessagePromptTemplate(_StringImageMessagePromptTemplate):
    _msg_class: type[BaseMessage] = SystemMessage

class ChatMessagePromptTemplate(BaseStringMessagePromptTemplate):
    role: str
    def format(self, **kwargs) -> BaseMessage:
        text = self.prompt.format(**kwargs)
        return ChatMessage(
            content=text, role=self.role, additional_kwargs=self.additional_kwargs
        )

前三个类继承自 _StringImageMessagePromptTemplate(稍后详述),它们通过 _msg_class 指定产出的消息类型——三者依次定义在 langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:664、:673、:682。ChatMessagePromptTemplate(同文件 :354)额外需要一个 role 字段,且它挂在 BaseStringMessagePromptTemplate 这一支下,而非 _StringImageMessagePromptTemplate。

classDiagram
    class BaseMessagePromptTemplate {
        +format_messages(**kwargs) list~BaseMessage~*
        +input_variables: list~str~*
        +pretty_repr(html) str
        +pretty_print()
    }
    class BaseStringMessagePromptTemplate {
        +prompt: StringPromptTemplate
        +additional_kwargs: dict
        +from_template(template) Self
        +format(**kwargs) BaseMessage*
    }
    class _StringImageMessagePromptTemplate {
        +prompt: StringPromptTemplate | list
        +_msg_class: type~BaseMessage~
        +from_template(template) Self
        +format(**kwargs) BaseMessage
    }
    class MessagesPlaceholder {
        +variable_name: str
        +optional: bool
        +n_messages: int | None
        +format_messages(**kwargs) list~BaseMessage~
    }
    class HumanMessagePromptTemplate {
        +_msg_class = HumanMessage
    }
    class AIMessagePromptTemplate {
        +_msg_class = AIMessage
    }
    class SystemMessagePromptTemplate {
        +_msg_class = SystemMessage
    }
    class ChatMessagePromptTemplate {
        +role: str
        +format(**kwargs) BaseMessage
    }
    BaseMessagePromptTemplate <|-- BaseStringMessagePromptTemplate
    BaseMessagePromptTemplate <|-- _StringImageMessagePromptTemplate
    BaseMessagePromptTemplate <|-- MessagesPlaceholder
    BaseStringMessagePromptTemplate <|-- ChatMessagePromptTemplate
    _StringImageMessagePromptTemplate <|-- HumanMessagePromptTemplate
    _StringImageMessagePromptTemplate <|-- AIMessagePromptTemplate
    _StringImageMessagePromptTemplate <|-- SystemMessagePromptTemplate

6.4.4 _StringImageMessagePromptTemplate:多模态模板

# libs/core/langchain_core/prompts/chat.py:397(两处构造消息时源码还传了
# additional_kwargs=self.additional_kwargs,此处从略)
class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
    prompt: StringPromptTemplate | list[StringPromptTemplate | ImagePromptTemplate | DictPromptTemplate]
    _msg_class: type[BaseMessage]

    def format(self, **kwargs: Any) -> BaseMessage:
        if isinstance(self.prompt, StringPromptTemplate):
            text = self.prompt.format(**kwargs)
            return self._msg_class(content=text)

        # 多模态情况
        content: list = []
        for prompt in self.prompt:
            inputs = {var: kwargs[var] for var in prompt.input_variables}
            if isinstance(prompt, StringPromptTemplate):
                formatted_text = prompt.format(**inputs)
                if formatted_text != "":
                    content.append({"type": "text", "text": formatted_text})
            elif isinstance(prompt, ImagePromptTemplate):
                formatted_image = prompt.format(**inputs)
                content.append({"type": "image_url", "image_url": formatted_image})
            elif isinstance(prompt, DictPromptTemplate):
                formatted_dict = prompt.format(**inputs)
                content.append(formatted_dict)
        return self._msg_class(content=content)

当 prompt 是列表时,_StringImageMessagePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:397,format 方法在同文件 :584)进入多模态处理模式。它遍历列表中的每个子模板,根据类型生成对应的内容块:StringPromptTemplate 被格式化为 {"type": "text", "text": "..."} 块(空字符串会被跳过),ImagePromptTemplate 被格式化为 {"type": "image_url", "image_url": {...}} 块,DictPromptTemplate 被格式化为任意结构的字典块。最终,所有内容块被组合为一个列表,作为消息的 content 字段。

这种设计使得创建包含文本和图片的多模态消息模板变得非常简洁自然:

prompt = HumanMessagePromptTemplate.from_template([
    {"text": "请描述这张图片:"},
    {"image_url": "{image_url}"},
])

message = prompt.format(image_url="https://example.com/photo.png")
# HumanMessage(content=[
#   {"type": "text", "text": "请描述这张图片:"},
#   {"type": "image_url", "image_url": {"url": "https://example.com/photo.png"}}
# ])

from_template 方法的列表处理逻辑非常精细。它通过检查字典中的键集合来判断内容块类型:如果包含 text 键(且键集合是 {type, text} 的子集),创建 PromptTemplate;如果包含 image_url 键,创建 ImagePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/image.py:16,支持 URL 字符串或字典;字典里只从 url/path/detail 三个键提取变量,但 path 已不可用——format 时一旦出现 path 就抛 ValueError: Loading images from 'path' has been removed as of 0.3.15 for security reasons. Please specify images by 'url'.,见 langchain-core-1.2.26/libs/core/langchain_core/prompts/image.py:115-120);其他字典结构则创建 DictPromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/dict.py:18,只支持 f-string 与 mustache,列表里的字典配 jinja2 会被直接拒绝),可以表示任意的内容块格式。每种模板类型都会自动从模板字符串中提取变量名,确保多模态模板的变量推断与纯文本模板一样自动化。列表写法不支持 partial_variables,传了会报 ValueError: Partial variables are not supported for list of templates.。

需要注意的是,字符串写法的图片 URL 模板(如 {"image_url": "{image_url}"})只允许包含一个变量;如果检测到多个变量,from_template(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:411)会抛出 ValueError: Only one format variable allowed per image template.(后接 Got: 与 From: 两行,报错在该方法体 :482-484)。

这条限制只落在字符串分支上。字典写法 {"image_url": {"url": "{u}", "detail": "{d}"}} 走的是另一个分支,逐个从 url/path/detail 键里收集变量,因此可以有多个。

另一个容易踩的坑:变量名不能叫 url、path 或 detail,{"image_url": "{url}"} 会在构造 ImagePromptTemplate 时报 ValueError: input_variables for the image template cannot contain any of 'url', 'path', or 'detail'. Found: {'url'}(langchain-core-1.2.26/libs/core/langchain_core/prompts/image.py:38-45,实跑原文)。

6.5 MessagesPlaceholder:动态消息列表

6.5.1 核心机制

MessagesPlaceholder 是 ChatPromptTemplate 中最强大也是最常用的组件之一。与其他消息模板(如 HumanMessagePromptTemplate)将一个变量格式化为一条消息不同,MessagesPlaceholder 将一个变量展开为完整的消息列表。这使得它成为对话历史注入、动态系统指令、以及 Agent 中间步骤传递的标准方式:

# libs/core/langchain_core/prompts/chat.py:53
class MessagesPlaceholder(BaseMessagePromptTemplate):
    variable_name: str
    optional: bool = False
    n_messages: PositiveInt | None = None

    def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
        value = (
            kwargs.get(self.variable_name, [])
            if self.optional
            else kwargs[self.variable_name]
        )
        if not isinstance(value, list):
            raise ValueError(...)
        value = convert_to_messages(value)
        if self.n_messages:
            value = value[-self.n_messages:]
        return value

    @property
    def input_variables(self) -> list[str]:
        return [self.variable_name] if not self.optional else []

以下是 format_messages 方法的几个关键设计决策:

  1. optional 参数:为 True 时,变量缺失会返回空列表而非报错。同时,可选的 placeholder 不会出现在 input_variables 中,这确保了 _validate_input 不会要求用户提供它。

  2. n_messages 参数:限制最多保留的消息数量,只保留最后 N 条。这在对话历史管理中非常有用。

  3. convert_to_messages 调用:传入的值不必是 BaseMessage 列表,2-元组等格式也被接受。

6.5.2 对话历史管理模式

prompt = ChatPromptTemplate([
    ("system", "You are a helpful assistant."),
    ("placeholder", "{history}"),
    ("human", "{question}"),
])

# 使用示例
result = prompt.invoke({
    "history": [
        ("human", "What is 2+2?"),
        ("ai", "4"),
        ("human", "And 3+3?"),
        ("ai", "6"),
    ],
    "question": "Now what is 5+5?",
})

当 placeholder 使用元组格式 ("placeholder", "{history}") 定义时,_convert_to_message_template 函数会自动创建一个 optional=True 的 MessagesPlaceholder。效果是对话刚开始、还没有历史记录时可以不传这个变量。如果需要强制要求传入消息列表,可以直接使用 MessagesPlaceholder(variable_name="history", optional=False) 构造器形式(optional 默认就是 False),或写成 ("placeholder", ["{history}", False]);此时漏传会在 invoke 时报 Input to ChatPromptTemplate is missing variables {'history'} 的 KeyError。

n_messages 参数为对话历史的管理提供了一种轻量级的控制方式。与 trim_messages 按 token 计数裁剪不同,n_messages 简单地只保留最后 N 条消息。虽然这种方式不够精确(不同消息的 token 数可能差异很大),但它的执行开销几乎为零(只是一个列表切片操作),适用于对 token 预算控制不那么严格的场景。

6.5.3 与 trim_messages 的配合

MessagesPlaceholder 和 trim_messages 经常配合使用来实现智能的对话历史管理:

from langchain_core.messages import trim_messages
from langchain_core.prompts import ChatPromptTemplate

model = ...  # 任意聊天模型实例,如第 5 章的 ChatOpenAI

prompt = ChatPromptTemplate([
    ("system", "You are helpful."),
    ("placeholder", "{history}"),
    ("human", "{input}"),
])

# 在链中使用 trim_messages 控制历史长度
chain = (
    {
        "history": lambda x: trim_messages(
            x["history"],
            max_tokens=1000,
            token_counter="approximate",
            strategy="last",
            include_system=False,
        ),
        "input": lambda x: x["input"],
    }
    | prompt
    | model
)

6.6 FewShotPromptTemplate:少样本提示

6.6.1 核心设计

少样本提示(Few-Shot Prompting)是提升大语言模型输出质量的常用技术之一。通过在提示词中提供几个输入-输出示例,模型可以更好地理解期望的输出格式和任务语义。FewShotPromptTemplate 将这一技术抽象为一个可复用的模板类,通过将多个示例格式化后插入到提示词中来实现少样本学习:

# langchain_core/prompts/few_shot.py

class _FewShotPromptTemplateMixin(BaseModel):
    examples: list[dict] | None = None
    """静态示例列表"""

    example_selector: BaseExampleSelector | None = None
    """动态示例选择器"""

class FewShotPromptTemplate(_FewShotPromptTemplateMixin, StringPromptTemplate):
    example_prompt: PromptTemplate
    """用于格式化单个示例的模板"""

    suffix: str
    """示例之后的文本(通常包含用户输入变量)"""

    example_separator: str = "\n\n"
    """示例之间的分隔符"""

    prefix: str = ""
    """示例之前的文本(通常包含任务说明)"""

    template_format: Literal["f-string", "jinja2"] = "f-string"

6.6.2 静态与动态示例

# libs/core/langchain_core/prompts/few_shot.py:54-99(docstring 从略;_get_examples 末尾的兜底 raise 从略)
@model_validator(mode="before")
@classmethod
def check_examples_and_selector(cls, values: dict) -> Any:
    examples = values.get("examples")
    example_selector = values.get("example_selector")
    if examples and example_selector:
        raise ValueError("Only one of 'examples' and 'example_selector' should be provided")
    if examples is None and example_selector is None:
        raise ValueError("One of 'examples' and 'example_selector' should be provided")
    return values

def _get_examples(self, **kwargs: Any) -> list[dict]:
    if self.examples is not None:
        return self.examples
    if self.example_selector is not None:
        return self.example_selector.select_examples(kwargs)

examples 和 example_selector 必须且只能提供一个,这通过 check_examples_and_selector 验证器在构造时强制执行。examples 是一个静态的示例字典列表,适用于示例集较小且固定的场景。example_selector 实现了 BaseExampleSelector 接口,它可以根据当前输入动态选择最相关的示例。core 自带的 selector 实现(langchain_core/example_selectors/)包括基于语义相似度的 SemanticSimilarityExampleSelector / MaxMarginalRelevanceExampleSelector(用向量存储找与当前输入最相似的示例)和基于长度的 LengthBasedExampleSelector(按顺序加入示例,直到超出 max_length 预算为止;预算默认 2048,长度默认按空格和换行切分计数,并非 token 数)。这项检查跑在 Pydantic 验证器里,所以两个都不传时,实跑报 ValidationError:Value error, One of 'examples' and 'example_selector' should be provided。

动态示例选择的引入使得 FewShotPromptTemplate 可以从大型示例库中自动挑选最具参考价值的示例,而不是一成不变地使用固定的几个示例。

6.6.3 format 方法

# libs/core/langchain_core/prompts/few_shot.py:180
def format(self, **kwargs: Any) -> str:
    kwargs = self._merge_partial_and_user_variables(**kwargs)
    examples = self._get_examples(**kwargs)
    # 只提取 example_prompt 需要的字段
    examples = [
        {k: e[k] for k in self.example_prompt.input_variables} for e in examples
    ]
    example_strings = [self.example_prompt.format(**example) for example in examples]
    # 组装:prefix + examples + suffix(空串会被滤掉,prefix 默认 "" 时不会多出分隔符)
    pieces = [self.prefix, *example_strings, self.suffix]
    template = self.example_separator.join([piece for piece in pieces if piece])
    return DEFAULT_FORMATTER_MAPPING[self.template_format](template, **kwargs)

格式化流程:

flowchart LR
    A["获取示例<br/>(静态或动态)"] --> B["用 example_prompt<br/>格式化每个示例"]
    B --> C["用 separator<br/>连接 prefix + examples + suffix"]
    C --> D["用 template_format<br/>填充最终模板中的变量"]
    D --> E["输出完成的<br/>提示词字符串"]
# 使用示例
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate

example_prompt = PromptTemplate.from_template("Input: {input}\nOutput: {output}")

few_shot = FewShotPromptTemplate(
    examples=[
        {"input": "happy", "output": "sad"},
        {"input": "tall", "output": "short"},
    ],
    example_prompt=example_prompt,
    prefix="Give the antonym of every input:",
    suffix="Input: {adjective}\nOutput:",
)

print(few_shot.format(adjective="big"))
# Give the antonym of every input:
#
# Input: happy
# Output: sad
#
# Input: tall
# Output: short
#
# Input: big
# Output:

6.6.4 变量提取

# libs/core/langchain_core/prompts/few_shot.py:156
@model_validator(mode="after")
def template_is_valid(self) -> Self:
    if self.validate_template:
        check_valid_template(
            self.prefix + self.suffix,
            self.template_format,
            self.input_variables + list(self.partial_variables),
        )
    elif self.template_format:
        self.input_variables = [
            var for var in get_template_variables(
                self.prefix + self.suffix, self.template_format
            )
            if var not in self.partial_variables
        ]
    return self

FewShotPromptTemplate 的 input_variables 只从 prefix + suffix 中提取,不从 example_prompt 中提取。这是一个重要的设计区分:example_prompt 的变量(如上例中的 input 和 output)由示例数据字典填充,它们属于模板的内部实现细节,不应暴露给模板的调用者。调用者只需要关心 suffix 中出现的变量(如 adjective),这就是最终模板对外暴露的输入接口。

值得注意的是,format 方法在格式化示例时会从示例字典中只提取 example_prompt.input_variables 指定的键,忽略示例中多余的键。这种严格的键过滤确保了即使示例数据包含额外的元数据字段(如 category、source 等),也不会干扰示例的格式化过程。

6.7 format_document:文档格式化

6.7.1 核心实现

# libs/core/langchain_core/prompts/base.py:421(format_document)
# + :403(_get_document_info)—— 为便于阅读把调用方放在了前面,源码里顺序相反

def format_document(doc: Document, prompt: BasePromptTemplate[str]) -> str:
    return prompt.format(**_get_document_info(doc, prompt))

def _get_document_info(doc: Document, prompt: BasePromptTemplate[str]) -> dict:
    base_info = {"page_content": doc.page_content, **doc.metadata}
    missing_metadata = set(prompt.input_variables).difference(base_info)
    if len(missing_metadata) > 0:
        required_metadata = [iv for iv in prompt.input_variables if iv != "page_content"]
        raise ValueError(
            f"Document prompt requires documents to have metadata variables: "
            f"{required_metadata}. Received document with missing metadata: "
            f"{list(missing_metadata)}."
        )
    return {k: base_info[k] for k in prompt.input_variables}

format_document 将 Document 对象的 page_content 和 metadata 合并为一个变量字典传入提示词模板。_get_document_info 辅助函数首先构建完整的变量映射:page_content 来自文档的正文内容,其余变量来自文档的 metadata 字典。然后它检查模板需要的所有变量是否在映射中都能找到,如果有缺失的元数据字段,会抛出一个包含详细诊断信息的 ValueError,帮助用户快速定位问题。

6.7.2 使用场景

from langchain_core.documents import Document
from langchain_core.prompts import PromptTemplate
from langchain_core.prompts.base import format_document

doc = Document(
    page_content="LangChain 是一个 AI 应用开发框架",
    metadata={"source": "docs.langchain.com", "page": "1"}
)

prompt = PromptTemplate.from_template(
    "Source: {source} (Page {page})\n{page_content}"
)

formatted = format_document(doc, prompt)
# Source: docs.langchain.com (Page 1)
# LangChain 是一个 AI 应用开发框架

这个函数是 RAG(检索增强生成)管道中的核心组件,用于将检索到的文档格式化为模型可以理解的上下文文本。在典型的 RAG 流程中,检索器返回多个 Document 对象,每个文档通过 format_document 格式化后,再被组合成完整的上下文传入模型提示词。format_document 的异步版本 aformat_document 也已提供,支持在异步管道中使用。

6.8 设计决策分析

6.8.1 为什么模板默认不验证

PromptTemplate 和 ChatPromptTemplate 都有 validate_template = False 的默认设置(langchain-core-1.2.26/libs/core/langchain_core/prompts/prompt.py:86 与 langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:900)。源码没有写明默认关闭的理由,从实现看可以这样理解:

  1. 自动推断:变量提取已经是自动的,大多数情况下不需要额外的显式验证
  2. 验证的前提更苛刻:打开验证就必须显式给出 input_variables,而且 mustache 格式根本不支持验证(实跑报 Value error, Mustache templates cannot be validated.)
  3. 开销只在构造时:验证在 Pydantic 验证器里执行,只发生在模板构造时,不影响每次格式化

当需要核对手写的 input_variables 与模板是否一致时,可以设置 validate_template=True:PromptTemplate(template="{a}{b}", input_variables=["a"], validate_template=True) 实跑报 Value error, Invalid prompt schema; check for mismatched or missing input parameters from ['a'].;ChatPromptTemplate 对应报 Got mismatched input_variables. Expected: {'a'}. Got: ['b'] 这类错误。注意对 PromptTemplate 而言,验证通过后 input_variables 仍会被自动推断的结果覆盖(见 §6.8.4);它也不是安全措施,不能用来放行来源不可信的 jinja2 模板。

6.8.2 输入格式的泛化设计

MessageLikeRepresentation = (
    MessageLike
    | tuple[str | type, str | Sequence[dict] | Sequence[object]]
    | str
    | dict[str, Any]
)

MessageLikeRepresentation(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:782;langchain-core-1.2.26/libs/core/langchain_core/messages/utils.py:578 另有一个面向消息工具函数的同名别名,两者不是同一个对象)这个联合类型允许用户用最自然的方式表达消息。元组格式 ("role", "content") 的简洁性大幅降低了使用门槛(两个字符串组成的元组是最小的信息量),而完整的 BaseMessage 格式则保留了全部表达力(包括 name、id、additional_kwargs 等所有字段)。字典格式 {"role": "...", "content": "..."} 兼容了 OpenAI 的 API 格式,降低了从原生 API 调用迁移到 LangChain 的门槛。纯字符串则被视为 HumanMessage,这是对"大多数简单调用只需要用户消息"这一常见场景的直接支持。

6.8.3 prompt 字段的组合设计

_StringImageMessagePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:397)中 prompt 字段的联合类型 StringPromptTemplate | list[...] 是组合优于继承的典型体现。它不需要为"纯文本消息模板"和"多模态消息模板"创建不同的子类,而是通过 prompt 字段的类型在运行时自适应。

6.8.4 自动推断 vs 显式声明的演进

LangChain 最早的版本要求用户在创建模板时显式声明 input_variables 参数,这容易导致用户错误:变量名拼写不一致(模板中写了 {question} 但声明了 query)、忘记声明新添加的变量、删除变量后忘记更新声明等。每一个这样的错误都会在运行时才暴露,增加了调试成本。

自动推断很早就成了默认行为——langchain-core 从独立成包那一刻(PR #13577)起,PromptTemplate 的验证器(当时叫 template_is_valid 的 root_validator)里就已经是"validate_template 为真才校验,否则直接用 get_template_variables 的结果覆盖 input_variables"这套逻辑。PR #22829(2024-06)把它改写成 root_validator(pre=True) 的 pre_init_validation,顺带让覆盖不再受 validate_template 影响:校验完照样覆盖;PR #26443(2024-09,langchain-core 0.3 的 Pydantic 2 迁移)再把它换成 model_validator(mode="before")。1.2.26 的实现见 langchain-core-1.2.26/libs/core/langchain_core/prompts/prompt.py:89,其中 :102 是 validate_template 分支、:116 是无条件用 get_template_variables 覆盖 input_variables 的分支。用户不再需要声明 input_variables——框架会从模板字符串中自动提取,即使手写了也会被覆盖(实跑 PromptTemplate(template="{a}{b}", input_variables=["zzz"]).input_variables 得 ['a', 'b']);只有在设置 validate_template=True 时,才会检查用户声明的变量与实际模板变量的一致性。这是一个典型的"渐进式严格性"设计:默认给予最大便利,需要时可以开启严格验证。这种"约定优于配置"(convention over configuration)的理念贯穿了 LangChain 的许多设计决策。

6.9 StringPromptTemplate:字符串模板的抽象层

StringPromptTemplate 是 PromptTemplate 和 FewShotPromptTemplate 的共同父类。它在模板体系的层级中扮演着连接 BasePromptTemplate 和具体字符串模板的桥梁角色:

# langchain_core/prompts/string.py

class StringPromptTemplate(BasePromptTemplate, ABC):   # string.py:311,未填泛型参数
    def format_prompt(self, **kwargs: Any) -> PromptValue:
        return StringPromptValue(text=self.format(**kwargs))

    async def aformat_prompt(self, **kwargs: Any) -> PromptValue:
        return StringPromptValue(text=await self.aformat(**kwargs))

StringPromptTemplate 的 format_prompt 实现非常简单但意义重大:它将 format 方法返回的字符串结果包装为 StringPromptValue 对象。这个包装看似多此一举,实际上是整个模板体系能够与语言模型无缝集成的关键。所有的 PromptValue 都实现了 to_messages() 和 to_string() 两个方法,分别将自身转换为 Chat 模型和文本补全模型能够理解的输入格式:

  • StringPromptValue.to_messages() 返回 [HumanMessage(content=text)] -- 将整个字符串作为一条用户消息
  • StringPromptValue.to_string() 直接返回原始文本字符串
  • ChatPromptValue.to_messages() 返回完整的消息列表(多条不同角色的消息)
  • ChatPromptValue.to_string() 将所有消息通过 get_buffer_string 拼接为字符串

这种双重转换接口意味着,无论使用 PromptTemplate(产出 StringPromptValue)还是 ChatPromptTemplate(产出 ChatPromptValue),最终都可以通过 PromptValue 这个统一的中间类型交给聊天模型:BaseChatModel._convert_input(langchain-core-1.2.26/libs/core/langchain_core/language_models/chat_models.py:427)遇到 PromptValue 原样返回,调用方随即 .to_messages() 取出消息列表(如同文件 :548)。这种设计使得字符串模板和对话模板可以互换使用,不需要修改下游的模型调用代码。

6.9.1 实测:langchain_core/prompts/ 12 文件 4359 行的真实拆分

把 §6.1-6.7 涉及的所有模板类按文件实测——

文件 行 角色
chat.py 1491 本目录最大占 34%——ChatPromptTemplate(§6.3)+ MessagePromptTemplate 子类家族(§6.4 全部)+ MessagesPlaceholder(§6.5)
few_shot.py 483 FewShotPromptTemplate(§6.6)
base.py 477 BasePromptTemplate ABC(§6.1)
string.py 382 StringPromptTemplate(§6.9)+ 三种 formatter mapping(f-string / mustache / jinja2、§6.2.1)
prompt.py 312 PromptTemplate(§6.2)
loading.py 289 YAML/JSON 模板序列化加载(本章未提)
few_shot_with_templates.py 237 FewShot 的模板化变体
structured.py 183 StructuredPrompt——结构化输出模板
image.py 157 图像消息支持(§6.4.4 多模态)
dict.py 150 字典模板
__init__.py 101 公共 export
message.py 97 BaseMessagePromptTemplate 抽象基类(§6.4.1)
合计 4359 —

两条值得记住的物理事实——

  1. chat.py 1491 行 = 整个 prompts/ 34%——ChatPromptTemplate + BaseStringMessagePromptTemplate + HumanMessagePromptTemplate / SystemMessagePromptTemplate / AIMessagePromptTemplate / _StringImageMessagePromptTemplate + MessagesPlaceholder 全部塞在这一文件——是本专栏反复见到的"单文件大方法集"风格之一(同类还有 core runnables/base.py 6261、core libs/core/langchain_core/callbacks/manager.py 2697、classic agents/agent.py 1792、core libs/core/langchain_core/tools/base.py 1586、core tracers/event_stream.py 1100;完整清单见 §3.12.1)——证明这是 LangChain 反复出现的代码组织偏好
  2. message.py 仅 97 行 vs chat.py 1491 行 = 1:15——基类极薄、具体实现极厚——印证"ABC 表面薄、实现承担重"模式(与《Rust 编译器与运行时揭秘》专栏第 10 章 §10.11.3 测得的 future.rs 仅 135 行 + Pin/Waker 数千行同款);这种结构让模板抽象层保持精简,具体模板类型可以在 chat.py 里自由演化

串联 §3.12.1 runnables/ 13730 + §7.12.1 output_parsers/ 2227 + ch12 §12.9.5 callbacks 4677 + tracers 5057 + 本节 prompts 4359 + ch08 §8.5.4 tools 2793 + ch09 讲的 core documents/ 555 = 33398 行——是 langchain_core/ 全部 176 个 .py 共 64050 行的 52%——这 7 个核心子模块构成 LCEL 的"Runnable + Prompt + Tool + Output + Callback + Tracer + Document" 完整骨架。

6.10 小结

LangChain 的提示词模板引擎通过精心设计的层级结构,将提示词的构建从"拼接字符串"提升为一个可组合、可验证、可序列化的模板系统:

  • BasePromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:40)作为 Runnable,将模板无缝融入 LCEL 链式调用。它定义了输入验证、部分变量绑定和格式化的标准流程。

  • PromptTemplate 支持三种模板格式(f-string/mustache/jinja2),通过 pre_init_validation(langchain-core-1.2.26/libs/core/langchain_core/prompts/prompt.py:89)自动提取变量,通过 DEFAULT_FORMATTER_MAPPING(langchain-core-1.2.26/libs/core/langchain_core/prompts/string.py:210)统一格式化入口。

  • ChatPromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:790)是最常用的模板类型,支持多种消息格式输入(元组、消息实例、字符串),自动推断输入变量和类型,通过 format_messages(同文件 :1170)将模板展开为消息列表。

  • MessagePromptTemplate 子类 通过 _msg_class 和 prompt 字段的组合设计,用最少的代码支持了纯文本和多模态两种消息模板。_StringImageMessagePromptTemplate 能够在 format 时根据 prompt 字段的类型自动切换处理逻辑(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:584)。

  • MessagesPlaceholder(langchain-core-1.2.26/libs/core/langchain_core/prompts/chat.py:53)以其 optional 和 n_messages(PositiveInt | None,见同文件 :139)参数,成为对话历史管理的核心组件。它与 trim_messages 的配合使用是管理对话历史的常见做法。

  • FewShotPromptTemplate(langchain-core-1.2.26/libs/core/langchain_core/prompts/few_shot.py:121;examples / example_selector 两个字段来自 _FewShotPromptTemplateMixin,见同文件 :37 与 :43)通过 examples / example_selector 二选一的设计,同时支持静态和动态的少样本示例选择。

  • format_document(langchain-core-1.2.26/libs/core/langchain_core/prompts/base.py:421)将文档的 page_content 和 metadata 映射为模板变量,是 RAG 管道中的关键组件。

整个模板引擎的设计哲学可以概括为:自动化默认行为(如变量提取),保留手动控制能力(如显式验证),通过宽泛的输入类型降低使用门槛,通过 Runnable 集成融入更大的组合体系。


跨专栏对照

  • 模板即资源:LangChain 的 PromptTemplate / ChatPromptTemplate / FewShotPromptTemplate 把 prompt 从"运行时字符串拼接"提升为 Serializable 对象——能用 langchain_core.load 的 dumpd/dumps 与 load/loads 序列化、通过 LangChain Hub 版本化(langchain-core-1.2.26/libs/langchain/langchain_classic/hub.py 的 push/pull 走的就是 dumps/loads;旧的 save() 自 1.2.21 起已标记弃用;FewShotPromptTemplate 的 is_lc_serializable() 返回 False,不在此列)。MCP 协议的 prompts/list + prompts/get(见《MCP 协议设计与实现》专栏第 7 章)走的是同一条"prompt 作为可序列化资源"路线。
  • MessagesPlaceholder + trim_messages:对话历史管理不是一行 messages.append,而是"占位 + 裁剪 + Reducer"三段式。LangGraph 的 messages channel(见《LangGraph 设计与实现》专栏第 4 章)把这个模式上升为状态图级别的 Reducer 定义。
  • Example Selector 的动态检索:SemanticSimilarityExampleSelector(langchain-core-1.2.26/libs/core/langchain_core/example_selectors/semantic_similarity.py)内部接一个 VectorStore,直接调它的 similarity_search——其实就是把第 10 章讨论的向量存储抽象复用到 Few-Shot 场景——"用同一套组件解不同层级问题"是 LCEL 的典型红利。
  • Prompt Injection 防御:在代码层做权限验证、不靠 prompt 里的"请不要 X"——和第 8 章讨论的 InjectedToolArg(langchain-core-1.2.26/libs/core/langchain_core/tools/base.py:1367)+ tool-level permission check 是同一防御纵深。模型指令和用户输入永远要以"消息类型"而不是文本位置来区分。