📝 提示词模板

手写一长串消息既繁琐又难复用。提示词模板(Prompt Template)让你声明式地定义消息结构,用变量动态填充——这是构建可维护 LLM 应用的基础工程实践。

本章目标

  • 理解 BasePromptTemplate 的"模板 + 变量"思想
  • 熟练使用 ChatPromptTemplate.from_messages() 构建消息序列
  • 掌握 MessagesPlaceholder 注入历史消息(多轮对话的关键)
  • 认识到 Prompt 本身是一个 Runnable(为 LC5 LCEL 铺垫)

为什么需要模板

看一段不用模板的代码,感受痛点:

📄 痛点:硬编码消息,难复用、难维护 python
# 每次调用都要手写一遍,变量靠 f-string 拼接,容易出错
def ask(name, question, history):
    msgs = [SystemMessage(content=f"你是 {name} 的助手")]
    msgs += history   # 历史消息
    msgs.append(HumanMessage(content=question))
    return model.invoke(msgs)

问题很明显:消息结构散落在代码里、变量类型不明确、无法复用、无法序列化。模板解决这一切:把"消息骨架"和"变量填充"分离

模板(骨架) "你好 {name}" 变量: {name} .invoke() name="小明" 消息(实例) HumanMessage( content="你好小明")
图 LC2.1 · 模板 = 骨架 + 变量;填充后得到具体消息

ChatPromptTemplate.from_messages

这是日常最常用的构造方式。它接受一个"消息表示序列",每条可以是元组 (role, content),也可以是现成的 Message 对象:

📄 langchain_core/prompts/chat.py:1124 · from_messages 工厂方法 python
class ChatPromptTemplate(BaseChatPromptTemplate):
    @classmethod
    def from_messages(cls, messages, template_format="f-string"):
        """从多种消息格式创建聊天提示模板。
        每条消息可以是: 元组 (role, content)、BaseMessage、或模板对象。
        """
        ...

# 用法示例
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个翻译助手,把中文翻译成{target_lang}。"),
    ("human", "请翻译这句话:{text}"),
])

# 填充变量,得到消息列表
messages = prompt.invoke({"target_lang": "英文", "text": "你好世界"})
print(messages)
# → ChatPromptValue(messages=[
#     SystemMessage(content="你是一个翻译助手,把中文翻译成英文。"),
#     HumanMessage(content="请翻译这句话:你好世界"),
# ])
💡 元组 role 的简写

元组的第一个元素(role)支持简写:"system" / "human" / "ai" / "user" / "assistant" 等,会自动映射到对应 Message 类。这是最省事的写法。

MessagesPlaceholder:注入历史

多轮对话场景下,你需要把"历史消息列表"作为一个整体插进模板。直接用 {history} 变量不行——因为历史是消息对象的列表,不是字符串。这时要用 MessagesPlaceholder

📄 langchain_core/prompts/chat.py:53 · MessagesPlaceholder python
class MessagesPlaceholder(BaseMessagePromptTemplate):
    """一个占位符,假设变量本身已经是消息列表。"""

# 经典用法:system + 历史 + 当前问题
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个有帮助的助手。"),
    MessagesPlaceholder("history"),   # ← 历史消息整个插在这里
    ("human", "{question}"),
])

# 调用时传入历史
messages = prompt.invoke({
    "history": [
        HumanMessage(content="我叫小明"),
        AIMessage(content="你好小明!"),
    ],
    "question": "我叫什么名字?",
})
# 得到: [SystemMessage, HumanMessage("我叫小明"), AIMessage("你好小明!"), HumanMessage("我叫什么名字?")]
⚠️ MessagesPlaceholder 是 Agent 的关键拼图

回顾 F3 章的 Agent 循环:每一步都要把历史思考 + 行动 + 观察拼成输入。这些"历史"就是一串消息。MessagesPlaceholder("agent_scratchpad") 正是用来插入这部分动态历史的标准做法——LC8 章 create_tool_calling_agent 就会用到它。

SystemMessage("你是助手,可用工具...") MessagesPlaceholder("history") ← 注入对话历史 MessagesPlaceholder("agent_scratchpad") ← 注入本轮思考+工具结果 HumanMessage("{input}")
图 LC2.2 · 一个典型 Agent Prompt 的四层结构

其他模板类型(了解)

源码用途
PromptTemplateprompts/prompt.py:24纯字符串模板(传统 LLM 补全用),不是聊天场景
FewShotPromptTemplateprompts/few_shot.pyfew-shot 示例模板,自动插入若干示例
HumanMessagePromptTemplateprompts/chat.py:668单条 Human 消息的模板,from_messages 内部会构造它

现代应用 95% 的场景用 ChatPromptTemplate 就够了,其他类型按需了解。

可运行代码

🚀 这章代码无需模型 API

模板是纯本地逻辑,pip install langchain 即可跑通,不消耗 token。

📄 lc2_prompts.py · 模板实操 python
# pip install langchain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage

# ① 基础模板:用元组简写
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是{role},用{style}的语气回答。"),
    ("human", "{question}"),
])

result = prompt.invoke({"role": "诗人", "style": "浪漫", "question": "什么是爱?"})
for m in result.to_messages():
    print(f"[{m.type}] {m.content}")
# [system] 你是诗人,用浪漫的语气回答。
# [human] 什么是爱?

# ② 多轮对话模板:MessagesPlaceholder 注入历史
chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是有帮助的助手,会记住用户告诉你的信息。"),
    MessagesPlaceholder("history"),
    ("human", "{question}"),
])

# 模拟两轮对话
history = []
q1 = "我叫张三,我喜欢Python"
# 假设模型回答了,加入历史
history.append(HumanMessage(content=q1))
history.append(AIMessage(content="你好张三!记住了你喜欢Python。"))

q2 = "我叫什么?我喜欢什么?"
messages = chat_prompt.invoke({"history": history, "question": q2})
print("\n第二轮的完整输入消息:")
for m in messages.to_messages():
    print(f"  [{m.type}] {m.content}")

# ③ MessagesPlaceholder 的 optional 参数
optional_prompt = MessagesPlaceholder("history", optional=True)
# 当 history 未提供时,返回空列表而不报错(适合首轮对话)
print(optional_prompt.format_messages())  # → []

重要伏笔:Prompt 本身是 Runnable

🔮 为 LC5 LCEL 章铺垫

注意上面我们用的是 prompt.invoke(...)——没错,模板也是一个 Runnable(继承自 RunnableSerializable)。这意味着它可以和模型、解析器用 | 管道串联。这是 LC5 章的核心,先留个印象:

📄 预告:LC5 会讲这个管道(现在看不懂没关系) python
# 模板 | 模型 | 解析器 —— 这就是 LCEL 的精髓,LC5 详解
chain = prompt | model | StrOutputParser()
result = chain.invoke({"question": "你好"})
# prompt 把字典 → 消息;model 把消息 → AIMessage;parser 取出文本

与生产实践对照

LangChain 概念OpenCode 对应
SystemMessage / 系统提示session/system.ts 动态生成(按模型选 anthropic.txt/gpt.txt)
ChatPromptTemplate 模板字符串拼接 + 环境变量注入(工作目录/git/平台)
MessagesPlaceholder从 Session 数据库读取历史消息列表注入
few-shot 示例工具的 .txt 描述文件(read.txt/edit.txt 等)

小结

下一章

下一章 LC3 · 语言模型抽象:消息和提示都准备好了,该让真正的 LLM "接收消息、吐出回答"了。我们看 BaseChatModel 如何统一所有模型,以及 bind_tools 怎么把工具绑到模型上。