# LangChain Prompt 组件实践:模板、消息占位符与提示词工程化

Prompt 是 LLM 应用里最容易被低估的工程资产。很多项目一开始只是把用户输入拼进一段字符串里,等需求复杂起来后,才发现提示词已经散落在接口、服务、工具函数和测试脚本中:变量不统一、格式不可控、版本不可追踪、输出不稳定,最后每次改 Prompt 都像在拆一个没有测试的线上逻辑。

LangChain 的 Prompt 组件解决的不是“怎么写一句提示词”,而是“怎么把提示词变成可组合、可测试、可复用、可迭代的工程模块”。它让 Prompt 可以声明变量、格式化输入、组织聊天消息、插入历史上下文、拼接多个模板,并和 Model、Parser、Retriever、Agent 组合起来。

# Prompt 为什么需要组件化

直接拼字符串当然可以:

question = "SQLAlchemy Session 应该在哪里 commit?"
prompt = f"你是一个 Python 后端专家,请回答这个问题:{question}"
1
2

但真实项目很快会遇到这些问题:

  • 系统角色、业务规则、用户输入混在一起。
  • Prompt 变量越来越多,漏传时运行期才报错。
  • 多轮对话需要插入历史消息,普通字符串不好表达。
  • RAG 需要插入检索上下文、引用来源和回答约束。
  • 结构化输出需要固定格式说明。
  • 不同业务场景需要复用同一段系统提示词。
  • Prompt 修改缺少版本、评审和测试。

Prompt 组件化的核心价值是:把提示词从临时字符串提升为应用的一部分,让它有清晰输入、组合边界和可验证行为。

# PromptTemplate:文本提示模板

PromptTemplate 适合传统文本输入场景。它通过变量占位符生成最终字符串。

from langchain_core.prompts import PromptTemplate


prompt = PromptTemplate.from_template(
    "请用 {language} 解释一下 {subject},要求简洁准确。"
)

result = prompt.invoke({
    "language": "中文",
    "subject": "LangChain PromptTemplate",
})

print(result.to_string())
1
2
3
4
5
6
7
8
9
10
11
12
13

如果只是给普通 LLM 或一个文本处理链路构造输入,PromptTemplate 足够简单。

但现在大多数模型应用都使用 Chat Model,更推荐使用 ChatPromptTemplate,因为它可以明确区分 system、human、ai、tool 等消息角色。

# ChatPromptTemplate:聊天消息模板

ChatPromptTemplate 用来生成消息列表。它比纯文本模板更适合现代 Chat Model。

from langchain_core.prompts import ChatPromptTemplate


prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个严谨的 Python 后端工程师。"),
    ("human", "{question}"),
])

prompt_value = prompt.invoke({
    "question": "Flask 项目如何管理数据库迁移?"
})

print(prompt_value.to_messages())
1
2
3
4
5
6
7
8
9
10
11
12
13

这段代码生成的是消息结构,而不是简单字符串。消息结构的好处是角色清晰:

  • system:定义模型身份、边界、规则。
  • human:用户输入。
  • ai:历史模型回复或 few-shot 示例。
  • tool:工具返回结果。

在生产项目里,角色边界非常重要。系统规则应该放在 system message,用户输入应该放在 human message,不要把它们全部拼成一段无结构文本。

# MessagesPlaceholder:插入对话历史

多轮对话里,历史消息往往不是固定长度,也不是固定内容。MessagesPlaceholder 可以在 Prompt 中预留一个消息列表位置。

from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder


prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个技术助手,回答要简洁。"),
    MessagesPlaceholder("chat_history"),
    ("human", "{question}"),
])

prompt_value = prompt.invoke({
    "chat_history": [
        HumanMessage(content="你是谁?"),
        AIMessage(content="我是一个技术助手。"),
    ],
    "question": "继续解释一下 LangChain 的 Prompt 组件。",
})

print(prompt_value.to_messages())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用 MessagesPlaceholder 时要注意:传入的必须是消息列表或可转换为消息的对象。如果传错类型,LangChain 会报 prompt input 相关错误。

生产中不要无限塞历史消息。对话历史应该有策略:

  • 只保留最近 N 轮。
  • 对长历史做摘要。
  • 抽取稳定用户偏好。
  • 对敏感内容做过滤。
  • 对 token 数做上限控制。

Prompt 只是插入历史的入口,真正的上下文治理要结合 memory、middleware 或 LangGraph 状态管理。

# partial:提前固定部分变量

有些变量每次调用都一样,或者由系统在构建 Prompt 时确定,可以用 partial 固定。

from datetime import datetime
from langchain_core.prompts import ChatPromptTemplate


prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个技术助手。当前时间:{now}"),
    ("human", "{question}"),
]).partial(now=datetime.now().isoformat())

prompt_value = prompt.invoke({
    "question": "今天应该如何安排数据库迁移上线?"
})
1
2
3
4
5
6
7
8
9
10
11
12

partial 适合这些场景:

  • 固定业务角色。
  • 固定输出语言。
  • 注入当前时间。
  • 注入租户名称。
  • 注入 Prompt 版本。
  • 固定通用格式说明。

不要把所有变量都变成 partial。用户输入、检索上下文、会话历史这些运行时变化的数据,仍然应该在 invoke 时显式传入。

# Prompt 的组合

Prompt 可以拆成多个片段,再组合起来。这样做有利于复用系统规则、业务约束和用户输入模板。

from langchain_core.prompts import ChatPromptTemplate


system_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业、克制、准确的技术助手。"),
])

task_prompt = ChatPromptTemplate.from_messages([
    ("human", "请回答这个问题:{question}"),
])

prompt = system_prompt + task_prompt

prompt_value = prompt.invoke({
    "question": "LangChain Prompt 组件适合解决什么问题?"
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

组合 Prompt 时,最重要的是边界清楚:

  • 全局规则单独维护。
  • 业务任务单独维护。
  • 输出格式单独维护。
  • few-shot 示例单独维护。
  • RAG 上下文单独维护。

不要把所有内容写进一个超长模板。超长模板后期很难 review,也很难判断是哪一段影响了模型行为。

# Few-shot 示例

Few-shot prompting 是提升模型稳定性的重要方式。它通过示例告诉模型“什么样的输入应该产生什么样的输出”。

from langchain_core.prompts import ChatPromptTemplate


prompt = ChatPromptTemplate.from_messages([
    ("system", "你负责把用户问题分类为 bug、feature、question 三类。"),
    ("human", "登录后页面一直转圈"),
    ("ai", "bug"),
    ("human", "能不能加一个导出 Excel 的功能"),
    ("ai", "feature"),
    ("human", "{input}"),
])
1
2
3
4
5
6
7
8
9
10
11

Few-shot 适合:

  • 意图识别。
  • 分类任务。
  • 格式模仿。
  • 工单摘要。
  • SQL 生成约束。
  • 特定风格写作。

示例不宜过多。太多示例会增加 token 成本,也可能让模型过拟合示例风格。生产中应该用评估集验证示例是否真的提升效果。

# RAG 场景里的 Prompt

RAG Prompt 通常要包含三类信息:

  • 角色和回答规则。
  • 检索到的上下文。
  • 用户问题。

示例:

from langchain_core.prompts import ChatPromptTemplate


rag_prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        """
你是企业知识库问答助手。
请只根据给定上下文回答问题。
如果上下文中没有答案,请明确说不知道。
不要编造不存在的事实。

上下文:
{context}
""",
    ),
    ("human", "{question}"),
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

RAG Prompt 的关键不是把上下文塞进去就完了,而是要明确模型行为边界:

  • 只能根据上下文回答。
  • 不知道时要承认不知道。
  • 尽量引用来源。
  • 不要泄露无关上下文。
  • 不要把检索片段当成用户指令执行。

最后一点很重要。检索文档里可能包含恶意文本,例如“忽略之前所有规则”。生产 RAG 要把外部文档当成不可信上下文,而不是系统指令。

# 结构化输出 Prompt

当业务需要 JSON、枚举或 Pydantic 模型时,Prompt 要和结构化输出策略配合,而不是只写一句“请返回 JSON”。

更推荐使用模型或 Agent 的结构化输出能力。如果只是 Prompt 约束,也要写清楚格式:

from langchain_core.prompts import ChatPromptTemplate


prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        """
你是工单分类器。
只返回 JSON,不要输出额外解释。
字段:
- intent:create_ticket、query_status、other
- priority:low、medium、high
- summary:20 字以内摘要
""",
    ),
    ("human", "{user_input}"),
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

生产中还必须做输出校验。Prompt 约束不是类型系统,模型仍然可能返回不合法 JSON、缺字段或枚举值错误。正确做法是:Prompt 约束 + schema 校验 + 失败重试 + 降级处理。

# Prompt 输入校验

Prompt 模板中的变量必须和调用时传入的变量一致。

prompt = ChatPromptTemplate.from_messages([
    ("human", "请解释 {subject}"),
])

prompt.invoke({"topic": "LangChain"})
1
2
3
4
5

这里会失败,因为模板需要 subject,但传入的是 topic。

项目里建议:

  • 给 Prompt 变量命名建立规范。
  • 避免同义变量混用,例如 query、question、input 到处混杂。
  • 对业务入参先做 Pydantic 校验。
  • 为重要 Prompt 写最小单元测试。

Prompt 测试不一定要调用模型。至少可以测试模板能否正常 format、变量是否完整、消息角色是否符合预期。

# 大括号转义

LangChain 默认常用 f-string 风格模板。模板里如果要展示 JSON 示例,要注意大括号转义。

错误示例:

prompt = ChatPromptTemplate.from_template("""
请返回如下 JSON:
{
  "answer": "...",
  "confidence": 0.9
}
问题:{question}
""")
1
2
3
4
5
6
7
8

这里 JSON 的 {} 可能被当成模板变量。应该写成:

prompt = ChatPromptTemplate.from_template("""
请返回如下 JSON:
{{
  "answer": "...",
  "confidence": 0.9
}}
问题:{question}
""")
1
2
3
4
5
6
7
8

这是 Prompt 模板里非常常见的坑。

# Prompt 与 Agent

在 Agent 场景里,Prompt 不只是告诉模型“你是谁”,还要定义工具使用边界。

一个好的 Agent system prompt 应该说明:

  • Agent 的职责。
  • 可以使用哪些工具。
  • 什么时候必须调用工具。
  • 什么时候不能猜测。
  • 敏感操作是否需要确认。
  • 输出格式要求。
  • 错误或信息不足时如何处理。

示例:

from langchain.agents import create_agent
from langchain.tools import tool


@tool
def get_order_status(order_id: str) -> str:
    """查询订单状态。"""
    return f"订单 {order_id} 已发货"


SYSTEM_PROMPT = """
你是一个订单客服助手。

规则:
- 查询订单状态时必须调用订单查询工具。
- 不要猜测订单状态。
- 涉及退款、取消订单、修改地址时,只能解释流程,不能直接执行。
- 如果用户没有提供订单号,请先追问。
"""

agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[get_order_status],
    system_prompt=SYSTEM_PROMPT,
)

result = agent.invoke({
    "messages": [
        {"role": "user", "content": "帮我查一下订单 A1001 的状态"}
    ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

静态规则适合放进 system_prompt。如果系统提示词需要根据用户、租户、权限、当前状态动态变化,更适合使用 middleware 或 LangGraph state 注入,而不是在业务函数里临时拼一个大字符串。

Agent 的效果很大程度取决于 harness:Prompt、tools、middleware、memory 和控制流。Prompt 只是其中一层,但它决定了模型如何理解自己的职责边界。长短期记忆也不要简单地全部塞回 Prompt,要结合 state、store、summary 和上下文窗口预算来设计。

# Prompt 版本管理

生产项目里,Prompt 应该像代码一样管理。

建议记录:

  • Prompt 名称。
  • Prompt 版本。
  • 适用场景。
  • 输入变量。
  • 输出格式。
  • 修改原因。
  • 评估结果。

一个简单做法是把 Prompt 单独放在模块里:

# internal/ai/prompts/ticket.py
from langchain_core.prompts import ChatPromptTemplate


TICKET_CLASSIFIER_PROMPT_VERSION = "2026-08-06-v1"

ticket_classifier_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是工单分类器,只返回结构化分类结果。"),
    ("human", "{user_input}"),
])
1
2
3
4
5
6
7
8
9
10

更成熟的团队可以把 Prompt 放进数据库、配置中心或 Prompt 管理平台,配合 LangSmith 做版本对比和评估。

# 项目目录建议

不要把 Prompt 散落在 route 或 service 里。推荐集中管理:

internal/
  ai/
    prompts/
      common.py
      chat.py
      rag.py
      ticket.py
    chains.py
    models.py
    parsers.py
1
2
3
4
5
6
7
8
9
10

示例:

# internal/ai/prompts/common.py
BASE_TECH_ASSISTANT_SYSTEM_PROMPT = """
你是一个严谨的技术助手。
回答要准确、简洁。
不确定时要说明不确定,不要编造。
"""
1
2
3
4
5
6
# internal/ai/prompts/chat.py
from langchain_core.prompts import ChatPromptTemplate
from internal.ai.prompts.common import BASE_TECH_ASSISTANT_SYSTEM_PROMPT


chat_prompt = ChatPromptTemplate.from_messages([
    ("system", BASE_TECH_ASSISTANT_SYSTEM_PROMPT),
    ("human", "{question}"),
])
1
2
3
4
5
6
7
8
9

这样后续调整全局风格、RAG 规则、结构化输出规则时,都能找到明确位置。

# 常见坑

# Prompt 过长

Prompt 越长,不一定效果越好。过长的 Prompt 会增加成本、延迟和注意力干扰。应该把规则压缩到必要内容,复杂知识交给检索,不要把所有业务文档塞进 system prompt。

# 规则互相冲突

例如同时写“回答要简洁”和“必须详细解释每一步”。模型遇到冲突规则时会不稳定。Prompt review 时要检查规则是否一致。

# 用户输入污染系统规则

不要把用户输入拼进 system message。用户输入应该作为 human message,外部文档应该作为 context,并明确它不是指令来源。

# 没有输出校验

Prompt 写得再好,也不能保证模型永远按格式输出。关键业务必须做 parser 和 schema 校验。

# 没有评估集

Prompt 优化不能只靠主观感觉。至少准备一批典型问题和预期结果,每次修改后对比效果。

# 小结

LangChain 的 Prompt 组件把提示词从字符串变成了可组合的工程对象。PromptTemplate 适合文本模板,ChatPromptTemplate 适合聊天模型,MessagesPlaceholder 适合插入动态历史,partial 适合固定部分变量,Prompt 组合和 few-shot 可以提高复用性和稳定性。

生产项目里,Prompt 要有边界、有变量规范、有版本、有测试、有评估。它不是随手写在代码里的文案,而是 LLM 应用的核心控制面。Model 决定能力上限,Prompt 决定能力如何被约束和释放。