# 13 - 提示词与消息模板 --- **本章课程目标:** - 理解 **Prompt** 是什么,知道它为什么会从“一段字符串”逐步演化成“多角色消息 + 模板 + 占位符”。 - 掌握 LangChain 中与输入组织最相关的三块内容:**消息类型(Message)**、**模型调用方式(invoke / stream / batch)**、**提示词模板(PromptTemplate / ChatPromptTemplate)**。 - 会运行并理解本章全部案例:**输入类型、同步与异步调用、文本模板、对话模板、消息占位符、从 JSON / YAML 加载提示词**,为后续 [输出解析器](14-输出解析器.md)、[LCEL 与链式调用](15-LCEL与链式调用.md)、[记忆与对话历史](16-记忆与对话历史(含Redis基础).md) 打基础。 **学习建议:** 读这一章时,拿一条普通用户问题练手,把它改造成消息列表:系统消息放规则,用户消息放任务,历史消息放上下文。先理解模型到底吃进去什么,再看 `PromptTemplate`、`ChatPromptTemplate`、`MessagesPlaceholder` 怎么帮你复用这些结构。读完后最好能判断:什么时候写死提示词,什么时候抽成模板,什么时候需要把模板放到文件里。 **官方文档与资源**:详见 [工具导航与参考资料索引 - 提示词与结构化输出](工具导航与参考资料索引.md#提示词与结构化输出)。 --- ## 1、Prompt 简介 本章对应 [第 11 章 Model I/O](11-Model-I-O与模型接入.md) 里的 **输入格式化(Format)** 与 **模型调用(Predict)** 两部分。模型并不是“凭空思考”,它始终是在读取我们提供的输入后再生成输出;而 Prompt、Message、Template,正是组织这份输入的核心工具。 如果你已经学过 [1-2 提示词工程基础](1-2-提示词工程基础.md),那么本章可以直接理解为它的代码实现版:前一章讲“怎么把角色、任务、上下文、输入、输出和约束说清楚”,这一章讲“这些内容进入 LangChain 后,应该以什么输入形态存在、如何复用、如何插入多轮历史、如何外置到文件”。 ### 1.1 定义 **Prompt(提示词)**,就是你发给大模型的输入内容。 最简单的 Prompt,是一句自然语言,比如“什么是 LangChain?”;再进一步,你会给它增加角色,比如“你是一名法律顾问,请用 50 字介绍广告法”;再往后,为了让代码可复用、可维护、可协作,你会把这段输入写成模板,并把会变化的部分改成占位符。这就是从“随手提问”走向“工程化输入管理”的过程。 入门阶段可先把握一个基本判断:**Prompt 不只是把一句话写漂亮,而是把模型输入组织清楚。** 在真实项目中,Prompt 通常承担几件事: - 告诉模型它是谁,要扮演什么角色。 - 告诉模型当前任务是什么,回答边界是什么。 - 告诉模型输出格式应该长什么样。 - 把用户问题、历史对话、检索结果、工具结果组合成一次完整输入。 这也是为什么随着项目复杂度提高,Prompt 会从“一个字符串”逐步演化成“多角色消息 + 模板 + 占位符 + 外部配置文件”。 ![DeepSeek 官方提示词示例库入口](images/13/13-1-1-1.jpeg) ### 1.2 Prompt 的作用 很多人在学 LangChain 时,会把注意力都放在“接哪个模型”“换哪个平台”“怎么配 API Key”上,但真正到了项目里,最容易让系统效果不稳定的,往往不是模型接入,而是**输入组织得不够清楚**。 下面这些真实开发场景,都离不开本章内容: - **智能客服**:需要系统提示词规定语气、身份和拒答策略。 - **企业知识库问答**:需要把“检索出来的上下文 + 用户问题”组合成一条清晰提示。 - **多轮聊天**:需要把历史对话插回当前输入,而不是每轮都从头问。 - **结构化输出**:需要提前在 Prompt 里写清楚输出格式要求,方便后面交给解析器处理。 - **团队协作与 A/B 测试**:需要把 Prompt 模板从代码里抽出来,放到 JSON / YAML 中做版本管理。 一句话:**模型能力决定上限,Prompt 设计决定你能不能稳定接近这个上限。** ### 1.3 本章在项目中的位置 本章对应仓库中的 `案例与源码-2-LangChain框架/04-prompt` 目录,按学习顺序可以分成 4 类: | 目录 | 作用 | 你会学到什么 | | ----------------------- | ------------ | ------------------------------------------ | | `invoke/` | 模型调用方式 | `invoke`、`stream`、`batch` 及异步版本 | | `prompt_templates/` | 文本模板 | `PromptTemplate` 的创建、格式化、复用 | | `chat_prompt_template/` | 对话模板 | `ChatPromptTemplate`、消息参数、消息占位符 | | `load_external/` | 外部文件加载 | 如何从 JSON / YAML 加载 Prompt | 这一章不是零散的 API 介绍,而是在回答一个问题:**开发 LLM 应用时,怎样把 [1-2 提示词工程基础](1-2-提示词工程基础.md) 里学到的写法和原则,组织成一套可复用、可维护、可扩展的结构?** --- ## 2、调用大模型的入参类型 初学者容易误以为:调用聊天模型时,输入永远只能是“一段字符串”。实际上,LangChain 聊天模型为了适配真实对话场景,通常支持多种输入形态。这些写法表面不同,核心都一样:**把 [1-2 章](1-2-提示词工程基础.md) 里讲过的角色、任务、上下文、输入和约束,以合适的消息结构交给模型。** ### 2.1 入参形态总览 同一次 `invoke`,左侧可以是多种类型的输入,中间由聊天模型处理,右侧典型返回值是 `AIMessage`。这也是为什么你在 [第 11 章](11-Model-I-O与模型接入.md) 会经常看到“正文一般通过 `.content` 读取”。 ![聊天模型 invoke:常见入参类型与 AIMessage 输出](images/13/13-2-1-1.png) 常见对应关系如下: | 入参形态 | 调用时大致长什么样 | 适合场景 | | -------------------------------------------- | ------------------------------------------------------- | ---------------------------------- | | `str` | `model.invoke("请解释什么是 LangChain")` | 单轮、轻量、快速试接口 | | `PromptTemplate.format(...)` 后的字符串 | `model.invoke(prompt_str)` | 固定句式 + 少量变量替换 | | 消息对象列表 | `model.invoke([SystemMessage(...), HumanMessage(...)])` | 推荐写法,适合系统提示、多轮对话 | | `(role, content)` 元组列表 | `[("system", "..."), ("user", "...")]` | 简洁、贴近 ChatPromptTemplate 风格 | | `{"role": "...", "content": "..."}` 字典列表 | `[{"role":"system","content":"..."}, ...]` | 贴近 OpenAI 风格 JSON 数据 | 【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke_InputTypes.py` [LLM_Invoke_InputTypes.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke_InputTypes.py ":include :type=code") ### 2.2 写法一:纯字符串 最简单的输入方式,把整段说明写在一个字符串里交给模型,适合快速试接口、或单轮一句话任务。 ```python resp = model.invoke("用一句话解释什么是 LangChain") print(resp.content) ``` 不过在真实项目里,纯字符串也有明显局限:不方便表达系统角色与用户问题的边界;不方便插入历史对话;不利于后期维护和多人协作。 所以,**纯字符串适合起步,不适合复杂对话场景长期使用。** ### 2.3 写法二:模板 + 占位符 如果一句 Prompt 里只有少数变量会变化,就没必要每次手写整段长文本。更合理的做法,是把固定部分写成模板,把变化部分留成占位符。 这一节本质上仍然是“字符串输入”,只是我们把字符串的生成过程工程化了。 ```python from langchain_core.prompts import PromptTemplate template = PromptTemplate.from_template( "用不超过 50 字介绍:{topic} 是什么?" ) prompt_str = template.format(topic="LangChain") resp = model.invoke(prompt_str) print(resp.content) ``` 也就是说: - **`PromptTemplate` 负责生产输入** - **`model.invoke(...)` 负责把输入发给模型** 它和 2.2 的区别不在于模型收到了不同类型,而在于**我们是手写字符串,还是用模板生成字符串**。 ### 2.4 写法三:多角色消息列表 当你开始做聊天机器人、问答助手、企业知识库、代码助手时,最推荐的入参方式通常不是字符串,而是**消息列表**。 原因很简单:聊天模型更擅长理解“谁在说话”。把输入拆成 `SystemMessage`、`HumanMessage`、`AIMessage` 等不同角色,模型更容易正确理解上下文结构,而不是把所有东西都当成一大段平铺文本。 ```python from langchain_core.messages import SystemMessage, HumanMessage, AIMessage messages = [ SystemMessage(content="你是只回答技术问题的助手,回答要简短。"), HumanMessage(content="什么是 LangChain?"), # 多轮示例: # AIMessage(content="LangChain 是用于编排 LLM 应用的框架……"), # HumanMessage(content="它和直接调 API 有什么区别?"), ] resp = model.invoke(messages) print(resp.content) ``` 在实际项目里,这种写法特别常见: - **系统提示词**放在 `SystemMessage` - **用户问题**放在 `HumanMessage` - **历史回复**可放回 `AIMessage` - **工具执行结果**后续可用 `ToolMessage` 如果你后面要做多轮对话、Agent、RAG,这种消息列表思维会反复用到。 还有一种等价包装是 `ChatPromptValue`:链式编排里更常见的是由 `ChatPromptTemplate.invoke(...)` 得到 `ChatPromptValue`;下面直接构造对象,便于理解「`PromptValue` → `to_messages()` → `model.invoke`」的衔接。 ```python from langchain_core.messages import SystemMessage, HumanMessage, AIMessage from langchain_core.prompt_values import ChatPromptValue prompt_value = ChatPromptValue( messages=[ SystemMessage(content="You are a helpful AI bot. Your name is Bob."), HumanMessage(content="Hello, how are you doing?"), AIMessage(content="I'm doing well, thanks!"), HumanMessage(content="What is your name?"), ] ) resp = model.invoke(prompt_value.to_messages()) ``` ### 2.5 写法四:元组列表与字典列表 除了显式使用 `SystemMessage`、`HumanMessage` 等类,LangChain 聊天模型通常还支持两种常见简写。 - **元组列表**:每项写成 `(role, content)` - **字典列表**:每项写成 `{"role": "...", "content": "..."}` 这两种写法与消息对象列表在语义上基本等价,只是更接近手写列表或 OpenAI 风格的数据结构。 ```python import os from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.0, base_url=os.getenv("OPENAI_BASE_URL"), api_key=os.getenv("OPENAI_API_KEY"), ) messages_as_tuples = [ ("system", "你是一个专业的数学助手"), ("user", "你好,你是谁"), ] messages_as_dicts = [ {"role": "system", "content": "你是一个专业的数学助手"}, {"role": "user", "content": "你好,你是谁"}, ] resp = llm.invoke(messages_as_tuples) print(type(resp)) print(resp.content) resp2 = llm.invoke(messages_as_dicts) print(resp2.content) ``` 关于选择策略,可按下面的思路理解: - **想学得最清楚**:优先用 `Message` 类 - **想写得最简洁**:可以用元组列表 - **想和 OpenAI 风格请求体对齐**:可以用字典列表 无论哪种方式,同步 `invoke` 的返回值通常仍是 `AIMessage`。 ### 2.6 扩展:Java 生态中的多角色 “System / User / Assistant / Tool” 这套思路并不是 LangChain Python 独有的,Java 生态中的 **LangChain4J**、**Spring AI** 也有类似设计。理解这一点很有价值,因为它说明: **多角色消息并不是某个框架的语法技巧,而是现代聊天模型交互的一种通用抽象。** **LangChain4J:** ```java package dev.langchain4j.data.message; public enum ChatMessageType { SYSTEM(SystemMessage.class), USER(UserMessage.class), AI(AiMessage.class), TOOL_EXECUTION_RESULT(ToolExecutionResultMessage.class), CUSTOM(CustomMessage.class); private final Class messageClass; ChatMessageType(Class messageClass) { this.messageClass = messageClass; } public Class messageClass() { return messageClass; } } ``` **Spring AI:** ```java package org.springframework.ai.chat.messages; public enum MessageType { USER("user"), ASSISTANT("assistant"), SYSTEM("system"), TOOL("tool"); private final String value; MessageType(String value) { this.value = value; } public static MessageType fromValue(String value) { ... } public String getValue() { return this.value; } } ``` --- ## 3、入参的消息类型 当你把输入组织成“消息列表”之后,就需要知道每种消息类型代表什么。在 LangChain 官方语境里,最核心的几类消息是:**SystemMessage**、**HumanMessage**、**AIMessage**、**ToolMessage**。 **文档**:https://docs.langchain.com/oss/python/langchain/messages (英文);https://docs.langchain.org.cn/oss/python/langchain/messages (中文) ### 3.1 四类核心消息 | 类型 | 说明 | 项目里最常见的用途 | | --------------- | ------------------------------------------------ | ----------------------------------- | | `SystemMessage` | 系统消息,通常用于规定角色、风格、边界、输出格式 | 设定人设、回答规则、拒答策略 | | `HumanMessage` | 用户消息,对应用户当前输入 | 放用户问题、补充条件、后续追问 | | `AIMessage` | 模型回复消息 | 保存上一轮回复,支持多轮上下文 | | `ToolMessage` | 工具执行结果消息 | 把外部工具/函数的返回结果回传给模型 | 需要注意两个细节: 1. **LangChain 的类名叫 `HumanMessage`,但很多平台的角色字段写的是 `user`。**这不是冲突,而是不同层的命名习惯。你可以把它们理解成同一个角色。 2. **旧版本资料里可能会看到 `FunctionMessage`。**在 LangChain 1.x 语境下,更常见的是 `ToolMessage`。如果你阅读旧教程或旧代码,看到这类差异,先把它理解为工具调用结果的旧命名即可。 ### 3.2 SystemMessage 的作用 很多新手会把系统提示词和用户问题混在一段字符串里写,这样虽然也能跑,但可维护性很差。更稳妥的做法是把“规则”和“问题”拆开: - **SystemMessage** 负责定义系统层规则 - **HumanMessage** 负责承载当前用户问题 例如: - “你是一个法律助手,只回答法律问题” - “输出请控制在 80 字内” - “如果超出范围,请明确拒答” 这些内容更适合放在 `SystemMessage` 里,因为它们属于“长期规则”,而不是某一轮具体问题。 这正对应了 [1-2 提示词工程基础](1-2-提示词工程基础.md) 里强调的那条原则:**稳定约束和动态输入要拆开写。**在 LangChain 里,这条原则最直接的落地方式就是把稳定规则放进 `SystemMessage`,把当前任务放进 `HumanMessage`。 在真实项目里,SystemMessage 往往决定: - 回答口吻是否专业 - 是否允许推测 - 是否必须引用上下文 - 是否必须按 JSON 输出 - 是否需要保守拒答 这也是为什么 Prompt 工程里经常会说:**系统提示词是行为边界,用户提示词是当前任务。** ### 3.3 ToolMessage 什么时候会出现 `ToolMessage` 不会在普通问答里频繁出现,它更常见于函数调用、工具调用、Agent 编排场景。 简单理解就是: - 模型先在 `AIMessage` 里表达“我想调用某个工具” - 代码真的去执行工具 - 工具结果再以 `ToolMessage` 的形式回传给模型 这样模型才能基于工具结果继续回答。 下面是一个简化示例: ```python from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage messages = [ SystemMessage(content="你是一位乐于助人的智能小助手"), HumanMessage(content="你好,请你介绍一下你自己"), AIMessage(content="我是一名人工智能助手,请问您有什么想问的吗?"), ToolMessage( content='{"population": 21540000, "area": "16410平方公里"}', tool_call_id="call_abc123", ), ] print(messages) ``` 对当前章节来说,你只需要先建立基本印象:**最常用的是 System / Human / AI 三类;ToolMessage 是后续 Agent、工具调用章节的重要铺垫。** --- ## 4、调用大模型的调用方式 当输入组织好之后,下一步就是把它交给模型。LangChain 聊天模型常见的调用方式有四类:普通调用、流式调用、批量调用,以及它们各自的异步版本。 这部分看起来像 API 记忆题,其实可以用一个更直观的方式理解: - **invoke / ainvoke**:一次发一条 - **stream / astream**:一边生成一边返回 - **batch / abatch**:一次发很多条 ### 4.1 普通调用(invoke / ainvoke) 【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke.py`、`LLM_aInvoke.py` - `invoke`:同步调用,最常用,适合单轮问答与脚本演示。 - `ainvoke`:异步调用,适合异步 Web 服务、并发任务和高吞吐场景。 ```python import os from langchain.chat_models import init_chat_model from langchain_core.messages import HumanMessage, SystemMessage model = init_chat_model( model="qwen-plus", model_provider="openai", api_key=os.getenv("aliQwen-api"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) messages = [ SystemMessage(content="你是一个法律助手,只回答法律问题,超出范围回答:非法律问题无可奉告"), HumanMessage(content="简单介绍下广告法,一句话 50 字以内") ] response = model.invoke(messages) print(type(response)) print(response.content) ``` 实际项目里怎么选: - **命令行脚本、教学示例、简单后台任务**:优先 `invoke` - **FastAPI、异步服务、并发请求**:优先 `ainvoke` - **本地开源模型(Ollama 等)**:实例化与端点配置见 [第 12 章 Ollama 本地部署与调用](12-Ollama本地部署与调用.md),本章的 `invoke` / `stream` / `batch` 用法同样适用。 [LLM_Invoke.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Invoke.py ":include :type=code") [LLM_aInvoke.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_aInvoke.py ":include :type=code") ### 4.2 流式调用(stream / astream) 【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Stream.py`、`LLM_aStream.py` - **stream**:同步流式输出 - **astream**:异步流式输出 流式的最大价值不是“更快算完”,而是**更快把正在生成的内容展示给用户**。在聊天机器人、报告生成、代码生成等场景里,用户体验会明显更好。 ```python messages = [ SystemMessage(content="你叫小问,是一个乐于助人的AI助手"), HumanMessage(content="你是谁") ] for chunk in model.stream(messages): print(chunk.content, end="", flush=True) print() ``` 真实项目里,`stream` / `astream` 很常用于: - 聊天界面的“打字机效果” - 长回答提前回显 - 减少用户等待焦虑 [LLM_Stream.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Stream.py ":include :type=code") [LLM_aStream.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_aStream.py ":include :type=code") ### 4.3 批处理(batch / abatch) 【案例源码】`案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Batch.py`、`LLM_aBatch.py` - **batch**:一次提交多条输入,统一获得多条结果 - **abatch**:异步批处理 它特别适合离线任务,而不是交互式聊天。例如: - 批量摘要一批文档 - 批量清洗问答数据 - 批量评估 Prompt 效果 - 批量为商品、评论、工单做标签分类 ```python questions = [ "什么是 Redis?简洁 100 字以内", "Python 的生成器是做什么的?简洁 100 字以内", ] response = model.batch(questions) for q, r in zip(questions, response): print(f"问题:{q}\n回答:{r.content}\n") ``` [LLM_Batch.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_Batch.py ":include :type=code") [LLM_aBatch.py](案例与源码-2-LangChain框架/04-prompt/invoke/LLM_aBatch.py ":include :type=code") ### 4.4 小结 | 场景 | 同步 | 异步 | 适合什么情况 | | -------- | -------- | --------- | ------------------- | | 单条调用 | `invoke` | `ainvoke` | 单轮问答、接口服务 | | 流式输出 | `stream` | `astream` | 聊天 UI、长文本生成 | | 批量处理 | `batch` | `abatch` | 离线任务、批量评估 | 如果你一时记不住,也没关系,先记住这条基本规则: **先会 `invoke`,再学 `stream`,最后再补 `batch` 和异步版本。** --- ## 5、提示词模板概览 ### 5.1 提示词简介 在真正的项目里,Prompt 几乎不可能永远写死在代码中。原因很现实: - 用户问题会变 - 角色设定会变 - 输出要求会变 - 业务策略会调整 - 团队成员需要一起维护 如果每次都手写一整段 Prompt,不仅容易重复,还非常难维护。提示词模板的作用,就是把**固定部分沉淀下来,把变化部分改成变量**,从而让同一套提示逻辑可以反复复用。 这和 Python 的 `f-string` 很像: ```python def hello(name: str) -> None: print(f"你好:{name}") if __name__ == "__main__": hello("李四") ``` 这里的 `{name}` 就像 Prompt 模板里的占位符,直接把它看成“先留坑,后填值”就行。 ### 5.2 提示词模板类型 LangChain 中常见的提示词模板主要有下面几类,本课程重点掌握前两种即可: | 类型 | 说明 | 本课程定位 | | ------------------------- | -------------------------------------------- | ---------- | | **PromptTemplate** | 面向纯文本模板,填值后通常得到一条字符串 | 重点掌握 | | **ChatPromptTemplate** | 面向聊天模型的多角色模板,填值后得到多条消息 | 重点掌握 | | **FewShotPromptTemplate** | 把若干“示例输入-输出”嵌入提示词 | 了解即可 | | **PipelinePrompt** | 把多个子提示按顺序组合 | 了解即可 | 入门阶段可以先作一个简化理解: - **单条文本任务**,先看 `PromptTemplate` - **聊天模型、多角色、多轮对话**,重点看 `ChatPromptTemplate` ### 5.3 Few-shot 模板 Few-shot 的意思是:不要只告诉模型“按什么规则做”,还给它几组“输入应该怎么变成输出”的示例。 它适合下面这类场景: - 分类标签容易混,需要给几个标准样例 - 输出风格有要求,单靠文字说明不够稳 - 任务规则不复杂,但希望模型模仿固定格式 入门阶段不用急着背 `FewShotPromptTemplate` 的所有参数,先记住一句话:**Few-shot 是把示例变成提示词的一部分,让模型照着样子做。**等你后面做分类、抽取、客服话术生成时,再把它作为提高稳定性的手段即可。 --- ## 6、文本提示词模板(PromptTemplate) ### 6.1 简介 `PromptTemplate` 是 LangChain 中最基础的模板类,适合把一段文本 Prompt 做成“固定骨架 + 动态变量”的形式。 它最适合的场景是: - 摘要、改写、翻译、分类等单轮文本任务 - 还不需要明确区分 system / user 角色 - 需要频繁替换少量变量 如果你把它和第 2 节联系起来看,会更容易理解:**PromptTemplate 的结果通常还是字符串,只不过这条字符串不再靠手写,而是由模板生成。** ### 6.2 常用参数 | 参数 | 说明 | | ------------------- | ---------------------------------------- | | `template` | 模板字符串,内部可包含 `{变量名}` 占位符 | | `input_variables` | 调用时需要传入的变量名列表 | | `partial_variables` | 在模板创建阶段就预先固定的一部分变量 | 其中,`partial_variables` 尤其值得理解。它的作用很直接: **把那些“经常不变”的变量先固定住,后续每次只传真正会变化的部分。** 典型例子: - 系统角色长期固定为“Python 工程师” - 但用户问题每次都不同 这样一来,你就不用每次都重复传 `role="Python 工程师"`。 ### 6.3 常用方法 | 方法 | 返回值 | 适合场景 | 案例源码 | | --------------- | --------------------- | -------------------------------------------- | --------------------------------- | | `format(...)` | `str` | 最常用,拿到字符串后直接给模型或自己继续拼接 | `PromptTemplate_FormatMethod.py` | | `invoke({...})` | `PromptValue` | 需要接入 LangChain 链时更自然 | `PromptTemplate_InvokeMethod.py` | | `partial(...)` | 新的 `PromptTemplate` | 先固定部分变量,再多次复用 | `PromptTemplate_PartialMethod.py` | ```python from langchain_core.prompts import PromptTemplate template = PromptTemplate.from_template( "你是一个专业的{role}工程师,请回答我的问题,我的问题是:{question}" ) # 1)format:得到 str prompt_str = template.format(role="python开发", question="二分查找怎么写?") # 2)invoke:得到 PromptValue prompt_value = template.invoke({"role": "python开发", "question": "冒泡排序怎么写?"}) prompt_value.to_string() prompt_value.to_messages() # 3)partial:固定 role,得到新模板 new_template = template.partial(role="python开发") prompt_str = new_template.format(question="快速排序怎么写?") ``` 对初学者的实用建议是: - **刚入门时优先用 `format`** - **做 LCEL 或链式调用时再逐渐理解 `invoke`** - **同一模板长期复用时再考虑 `partial`** 【案例源码】 format:`案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_FormatMethod.py` invoke:`案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_InvokeMethod.py` partial:`案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_PartialMethod.py` [PromptTemplate_FormatMethod.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_FormatMethod.py ":include :type=code") [PromptTemplate_InvokeMethod.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_InvokeMethod.py ":include :type=code") [PromptTemplate_PartialMethod.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/method/PromptTemplate_PartialMethod.py ":include :type=code") ### 6.4 创建方式 `PromptTemplate` 常见有两种创建方式: - 构造函数:手动指定 `template` 和 `input_variables` - `from_template(...)`:由 LangChain 自动推断变量名 ```python from langchain_core.prompts import PromptTemplate # 方式一:构造函数 template = PromptTemplate( template="你是一个专业的{role}工程师,请回答:{question}", input_variables=["role", "question"] ) prompt = template.format(role="python开发", question="快速排序怎么写?") # 方式二:from_template template = PromptTemplate.from_template("请给我一个关于{topic}的{type}解释。") prompt = template.format(topic="量子力学", type="详细") ``` 从经验上看,可按下面的方式选择: - **模板简单**:优先 `from_template(...)` - **你想显式表达变量名**:用构造函数 【案例源码】构造函数:`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Constructor.py` 【案例源码】`from_template`:`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_FromTemplate.py` [PromptTemplate_Constructor.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Constructor.py ":include :type=code") [PromptTemplate_FromTemplate.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_FromTemplate.py ":include :type=code") 除了创建方式,本章还保留了两个补充案例。 **第一,组合多个模板。** 当一个 Prompt 由多个子部分拼起来时,可以通过 `+` 组合模板,而不是手写超长字符串。真实项目里,这在“角色说明 + 业务规则 + 当前任务”这种分段组织里很有用。 【案例源码】`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Combined.py` [PromptTemplate_Combined.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_Combined.py ":include :type=code") **第二,比较 `partial_variables` 与 `partial()`。** 二者都能做到“先固定一部分变量,后续只传剩余变量”,但一个发生在模板创建阶段,一个发生在已有模板基础上。 【案例源码】`案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_PartialVariables.py` [PromptTemplate_PartialVariables.py](案例与源码-2-LangChain框架/04-prompt/prompt_templates/PromptTemplate_PartialVariables.py ":include :type=code") --- ## 7、对话提示词模板(ChatPromptTemplate) ### 7.1 简介 如果说 `PromptTemplate` 更适合“单条文本输入”,那么 `ChatPromptTemplate` 就是为“聊天模型场景”准备的模板类。 它比 `PromptTemplate` 更贴合真实项目,因为真实聊天应用往往不只是“一句话”,而是: - 一条系统设定 - 一条或多条用户消息 - 可能还有历史 AI 回复 - 可能再插入工具结果或历史上下文 这时,把所有内容写成一大段纯文本虽然也能跑,但不如把它们拆成带角色的消息来得清晰。 ```python from langchain_core.messages import SystemMessage, HumanMessage, AIMessage messages = [ SystemMessage(content="你是一个AI开发工程师"), HumanMessage(content="你能开发哪些AI应用?"), AIMessage(content="我能开发很多AI应用,比如聊天机器人、图像识别等") ] ``` 所以你可以把 `ChatPromptTemplate` 简单理解成:**“面向多角色消息的模板系统”。** ### 7.2 常用参数 `ChatPromptTemplate` 的核心不是单个 `template` 字符串,而是一组“消息模板”。每一项都可以是下面这些形式: | 类型 | 说明 | 案例源码 | | --------------------- | --------------------------------------------- | ------------------------------------ | | 元组 | `("system", "你是{name}")` | `ChatPromptTemplate_TupleParam.py` | | 字典 | `{"role": "system", "content": "你是{name}"}` | `ChatPromptTemplate_DictParam.py` | | Message 类 | `SystemMessage(content="你是{name}")` | `ChatPromptTemplate_MessageParam.py` | | `MessagesPlaceholder` | 在模板中预留一段“消息列表占位” | 见 7.5 节 | 三种常规写法里,没有绝对的谁对谁错,可以按下面的经验来选: - **元组**:最简洁,教学和业务代码里都很常见 - **字典**:更贴近 OpenAI 风格 JSON,方便和网关数据结构对齐 - **Message 类**:最显式,角色最清楚,适合教学和复杂场景 【案例源码】 元组:`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_TupleParam.py` 字典:`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_DictParam.py` Message 类:`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_MessageParam.py` [ChatPromptTemplate_TupleParam.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_TupleParam.py ":include :type=code") [ChatPromptTemplate_DictParam.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_DictParam.py ":include :type=code") [ChatPromptTemplate_MessageParam.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/parameter/ChatPromptTemplate_MessageParam.py ":include :type=code") 下面这个基础示例能帮助你直观看懂三种写法的共同点:它们都是在定义“系统说什么、用户说什么、哪些部分由运行时填值”。 ```python from langchain_core.messages import SystemMessage, HumanMessage from langchain_core.prompts import ChatPromptTemplate prompt1 = ChatPromptTemplate.from_messages([ ("system", "你是助手,名字叫{name}。"), ("human", "{question}") ]) prompt2 = ChatPromptTemplate.from_messages([ {"role": "system", "content": "你是助手,名字叫{name}。"}, {"role": "user", "content": "{question}"} ]) prompt3 = ChatPromptTemplate.from_messages([ SystemMessage(content="你是助手,名字叫{name}。"), HumanMessage(content="{question}") ]) ``` ### 7.3 常用方法 | 方法 | 返回值 | 使用建议 | | ---------------------- | ------------------- | ------------------------------------------- | | `format_messages(...)` | `List[BaseMessage]` | 最直观,得到消息列表后交给模型 | | `invoke({...})` | `ChatPromptValue` | 适合与 LangChain 链条衔接,也可直接交给模型 | | `format(...)` | `str` | 适合查看最终拼接效果,不推荐作为聊天主写法 | 这三个方法最容易混淆,建议这样理解: - `format_messages`:我要的是“消息列表” - `invoke`:我要的是“PromptValue 对象” - `format`:我要的是“纯字符串” ```python from langchain_core.prompts import ChatPromptTemplate chat_prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个{role},请回答我提出的问题"), ("human", "请回答:{question}") ]) # 方式一:得到消息列表 messages = chat_prompt.format_messages( role="python开发工程师", question="堆排序怎么写" ) result = model.invoke(messages) # 方式二:得到 ChatPromptValue prompt_value = chat_prompt.invoke({ "role": "python开发工程师", "question": "快速排序怎么写" }) result = model.invoke(prompt_value) # 方式三:得到纯字符串 prompt_str = chat_prompt.format( role="python开发工程师", question="快速排序怎么写" ) print(prompt_str) ``` 对实际项目来说,建议优先采用这两种: - `format_messages(...) -> model.invoke(messages)` - `invoke({...}) -> model.invoke(prompt_value)` 因为这两种方式都能保留清晰的消息角色结构。 【案例源码】`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_FormatMessages.py` [ChatPromptTemplate_FormatMessages.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_FormatMessages.py ":include :type=code") ### 7.4 创建方式 `ChatPromptTemplate` 常见也有两种创建方式: - `ChatPromptTemplate.from_messages([...])` - `ChatPromptTemplate([...])` 它们的核心差别不大,本质上都是把一组消息模板交给 `ChatPromptTemplate`。 ```python from langchain_core.prompts import ChatPromptTemplate messages = [ ("system", "你是一个{role},请回答我提出的问题"), ("human", "请回答:{question}") ] chat_prompt1 = ChatPromptTemplate.from_messages(messages) chat_prompt2 = ChatPromptTemplate(messages) print(chat_prompt1.format_messages(role="python开发工程师", question="堆排序怎么写")) print(chat_prompt2.format_messages(role="python开发工程师", question="堆排序怎么写")) ``` 经验上更推荐优先使用 `from_messages(...)`,因为可读性更好,也更符合官方文档和社区示例的主流写法。 【案例源码】`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_Constructor.py` [ChatPromptTemplate_Constructor.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/ChatPromptTemplate_Constructor.py ":include :type=code") ### 7.5 MessagesPlaceholder MessagesPlaceholder(消息占位符)这是本章最重要的知识点之一,很多人一开始会觉得它抽象,但一旦理解,就会发现它几乎是多轮对话、记忆、历史上下文拼接的关键。 **先说结论:`MessagesPlaceholder` 的作用,就是在模板里先留出一段“消息列表的位置”,等真正调用时再把历史对话整块塞进去。** ![MessagesPlaceholder 将历史消息动态插入 ChatPromptTemplate:模板先留占位,运行时再填入多轮消息](images/13/13-7-5-1.svg) 为什么它重要?因为真实项目里的“历史对话”往往不是固定写死的: - 有时只有 2 轮 - 有时有 10 轮 - 有时还要先裁剪、总结、过滤 如果没有占位符,你就只能在代码里手动拼接 `HumanMessage`、`AIMessage`,又乱又难维护。 它常见有两种写法: - **显式写法**:`MessagesPlaceholder("memory")` - **隐式写法**:`("placeholder", "{memory}")` 二者的核心思想完全一样,只是语法风格不同。 典型结构通常是: - 系统设定 - 历史消息占位 - 当前用户问题 ```python from langchain_core.messages import HumanMessage, AIMessage from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个资深的Python应用开发工程师,请认真回答我提出的Python相关的问题"), MessagesPlaceholder("memory"), ("human", "{question}") ]) prompt_value = prompt.invoke({ "memory": [ HumanMessage(content="我的名字叫亮仔,是一名程序员"), AIMessage(content="好的,亮仔你好") ], "question": "请问我的名字叫什么?" }) print(prompt_value.to_string()) ``` 这段代码的价值在于,它让模板本身变得非常稳定,而把“历史有几轮、具体内容是什么”延迟到运行时再决定。后面学 [第 16 章 记忆与对话历史](16-记忆与对话历史(含Redis基础).md) 时,你会频繁看到这种模式。 【案例源码】 显式:`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ExplicitPlaceholder.py` 隐式:`案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ImplicitPlaceholder.py` [ChatPromptTemplate_ExplicitPlaceholder.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ExplicitPlaceholder.py ":include :type=code") [ChatPromptTemplate_ImplicitPlaceholder.py](案例与源码-2-LangChain框架/04-prompt/chat_prompt_template/placeholder/ChatPromptTemplate_ImplicitPlaceholder.py ":include :type=code") --- ## 8、从文件加载提示词 当 Prompt 还很短时,把它直接写在 Python 代码里问题不大;但只要你开始做真实项目,就会很快遇到几个问题:Prompt 越来越长,代码越来越乱;产品、运营、算法同学希望一起改 Prompt;需要保留多个版本做 A/B 测试;想把 Prompt 与代码逻辑分离。 这时候,把 Prompt 放到 **JSON / YAML** 等外部文件中,就会更工程化。LangChain 提供了 `load_prompt(...)`,可以根据文件内容加载为模板对象。对于本章案例来说,最常见的是 `_type: "prompt"`,即加载为 `PromptTemplate`。 ### 8.1 从 JSON 加载 ```json { "_type": "prompt", "input_variables": ["name", "what"], "template": "请{name}讲一个{what}的故事" } ``` ```python from pathlib import Path from langchain_core.prompts import load_prompt prompt_path = Path(__file__).resolve().with_name("prompt.json") template = load_prompt(prompt_path, encoding="utf-8") print(template.format(name="张三", what="搞笑")) ``` ### 8.2 从 YAML 加载 YAML 与 JSON 的核心思路完全相同,只是文件格式更适合人工阅读,也更方便写注释。对团队协作来说,很多场景会更喜欢 YAML。 本章配套脚本通过 `Path(__file__)` 按脚本所在目录定位 JSON / YAML,因此不受启动命令时工作目录的影响。你在自己的项目中加载外部文件时,也建议使用绝对路径,或以脚本目录、项目根目录为基准构造路径,避免把路径解析隐式交给当前工作目录。 【案例源码】 JSON:`案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo01.py` YAML:`案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo02.py` 配套文件:`prompt.json`、`prompt.yaml` [PromptLoadDemo01.py](案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo01.py ":include :type=code") [PromptLoadDemo02.py](案例与源码-2-LangChain框架/04-prompt/load_external/PromptLoadDemo02.py ":include :type=code") 到这里你可以看到,本章知识已经形成了一个比较完整的工程化闭环: - **消息类型** 解决“输入按什么角色组织” - **调用方式** 解决“输入如何发给模型” - **PromptTemplate / ChatPromptTemplate** 解决“输入如何复用” - **MessagesPlaceholder** 解决“历史对话如何动态插入” - **JSON / YAML 外置文件** 解决“模板如何协作与版本管理” --- **章节思考题:** 1. 普通字符串 Prompt 和消息模板最大的工程差别是什么? **参考思路:** 普通字符串适合临时调用,消息模板更适合长期维护。它把系统规则、用户输入、历史消息和变量占位分清楚,后续改规则、换输入、接多轮历史都会更稳。 2. 什么时候应该用 `MessagesPlaceholder`,而不是把历史对话拼成一大段字符串? **参考思路:** 需要保留消息角色、顺序和多轮结构时,应使用 `MessagesPlaceholder`。拼字符串会丢掉角色信息,也不利于后续和记忆、工具调用、消息对象体系衔接。 3. 如果一个模板变量越来越多,你会如何判断它是不是该拆分? **参考思路:** 看变量是否服务同一个任务、是否来自同一层上下文、是否经常一起变化。如果一个模板同时管规则、业务输入、检索结果、历史消息和输出格式,可能就该拆成更小的模板或链路节点。 4. 把提示词放到外部文件有什么好处和风险? **参考思路:** 好处是便于版本管理、运营调整和复用;风险是变量名、格式和代码调用容易不一致。外置后要配套校验、示例输入和变更记录,不能只把文本搬出去。 **本章小结:** - **Prompt 的本质**:Prompt 不是“随便写一句话”,而是对模型输入进行结构化组织。随着项目复杂度提升,输入会从纯字符串演化成多角色消息,再进一步演化成模板、占位符与外部配置文件。 - **消息与调用**:聊天模型常见输入包括 `str`、消息对象列表、元组列表、字典列表;常见调用方式包括 `invoke / ainvoke`、`stream / astream`、`batch / abatch`。返回值通常是 `AIMessage`,正文一般通过 `.content` 读取。 - **模板与工程化**:`PromptTemplate` 适合文本模板,`ChatPromptTemplate` 适合聊天模型与多角色场景,`MessagesPlaceholder` 是多轮历史拼接的关键;将 Prompt 放入 JSON / YAML 更适合真实项目中的版本管理、多人协作与 A/B 测试。 - 学完本章后,你至少应该能区分四类输入组织方式:**纯字符串、消息列表、模板 + 占位符、外部文件加载**;也要知道 `PromptTemplate`、`ChatPromptTemplate`、`MessagesPlaceholder` 分别适合什么场景。 **建议下一步:** 继续学习 [第 14 章 输出解析器](14-输出解析器.md),把本章的“输入组织”与下一章的“输出结构化”连起来;再配合 [第 15 章 LCEL 与链式调用](15-LCEL与链式调用.md),就能形成 LangChain 中最核心的“输入 -> 模型 -> 输出 -> 链式编排”主线。如果你读到这里,仍然对“为什么要这样拆 Prompt”不够踏实,也可以回看 [1-2 提示词工程基础](1-2-提示词工程基础.md) 中的六要素、Few-shot 和结构化组织方式部分。