前言
在前三篇文章中,我们建立了GPT模型微调的环境,并使用微调让GPT学习了新知识,同时了解了ChatGPT中的Prompt技巧与设计。在本章中,我们将利用本地数据库,构建一个基于本地知识库的GPT问答机器人,而非使用Langchain。
附录:
《打造个性化离线ChatGPT - GPT模型微调实战指南》
《微调的小型模型在专业领域性能勉强接近GPT-3.5 - GPT 模型微调实战指南(二)》
《ChatGPT中的Prompt技巧与设计 - GPT 模型微调实战指南(三)》
环境准备
由于modelscope社区正在举办活动,新注册用户可以免费获得100小时的GPU算力,因此我们将使用modelscope提供的环境来进行本次实验。这将为大家提供更方便的测试和体验。
- 打开 https://modelscope.cn/my/mynotebook
- 依次点击 我的Notebook→阿里云弹性加速计算EAIS→GPU环境→快速启动。
- 稍等一会,即可进入界面。
至此,我们的基础环境准备好了。
注意,该环境乃测试环境,使用的时间和最大时长有限制任何数据记得及时做好备份!!!
由于我们希望在后期可以进行自定义定制,并且架构相对简单,因此本文决定不使用Langchain,而是准备自己实现整个流程。
主要涉及Prompt编写、向量库、向量算法以及文本切割。下面将详细介绍如何实现的过程。
Prompt编写
根据吴恩达在《ChatGPT中的Prompt技巧与设计》课程中介绍的范式,我们经过多次尝试后,决定采用该模板来构建的问答的Prompt。
请根据上下文来回答问题, 如果上下文的信息无法回答问题,请回答"我不知道"。 上下文: """ {根据用户输入的问题,从向量数据库查询的结果。一行一个} """ 问题:"""{用户输入的问题}""" 回答:
为了让LLM认为从向量数据库查询的结果是一句完整的话,我在每个结果的末尾添加一个了句号。当然,这个模板仍然无法完全约束模型的生成结果,仍可能会存在编造结果的情况。
我尝试了调整模型的 top_p 和 temperature 这两个参数,但是并没有消除这种情况的出现。要达到更好的效果,还是需要进行微调,并限制模型的思考范围和输出结果的格式。
向量数据库的选型
在众多的向量库类型中,我选择了Chroma向量库,原因如下:
- 可以本地部署,不需要连接到外部服务,保证了数据的隐私性。
- 结构简单,易于理解和魔改。
- 可以自定义实现量化算法,从而优化检索速度和准确度。
- 数据可以持久化,便于长期存储和管理。
因此,我们选择了Chroma向量库作为本次实验的数据库。
详细文档 https://docs.trychroma.com/
注:默认Chroma会收集匿名使用的信息,如何关闭请查阅https://docs.trychroma.com/telemetry。
文本的分割
在导入文本到向量库之前,我们可以参考Langchain是如何对文本分割的,以便更好地理解该过程。
https://python.langchain.com/en/latest/modules/chains/index_examples/summarize.html
我们可以看到,默认使用的文本分割器是 CharacterTextSplitter。该文本分割器需要一个字符列表separator,它会尝试根据第一个字符进行分割,但如果任何块太大,则会转移到下一个字符。默认情况下,它会尝试拆分的字符是["\n\n", "\n", " ", ""]。
除了控制separator可以拆分哪些字符外,我还可以通过以下参数来控制文本分割器的行为:
- length_function:用于计算块的长度的函数。默认情况下,只计算字符数量,但通常会传递一个标记计数器。
- chunk_size:块的最大大小(由长度函数测量)。
- chunk_overlap:块之间的最大重叠。保持一定的连续性(例如滑动窗口)可以很好地维护一些重叠。
除了使用文本分割器对文本进行分割外,langchain还介绍了其他几种方式来对大段文本进行切割。
https://docs.langchain.com/docs/components/chains/index_related_chains
stuff: Stuffing 方法是最简单的方法,只需将所有相关数据作为上下文放入提示中,以传递给语言模型。在 LangChain 中实现为 StuffDocumentsChain。
优点:只需要向 LLM 进行一次调用。在生成文本时,LLM 可以一次性访问所有数据。
缺点:大多数 LLM 都有一个上下文长度,对于大型文档(或许多文档),这种方法将不起作用,因为它会导致提示大于上下文长度。
map_reduce: 这种方法涉及对每个数据块运行初始提示(对于摘要任务,这可能是该块的摘要;对于问答任务,这可能是仅基于该块的答案)。然后,运行一个不同的提示来组合所有初始输出。这在 LangChain 中实现为 MapReduceDocumentsChain。
优点:可扩展到比 StuffDocumentsChain 更大的文档(和更多的文档)。对单个文档的 LLM 调用是独立的,因此可以并行化。
缺点:需要比 StuffDocumentsChain 更多的 LLM 调用。在最终的合并调用过程中会丢失一些信息。
refine: 这种方法涉及在第一个数据块上运行初始提示,生成一些输出。对于其余文档,将传递该输出以及下一个文档,并要求LLM基于新文档来改进输出。
优点:可以提取更相关的上下文,比 MapReduceDocumentsChain 的信息丢失可能更少。
缺点:需要比 StuffDocumentsChain 更多的 LLM 调用。这些调用也不是独立的,意味着不能像 MapReduceDocumentsChain 那样并行化。文档的排序也可能存在一些潜在的依赖关系。‘
Map-rerank: 这种方法涉及在每个数据块上运行初始提示,它不仅尝试完成任务,还为其答案的确定性打分。然后根据这个得分对响应进行排序,并返回最高得分。
优点:类似于 MapReduceDocumentsChain。与 MapReduceDocumentsChain 相比,需要较少的调用。
缺点:不能在文档之间合并信息。这意味着当你期望在单个文档中有单一简单的答案时,它最有用。
本次用来做测试的是西游记,这种大段的文本通过map_reduce或者refine 的效果会更好一些,因为是小说,有上下文的含义。不过这里为了方便展 , 直接按照 100的长度进行分割打入数据库了。
不同的业务场景需要根据实际的需求来选择不同的方式进行分割文本,这样才可以达到比较好的效果。