知识点思维导图
21 个知识节点
Prompt Engineering(16) - 多轮对话与上下文管理
读完后,你应能完成以下任务:
- 绘制“Prompt Engineering(16) - 多轮对话与上下文管理 / history 是你手动维护的列表”的关键对象与数据流,解释“先明确:所谓「对话历史」不是模型存的,是你的后端存的。”,并用源码位置、日志或 Trace 标注证据。
- 为“Prompt Engineering(16) - 多轮对话与上下文管理 / 两个控制手段:截断和摘要”设计正常与异常输入,验证“代价是:被扔掉的早期信息模型彻底失忆。”,输出首个偏差位置与回归测试结果。
- 实现“Prompt Engineering(16) - 多轮对话与上下文管理 / 摘要:把旧历史压成一句话”的最小代码或配置,检验“更聪明的做法:旧历史不整段扔,而是压缩成一句摘要塞进 system,关键信息用少量 token 保住。”,输出命令、结果与 Diff,并说明不适用边界。
一、多轮对话与上下文管理的真实应用场景
用户和你的客服助手聊了 20 轮。第 21 轮他问「那这个能报销吗?」——「这个」指的是第 3 轮提到的出差住宿。
模型要答对,就得知道前面聊了啥。但 大模型基础(07) - 大模型 API 基础 已经说明:模型没有持久会话记忆,每轮都得由应用重新发送需要的历史。于是问题来了:20 轮历史全发,token 爆了,而且越发越贵越慢;只发最近 2 轮,模型又忘了「这个」是啥。
上下文管理就是在这两难之间找平衡:发得够多让对话连贯,发得够少不撑爆预算。这跟前端管理 store 很像——状态要有边界、能清理、不能无限膨胀。
二、history 是你手动维护的列表
先明确:所谓「对话历史」不是模型存的,是你的后端存的。每轮你都要干这三件事:
1. 把用户这句 append 进 history(role: user)
2. 把 system + history 一起发给模型
3. 把模型回复 append 进 history(role: assistant)
只要不管理,history 就会无限长。第 N 轮发给模型的 token ≈ 前 N-1 轮所有内容之和。这是个累加曲线,长会话必然撑爆。
三、两个控制手段:截断和摘要
3.1 截断:只保留最近 N 轮
最简单的策略:超过预算,就把最早的几轮直接扔掉,只留最近 N 条。
代价是:被扔掉的早期信息模型彻底失忆。如果用户突然回头引用很早的内容,就接不上了。所以纯截断适合「话题不怎么回溯」的场景。
3.2 摘要:把旧历史压成一句话
更聪明的做法:旧历史不整段扔,而是压缩成一句摘要塞进 system,关键信息用少量 token 保住。
发给模型的 = system + 摘要(旧历史) + 最近 N 轮原文
比如前 10 轮压成「用户先后咨询了报销流程、所需材料、电子发票、审批时长」,只占十几个 token,但模型还知道聊过这些。摘要本身一般是再调一次模型生成的(「请把以下对话概括成一句话」)。
| 策略 | 优点 | 代价 | 适用 |
|---|---|---|---|
| 截断 | 简单、零额外成本 | 早期信息全丢 | 短会话、话题不回溯 |
| 摘要 | 保住关键信息、省 token | 多一次模型调用、摘要可能丢细节 | 长会话、需要回溯 |
实战常组合用:最近几轮保原文(细节不丢),更早的压成摘要(省 token),system 永远保留(规则不能丢)。
四、工程上真正会踩的坑
- 会话 id 不稳定 / 不隔离。多用户共用一个 history,张三的对话串到李四那里,既是 bug 也是隐私事故。每个会话独立存,按 sessionId 隔离。
- system 也被当历史截断掉。截断时只能裁 user/assistant,system 规则必须永远保留,否则模型连「自己是谁」都忘了。
- 摘要丢了关键事实。摘要是有损压缩,可能把「用户说他是 VIP」这种关键信息丢掉。重要事实(用户身份、确认过的金额)应单独存成结构化字段,别只靠摘要兜。
- 敏感信息长期留在 history。手机号、身份证这类不该长期存。落库前脱敏,或对长期记忆设过期和删除能力。
五、一句话面试答法
多轮对话的上下文怎么管理? 模型本身无记忆,历史是后端维护的列表,每轮把 system 加历史重发。不能无限发,会爆 token 上限也费钱,所以要做上下文管理:最近几轮保留原文保证细节,更早的历史调模型压成摘要塞进 system 省 token,system 规则永远保留。会话按 sessionId 严格隔离,敏感信息落库前脱敏。本质和前端管理 store 一样——状态要有边界、可清理、可复现。
六、动手实践:14 多轮对话与上下文管理
模拟一段 6 轮对话,演示 history 怎么一直累积,以及超过 token 预算后怎么把旧历史压成摘要、只保留最近几轮原文,让对话连贯又不撑爆上下文。
6.1 在线运行
零依赖,纯标准库。
6.2 预期输出
--- 第 1 轮 ---
用户:公司的报销流程是什么?
完整历史条数:2,本轮实际发给模型:2 条 / 约 20 token
模型:报销需在 30 天内提交。
--- 第 2 轮 ---
用户:需要哪些材料?
完整历史条数:4,本轮实际发给模型:4 条 / 约 37 token
模型:好的,请继续说。
--- 第 3 轮 ---
用户:电子发票可以吗?
完整历史条数:6,本轮实际发给模型:6 条 / 约 52 token
模型:好的,请继续说。
--- 第 4 轮 ---
用户:审批一般要多久?
完整历史条数:8,本轮实际发给模型:8 条 / 约 67 token
模型:刚说过了,是 30 天内。
--- 第 5 轮 ---
用户:出差住宿能报销吗?
完整历史条数:10,本轮实际发给模型:5 条 / 约 42 token
已摘要旧历史:(早前对话摘要)用户先后问过:公司的报销流程是什么?;需要哪些材料?;电子发票可以吗?
模型:报销需在 30 天内提交。
--- 第 6 轮 ---
用户:报销要几天内提交?
完整历史条数:12,本轮实际发给模型:5 条 / 约 46 token
已摘要旧历史:(早前对话摘要)用户先后问过:公司的报销流程是什么?;需要哪些材料?;电子发票可以吗?;审批一般要多久?
模型:报销需在 30 天内提交。
结论:历史一直累积,但发给模型的上下文被控制住了——旧的压成摘要,近的保原文。
盯着第 4 轮和第 5 轮的对比:完整历史从 8 条涨到 10 条,但「实际发给模型」从 8 条(67 token)反而降到 5 条(42 token)。因为第 5 轮超了 80token 预算,旧历史被压成了一句摘要。
6.3 代码↔概念对应
| 概念 | 在 main.py 哪里 |
|---|---|
| 完整历史累积 | Conversation.history + add |
| token 预算估算 | estimate_tokens |
| 超预算时裁剪:保最近 N 条 | build_messages 里 keep_recent |
| 旧历史压缩成摘要 | _summarize + summary |
| 每轮实际发给模型的上下文 | build_messages 返回值 |
6.4 动手改
- 把
max_tokens从 80 调大到 200,会发现 6 轮都不触发摘要——预算够就全量发。 - 把
keep_recent从 4 调成 2,摘要会更早触发、保留的原文更少,体会「记得多少」和「省多少 token」的取舍。 _summarize现在是规则抽关键词,真实项目换成调模型「把这段对话概括成一句话」,摘要质量会高很多。
6.5 可运行源码:多轮对话与上下文管理
main.py
七、总结
- history 是你手动维护的列表:先明确:所谓「对话历史」不是模型存的,是你的后端存的。
- 两个控制手段:截断和摘要:代价是:被扔掉的早期信息模型彻底失忆。
- 工程上真正会踩的坑:多用户共用一个 history,张三的对话串到李四那里,既是 bug 也是隐私事故。
- 一句话面试答法:模型本身无记忆,历史是后端维护的列表,每轮把 system 加历史重发。
- 摘要:把旧历史压成一句话:更聪明的做法:旧历史不整段扔,而是压缩成一句摘要塞进 system,关键信息用少量 token 保住。
学完自测
选择所有正确答案;提交后逐项核对判断依据。