第31章 Prompt 与聊天模型笔记

1. 这一集的主线

这一集讲的是 Prompt Engineering,也就是提示词工程。

它的核心思想可以先记成一句话:

复制代码
Prompt 工程 = 把任务目标、输出形式、约束条件讲清楚,让模型更稳定地按预期工作

对初学者来说,这一章非常重要,因为你前面学的 ModelChatOpenAIinvoke() 等更像"把模型调用起来",而这一章开始真正进入:

  • 怎样提任务
  • 怎样控制输出
  • 怎样让结果更稳定

2. 什么是 Prompt Engineering

提示词工程,简单说就是:

  • 通过设计输入文本
  • 引导大模型理解你的需求
  • 并生成你更想要的结果

它不是"随便写一句提示词",而是更像:

  • 任务说明书
  • 交付标准
  • 输出格式约束

如果用你视频里的类比:

  • 大模型像你的员工
  • Prompt 像你给员工的任务文档

任务越清晰:

  • 模型越不容易误解
  • 输出越不容易发散
  • 结果越容易符合预期

3. 为什么要学 Prompt 工程

这部分你可以从工程角度理解。

传统编程更像:

复制代码
写代码 -> 计算机严格执行

Prompt 工程更像:

复制代码
写自然语言指令 -> 模型根据指令生成结果

所以 Prompt 的作用不是替代代码,而是:

  • 降低模型误解任务的概率
  • 提高输出稳定性
  • 减少后续人工修正
  • 让模型更像"按要求做事",而不是"自由发挥"

4. 这节课最重要的四要素

这一集最值得记住的是 Prompt 设计四要素:

  1. 角色设定
  2. 任务描述
  3. 输出格式
  4. 约束条件

你可以把它们理解成:

复制代码
角色设定:你要模型"像谁一样回答"
任务描述:你要模型"完成什么"
输出格式:你要模型"怎么输出"
约束条件:你要模型"控制到什么程度"

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 个案例虽然领域不同,但优化逻辑其实是一致的:

  1. 把任务从模糊变具体
  2. 把回答对象说清楚
  3. 把输出结构固定住
  4. 把边界条件写出来
  5. 让结果更适合直接使用

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 模板的组件。

它主要解决两个问题:

  1. 动态内容组装
  2. 避免 Prompt 硬编码

你可以把它理解成:

复制代码
Prompt 版模板引擎

它和普通字符串拼接相比,更适合:

  • 管理复杂 Prompt
  • 明确模板需要哪些变量
  • 让 Prompt 更容易复用和维护

27. 这一集最重要的四个成员

你这集里真正最值得记住的是这 4 个:

  • template
  • input_variables
  • partial_variables
  • format()

你可以直接这样理解:

复制代码
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 语境里不算主流。

你后面更应该重点认识的是:

  • ChatPromptTemplate
  • FewShotPromptTemplate
  • PipelinePromptTemplate

尤其是对接聊天模型时,当前更重要的一般是:

复制代码
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 templateinput_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:这一集在讲什么

这一集开始把前面学过的三个组件串起来:

  • PromptTemplate
  • LLM
  • OutputParser

它的主线可以先记成:

复制代码
输入变量
-> 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,
)
modelmodel_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 成本就越高。

你可以把这一集拆成两个问题:

  1. ChatModel 和普通文本模型有什么不同
  2. 为什么聊天越聊越贵,越聊越容易碰到上下文上限

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-003
  • gpt-3.5-turbo
  • gpt-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 -> SystemMessage
  • user -> HumanMessage
  • assistant -> 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 对应 system
  • HumanMessage 对应 user
  • llm.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}")
])

这段代码的作用不是调模型,而是:

复制代码
先定义一套对话模板骨架

里面一共有四条消息:

  1. system:设定角色和规则
  2. human:第一句用户消息
  3. ai:上一轮 AI 的历史回复
  4. 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_messagesformat_messagesinvoke 到底怎么记

这是这节最关键的记忆点。

不要死背整串方法名,要按"动作"去记。

75.1 第一层记忆法:只记动词

你先只记这三个动词:

  • from
  • format
  • invoke

它们分别表示:

  • 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 里依然是非常主流、非常推荐的写法。

这点和之前有些旧教程里的 LLMChaininitialize_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 去推理。
相关推荐
左左右右左右摇晃2 小时前
Java笔记——包装类(自动拆装箱)
java·笔记·python
青瓷程序设计2 小时前
【果蔬识别系统】Python+深度学习+人工智能+算法模型+图像识别+2026原创
人工智能·python·深度学习
Fairy要carry2 小时前
面试08-“生产者-消费者” 模型实现并发 Agent
python·面试
chushiyunen2 小时前
python和java的区别
python
Yeh2020582 小时前
MySQL笔记二
笔记
DamianGao2 小时前
MiniMax-M2.7 与 LangChain ToolStrategy 兼容性问题解决
python·langchain
elseif1232 小时前
CSP-S提高级大纲
开发语言·数据结构·c++·笔记·算法·大纲·考纲
兰.lan2 小时前
【黑马ai测试】Day01课堂笔记+课后作业
软件测试·笔记·python·ai·单元测试
国医中兴3 小时前
Python AI入门:从Hello World到图像分类
人工智能·python·分类