1. 这一集的主线
这一集讲的是 Prompt Engineering,也就是提示词工程。
它的核心思想可以先记成一句话:
Prompt 工程 = 把任务目标、输出形式、约束条件讲清楚,让模型更稳定地按预期工作
对初学者来说,这一章非常重要,因为你前面学的 Model、ChatOpenAI、invoke() 等更像"把模型调用起来",而这一章开始真正进入:
- 怎样提任务
- 怎样控制输出
- 怎样让结果更稳定
2. 什么是 Prompt Engineering
提示词工程,简单说就是:
- 通过设计输入文本
- 引导大模型理解你的需求
- 并生成你更想要的结果
它不是"随便写一句提示词",而是更像:
- 任务说明书
- 交付标准
- 输出格式约束
如果用你视频里的类比:
- 大模型像你的员工
- Prompt 像你给员工的任务文档
任务越清晰:
- 模型越不容易误解
- 输出越不容易发散
- 结果越容易符合预期
3. 为什么要学 Prompt 工程
这部分你可以从工程角度理解。
传统编程更像:
写代码 -> 计算机严格执行
Prompt 工程更像:
写自然语言指令 -> 模型根据指令生成结果
所以 Prompt 的作用不是替代代码,而是:
- 降低模型误解任务的概率
- 提高输出稳定性
- 减少后续人工修正
- 让模型更像"按要求做事",而不是"自由发挥"
4. 这节课最重要的四要素
这一集最值得记住的是 Prompt 设计四要素:
- 角色设定
- 任务描述
- 输出格式
- 约束条件
你可以把它们理解成:
角色设定:你要模型"像谁一样回答"
任务描述:你要模型"完成什么"
输出格式:你要模型"怎么输出"
约束条件:你要模型"控制到什么程度"
5. 角色设定(Role Prompting)
5.1 它的作用
角色设定的作用是:
- 限定模型的回答视角
- 限定输出风格
- 限定关注重点
例如:
差一点的写法:
写一首关于春天的诗
更好的写法:
你是一位擅长写现代诗的诗人,请用比喻手法创作一首 8 行的春天主题短诗
这里发生的变化是:
- 模型不再只是"随便写"
- 而是被限制成"现代诗人"这个视角
- 同时还被约束了手法和篇幅
5.2 当前工程里的升级理解
角色设定有用,但不能迷信。
旧一点的 Prompt 教程很喜欢写:
你是世界顶级专家......
现在更稳的工程做法是:
- 少堆头衔
- 多写清楚任务
- 多写清楚输出标准
也就是说:
- "你是资深 Java 架构师"可以写
- 但比起头衔,更重要的是后面到底让他分析什么、怎么分析、输出成什么
6. 任务描述
6.1 任务描述为什么重要
很多模型回答不理想,不是因为模型太弱,而是因为任务写得太模糊。
例如:
分析数据
这个任务就很模糊,因为没说:
- 分析什么数据
- 从什么角度分析
- 最后输出什么结果
6.2 STAR 思路怎么理解
视频里用了 STAR:
Situation:场景Task:任务Action:行动Result:结果
这个思路本质上是在帮你把任务拆清楚。
例如:
- 场景:用户提交了一个技术问题
- 任务:给出准确且易懂的解答
- 行动:分步骤说明解决方案
- 结果:最后用一句话总结重点
这样模型会更容易理解:
- 现在自己在处理什么场景
- 应该做什么事情
- 用什么方式做
- 最后交付什么
7. 输出格式
7.1 为什么输出格式很重要
很多时候模型"内容差不多对",但"格式没法直接用"。
例如:
- 你想要表格,它给你散文
- 你想要 JSON,它给你解释性文字
- 你想要代码块,它给你一整段普通文本
所以 Prompt 里明确输出格式,是工程上非常关键的一步。
7.2 常见格式约束
常见格式要求包括:
- 分点列表
- 指定段落数
- Markdown 表格
- 代码块
- JSON 结构
例如视频里的例子:
{
"summary": "不超过50字的摘要",
"keywords": ["关键词1", "关键词2", "关键词3"]
}
它的意义在于:
- 明确字段名
- 明确字段类型
- 明确输出结构
7.3 当前工程里的升级理解
这里只靠 Prompt 写一句:
请输出标准 JSON
在现代工程里通常已经不够稳了。
为什么?
因为模型仍然可能:
- 多输出一句解释
- JSON 语法不合法
- 字段漏掉
- 字段类型写错
所以现在更推荐:
- 如果模型或框架支持,优先使用结构化输出能力
- 不要只依赖自然语言要求"请输出 JSON"
8. 这里出现的新术语解释
这部分是现代工程里很重要、但初学者容易一听就懵的地方。
8.1 schema 是什么
schema 可以理解成:
数据结构说明书
它规定:
- 应该有哪些字段
- 每个字段是什么类型
- 哪些字段必须出现
例如:
{
"summary": "字符串",
"keywords": "字符串列表"
}
这就是一种结构说明。
在工程里,schema 的作用是:
- 让输出结构更稳定
- 让程序更容易解析结果
8.2 function calling / tool calling 是什么
这两个词可以先粗暴理解成:
让模型不要只会说话,还能按固定参数格式去调用工具
例如你有一个工具:
get_weather(city: str)
如果用户问"上海天气怎么样",模型不一定直接编答案,而是可能先输出一个结构化调用请求,例如:
{
"tool": "get_weather",
"arguments": {
"city": "上海"
}
}
然后程序真的去调用工具。
它的价值是:
- 参数更稳定
- 更适合接外部能力
- 比"纯 Prompt 让模型自己写 JSON 参数"更稳
8.3 Pydantic 模型是什么
Pydantic 可以理解成:
Python 里专门定义结构化数据模型的工具
例如:
from pydantic import BaseModel
class ArticleSummary(BaseModel):
summary: str
keywords: list[str]
它的作用是:
- 定义字段结构
- 校验字段类型
- 帮你把输出数据变得更规范
在现代 LLM 工程里,Pydantic 经常被用来:
- 定义结构化输出格式
- 校验模型输出
- 和 LangChain 的 structured output 功能结合使用
9. 约束条件
约束条件的作用是:
- 防止模型发散
- 防止输出太长
- 防止表达太难
- 防止顺序混乱
视频里提到的约束类型非常实用:
- 长度:控制在 200 字内
- 风格:用初中生能理解的语言
- 内容:避免使用专业术语
- 逻辑:先解释概念,再举例说明
你可以把约束条件理解成:
告诉模型"不能无限发挥,要按边界来"
10. 模板结构设计:为什么说它是黄金公式
视频里的模板:
prompt_template = """
[角色设定]
你是一个具有10年经验的{领域}专家,擅长{特定技能}
[任务说明]
需要完成以下任务:
1. {步骤1}
2. {步骤2}
3. {步骤3}
[输出要求]
请按照以下格式响应:
{示例格式}
"""
它的核心思想不是"写得好看",而是:
- 固定骨架
- 留出变量
- 方便复用
- 方便在 LangChain 里做模板注入
这和你前面学的 PromptTemplate 是一致的。
你可以把它理解成:
Prompt 版模板引擎
11. 常见问题排查思路
这一集后面的"现象 - 原因 - 解决方案"非常有价值,因为它已经接近真实工程里的 Prompt 调优方式。
例如:
- 输出偏题:角色设定不明确
- 结果笼统:没有具体步骤要求
- 格式不对:没有提供清晰格式示例
这说明 Prompt 调优不是"凭感觉乱改",而是一个排查过程:
结果不好
-> 先判断任务是不是没说清
-> 再判断格式是不是没约束清
-> 再判断边界是不是没限制清
-> 最后再考虑模型能力和上下文问题
12. 这一集里现在仍然有效的内容
下面这些内容现在依然有效:
- 角色设定有帮助
- 任务描述越具体越好
- 输出格式越明确越稳定
- 约束条件能显著改善输出质量
- Prompt 模板化对工程开发非常有价值
13. 这一集里需要升级的工程理解
这部分是你特别要求我要主动指出的"工程上可能已经偏旧"的点。
13.1 Prompt 很重要,但不是一切
旧一点的理解容易变成:
Prompt 写好了,问题就解决了
现在更完整的工程理解是:
Prompt + Context + Structured Output + Tools + Retrieval + Evaluation
也就是说,Prompt 是核心环节之一,但不是唯一核心。
13.2 只靠自然语言要求 JSON,不够稳
旧思路:
- 让模型"请严格输出 JSON"
现在更推荐:
- schema
- Pydantic 模型
- structured output
- function/tool calling
13.3 夸张角色设定不如明确任务边界
与其写:
你是世界顶级专家......
不如更明确地写:
- 你现在负责什么
- 你输出给谁看
- 你按什么格式输出
- 你不能做什么
13.4 Prompt 调优不能只靠主观感觉
现代工程更强调:
- 基准样例
- A/B 对比
- 自动评测
- 可复现验证
也就是说,Prompt 工程越来越像"实验和评估工程",而不只是"提示词写作技巧"。
14. 可迁移的同类型例子
为了帮助你触类旁通,这里补几组和本节同类型的例子。
14.1 普通问答 Prompt
差一点:
解释一下 JVM
更好:
你是一名 Java 培训讲师。
请用初学者能理解的语言解释 JVM 是什么、作用是什么、为什么 Java 能跨平台。
先讲概念,再举例,最后用一句话总结。
控制在 300 字内。
14.2 结构化输出 Prompt
差一点:
总结这篇文章
更好:
请阅读下面内容,并以 JSON 输出:
{
"summary": "不超过50字",
"keywords": ["关键词1", "关键词2", "关键词3"]
}
不要输出任何额外说明。
14.3 代码解释 Prompt
差一点:
帮我看这段代码
更好:
你是一名 Python 导师。
请解释这段代码的作用、输入、输出、执行流程和边界情况。
先讲整体逻辑,再讲关键代码,最后指出一个容易踩坑的点。
14.4 代码评审 Prompt
差一点:
帮我 review 这段代码
更好:
你现在是一名资深后端代码审查者。
请重点检查这段代码中的:
1. 逻辑错误
2. 边界条件
3. 可维护性问题
4. 可能的性能风险
请按严重程度排序输出。
15. 适合复习的速记版
| 模块 | 核心作用 |
|---|---|
| 角色设定 | 限定模型回答视角 |
| 任务描述 | 明确模型要完成什么 |
| 输出格式 | 规定模型怎样输出 |
| 约束条件 | 控制长度、风格、逻辑和边界 |
| schema | 数据结构说明书 |
| function/tool calling | 让模型按结构化参数调用工具 |
| Pydantic 模型 | 用 Python 定义和校验结构化数据 |
16. 一句话总结这一集
这一集最该掌握的主线是:
好 Prompt 的本质,不是"写得花",而是把任务目标、输出结构和边界条件讲清楚。
17. Prompt 多案例最佳实践:这一集到底在讲什么
这一集的重点不是再讲一遍 Prompt 定义,而是通过多个真实场景说明:
差 Prompt 往往不是"字少"
而是"任务范围太大、没有边界、没有输出标准"
这 4 个案例虽然领域不同,但优化逻辑其实是一致的:
- 把任务从模糊变具体
- 把回答对象说清楚
- 把输出结构固定住
- 把边界条件写出来
- 让结果更适合直接使用
18. 案例一:通用回答
差 Prompt:
告诉我关于人工智能的信息
18.1 为什么差
这个 Prompt 的问题不是"模型答不出来",而是:
- 主题太大
- 没说给谁讲
- 没说讲多深
- 没说怎么输出
"人工智能"这个话题太宽,模型可能会从这些方向任意展开:
- 历史发展
- 技术原理
- 应用场景
- 未来趋势
- 风险与伦理
结果通常会变成:
- 正确
- 但冗长
- 且不聚焦
18.2 好 Prompt 为什么更好
优化后的 Prompt:
你是一位科技专栏作家,请用通俗易懂的方式向高中生解释:
1. 什么是人工智能(用1个生活化比喻说明)
2. 列举3个当前主流应用场景
3. 字数控制在300字以内
要求使用「首先」、「其次」、「最后」的结构
这个版本做对了几件事:
- 限定了回答视角:科技专栏作家
- 限定了受众:高中生
- 限定了内容范围:定义 + 比喻 + 应用场景
- 限定了输出结构:首先 / 其次 / 最后
- 限定了篇幅:300 字以内
这类优化的本质是:
把开放作文题变成结构化简答题
18.3 同类型迁移例子
差一点:
解释一下 JVM
更好:
你是一名 Java 培训讲师,请向大学生解释:
1. JVM 是什么
2. 为什么 Java 可以跨平台
3. 用一个生活中的比喻帮助理解
控制在 250 字内,按"先定义、再原因、后举例"的顺序输出
19. 案例二:代码生成
差 Prompt:
写个Python程序
19.1 为什么差
这个需求几乎等于没有定义需求。
它没有说:
- 功能是什么
- 输入是什么
- 输出是什么
- 是否需要异常处理
- 是否需要测试
所以模型很容易生成:
- Hello World
- 随便一个简单函数
- 跟真实需求无关的示例代码
19.2 好 Prompt 的关键改进
优化后的 Prompt:
编写一个Python函数,实现以下功能:
- 输入:字符串形式的日期(格式:YYYY-MM-DD)
- 输出:该日期对应的季度(1-4)
- 要求:
- 包含参数校验(不符合格式时抛出ValueError)
- 使用datetime模块
- 编写对应的单元测试用例
示例:
输入 "2024-03-15" → 返回 1
这个版本强在:
- 明确输入
- 明确输出
- 指定异常处理
- 指定实现方式
- 要求测试
- 给出样例输入输出
这已经非常接近真实开发任务单。
19.3 当前工程里的升级理解
如果按更现代、更工程化的标准,还可以再补:
- Python 版本
- 函数签名
- 测试框架
- 是否要求类型注解
例如:
请编写一个 Python 3.12 函数:
def get_quarter(date_str: str) -> int
要求:
1. 输入格式必须是 YYYY-MM-DD
2. 非法输入抛出 ValueError
3. 使用 datetime 标准库
4. 添加类型注解
5. 使用 pytest 编写正常和异常测试用例
19.4 同类型迁移例子
差一点:
帮我写SQL
更好:
请编写一条 MySQL 查询语句:
表名:orders
字段:id, user_id, amount, created_at
需求:查询 2026 年每个月的订单总金额
要求:
1. 返回月份和总金额两列
2. 按月份升序排序
3. 给出 SQL 后,再用一句话解释 group by 的作用
20. 案例三:技术问答
差 Prompt:
如何优化网站性能?
20.1 为什么差
"网站性能"这个范围太大,可能包括:
- 首屏加载
- 接口响应速度
- 数据库查询
- 静态资源加载
- 图片优化
- 前端渲染
- 缓存策略
所以模型很容易输出一堆:
- 正确
- 但泛泛而谈
- 缺乏可执行性的建议
20.2 好 Prompt 的关键改进
优化后的 Prompt:
针对使用SpringBoot+Vue3的技术栈,请给出5项可量化的性能优化方案:
要求:
1. 每项方案包含:
- 实施步骤
- 预期性能提升指标(如LCP减少20%)
- 复杂度评估(低/中/高)
2. 优先前端和后端优化方案
3. 引用Web Vitals评估标准
限制条件:
- 不涉及服务器扩容等硬件方案
- 排除已广泛采用的方案(如代码压缩)
这个版本做对了:
- 限定技术栈
- 要求量化指标
- 增加复杂度评估
- 指定评估标准
- 排除偷懒方案
这让输出更接近"技术顾问建议",而不是普通科普回答。
20.3 新术语解释:Web Vitals 是什么
Web Vitals 可以理解成:
Google 提出的一组网页体验指标,用来衡量页面对用户来说快不快、稳不稳
常见指标包括:
LCP:页面主要内容多久显示出来CLS:页面布局会不会乱跳INP:用户操作后的响应速度
所以:
LCP 减少 20%
就是在说首屏主要内容显示得更快了。
20.4 当前工程里的升级理解
这一版 Prompt 已经不错,但从工程角度还可以更完整:
Web Vitals更偏前端体验指标- 对于
SpringBoot + Vue3的整站性能,还应该补充:- 接口响应时间
- 慢查询
- 吞吐量
- 缓存命中率
也就是说,这个案例不是错,而是:
前端指标讲得比较完整
后端指标还可以再补
20.5 同类型迁移例子
差一点:
怎么优化数据库?
更好:
针对 MySQL 8 + Spring Data JPA 场景,请给出 5 项数据库性能优化建议。
每项建议包含:
1. 适用场景
2. 实施步骤
3. 预期效果
4. 风险或副作用
限制:
- 不涉及硬件扩容
- 不重复索引优化这种基础建议
21. 案例四:数据分析
差 Prompt:
分析这份销售数据
21.1 为什么差
这里的问题是:
- 没说数据有哪些字段
- 没说时间范围
- 没说分析目标
- 没说输出格式
- 没说是否需要可视化
所以模型容易给出:
- 很泛的统计描述
- 但缺少真正洞察
21.2 好 Prompt 的关键改进
优化后的 Prompt:
你是一位资深数据分析师,请完成以下任务:
数据集特征:
- 时间范围:2027年1-12月
- 字段:日期/产品类别/销售额/利润率
要求:
1. 找出销售额top3的月份,分析增长原因
2. 识别利润率低于5%的产品类别
3. 生成包含趋势图的Markdown报告
输出格式:
## 分析报告
### 关键发现
- 要点1(数据支撑)
- 要点2(对比分析)
### 可视化
趋势图描述,生成base64编码的折线图
这个版本的改进点包括:
- 明确分析者角色
- 明确数据集特征
- 明确分析目标
- 明确输出格式
- 明确需要可视化
21.3 新术语解释:base64 是什么
base64 可以理解成:
一种把二进制数据(比如图片)编码成文本字符串的方式
它常见于:
- 传输图片内容
- 在文本协议里嵌入二进制文件
- 某些特殊接口场景
21.4 当前工程里的升级理解
这里有一个值得你注意的工程点:
在真实数据分析场景里,让模型"输出 base64 图像"通常不是最自然的做法。
因为它会带来:
- 文本特别长
- 可读性差
- 不利于直接阅读和维护
当前更常见的工程做法通常是:
- 输出图表描述
- 输出图表配置(如 ECharts 配置)
- 输出可视化所需数据
- 或者在代码里直接用 matplotlib / plotly 生成图
所以这里要记住:
这个案例的"需要可视化"思路是对的,
但"base64 折线图"这种表达在工程上不算最优实践。
21.5 同类型迁移例子
差一点:
分析一下用户数据
更好:
你是一位增长分析师,请分析以下用户注册数据:
字段:注册日期 / 渠道 / 注册人数 / 次日留存率
要求:
1. 找出注册人数最高的 3 个渠道
2. 找出次日留存率低于 20% 的渠道
3. 用 Markdown 表格输出"渠道 / 注册人数 / 留存率 / 结论"
4. 最后给出 2 条增长优化建议
22. 这 4 个案例共同体现的优化公式
你可以把这一整集压缩成一个通用 Prompt 优化公式:
模糊任务
-> 限定角色
-> 限定对象
-> 限定任务边界
-> 限定输出格式
-> 限定约束条件
-> 必要时加入示例
这个公式几乎可以迁移到:
- 通用问答
- 代码生成
- 技术分析
- 数据分析
- 文本总结
- 代码评审
23. 这一集最值得带走的工程理解
23.1 Prompt 优化不是把字写长
真正的优化不是:
- 把 Prompt 写得更花哨
而是:
- 把任务边界讲清楚
- 把输出结构讲清楚
- 把评价标准讲清楚
23.2 Prompt 设计要服务于"可直接使用"
工程里最重要的不是"模型答得好像挺不错",而是:
- 输出能不能直接用
- 结果能不能复现
- 格式能不能稳定
- 后续能不能自动化处理
23.3 现代工程不能只靠自然语言技巧
这一集的案例仍然有价值,但在现代工程里还要补上:
- 结构化输出
- schema
- tool/function calling
- 自动评测
- 可复现验证
也就是说:
Prompt 工程现在越来越像"实验和约束设计",
而不只是"写提示词技巧"。
24. 一句话总结这一集
这一集最该掌握的主线是:
Prompt 优化不是把话写长,而是把任务变成一个边界清晰、结构明确、可验证的指令。
25. PromptTemplate:这一集的核心目标
这一集开始从"Prompt 怎么写"进入"Prompt 怎么模板化管理"。
主线可以先记成一句话:
PromptTemplate = 把固定的提示词骨架和动态变量分开管理,
再在运行时拼成最终 Prompt 的工具
这非常重要,因为前面你学的是:
- Prompt 要写清楚
而这一集开始解决的是:
- 不要把 Prompt 全部硬编码在代码里
- 要支持动态变量填充
- 要支持模板复用
26. PromptTemplate 到底是什么
PromptTemplate 是 LangChain 里构建字符串型 Prompt 模板的组件。
它主要解决两个问题:
- 动态内容组装
- 避免 Prompt 硬编码
你可以把它理解成:
Prompt 版模板引擎
它和普通字符串拼接相比,更适合:
- 管理复杂 Prompt
- 明确模板需要哪些变量
- 让 Prompt 更容易复用和维护
27. 这一集最重要的四个成员
你这集里真正最值得记住的是这 4 个:
templateinput_variablespartial_variablesformat()
你可以直接这样理解:
template = 模板正文
input_variables = 运行时必须补进去的变量
partial_variables = 提前固定好的变量
format() = 把变量填进模板,生成最终 Prompt
28. 第一段代码:手动声明变量
示例代码:
from langchain.prompts import PromptTemplate
template = """
你是一位专业的{domain}顾问,请用{language}回答:
问题: {question}
回答:
"""
prompt = PromptTemplate(
template=template,
input_variables=["domain", "language", "question"]
)
result = prompt.format(
domain="法律",
language="中文",
question="法律合同的法律效果"
)
28.1 template 在做什么
template = """
你是一位专业的{domain}顾问,请用{language}回答:
问题: {question}
回答:
"""
这里定义的是 Prompt 骨架。
其中:
{domain}{language}{question}
都是占位符。
占位符的意思就是:
这里先留空,运行时再填值
28.2 input_variables 在做什么
input_variables=["domain", "language", "question"]
它不是在"填值",而是在"声明这个模板运行时需要哪些输入变量"。
所以这里要分清:
template负责定义模板长什么样input_variables负责声明这个模板依赖哪些参数
28.3 为什么看起来像重复
这是你这节最关键的疑问之一。
确实,表面上看:
- 模板里已经写了
{language}、{question} - 又手工写一遍
input_variables=["language", "question"]
看起来像重复信息。
这个感觉是对的。
也正因为如此,在当前 LangChain 里更常见的方式是:
PromptTemplate.from_template(...)
让系统自动推断变量,而不必手工写两遍。
也就是说:
- 手工写
input_variables是更显式、更教学化的写法 - 自动推断是更简洁、当前更常见的写法
28.4 format() 在做什么
result = prompt.format(
domain="法律",
language="中文",
question="法律合同的法律效果"
)
这一步才是真正把值填进去。
最终生成的字符串大致是:
你是一位专业的法律顾问,请用中文回答:
问题: 法律合同的法律效果
回答:
这才是最后真正会送给模型的 Prompt 文本。
29. 第二段代码:自动推断变量
示例代码:
template = "请将以下文本翻译成{target_language}:{text}"
prompt = PromptTemplate.from_template(template)
print(prompt.input_variables) # ['target_language', 'text']
29.1 这段在解决什么问题
它解决的是:
既然模板里已经写了变量,
那就别再手工重复写一份 input_variables 了
所以:
PromptTemplate.from_template(template)
会自动扫描:
{target_language}{text}
并推断出:
["target_language", "text"]
29.2 什么时候适合用
适合:
- 模板简单
- 占位符明确
- 不想重复维护变量名列表
它和手动写 input_variables 做的本质上是同一件事,只是少了一步手工声明。
30. 第三段代码:partial_variables 预填变量
示例代码:
template = """分析用户情绪(默认分析类型: {analysis_type}):
用户输入: {user_input}
分析结果: """
prompt_template = PromptTemplate(
input_variables=["user_input"],
template=template,
template_format="f-string",
partial_variables={"analysis_type": "情感极性分析"}
)
30.1 partial_variables 是什么
它可以理解成:
有些变量我不想每次都传,
希望先固定住
这里:
analysis_type被提前固定成"情感极性分析"- 所以运行时只需要再传
user_input
30.2 为什么 input_variables 只有 user_input
因为模板里虽然有:
{analysis_type}{user_input}
但:
partial_variables={"analysis_type": "情感极性分析"}
已经把 analysis_type 占掉了。
所以外部还需要传入的变量只剩:
user_input
30.3 最终生成效果
执行:
prompt_template.format(user_input="这个产品太难用了")
得到的大致结果是:
分析用户情绪(默认分析类型: 情感极性分析):
用户输入: 这个产品太难用了
分析结果:
31. template_format="f-string" 到底是什么意思
这一行很容易让初学者误会:
template_format="f-string"
它不是说这里真的在执行 Python 原生 f-string 代码。
更准确地说,它表示:
模板使用的是 Python 花括号占位风格
也就是 {变量名} 这种形式
在当前 LangChain 里,默认就是:
template_format="f-string"
这是当前最推荐、最常见的模板格式。
32. 你打印的内部属性分别是什么意思
这几行代码的作用是帮助你观察 PromptTemplate 对象内部状态:
print(prompt_template.template)
print(prompt_template.input_variables)
print(prompt_template.template_format)
print(prompt_template.input_types)
print(prompt_template.partial_variables)
它们分别表示:
template:模板原文input_variables:当前还需要传入的变量template_format:模板语法格式input_types:输入变量的类型信息partial_variables:提前固定的变量字典
这部分的教学意义是:
PromptTemplate 不只是一个字符串,
它是一个带元信息的模板对象
33. 当前更推荐的工程写法
33.1 导入路径
视频里写的是:
from langchain.prompts import PromptTemplate
当前更推荐:
from langchain_core.prompts import PromptTemplate
原因是:
- 现在 Prompt 核心抽象更明确地归到
langchain_core - 新版生态更强调 core 层
33.2 术语纠正
代码注释里提到:
后续还有进阶的ChainPromptTemplate
这个说法在当前 LangChain 语境里不算主流。
你后面更应该重点认识的是:
ChatPromptTemplateFewShotPromptTemplatePipelinePromptTemplate
尤其是对接聊天模型时,当前更重要的一般是:
from langchain_core.prompts import ChatPromptTemplate
34. 这四种写法其实都在做同一件事
你已经问到了这一节真正最重要的抽象点。
本质上,这几种方式都在做:
把"模板 + 变量"变成最终 Prompt 字符串
只是变量管理方式不同。
35. 用同一件事演示四种写法
目标都一样:
生成最终 Prompt:请用中文解释线程池
35.1 方法一:最朴素的字符串拼接
language = "中文"
topic = "线程池"
prompt = f"请用{language}解释{topic}"
print(prompt)
作用:
- 最原始方式
- 直观
- 但不适合复杂 Prompt 管理
35.2 方法二:手动写 PromptTemplate
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(
template="请用{language}解释{topic}",
input_variables=["language", "topic"]
)
result = prompt.format(language="中文", topic="线程池")
print(result)
作用:
- 手动定义模板
- 手动声明变量
- 手动填充值
35.3 方法三:from_template() 自动推断变量
from langchain.prompts import PromptTemplate
prompt = PromptTemplate.from_template("请用{language}解释{topic}")
result = prompt.format(language="中文", topic="线程池")
print(result)
print(prompt.input_variables)
作用:
- 不再重复写
input_variables - 系统自动识别占位符
35.4 方法四:partial_variables 固定一部分变量
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(
template="请用{language}解释{topic}",
input_variables=["topic"],
partial_variables={"language": "中文"}
)
result = prompt.format(topic="线程池")
print(result)
print(prompt.input_variables)
print(prompt.partial_variables)
作用:
- 最终仍然生成同一个 Prompt
- 但把
language提前固定了 - 所以运行时只需要再传
topic
36. 四种写法并排理解
| 方法 | 做的事 | 适合理解成 |
|---|---|---|
| 字符串拼接 | 直接生成 Prompt | 原始写法 |
PromptTemplate(...) |
手动声明模板和变量 | 基础教学写法 |
from_template() |
自动推断变量 | 当前更常见写法 |
partial_variables |
固定部分变量 | 减少重复传参 |
所以你现在可以直接记住:
它们本质上都在做"生成最终 Prompt",
区别只是变量怎么管理。
37. 这一集最容易混淆的点
37.1 template 和 input_variables 不是一回事
template是模板正文input_variables是参数清单
37.2 format() 不会调用模型
- 它只会生成最终字符串
- 不会真正向 LLM 发请求
37.3 partial_variables 不是"可选参数"
- 它更像"提前固定的模板变量"
37.4 print(prompt) 和 print(result) 看的不是同一个东西
print(prompt)看的是模板对象print(result)看的是填充后的最终文本
38. 一句话总结这一集
这一集最该掌握的主线是:
PromptTemplate 的本质,是把固定 Prompt 骨架和动态变量分开管理,
再在运行时清晰地拼成最终提示词。
39. PromptTemplate 结合 LLM:这一集在讲什么
这一集开始把前面学过的三个组件串起来:
PromptTemplateLLMOutputParser
它的主线可以先记成:
输入变量
-> PromptTemplate 生成提示词
-> LLM 调用模型
-> OutputParser 整理输出
-> 得到最终结果
你可以把这一集理解成:
- 前面学的是"组件分别是什么"
- 这一集开始学"组件怎么串起来"
40. 这段代码的完整调用链
示例代码的核心结构是:
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
llm = ChatOpenAI(...)
prompt = PromptTemplate(
input_variables=["product"],
template="为{product}写三个吸引人的广告标语,只需要返回纯文本"
)
message = prompt.invoke({"product": "小滴课堂"})
response = llm.invoke(message)
answer = StrOutputParser().invoke(response)
你现在最该建立的不是"这几行能跑",而是"每一步到底在返回什么对象"。
真实流转是:
{"product": "小滴课堂"}
-> prompt.invoke(...)
-> PromptValue
-> llm.invoke(...)
-> AIMessage
-> StrOutputParser.invoke(...)
-> str
也就是说,这一集最核心的是:
PromptTemplate 不直接返回字符串给你看,
llm 也不直接返回普通字符串,
中间其实有对象层级。
41. 第一步:创建模型客户端
代码:
llm = ChatOpenAI(
model_name="DeepSeek-V3.2",
base_url="https://ark.cn-beijing.volces.com/api/coding/v3",
api_key="...",
temperature=0.7,
)
这一步是在创建聊天模型客户端。
参数作用:
model_name:要调用哪个模型base_url:请求发往哪个兼容 OpenAI 的服务地址api_key:认证密钥temperature:控制输出发散程度
41.1 当前更推荐的工程写法
这里有两个需要你记住的工程点:
api_key 不要写死在代码里
更推荐:
import os
llm = ChatOpenAI(
model="DeepSeek-V3.2",
base_url="https://ark.cn-beijing.volces.com/api/coding/v3",
api_key=os.getenv("ARK_API_KEY"),
temperature=0.7,
)
model 比 model_name 更符合当前风格
当前更主流的写法是:
model="DeepSeek-V3.2"
42. 第二步:创建 PromptTemplate
代码:
prompt = PromptTemplate(
input_variables=["product"],
template="为{product}写三个吸引人的广告标语,只需要返回纯文本"
)
这一段的作用是定义 Prompt 骨架。
模板里有一个占位符:
{product}
所以运行时你只需要传入:
product
例如:
{"product": "小滴课堂"}
填进去之后,最终 Prompt 会变成:
为小滴课堂写三个吸引人的广告标语,只需要返回纯文本
43. 第三步:prompt.invoke() 到底返回什么
代码:
message = prompt.invoke({"product": "小滴课堂"})
这一行非常关键。
它不是简单返回普通字符串,而是返回一个:
PromptValue
更具体地说,在这个例子里实际是:
StringPromptValue
你现在可以把它理解成:
已经格式化好的 Prompt,
但还包装在 LangChain 的统一输入对象里
所以这里要区分:
prompt.format(...)返回普通字符串prompt.invoke(...)返回PromptValue
这也是你后面看 Runnable 链路时必须知道的区别。
44. 第四步:llm.invoke(message) 返回什么
代码:
response = llm.invoke(message)
这一步才是真正调用模型。
这里有两个重点:
44.1 llm.invoke() 可以接收多种输入
它可以接收:
- 普通字符串
PromptValue- 消息列表
所以你把 prompt.invoke(...) 的结果直接传给 llm.invoke(...) 是可以的。
44.2 llm.invoke() 返回的不是普通字符串
它返回的是:
AIMessage
你可以把 AIMessage 理解成:
模型返回的一整条消息对象
里面不只是文本,还可能包含:
content- metadata
- token 信息
- 其他附加字段
所以:
print(response.content)
是在取这条消息里的文本内容。
45. 第五步:StrOutputParser 到底在做什么
代码:
output_parser = StrOutputParser()
answer = output_parser.invoke(response)
print(answer)
StrOutputParser 的作用很简单:
把模型输出解析成纯字符串
在当前这个例子里,它几乎等价于:
response.content
因为它本身不会做复杂转换,基本上就是把文本原样拿出来。
45.1 那为什么还要学它
因为它的意义不在于"这个例子里非它不可",而在于:
让你建立"输出解析层"的意识
也就是说,模型输出之后,后面还可以有一层专门负责:
- 取纯文本
- 解析 JSON
- 做结构化输出
- 校验格式
所以这一步是后面学习更复杂输出解析器的基础。
46. 这三个组件是不是缺一不可
这也是你这一集里问得非常关键的问题。
结论是:
不一定。
它们是一个常见最小链路,但不是永远缺一不可。
46.1 三者各自负责什么
prompt:负责模板化输入llm:负责调用模型StrOutputParser():负责拿到纯文本结果
46.2 什么时候可以不要 prompt
如果你的输入根本不需要模板化,直接一句固定字符串就行:
response = llm.invoke("你是谁?")
print(response.content)
这里就没有 prompt。
46.3 什么时候可以不要 StrOutputParser()
如果你愿意自己取:
response.content
那就可以不写:
StrOutputParser()
46.4 所以怎么记
最核心的可以直接记成:
llm 最核心
prompt 视是否需要模板而定
StrOutputParser 视你是否要直接拿纯文本而定
47. 当前更主流的 Runnable 写法
你现在代码里是拆成三步写:
message = prompt.invoke(...)
response = llm.invoke(message)
answer = output_parser.invoke(response)
这很适合教学,因为每一步都能单独看见。
但当前 LangChain 更主流、更工程化的写法是:
chain = prompt | llm | StrOutputParser()
answer = chain.invoke({"product": "小滴课堂"})
print(answer)
47.1 为什么这种写法更受推荐
因为它:
- 更短
- 更清晰
- 更符合 Runnable 体系
- 更方便继续扩展
所以你可以这样理解:
- 教学拆开写:更容易理解每一步
- 工程串起来写:更符合当前 LangChain 习惯
48. 第二段测试代码该怎么看
后半段代码里有一部分更像"测试输出对象长什么样",而不是完整新案例。
48.1 这段 template 没有真正参与执行
代码里有:
template = """
你是一位专业的{domain}顾问,请用{language}回答:
问题: {question}
回答:
"""
但后面并没有拿这个模板去构造新的 PromptTemplate。
所以这段在当前文件里是"定义了,但没用上"。
48.2 print(response) 和 print(response.content) 的区别
response.content:只看文本内容response:看整个AIMessage对象
这有助于你理解:
模型返回的不是普通字符串,
而是消息对象
48.3 llm.invoke("你是谁?") 在说明什么
这一步是在告诉你:
llm 不一定非要接 PromptTemplate 的结果,
也可以直接接普通字符串
所以:
llm.invoke(prompt生成的 PromptValue)可以llm.invoke(普通字符串)也可以
49. 用同一件事看三种常见写法
目标都一样:
让模型解释线程池
49.1 只用 llm
response = llm.invoke("请用中文解释线程池")
print(response.content)
特点:
- 最简单
- 没有模板
- 结果是
AIMessage
49.2 prompt + llm
prompt = PromptTemplate.from_template("请用中文解释{topic}")
message = prompt.invoke({"topic": "线程池"})
response = llm.invoke(message)
print(response.content)
特点:
- 有模板变量
- 结果仍然是
AIMessage
49.3 prompt + llm + StrOutputParser
prompt = PromptTemplate.from_template("请用中文解释{topic}")
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"topic": "线程池"}))
特点:
- 有模板变量
- 最终直接返回字符串
- 最适合后面继续扩展
50. 这一集最该记住的主线
你可以直接背这条链路:
PromptTemplate 负责生成提示词
LLM 负责生成回答
OutputParser 负责把回答整理成你真正想用的结果
51. 适合复习的速记版
| 组件 | 作用 | 返回结果 |
|---|---|---|
PromptTemplate |
生成提示词 | PromptValue |
ChatOpenAI |
调用模型 | AIMessage |
StrOutputParser |
取纯文本输出 | str |
| `prompt | llm | parser` |
52. 一句话总结这一集
这一集最该掌握的主线是:
PromptTemplate 负责生成 Prompt,
LLM 负责生成消息,
Parser 负责把消息变成最终可用结果。
53. 补充理解:prompt | llm | StrOutputParser() 不是固定死规则
这个点很重要,复习的时候不要把它记成:
LangChain 调模型必须同时写 prompt、llm、parser
更准确的理解是:
这是一个非常常见、非常顺手的标准链路,
但不是每次都必须三个一起出现。
53.1 真正最核心的是谁
最核心的是:
llm
因为真正调用模型的是它。
没有 llm,就没有模型推理这一步。
53.2 什么情况下可以没有 prompt
如果你的输入就是一段固定文本,不需要变量模板,那就可以直接:
response = llm.invoke("你是谁?")
print(response.content)
这时候:
- 不需要
PromptTemplate - 因为没有占位符要填
- 也没有模板复用需求
所以 prompt 不是永远必须的,它主要负责:
- 模板化
- 变量注入
- 提高复用性
53.3 什么情况下可以没有 StrOutputParser()
如果你只想拿文本内容,直接这样也可以:
response = llm.invoke("请介绍一下线程池")
print(response.content)
因为:
llm.invoke(...)返回的是AIMessage- 其中的
response.content就是文本答案
所以:
StrOutputParser()不是语法强制- 它只是把"取文本"这件事单独做成一个解析步骤
53.4 那为什么工程里还是经常写上它
因为它能让链路结构更统一:
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"product": "小滴课堂"})
这样做的好处是:
- 最终结果直接就是
str - 后面如果换成
JsonOutputParser、结构化解析器,会更自然 - 更符合当前 LangChain 的 Runnable 风格
所以你可以把它理解成:
不是"必须有",
而是"作为标准链路很常见,也更利于后续扩展"。
54. 这一集的工程提醒
54.1 教学代码能跑,不代表就是当前最推荐写法
像这一集代码里:
api_key = "..."
教学里这样写是为了先跑通,但放到真实工程里已经不推荐。
更好的做法是:
import os
llm = ChatOpenAI(
model="DeepSeek-V3.2",
base_url="https://ark.cn-beijing.volces.com/api/coding/v3",
api_key=os.getenv("ARK_API_KEY"),
temperature=0.7,
)
原因是:
- 密钥不应该硬编码进代码
- 更方便切换环境
- 更安全,也更适合提交到 Git 仓库
54.2 教学里拆开写,工程里更常串起来写
教学写法:
message = prompt.invoke({"product": "小滴课堂"})
response = llm.invoke(message)
answer = StrOutputParser().invoke(response)
这种写法的优点是:
- 每一步都看得见
- 适合入门理解对象流转
但当前更常见的工程写法是:
chain = prompt | llm | StrOutputParser()
answer = chain.invoke({"product": "小滴课堂"})
优点是:
- 更短
- 更清晰
- 更符合新版本 LangChain 的 Runnable 组合方式
55. 这一集最后用一句最实用的话记住
llm 是核心,
prompt 负责把输入模板化,
parser 负责把输出整理成更好用的结果;
三者常一起出现,但不是死规定。
56. ChatModel 聊天模型:这一集的核心主线
这一集最重要的不是死记定义,而是先建立一条主线:
ChatModel 适合处理"消息式对话",
多轮对话之所以能连贯,是因为历史消息会一起传给模型,
而历史越长,Token 成本就越高。
你可以把这一集拆成两个问题:
- ChatModel 和普通文本模型有什么不同
- 为什么聊天越聊越贵,越聊越容易碰到上下文上限
57. 什么是 ChatModel
ChatModel 可以先理解成:
不是单纯"输入一段文本,输出一段文本",
而是"输入一组带角色的消息,再生成下一条回复"的模型。
它是专门为对话场景设计的,所以天然更适合:
- 多轮问答
- AI 助手
- 客服机器人
- 角色扮演
- 带规则约束的聊天系统
和普通文本生成相比,它更强调:
- 当前是谁在说话
- 前面聊过什么
- 当前这句是在接哪一句
- 模型应该以什么身份来回答
58. ChatModel 最容易误解的一点:它不是自己永久记住了聊天记录
这个点一定要记住。
很多人会说:
ChatModel 会记住上下文
这句话不算错,但更准确的说法应该是:
模型不是自己长期保存了聊天记录,
而是你每次请求时,把历史消息一起发给它,
所以它看起来像"记住了"。
也就是说,大多数模型 API 本质上是:
单次请求无状态
例如:
messages = [
{"role": "system", "content": "你是一个电影推荐助手"},
{"role": "user", "content": "我喜欢科幻片,推荐三部经典"},
{"role": "assistant", "content": "1.《银翼杀手2049》... 2.《星际穿越》... 3.《黑客帝国》"},
{"role": "user", "content": "第二部的主演是谁?"}
]
模型之所以知道"第二部"指的是《星际穿越》,不是因为它脑子里永久存了会话,而是因为:
- 这一轮请求把前面的历史消息也传进去了
- 模型在当前请求里同时看到了这些消息
- 所以它能理解"第二部"是在指前面的第二个推荐项
59. ChatModel 和传统 Text Model 的核心区别
这一集里的对比方向是对的,但你要把它理解得更工程化一些。
59.1 输入格式不同
传统 Text Model 更像:
给我一整段文本,我来续写或生成
ChatModel 更像:
给我一组结构化消息:
system 说了什么
user 说了什么
assistant 之前回了什么
我再继续生成下一条回复
所以 ChatModel 的输入不是一坨纯文本,而是:
消息序列
59.2 对话控制更清晰
因为输入是带角色的消息,所以你可以更自然地控制:
- system:总规则
- user:用户问题
- assistant:模型历史回答
这比你手动拼接一长串历史文本更稳定。
59.3 现代开发里,ChatModel 已经是主流
这里要注意一个"教程时代性"。
很多旧教程会拿:
text-davinci-003gpt-3.5-turbogpt-4
这些名字来做对比。
其中"Text Model vs ChatModel"这个知识点本身没有问题,但在今天的实际开发里:
Chat / message-based 模型接口早就已经是主流路线
所以你可以把 Text Model 视为:
- 帮你理解历史演进
- 帮你理解为什么消息列表比纯文本更适合对话
但不要把它当成现在最主流的新项目写法。
60. 三种角色怎么理解
这部分是聊天模型最基础的骨架。
60.1 system
system 的作用是:
定规矩
例如:
- 你是一个电影推荐助手
- 用中文回答
- 不要给医疗诊断
- 回答限制在 100 字内
它不是用户问题本身,而是整个对话的总规则。
60.2 user
user 就是用户当前输入的内容,例如:
- 我喜欢科幻片,推荐三部经典
- 第二部的主演是谁?
60.3 assistant
assistant 是模型之前已经说过的话。
它存在的意义是帮助下一轮继续保持上下文连贯。
例如上一轮 assistant 推荐了三部电影,这一轮用户问"第二部",模型就是根据那条 assistant 历史消息来理解的。
60.4 在 LangChain 里对应什么
OpenAI 风格常写成:
{"role": "system", "content": "..."}
{"role": "user", "content": "..."}
{"role": "assistant", "content": "..."}
LangChain 里则更常见:
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
它们的对应关系是:
system->SystemMessageuser->HumanMessageassistant->AIMessage
所以后面你看到 LangChain 消息对象,不要觉得是另一套东西,本质上是同一个概念。
61. ChatModel 的几个核心能力怎么理解
这一集列的几个特点都对,但你不要把它们当成孤立知识点,要把它们串起来。
61.1 上下文感知
例如:
- 用户先问:什么是量子计算?
- 后面又问:它有什么应用?
这里"它"是代词。
ChatModel 的优势就是能结合前文,把"它"解析成"量子计算"。
61.2 角色一致性
如果 system 中规定:
你是一个医疗信息助手,不做诊断,只做科普
那模型后续更可能维持这个身份边界。
注意这里是"更可能",不是绝对保证。
严肃工程里,不能只靠一句 Prompt 就假设模型永远不会越界。
61.3 意图识别
比如用户说:
我的订单还没到
表面上它是一句陈述,实际上用户可能是在:
- 投诉
- 催单
- 请求人工介入
ChatModel 比较擅长在上下文里推断这种真实意图。
61.4 情绪适配
如果用户明显不满,模型往往会用更缓和、更安抚的语气回复。
这就是为什么聊天模型在客服、助手场景里更自然。
62. Token 到底是什么
你可以先把 Token 理解成:
模型处理文本时的最小计算单位 / 计费单位
它不是"字数",也不是"单词数"。
模型会用自己的分词器,把文本切成若干 token。
所以:
- 一句中文
- 一句英文
- 一段代码
- 一段 JSON
它们的 token 数量都可能完全不同。
63. 为什么多轮对话里的 Token 会不断累积
这是这集最关键的成本概念。
在多轮聊天里,一次请求通常不只包含"这一轮新问题",而是会一起包含:
- system prompt
- 历史 user 消息
- 历史 assistant 消息
- 当前 user 消息
- 以及模型本轮要生成的输出
所以:
对话越长,
每一轮请求携带的上下文越多,
Token 消耗就越大。
64. 用这集里的 50 / 100 示例把 Token 算明白
假设每轮:
- 用户输入 50 token
- 模型输出 100 token
64.1 第 1 轮
第 1 轮大致是:
- 输入 50
- 输出 100
这一轮总量可以近似理解成:
50 + 100 = 150
64.2 第 2 轮
第 2 轮发请求时,通常会带上:
- 第 1 轮用户输入 50
- 第 1 轮模型输出 100
- 第 2 轮新用户输入 50
所以本轮输入大约是:
150 + 50 = 200
如果本轮再输出 100,那么第 2 轮这次调用总量大约是:
200 + 100 = 300
64.3 第 3 轮
第 3 轮还会继续带上更长的历史:
- 前两轮累计上下文 300
- 第 3 轮新用户输入 50
本轮输入大约:
350
如果输出还是 100,那么总量约为:
450
64.4 最容易混淆的点
教程里常写:
- 第 1 轮 150
- 第 2 轮 300
- 第 3 轮 450
这个写法没错,但容易让人误会成"总账单只有这么多"。
更准确地说:
150 / 300 / 450是每一轮单次调用的大致总量- 如果要算累计消耗,要把每轮都加起来
例如前三轮累计大约是:
150 + 300 + 450 = 900
所以聊天越长,真正贵的地方在于:
后面的每一轮都在重复带历史消息
65. 什么是上下文窗口
Context Window 可以直接理解成:
模型单次请求最多能处理多少 token
它像一个容量上限。
一次调用中,以下内容都要占用这个窗口:
- system 提示
- 历史对话
- 当前问题
- 以及模型将要输出的内容空间
如果总量太大,可能会出现:
- 历史被截断
- 旧消息看不到
- 模型前后不一致
- 请求失败
所以"上下文窗口更大"通常意味着:
- 能处理更长文档
- 能保留更长会话
- 更适合复杂多轮任务
但窗口大不代表就应该无脑塞满,因为上下文越长,往往也意味着:
- 成本更高
- 响应更慢
- 注意力更分散
66. 这一集里要特别注意的"旧教程时代性"
你特别在意这部分,所以这里单独列出来。
66.1 4k / 8k / 32k 这些数字更多是历史阶段示例
旧教程经常会讲:
- GPT-3.5 是 4k
- GPT-4 有 8k、32k
这些说法放在历史阶段里没有问题,它们很适合帮助你理解"上下文窗口"这个概念。
但你现在不能把这组数字当成今天模型的普遍结论。
更准确的理解应该是:
4k / 8k / 32k 是较早一代主流模型常见的窗口量级,
现在很多新模型的上下文能力已经远远超过这个范围。
所以这组数字:
- 适合拿来教学
- 不适合当成今天所有模型的固定标准
66.2 text-davinci-003 更像历史对照组
它适合帮助你理解:
- 过去偏 completion
- 现在偏 chat/messages
但你在实际新项目学习中,不要把它当成重点路线。
66.3 不能只知道"上下文会累积",还要知道工程上怎么处理
旧教程有时只讲:
聊天越多,Token 越多
但现代工程更强调下一步:
- 截断历史
- 摘要历史
- 只保留关键轮次
- 用检索补充上下文,而不是全量硬塞
所以今天你学这节课,不能只停留在"知道有 Token 成本",还要逐渐建立工程意识:
上下文是要管理的,不是无限追加的。
67. 同类型迁移例子
为了帮助你触类旁通,这里补两个和本节同类型的例子。
67.1 LangChain 里的聊天消息写法
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="DeepSeek-V3.2")
messages = [
SystemMessage(content="你是一个Java学习助手"),
HumanMessage(content="什么是线程池?"),
]
response = llm.invoke(messages)
print(response.content)
这个例子里你要看懂的是:
SystemMessage对应 systemHumanMessage对应 userllm.invoke(messages)可以直接接收消息列表
67.2 为什么第二轮一定比第一轮更贵
例如:
第1轮:
user: 什么是线程池?
assistant: ...
第2轮:
user: 它适合什么场景?
第 2 轮不是只传:
它适合什么场景?
而通常是传:
system + 第1轮user + 第1轮assistant + 第2轮user
所以第 2 轮 token 通常一定会比第 1 轮更大。
68. 这一集最该背下来的结论
ChatModel 的本质是"基于消息列表的对话模型";
多轮对话之所以能连贯,是因为历史消息会一起传入;
而历史越长,Token 消耗越大,上下文窗口压力也越大。
69. ChatPromptTemplate:这一集到底在讲什么
这一集是前面 PromptTemplate 的升级版。
前面你学的 PromptTemplate,核心是:
拼一段字符串 Prompt
而这一集的 ChatPromptTemplate,核心是:
拼一组带角色的消息 Prompt
所以你可以直接这样记:
PromptTemplate:适合普通字符串提示词ChatPromptTemplate:适合聊天模型消息提示词
这就是它为什么更适合:
- 聊天模型
- 多轮对话
- system / human / ai 角色控制
70. 先把你代码前面的中文"翻译成人话"
在 31_6.py:7 到 31_6.py:10 这里写的是:
- 支持消息角色
- 天然适配聊天模型
- 可维护对话上下文
这三句话不要机械背,应该理解成:
70.1 支持消息角色是什么意思
它不再是只拼一段纯文本,而是会区分:
system:系统规则human:用户输入ai:模型历史回复
也就是说,提示词不再是一坨字符串,而是:
一组有身份的消息
70.2 天然适配聊天模型是什么意思
因为现在大多数聊天模型,本来就是按"消息列表"来接收输入的。
所以 ChatPromptTemplate 生成的结果,和聊天模型想要的输入结构更匹配。
70.3 可维护对话上下文是什么意思
这里不要误会成"模型自己会记忆"。
更准确的意思是:
你可以把前面对话也按消息形式一起放进模板里,
这样模型能理解当前问题是在接哪一句。
71. 消息类型体系怎么理解
在 31_6.py:15 到 31_6.py:20 的表格里,有四个类。
你可以这样理解:
SystemMessagePromptTemplate:专门造 system 消息模板HumanMessagePromptTemplate:专门造 human 消息模板AIMessagePromptTemplate:专门造 ai 消息模板ChatPromptTemplate:把上面这些单条消息模板装起来,形成完整聊天模板
一句话记忆:
前三个是单条消息零件,
最后一个是总装容器。
72. 第一段代码在做什么
第一段核心代码在 31_6.py:46 到 31_6.py:56。
chat_template = ChatPromptTemplate.from_messages([
("system", "你是一个助手AI,名字是{name}"),
("human", "你好,最近怎么样?"),
("ai", "我很好,谢谢!"),
("human", "{user_input}")
])
这段代码的作用不是调模型,而是:
先定义一套对话模板骨架
里面一共有四条消息:
- system:设定角色和规则
- human:第一句用户消息
- ai:上一轮 AI 的历史回复
- human:当前用户真正要问的问题
72.1 from_messages([...]) 在这里是什么意思
这一步是在说:
从一组消息定义,创建一个聊天模板
所以它不是"执行",而是"搭骨架"。
72.2 format_messages(...) 又在做什么
看 31_6.py:53:
message = chat_template.format_messages(
name="mwdb",
user_input="你最喜欢的编程语言是什么?"
)
这一步是在做变量替换。
也就是说,把:
{name}{user_input}
换成真实值,最后得到一组真正可用的消息。
替换之后,逻辑上就会变成:
system: 你是一个助手AI,名字是 mwdb
human: 你好,最近怎么样?
ai: 我很好,谢谢!
human: 你最喜欢的编程语言是什么?
72.3 format_messages() 返回的是什么
它返回的不是普通字符串,而是:
消息对象列表
也就是一组 SystemMessage / HumanMessage / AIMessage 对象。
所以 print(message) 打出来像对象列表,这是正常现象。
73. 第二段代码想表达什么
第二段代码在 31_6.py:67 到 31_6.py:74。
system_template = SystemMessagePromptTemplate.from_template("你是一个{role},请用{language}回答问题")
user_template = HumanMessagePromptTemplate.from_template("{question}")
chat_template = ChatPromptTemplate.from_messages([
system_template,
user_template
])
这段的思想是:
- 先分别创建单条消息模板
- 再把这些模板组合成完整的聊天模板
和第一段相比,本质没有变化,只是写法更细。
你可以这样对比:
- 第一段:一次性直接把整组消息写好
- 第二段:先造单条消息零件,再组装
74. 第二段代码里有一个真实错误
在 31_6.py:77 这里,当前代码写成了:
message = chat_template.from_messages([
role="助手AI",
language="中文",
question="你最喜欢的编程语言是什么?"
])
这段代码当前是错的,而且会直接报:
SyntaxError
原因有两个:
74.1 from_messages 不是拿来填变量的
from_messages(...) 是类方法,用来:
创建模板
它不是用来:
把 role、language、question 这些变量填进去
74.2 [] 里不能写 role="..." 这种关键字参数
Python 的列表里不能这样写命名参数,所以语法本身就不合法。
74.3 正确写法应该是什么
应该改成:
message = chat_template.format_messages(
role="助手AI",
language="中文",
question="你最喜欢的编程语言是什么?"
)
print(message)
或者写成:
prompt_value = chat_template.invoke({
"role": "助手AI",
"language": "中文",
"question": "你最喜欢的编程语言是什么?"
})
print(prompt_value.to_messages())
75. from_messages、format_messages、invoke 到底怎么记
这是这节最关键的记忆点。
不要死背整串方法名,要按"动作"去记。
75.1 第一层记忆法:只记动词
你先只记这三个动词:
fromformatinvoke
它们分别表示:
from:从什么东西创建出来format:格式化、填变量invoke:按 LangChain 统一调用方式执行
75.2 第二层记忆法:只记三句口令
最推荐你记这三句:
from_messages:搭骨架
format_messages:填内容
invoke:按链路风格执行
或者更短一点:
from 负责建
format 负责填
invoke 负责跑
75.3 把它们翻成人话
ChatPromptTemplate.from_messages([...])
翻成人话就是:
从一组消息,创建聊天模板
chat_template.format_messages(...)
翻成人话就是:
把变量填进去,产出最终消息列表
chat_template.invoke({...})
翻成人话就是:
按 Runnable 风格执行这个模板,得到一个 ChatPromptValue
76. format_messages() 和 invoke() 的区别
这个点也很容易混。
76.1 format_messages(...)
返回的是:
list[BaseMessage]
也就是:
消息对象列表
76.2 invoke({...})
返回的是:
ChatPromptValue
你可以把它理解成:
已经格式化好的"聊天 Prompt 对象"
如果你想把它变成消息列表,可以再:
prompt_value.to_messages()
76.3 你该怎么记
最省脑子的记法是:
format_messages():直接拿消息列表invoke():拿 PromptValue 对象,更适合接 LangChain 链路
77. from_template() 和 from_messages() 怎么区分
这一集里还讲了两个常见类方法。
77.1 ChatPromptTemplate.from_template()
适合:
单条消息、简单场景
它更像是快速创建一个简单聊天模板。
77.2 ChatPromptTemplate.from_messages()
适合:
多角色、多消息、多轮对话场景
所以更真实、更常见的聊天工程里,通常更重要的是:
from_messages()
77.3 最实用的区分法
你可以直接记:
- 简单单轮:
from_template - 聊天骨架:
from_messages
78. 这一集的现代工程提醒
这一点你特别在意,所以这里继续提醒。
78.1 这节课的方向是对的,而且不算过时
ChatPromptTemplate 在当前 LangChain 里依然是非常主流、非常推荐的写法。
这点和之前有些旧教程里的 LLMChain、initialize_agent 不一样,它不是"老写法被淘汰"。
78.2 但真实多轮工程里,不建议把历史 AI 回复硬编码在模板里
例如第一段代码里写:
("ai", "我很好,谢谢!")
教学里这样写没问题,因为它是在演示"消息角色"。
但真实项目里,更常见的做法是:
- 历史消息来自真实对话记录
- 或者使用专门的消息占位机制
也就是说,教学写法适合理解结构,不等于真实系统会把历史回复手写死在模板里。
78.3 当前更主流的链路风格仍然是 Runnable 组合
例如后面你经常会看到:
chain = chat_template | llm | StrOutputParser()
这比手动拆很多步更符合当前 LangChain 的工程风格。
79. 同类型迁移例子
为了帮助你触类旁通,这里补两个同类型的小例子。
79.1 最简单的聊天模板
from langchain_core.prompts import ChatPromptTemplate
chat = ChatPromptTemplate.from_template("请用中文解释{topic}")
messages = chat.format_messages(topic="线程池")
print(messages)
这个例子适合理解:
from_template()也能创建聊天模板- 但通常是单条消息场景
79.2 常见的 system + human 组合
from langchain_core.prompts import ChatPromptTemplate
chat = ChatPromptTemplate.from_messages([
("system", "你是一个{role},回答要简洁"),
("human", "{question}")
])
messages = chat.format_messages(
role="Python导师",
question="什么是装饰器?"
)
print(messages)
这个例子适合理解:
from_messages()最常见的用法- 一般会把 system 规则和用户提问组合起来
80. 这一集最该背下来的结论
ChatPromptTemplate 的本质是"生成消息列表,而不是生成一整段普通字符串";
from_messages 用来搭聊天骨架;
format_messages 用来填变量并拿到消息列表;
invoke 用来按 LangChain 链路风格执行模板。
81. from_template() 和 from_messages() 是不是二选一
从"创建聊天模板"这个动作来看,通常可以理解成:
是的,二选一。
也就是说,当你要创建一个 ChatPromptTemplate 时,通常会在下面两种方式里选一种:
ChatPromptTemplate.from_template(...)ChatPromptTemplate.from_messages([...])
但这里的"二选一"是指:
这一段模板,通常选一种创建方式就够了
不是说整个项目里永远只能用一种。
81.1 什么时候选 from_template()
适合:
- 只有一条消息
- 没有复杂角色结构
- 只是简单把一个问题包装成聊天输入
例如:
from langchain_core.prompts import ChatPromptTemplate
chat = ChatPromptTemplate.from_template("请用中文解释{topic}")
messages = chat.format_messages(topic="线程池")
这个场景里,你只是想得到一条类似:
human: 请用中文解释线程池
所以用 from_template() 就够了。
81.2 什么时候选 from_messages()
适合:
- 有
system规则 - 有
human输入 - 可能还有
ai历史消息 - 需要多条消息组合
- 更接近真实聊天场景
例如:
from langchain_core.prompts import ChatPromptTemplate
chat = ChatPromptTemplate.from_messages([
("system", "你是一个{role},回答要简洁"),
("human", "{question}")
])
messages = chat.format_messages(
role="Python导师",
question="什么是装饰器?"
)
这个场景里,你已经不是在生成一条普通提问,而是在构造:
system + human
这样的消息结构。
81.3 最实用的选择标准
你可以直接记成:
- 只要一条简单输入:
from_template() - 只要涉及角色分工:
from_messages()
或者更短:
简单单条,用 from_template
真实聊天,用 from_messages
82. 如果不用 ChatPromptTemplate,和用了它分别是什么样
这个点很重要,因为你现在已经看到两条路线了。
82.1 不用 ChatPromptTemplate:手写消息对象
例如:
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
llm = ChatOpenAI(model="DeepSeek-V3.2")
messages = [
SystemMessage(content="你是一个Java老师"),
HumanMessage(content="解释volatile的作用")
]
response = llm.invoke(messages)
print(response.content)
这条路线的特点是:
- 直接
- 好理解
- 适合固定消息
- 不适合变量很多、模板要复用的场景
82.2 用 ChatPromptTemplate:先建模板,再生成消息
例如:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="DeepSeek-V3.2")
chat = ChatPromptTemplate.from_messages([
("system", "你是一个{role}"),
("human", "{question}")
])
messages = chat.format_messages(
role="Java老师",
question="解释volatile的作用"
)
response = llm.invoke(messages)
print(response.content)
这条路线的特点是:
- 更适合动态变量
- 更适合模板复用
- 更接近真实工程
82.3 它们的本质关系
你可以把这两种写法理解成:
最终目的都是得到 messages,
区别只是 messages 是手写出来的,还是由模板生成出来的。
83. 到现在为止,几种常见写法怎么重新梳理
这是你现在最需要建立的全局视角。
不要把每个方法都当成孤立知识点,而要按"数据怎么流动"来理解。
84. 第一类:最原始的纯字符串调用
这是最简单的:
response = llm.invoke("请用中文解释线程池")
print(response.content)
数据流:
普通字符串
-> llm.invoke(...)
-> AIMessage
-> response.content
-> 最终文本
适合:
- 最简单测试
- 临时提问
- 没有模板需求
85. 第二类:PromptTemplate 字符串模板路线
这一类还是"字符串 Prompt",不是消息列表。
例如:
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template("请用{language}解释{topic}")
text = prompt.format(language="中文", topic="线程池")
response = llm.invoke(text)
print(response.content)
数据流:
变量字典
-> PromptTemplate
-> format(...)
-> 字符串 Prompt
-> llm.invoke(...)
-> AIMessage
-> response.content
-> 最终文本
适合:
- 单轮任务
- 普通文本生成
- 不需要 system/human/ai 角色结构
86. 第三类:手写聊天消息路线
这一类开始进入聊天模型的典型输入。
例如:
from langchain_core.messages import SystemMessage, HumanMessage
messages = [
SystemMessage(content="你是一个Java老师"),
HumanMessage(content="解释volatile的作用")
]
response = llm.invoke(messages)
print(response.content)
数据流:
手写消息对象
-> messages 列表
-> llm.invoke(messages)
-> AIMessage
-> response.content
-> 最终文本
适合:
- 结构固定
- 角色少
- 先理解聊天模型输入结构
87. 第四类:ChatPromptTemplate 聊天模板路线
这是当前非常重要的一条路线。
例如:
from langchain_core.prompts import ChatPromptTemplate
chat = ChatPromptTemplate.from_messages([
("system", "你是一个{role}"),
("human", "{question}")
])
messages = chat.format_messages(
role="Java老师",
question="解释volatile的作用"
)
response = llm.invoke(messages)
print(response.content)
数据流:
变量字典
-> ChatPromptTemplate
-> format_messages(...)
-> 消息列表
-> llm.invoke(messages)
-> AIMessage
-> response.content
-> 最终文本
适合:
- 聊天模型
- 有 system/human 角色
- 模板需要复用
- 更真实的业务场景
88. 第五类:Runnable 链式写法
这是当前 LangChain 很常见的工程风格。
88.1 字符串模板链
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"language": "中文", "topic": "线程池"})
数据流:
变量字典
-> PromptTemplate
-> 字符串 Prompt
-> llm
-> AIMessage
-> StrOutputParser
-> 最终字符串
88.2 聊天模板链
chain = chat | llm | StrOutputParser()
result = chain.invoke({
"role": "Java老师",
"question": "解释volatile的作用"
})
数据流:
变量字典
-> ChatPromptTemplate
-> 消息 PromptValue
-> llm
-> AIMessage
-> StrOutputParser
-> 最终字符串
这条路线的优点是:
- 更统一
- 更适合后面继续接解析器
- 更符合当前 LangChain 风格
89. 现在你到底该怎么选
如果你现在觉得方法太多,可以先不要按"类名"记,而是按场景选。
89.1 只想快速问一句
用:
llm.invoke("...")
89.2 有变量,但只是普通文本 Prompt
用:
PromptTemplate
89.3 想明确区分 system / human / ai
用:
ChatPromptTemplate
89.4 消息结构固定、临时测试
用:
直接手写 SystemMessage / HumanMessage
89.5 想要更现代、可组合的工程写法
用:
prompt/chat_prompt | llm | parser
90. 最后压缩成一张总流程图
你现在可以把前面学到的内容压缩成下面这张图:
路线1:普通字符串
"请解释线程池"
-> llm.invoke(...)
-> AIMessage
-> content
路线2:字符串模板
变量
-> PromptTemplate
-> format(...)
-> 字符串 Prompt
-> llm.invoke(...)
-> AIMessage
-> content
路线3:手写聊天消息
SystemMessage + HumanMessage
-> messages
-> llm.invoke(messages)
-> AIMessage
-> content
路线4:聊天模板
变量
-> ChatPromptTemplate
-> format_messages(...)
-> messages
-> llm.invoke(messages)
-> AIMessage
-> content
路线5:Runnable 链
变量
-> PromptTemplate / ChatPromptTemplate
-> llm
-> OutputParser
-> 最终字符串或结构化结果
91. 这一阶段最该背下来的结论
你学到现在,其实不是"很多互相冲突的方法",
而是"同一个目标的不同输入组织方式":
要么直接给字符串,
要么先用 PromptTemplate 生成字符串,
要么直接给消息列表,
要么先用 ChatPromptTemplate 生成消息列表,
最后都交给 llm 去推理。