LangChain设计与实现-第6章-提示词模板引擎

第6章 提示词模板引擎

本书章节导航


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

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

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

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

::: tip 本章要点

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

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

6.1.1 类定义与 Runnable 集成

python 复制代码
# langchain_core/prompts/base.py

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

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

    input_types: 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: dict[str, Any] | None = None
    """追踪元数据"""

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

BasePromptTemplate 同时继承了 RunnableSerializableABC(抽象基类)和 Generic[FormatOutputType],这个三重继承的组合非常巧妙。RunnableSerializable[dict, PromptValue] 确定了模板在 LCEL 链中的输入输出类型:接受字典格式的变量映射,产出 PromptValue 对象。ABC 确保了基类不可直接实例化。Generic[FormatOutputType]format 方法的返回类型提供了泛型参数:对于 StringPromptTemplate 子树,FormatOutputType 绑定为 str,意味着 format 返回纯字符串;而 format_prompt 方法则始终返回 PromptValueStringPromptValueChatPromptValue)。

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

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

6.1.2 invoke 的调用链路

python 复制代码
@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 追踪系统的深度集成。它首先将模板自身的 metadatatags 合并到运行配置中------这意味着模板级别的追踪元数据会自动传播到整个链的执行过程中。然后通过继承自 Runnable_call_with_config 方法执行模板格式化。run_type="prompt" 参数告知 LangSmith 等追踪系统这是一个提示词格式化操作,使得在追踪面板中可以清晰地看到提示词格式化这个独立的步骤及其输入输出。serialized=self._serialized 传入了模板的序列化表示(使用 @cached_property 缓存,避免重复序列化),让追踪系统能够记录哪个模板被使用了。

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 输入验证

python 复制代码
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}."
        msg += f"\nNote: if you intended {{{example_key}}} to be part of the string"
        msg += " and not a variable, please escape it with double curly braces"
        raise KeyError(msg)
    return inner_input_

_validate_input 方法在每次格式化前被调用,它负责确保用户提供了所有必需的变量。这个方法包含两个精心设计的便利特性:

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

  2. 友好的错误提示:当变量缺失时,错误消息不仅列出缺失的变量,还贴心地提示用户如果想在模板中使用字面花括号,需要用双花括号转义。

6.1.4 partial:部分变量绑定

python 复制代码
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 方法是函数式编程中"偏应用"(partial application)概念在模板系统中的体现。它返回一个新的模板实例(不修改原实例),其中指定的变量已被预填充。被部分绑定的变量会从 input_variables 中移除,添加到 partial_variables 中。这意味着新模板的 invoke 方法不再要求用户提供这些变量。

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

python 复制代码
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 方法会检测并调用这些 callable:

python 复制代码
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 变量名校验

python 复制代码
@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'")
    if "stop" in self.partial_variables:
        raise ValueError("Cannot have a partial variable named 'stop'")
    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 内部保留的参数名(用于传递停止词给模型),不允许作为模板变量名使用。这个验证在模板创建时就会触发,避免运行时的名称冲突。

6.2 PromptTemplate:字符串模板引擎

6.2.1 三种模板格式

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

python 复制代码
# 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

三种格式的具体实现:

python 复制代码
# langchain_core/prompts/string.py

# f-string 格式化器
def f_string_formatter(template: str, /, **kwargs: Any) -> str:
    return formatter.format(template, **kwargs)

# 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 = {
    "f-string": f_string_formatter,
    "mustache": mustache_formatter,
    "jinja2": jinja2_formatter,
}

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

jinja2 格式化器使用 SandboxedEnvironment 来阻止对 dunder 属性(如 __class____globals__)的访问,这是防止模板注入攻击的关键安全措施。但是沙箱保护属于"opt-out"(默认允许、选择性禁止)而非"opt-in"(默认禁止、选择性允许)的安全模型,无法完全防御所有攻击向量。因此源码中反复强调:即使有沙箱保护,也不应该接受不可信来源的 jinja2 模板。

6.2.2 自动变量提取

python 复制代码
@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["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 函数根据不同的模板格式使用不同的解析策略:

python 复制代码
def get_template_variables(template: str, template_format: str) -> list[str]:
    if template_format == "jinja2":
        return sorted(_get_jinja2_variables_from_template(template))
    elif template_format == "f-string":
        # 使用 Python 标准库的 Formatter
        formatter = Formatter()
        return [
            field_name for _, field_name, _, _ in formatter.parse(template)
            if field_name is not None
        ]
    elif template_format == "mustache":
        # 使用自定义的 mustache 解析器
        return [var for _, _, var, _ in mustache.tokenize(template) if var]

对于 f-string 格式,LangChain 使用 Python 标准库的 string.Formatter 来解析模板,提取所有花括号包围的字段名。对于 jinja2 格式,使用 jinja2.meta.find_undeclared_variables 来分析模板 AST(抽象语法树),找出所有未声明的变量引用。对于 mustache 格式,使用 LangChain 自带的 mustache tokenizer 来解析双花括号中的变量名。

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

python 复制代码
# 不需要手动指定 input_variables
prompt = PromptTemplate.from_template("Tell me about {topic} in {language}")
print(prompt.input_variables)  # ['topic', 'language']

6.2.3 from_template 工厂方法

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 方法

python 复制代码
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 模板拼接

python 复制代码
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,
        )
    if isinstance(other, str):
        prompt = PromptTemplate.from_template(other, template_format=self.template_format)
        return self + prompt

+ 运算符支持两个 PromptTemplate 的拼接,也支持与字符串的拼接。但两个模板必须使用相同的格式,且不能有重复的部分变量。

6.3 ChatPromptTemplate:多角色对话模板

6.3.1 消息模板的构建

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

python 复制代码
# langchain_core/prompts/chat.py

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

    validate_template: bool = False

6.3.2 构造器的格式支持

ChatPromptTemplate 的构造器接受多种格式的消息定义:

python 复制代码
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 函数将各种格式的输入统一转换为标准的消息模板对象。这个函数是 ChatPromptTemplate 能够支持多种输入格式的关键。以下代码展示了它支持的所有输入格式:

python 复制代码
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
其他 ChatMessagePromptTemplate(role=...)

6.3.3 自动变量推断

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

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

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

python 复制代码
# input_types 自动设置为 list[AnyMessage]
if message.variable_name not in input_types:
    input_types[message.variable_name] = list[AnyMessage]

MessagesPlaceholder 的变量会被自动设置类型为 list[AnyMessage],这使得自动生成的 JSON Schema 能够正确反映消息列表的类型。

6.3.4 format_messages 的实现

python 复制代码
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.append(message_template)
        elif isinstance(message_template, BaseMessagePromptTemplate):
            rel_params = {
                k: kwargs[k] for k in message_template.input_variables if k in kwargs
            }
            message = message_template.format_messages(**rel_params)
            result.extend(message)
        elif isinstance(message_template, BaseChatPromptTemplate):
            # 嵌套的 ChatPromptTemplate
            message = message_template.format_messages(**kwargs)
            result.extend(message)
    return result

format_messages 方法是 ChatPromptTemplate 的核心引擎。它遍历所有消息模板,根据模板类型执行不同的处理策略。对于 BaseMessage 实例,直接将其添加到结果列表中(固定消息不需要格式化)。对于 BaseMessagePromptTemplate(包括 MessagesPlaceholder),先从 kwargs 中提取该模板需要的变量子集,再调用其 format_messages 方法。注意这里使用了 extend 而非 append,因为 MessagesPlaceholder 可能展开为多条消息。对于嵌套的 BaseChatPromptTemplate,传入全部 kwargs(因为嵌套模板可能引用任意变量)。

这种"按需传参"的设计有一个重要的好处:每个消息模板只能访问到它声明需要的变量,多余的变量不会传入。这种最小权限原则不仅减少了变量名冲突的风险,还使得错误定位更加容易------如果某个变量拼写错误,错误会准确地定位到引用该变量的那个消息模板。

flowchart TB Start["ChatPromptTemplate.format_messages(**kwargs)"] Merge["合并 partial_variables 和 kwargs"] Loop["遍历 self.messages"] Check{"消息类型?"} BM["BaseMessage
直接添加"] MPT["BaseMessagePromptTemplate
提取相关变量
调用 format_messages"] CPT["BaseChatPromptTemplate
传入全部 kwargs
调用 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 + 运算符拼接

python 复制代码
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)

ChatPromptTemplate+ 运算符支持与多种类型拼接,使得模板的动态组合非常灵活:

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

注意,BaseMessage.__add__ 也返回 ChatPromptTemplate,这意味着消息对象也可以参与拼接:

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

6.4 MessagePromptTemplate 子类体系

6.4.1 BaseMessagePromptTemplate:消息模板基类

python 复制代码
# 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 定义了消息模板的最小接口:format_messagesinput_variables。注意 format_messages 返回的是列表而非单条消息,这为 MessagesPlaceholder 等需要展开为多条消息的模板留出了空间。

6.4.2 BaseStringMessagePromptTemplate

python 复制代码
class BaseStringMessagePromptTemplate(BaseMessagePromptTemplate, ABC):
    prompt: StringPromptTemplate
    """内部的字符串模板"""

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

    @classmethod
    def from_template(cls, template: str, template_format="f-string", **kwargs) -> Self:
        prompt = PromptTemplate.from_template(template, template_format=template_format)
        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 通过组合一个 StringPromptTemplate 来实现消息模板功能。它的 input_variables 直接代理给内部的 prompt。

6.4.3 具体消息模板类

python 复制代码
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)

前三个类继承自 _StringImageMessagePromptTemplate(稍后详述),它们通过 _msg_class 指定产出的消息类型。ChatMessagePromptTemplate 额外需要一个 role 字段。

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:多模态模板

python 复制代码
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 进入多模态处理模式。它遍历列表中的每个子模板,根据类型生成对应的内容块:StringPromptTemplate 被格式化为 {"type": "text", "text": "..."} 块(空字符串会被跳过),ImagePromptTemplate 被格式化为 {"type": "image_url", "image_url": {...}} 块,DictPromptTemplate 被格式化为任意结构的字典块。最终,所有内容块被组合为一个列表,作为消息的 content 字段。

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

python 复制代码
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(支持 URL 字符串或包含 url/path/detail 的字典);其他字典结构则创建 DictPromptTemplate,可以表示任意的内容块格式。每种模板类型都会自动从模板字符串中提取变量名,确保多模态模板的变量推断与纯文本模板一样自动化。

需要注意的是,当模板列表中包含图片模板时,每个图片 URL 模板只允许包含一个变量。这是因为图片 URL 通常作为一个整体传入(而非拼接多个变量),多变量会导致歧义。如果检测到多个变量,from_template 会抛出明确的 ValueError 错误提示。

6.5 MessagesPlaceholder:动态消息列表

6.5.1 核心机制

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

python 复制代码
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 对话历史管理模式

python 复制代码
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=TrueMessagesPlaceholder。这是一个有意为之的便利设计:使用 "placeholder" 类型暗示了消息列表是可选的(因为对话可能刚刚开始,还没有历史记录)。如果需要强制要求传入消息列表,可以直接使用 MessagesPlaceholder(variable_name="history", optional=False) 构造器形式。

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

6.5.3 与 trim_messages 的配合

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

python 复制代码
from langchain_core.messages import trim_messages

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 将这一技术抽象为一个可复用的模板类,通过将多个示例格式化后插入到提示词中来实现少样本学习:

python 复制代码
# 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 静态与动态示例

python 复制代码
@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)

examplesexample_selector 必须且只能提供一个,这通过 check_examples_and_selector 验证器在构造时强制执行。examples 是一个静态的示例字典列表,适用于示例集较小且固定的场景。example_selector 实现了 BaseExampleSelector 接口,它可以根据当前输入动态选择最相关的示例。常见的 selector 实现包括基于语义相似度的选择器(使用向量数据库找到与当前输入最相似的示例)和基于长度的选择器(根据剩余 token 预算选择尽可能多的示例)。

动态示例选择的引入使得 FewShotPromptTemplate 可以从大型示例库中自动挑选最具参考价值的示例,而不是一成不变地使用固定的几个示例。这在实际应用中可以显著提升模型的输出质量。

6.6.3 format 方法

python 复制代码
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
    template = self.example_separator.join(
        [self.prefix, *example_strings, self.suffix]
    )
    return DEFAULT_FORMATTER_MAPPING[self.template_format](template, **kwargs)

格式化流程:

flowchart LR A["获取示例
(静态或动态)"] --> B["用 example_prompt
格式化每个示例"] B --> C["用 separator
连接 prefix + examples + suffix"] C --> D["用 template_format
填充最终模板中的变量"] D --> E["输出完成的
提示词字符串"]
python 复制代码
# 使用示例
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 变量提取

python 复制代码
@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

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

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

6.7 format_document:文档格式化

6.7.1 核心实现

python 复制代码
# langchain_core/prompts/base.py

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

6.7.2 使用场景

python 复制代码
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 为什么模板默认不验证

PromptTemplateChatPromptTemplate 都有 validate_template = False 的默认设置。这是因为:

  1. 性能:验证需要额外的模板解析和变量比对,对于高频调用的场景有开销
  2. 自动推断:变量提取已经是自动的,大多数情况下不需要额外的显式验证
  3. 灵活性:某些高级用法(如延迟绑定变量)可能不符合严格验证的预期

当需要严格验证时(例如加载用户提供的模板),可以设置 validate_template=True

6.8.2 输入格式的泛化设计

python 复制代码
MessageLikeRepresentation = (
    MessageLike
    | tuple[str | type, str | Sequence[dict] | Sequence[object]]
    | str
    | dict[str, Any]
)

MessageLikeRepresentation 这个联合类型允许用户用最自然的方式表达消息。元组格式 ("role", "content") 的简洁性大幅降低了使用门槛(两个字符串组成的元组是最小的信息量),而完整的 BaseMessage 格式则保留了全部表达力(包括 nameidadditional_kwargs 等所有字段)。字典格式 {"role": "...", "content": "..."} 兼容了 OpenAI 的 API 格式,降低了从原生 API 调用迁移到 LangChain 的门槛。纯字符串则被视为 HumanMessage,这是对"大多数简单调用只需要用户消息"这一常见场景的直接支持。

6.8.3 prompt 字段的组合设计

_StringImageMessagePromptTemplateprompt 字段的联合类型 StringPromptTemplate | list[...] 是组合优于继承的典型体现。它不需要为"纯文本消息模板"和"多模态消息模板"创建不同的子类,而是通过 prompt 字段的类型在运行时自适应。

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

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

1.0 版本将自动推断作为默认行为,用户不再需要声明 input_variables------框架会从模板字符串中自动提取。只有在设置 validate_template=True 时,才会检查用户声明的变量与实际模板变量的一致性。这是一个典型的"渐进式严格性"设计:默认给予最大便利,需要时可以开启严格验证。这种"约定优于配置"(convention over configuration)的理念贯穿了 LangChain 1.0 的许多设计决策。

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

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

python 复制代码
# langchain_core/prompts/string.py

class StringPromptTemplate(BasePromptTemplate[str], ABC):
    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))

StringPromptTemplateformat_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 方法,后者会调用 to_messages() 获取消息列表。这种设计使得字符串模板和对话模板可以互换使用,不需要修改下游的模型调用代码。

6.10 小结

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

  • BasePromptTemplate 作为 Runnable,将模板无缝融入 LCEL 链式调用。它定义了输入验证、部分变量绑定和格式化的标准流程。

  • PromptTemplate 支持三种模板格式(f-string/mustache/jinja2),通过 pre_init_validation 自动提取变量,通过 DEFAULT_FORMATTER_MAPPING 统一格式化入口。

  • ChatPromptTemplate 是最常用的模板类型,支持多种消息格式输入(元组、消息实例、字符串),自动推断输入变量和类型,通过 format_messages 将模板展开为消息列表。

  • MessagePromptTemplate 子类 通过 _msg_classprompt 字段的组合设计,用最少的代码支持了纯文本和多模态两种消息模板。_StringImageMessagePromptTemplate 能够在 format 时根据 prompt 字段的类型自动切换处理逻辑。

  • MessagesPlaceholder 以其 optionaln_messages 参数,成为对话历史管理的核心组件。它与 trim_messages 的配合使用是构建生产级对话系统的标准模式。

  • FewShotPromptTemplate 通过 examples / example_selector 二选一的设计,同时支持静态和动态的少样本示例选择。

  • format_document 将文档的 page_contentmetadata 映射为模板变量,是 RAG 管道中的关键组件。

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

相关推荐
杨艺韬2 小时前
LangChain设计与实现-第18章-设计模式与架构决策
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第16章-序列化与配置系统
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第15章-工具调用与Agent模式
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第9章-文档加载与文本分割
langchain·agent
杨艺韬2 小时前
langchain设计与实现-前言
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第2章-架构总揽
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第3章-Runnable 与 LCEL 表达式语言
langchain·agent
杨艺韬2 小时前
LangChain设计与实现-第4章-消息系统与多模态
langchain·agent
龙侠九重天2 小时前
OpenClaw 多 Agent 隔离机制:工作空间、状态与绑定路由
人工智能·机器学习·ai·agent·openclaw