跳到主要内容

让它记得上一句 — 历史、代词、以及越攒越贵的账

这一章讲三件事: 为什么它不记得你上一句;把历史塞回去为什么还不够; 以及历史攒长之后要付什么代价。

它在全书链条里的位置: 第 07 章那条链一问一答就结束了。 这一章把它变成能接着聊的东西,而这正是任何一个真实助手的最低要求。

本章主走查:两轮对话。 第一轮问「我可以学习到关于提示工程的知识吗?」;第二轮(一问一答算一轮)问「为什么这门课需要教这方面的知识?」。 第二轮那句话里有一个代词——「这方面」。它指不指得到,就是这一章的全部看点。 两轮的回答都是原书里真实跑出来的。

1. 它不记得你上一句说了什么

这一节摆现象,而这个现象你多半已经撞见过。

书的原话很直接:在与语言模型交互时,你可能已经注意到一个关键问题—— 它们并不记得你之前的交流内容。 书还点明后果: 这在构建聊天机器人这类应用时带来了很大的挑战,使得对话似乎缺乏真正的连续性1

注意这里的措辞:「并不记得」不是「记性不好」。 它是彻底的零—— 上一轮说过的话,对这一轮来说等于从未发生过。

2. 为什么不记得:第 02 章那个列表的直接后果

这一节回答「为什么」,答案在两章之前就埋好了。

回到第 02 章:发给模型的是一个列表,里面每条带一个身份。 每次调用,模型看到的就是这个列表的全部内容,一个字都不多。

所以「不记得」这件事其实是这样的:

第 1 轮你发过去: [ {user: "我可以学习到关于提示工程的知识吗?"} ]
← 它回:「是的,您可以……」

第 2 轮你发过去: [ {user: "为什么这门课需要教这方面的知识?"} ]
← 它看到的只有这一条。第 1 轮那两句话,它一句都看不见。

图说:两次调用之间,没有任何东西被保留下来。
不是它忘了,是根本没有一个地方存过。

第 02 章那张身份表里其实早就给了答案:assistant 这个身份, 书对它的说明是「一般是模型历史回复,作为提供给模型的参考内容」。 那个格子存在的全部理由,就是这一章。

3. 治法:把之前说过的原样再发一遍

这一节给出办法,而办法朴素到有点让人失望。

既然没有地方存,那就在你自己的程序里存一份,下次连同新问题一起发过去。 框架管这份存下来的历史叫记忆——它就是你程序里的一个普通列表,不在模型里面。 框架把这件事包成了一个现成的盒子2:

memory = ConversationBufferMemory(
memory_key="chat_history", # 这份历史在提示词里叫什么名字
return_messages=True # 以消息列表的形式返回,而不是一整段字符串
)

框架管这一类东西叫「储存模块」,而这一个是其中最简单的一种:全都留着,一句不删2

第二个参数值得看一眼。 设成 True 时,历史是一条一条带身份的消息; 设成 False 时,历史被拼成一整段文字前者能让模型分清哪句是你说的、哪句是它说的,后者不能。

4. 光塞回去还不够:代词指不到

这一节是主走查,也是这一章真正的机制所在。

先看第一轮3:

问:我可以学习到关于提示工程的知识吗?

答:是的,您可以学习到关于提示工程的知识。本模块内容基于吴恩达老师的
《Prompt Engineering for Developer》课程编写,旨在分享使用提示词开发大语言模型应用的
最佳实践和技巧。课程将介绍设计高效提示的原则,包括编写清晰、具体的指令和给予模型充足思考时间等。

图说:这一轮没有历史,和第 07 章那条链没有区别。
注意它答出了「吴恩达」——说明库里那份教程被取到了。

第二轮是关键3:

问:为什么这门课需要教这方面的知识?
↑↑↑↑
这四个字不指向任何具体内容

如果直接拿这句话去库里取块,会取到什么? 「这方面的知识」在向量模型眼里就是一句空话,它跟库里任何一块都不特别像。 取回来的多半是几块不相干的东西,而模型只能照着不相干的东西答。

所以对话检索链多做了一步。书列的四步是这样的4:

① 把之前的对话与新问题合并,生成一个完整的查询语句
「为什么这门课需要教这方面的知识?」
+ 上一轮的「提示工程」
→ 一句意思完整、可以拿去检索的话
② 拿这句完整的话去库里取块
③ 取到结果后,把答案也存进对话记忆区
④ 用户可以在界面里查看完整的对话流程

图说:第 ① 步是全部的秘密。它不是把历史塞给模型,
是先用历史把代词补全,再去检索。

跑第二轮,回答是3:

这门课程需要教授关于 Prompt Engineering 的知识,主要是为了帮助开发者更好地使用大型语言模型(LLM) 来完成各种任务。通过学习 Prompt Engineering,开发者可以学会如何设计清晰明确的提示词……

它把「这方面的知识」正确地理解成了「提示工程」。 走查到此结束。

书对这一步的评价是:这种链式方式把新问题放在之前对话的语境中进行检索, 可以处理依赖历史信息的查询,并保留所有信息在对话记忆中,方便追踪4

5. 历史越攒越长,而它按字收钱

这一节讲代价,以及书给的四种攒法。

每一轮都把全部历史重发一遍,意味着两件事:

后果具体是什么
越来越贵第 02 章那笔账:输入按词元收钱。第 10 轮发出去的是前 9 轮的全部内容 + 这一轮
会撞上限第 05 章那个上限。第 02 章讲过文心的做法:超了不报错,而是自行遗忘前文

所以「全都留着」这种最简单的攒法,只适合演示。 书给了另外三种,一句话带过5:

攒法做法
全都留着一句不删(这门课演示用的就是它)
只留最近几轮超出的最老那几轮直接丢掉
只留固定字数按词元数封顶,超了从最老的开始丢
留摘要把老的几轮压成一段总结,只留总结

第三部分那个项目用的是第二种,它给的接口是一个数:history_len—— 保留最近几次对话;另外还提供了一个清空历史的方法6

这四种没有优劣,它们在同一条轴上取不同的点: 留得越多,它越懂你在说什么;留得越少,越便宜、越不容易撞上限。

6. 边界:书在这一节抄错了一句结论

这一节必须单独说,因为照着它读会得出错误的理解。

先说清一个词:强化学习就是另一种机器学习办法——让程序在环境里反复试错, 按奖励高低调整下一步怎么做。记住这四个字,因为它马上要出现得毫无道理。

第二轮那次回答之后,原书写了这么一句7:

可以看到,LLM 它准确地判断了这方面的知识,指代内容是强化学习的知识, 也就是我们成功地传递给了它历史信息。

「强化学习」四个字在这段对话里从未出现过。 两轮问的都是提示工程, 模型的回答里也一句强化学习都没有。这是一处明显的复制粘贴残留。

为什么会出现「强化学习」? 因为原书第三部分那个项目的知识库里 确实有一份《强化学习入门指南》,而第 10 章那次「标明知识来源」的实验里, 模型答的正是强化学习的定义并附上了「【来源:蘑菇书】」—— 这句话大概率是从那个语境里搬过来的。

正确的说法应该是:它准确判断出「这方面的知识」指的是提示工程。 结论本身(历史信息成功传过去了)是对的,只有那半句指代内容写错了。

判断(我们的,不是书里的): 这一章的机制其实比书写得更普适—— 第 ① 步「用历史把代词补全,再去检索」是一个可以单独拿走的做法, 它不依赖框架、不依赖这条链,你在任何检索系统里都可以照做: 先调一次模型把问题改写成完整的、不含代词的一句,再拿改写后的去检索。 如果错,会错在: 如果你的场景里用户每次都问完整的问题(比如表单式输入、语音助手的固定句式), 那么这一步就是纯粹多花一次调用的钱,应该去掉。

7. 可带走的

  1. 它不记得你上一句——不是记性差,是根本没有存过;
  2. 原因在第 02 章那个列表:每次调用它看到的就是那个列表的全部,一个字不多;
  3. 治法是把之前说过的原样再发一遍,框架有现成的盒子;
  4. return_messages 那个参数决定历史是「一条条带身份」还是「一整段文字」——前者才分得清谁说的;
  5. 光把历史塞回去不够:「这方面的知识」这类代词在库里不像任何一块;
  6. 对话检索链多做的那一步是关键:先用历史把问题补全,再拿补全后的句子去取块;
  7. 走查证据:第二轮问「为什么这门课需要教这方面的知识?」,它答出了提示工程;
  8. 代价有两条:每轮都重发全部历史,按词元收钱;而且会撞上限,文心撞了不报错,只是静默遗忘前文;
  9. 四种攒法:全留、留最近几轮、留固定字数、留摘要;这门课演示用第一种,项目用第二种;
  10. 书在这一节抄错了一句:它把代词指的东西写成了「强化学习的知识」,实际是提示工程;结论对,那半句错。

8. 原文地图

主题原书章原文位置
它不记得你之前的交流构建检索问答链text/06-p101-120.txt:231(搜「它们并不记得你之前的交流内容」)
储存模块与最简单的那一种构建检索问答链text/06-p101-120.txt:236(搜「储存模块」) · text/06-p101-120.txt:237(搜「保存聊天消息历史记录的列表」)
另外三种攒法构建检索问答链text/06-p101-120.txt:247(搜「保留指定对话轮数」)
对话检索链的四步构建检索问答链text/06-p101-120.txt:252(搜「增加了处理对话历史的能」) · text/06-p101-120.txt:256(搜「将之前的对话与新问题合并」)
两轮对话的问与答构建检索问答链text/06-p101-120.txt:278(搜「我可以学习到关于提」) · text/06-p101-120.txt:282(搜「是的,您可以学习到关于提」) · text/06-p101-120.txt:292(搜「这⻔课程需要教授关于Prompt Engineering的知识」)
那句抄错的结论构建检索问答链text/06-p101-120.txt:297(搜「指代内容是强化学习的知识」)
项目里那种「留最近几轮」个人知识库助手项目text/09-p161-180.txt:123(搜「change_history_length」) · text/09-p161-180.txt:119(搜「clear_history」)

Footnotes

  1. 出处:「构建检索问答链」第 229 至 232 段(text/06-p101-120.txt:231,搜「它们并不记得你之前的交流内容」)。原文这一段紧接在第 07 章那次失败实验之后,原话里还有一句「这个问题该如何解决呢?」。

  2. 出处:「构建检索问答链」第 234 至 245 段(text/06-p101-120.txt:236,搜「储存模块」;代码见 text/06-p101-120.txt:242,搜「ConversationBufferMemory」)。「全都留着,一句不删」这个刻画是我们对这个类名的翻译;书只说它「保存聊天消息历史记录的列表,这些历史记录将在回答问题时与问题一起传递给聊天机器人」。 2

  3. 出处:「构建检索问答链」第 264 至 295 段(第一轮的问见 text/06-p101-120.txt:278,搜「我可以学习到关于提」;第一轮的答见 text/06-p101-120.txt:282,搜「是的,您可以学习到关于提」;第二轮的答见 text/06-p101-120.txt:292,搜「这⻔课程需要教授关于Prompt Engineering的知识」)。两轮的回答原文都比这里长一截,本章各截了前半段;截掉的部分是同一意思的展开。 2 3

  4. 出处:「构建检索问答链」第 250 至 262 段(text/06-p101-120.txt:252,搜「增加了处理对话历史的能」;四步见 text/06-p101-120.txt:256,搜「将之前的对话与新问题合并」)。图说里那句「第 ① 步是全部的秘密」是我们的判断;书把四步平铺列出,没有指出哪一步最要紧。 2

  5. 出处:「构建检索问答链」第 247 至 248 段(text/06-p101-120.txt:247,搜「保留指定对话轮数」)。原文只有一句话带过:关于更多储存模块的使用,包括保留指定对话轮数、保存指定 token 数量、保存历史对话的总结摘要等内容,请参考框架文档。这四种攒法的那张表是我们照这一句展开的,书没有展开过任何一种。

  6. 出处:「个人知识库助手项目」第 119 至 132 段(text/09-p161-180.txt:123,搜「change_history_length」;清空方法见 text/09-p161-180.txt:119,搜「clear_history」)。那个类的注释里写着「history_len:控制保留的最近 history_len 次对话」。注意那份代码里 self.history_len 那一行是被注释掉的——也就是说这个功能在项目里并没有真正启用。

  7. 出处:「构建检索问答链」第 297 至 298 段(text/06-p101-120.txt:297,搜「指代内容是强化学习的知识」)。「强化学习」的来处是我们的推测,依据是原书第 10 章那次实验里模型答的正是强化学习的定义并附了「【来源:蘑菇书一语二语二强化学习教程】」(text/07-p121-140.txt:62,搜「蘑菇书」),而第三部分那个项目的知识库里确实收了一份《强化学习入门指南》(text/08-p141-160.txt:372,搜「强化学习入」)。书本身没有解释这半句是怎么来的。