LangChain-ChatGLM
LangChain-ChatGLM
以 LLM 为基础的知识问答系统构建方法,其整体的流程如下图所示:
在整个流程中,核心在于:
- 将用户问题和本地知识进行 Embedding,通过向量相似度(Vector Similarity)实现召回;
- 通过 LLM 对用户问题进行意图识别;并对原始答案加工整合。
ChatGLM2-6B
ChatGLM2-6B 是开源中英双语对话模型 ChatGLM-6B 的第二代版本,在保留了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上,ChatGLM2-6B 引入了如下新特性:
- 更强大的性能:基于 ChatGLM 初代模型的开发经验,开发人员全面升级了 ChatGLM2-6B 的基座模型。ChatGLM2-6B 使用了 GLM 的混合目标函数,经过了 1.4T 中英标识符的预训练与人类偏好对齐训练,评测结果显示,相比于初代模型,ChatGLM2-6B 在 MMLU(+23%)、CEval(+33%)、GSM8K(+571%) 、BBH(+60%)等数据集上的性能取得了大幅度的提升,在同大小开源模型中具有较强的竞争力。
- 更长的上下文:基于 FlashAttention 技术,开发人员将基座模型的上下文长度(Context Length)由 ChatGLM-6B 的 2K 扩展到了 32K,并在对话阶段使用 8K 的上下文长度训练,允许更多轮次的对话。但当前版本的 ChatGLM2-6B 对单轮超长文档的理解能力有限,开发人员会在后续迭代升级中着重进行优化。
- 更高效的推理:基于 Multi-Query Attention 技术,ChatGLM2-6B 有更高效的推理速度和更低的显存占用:在官方的模型实现下,推理速度相比初代提升了 42%,INT4 量化下,6G 显存支持的对话长度由 1K 提升到了 8K。
LangChain
LangChain 抽象了业务应用和 LLM 交互的方式,内置通用环节实现,标准化工具链交互接口,极大提升开发效率。LangChain 的大致结构如下所示:
下面介绍一下LangChain中的重要模块:
- Schema:LangChain中的数据类型分为四种:Text,ChatMessages,Examples,Document。Text很好理解,整个LangChain都是文本进文本出的形式,因此Text是最常用的数据类型。ChatMessages分为三种,SystemChatMessage代表了开发人员定义的instruction,HumanChatMessage代表人类发送的信息,AIChatMessage代表LLM发送的信息。Examples代表的是输入和期望输出的对,可以用于微调或者评估等。Document代表了非结构化数据(例如txt文本形式的医学指南)。
- Models:LangChain中的模型有三种:LLMs,Chat Models,Text Embedding Models。LLMs很好理解,以文本字符串作为输入,并返回一个文本字符串作为输出。Chat Models通常由语言模型支持,但它们的API更加结构化。具体而言,这些模型以聊天消息列表作为输入,并返回一个聊天消息。Text Embedding Models以文本作为输入,并返回一个浮点数列表作为输出。
- Prompts:这个模块有四个类,分别如下:PromptValue是模型输入。PromptTemplates负责构建PromptValue,比如将固定的prompt部分和动态生成的内容结合到一起。在prompt中包含示例(few-shot)通常很有用,这些示例可以是硬编码的,但如果它们是动态选择的,通常会更加强大,Example Selectors就是这个作用,它是通过上面所说的embedding model实现的,也就是先把所有例子进行向量化处理,再把用户输入的问题向量化后和所有例子做相似度计算,最后选出相似度最高的例子作为few-shot的例子喂给LLMs。Output Parsers是输出解析器,负责:(1)指示模型如何格式化输出,(2)将输出解析为所需的格式(包括必要时的重试)。
- Indexes:Indexes(索引)是指对文档进行结构化,以便LLMs能够更好地与其进行交互的方式。该模块包含了用于处理文档、不同类型索引的实用函数,以及在chains中使用这些索引的示例。索引在chains中最常见的用法是在“检索”步骤中。该步骤指的是根据用户的查询返回最相关的文档。需要注意的是:(1)索引可以用于除检索之外的其他目的(2)检索可以使用除索引之外的其他逻辑来查找相关文档。这个模块共有4个类,分别如下:Document Loaders负责从各种来源加载文档。Text Splitters负责将文本拆分为较小块。VectorStores是最常见的索引类型,依赖于嵌入向量。Retrievers是用于获取相关文档并与语言模型组合的接口。
- Memory:Memory是在对话过程中存储和检索数据的概念。主要有两种方法:(1)基于输入,获取任何相关的数据(2)基于输入和输出,相应地更新状态。Memory主要分为两种类型:短期记忆和长期记忆。短期记忆通常涉及如何在单个对话的上下文中传递数据(通常是前面的ChatMessage或其摘要)。长期记忆处理如何在对话之间获取和更新信息。此外,memory有多种形式,比如全部存储,三元组存储,图存储,摘要存储等等。
- Chains:Chains是一个非常核心的模块,它将一系列模块化组件(或其他chains)以特定方式组合,以完成一个任务。最常用的chains类型是LLMChain,它将PromptTemplate、Model等组合在一起,以接收用户输入,相应地格式化输入,将其传递给模型并获取响应,然后对模型输出进行验证和修正(如果需要)。除此之外还有负责与Index交互的chain,选择prompt的chain,获取对话记录的chain,对文本进行摘要的chain等等。
- Agents:有些应用程序不仅需要对LLMs/其他工具进行调用的预先定义的链,还可能需要根据用户的输入生成未知的调用链。在这些类型的chains中,有一个agent,它可以访问一系列工具。根据用户的输入,agent可以决定是否需要调用这些工具。这一模块有四个部分:Tools定义了语言模型如何与其他资源进行交互。Agents是负责做决策的语言模型。Toolkits是一组tools,当它们一起使用时可以完成特定的任务。Agent Executor负责运行带有tools的agent。
例如,如果要完成”将文本翻译成其他语言”这个任务:
- 用户向Agent提出请求”请把这段英文翻译成中文”
- Agent会调用Translation chain(翻译模块链)
- Translation chain可能包含:
- 模块A:接收输入英文
- 模块B:执行英文到中文的机器翻译
- 模块C:进行中文修正
- 模块D:输出中文翻译结果
- 最后Agent会将Translation chain的输出结果(中文翻译)返回给用户
这样通过模块化的链条结构,可以方便地组合不同的能力,实现灵活的任务执行流程。Agent负责交互和决策,Chain负责任务执行。二者协同达成完整的对话AI能力。
除了整个LangChain的架构值得我们借鉴外,这个prompt也比较重要:
Agent 利用 LLM 的理解推理能力来简化原本十分复杂的逻辑判断,决定什么问题由什么工具来解决,最后汇总成一个“答案”返回给用户。langchain 把这个过程用一个非常经典的 Prompt 模板定义了出来这个 Prompt 结构贯穿了 Agent 在寻找答案过程中的每次调用,不管是寻求 LLM 的处理决定、还是处理 Tool 的调用结果、包括每次和 LLM 交互过程中的角色定义都可以定制,这些全部基于这一个 Prompt 模板完成。不过显然langchain的这个prompt是专门为chatgpt编写的,换为chatglm之后效果大打折扣,这个6B的基座模型似乎无法很好的理解指令,如果要使用我们自己训练的LLMs作为基座模型则需要通过更多测试找到合适的prompt。
现存问题总结
在整个部署及测试过程中,遇到了以下问题:
在通过计算向量相似度进行检索的时候,LangChain使用的是facebook团队开发的FAISS工具,而非更先进的专业向量数据库,因此检索速度上不如milvus。这个比较好解决,只需要将之前用milvus搭建的qabot的部分代码移植过来即可。
在将知识文本分成多个chunk的时候,选择了固定的分割长度,这会导致语义分离,如下图所示:
这一部分目前的想法是重写LangChain的textsplitter模块,不完全使用固定长度的切分方法。对于指南而言,天然具有分块的结构,可以使用启发式方法将两块文本之间用两个换行符分隔开来,这样在textsplitter中就可以按照两个换行符进行切分;而对于一些过于长的文本块(如测试指南中有长度4096的块)则需要按照最长长度进行划分。
因为token的限制,对知识文本进行划分是必要的,但是在将知识划分成chunks时,一个潜在的风险是一大段完整的文本被分割为多个文本后无法通过相似度检索全部召回或者两段语义不同的内容被整合到一个chunk中导致LLM理解混淆。亦如上图所示,此出处是一个完整的chunk,但是却将产时管理和肺表面活性物质的应用整合到了一个chunk中;又将本来都属于肺表面活性物质应用主题的文本中的第二点切分到了下一个chunk,致使模型输出治疗方案时并不完整。这个问题是一个很关键的问题,上面的例子其实证明了一个主题下的内容如果被切分到多个chunk,那么大概率无法通过相似度搜索进行召回(即使将阈值设定的很低也不靠谱,因为有可能将呼吸窘迫综合征和呼吸暂停综合征的治疗方案都召回了)。针对这个问题其实我只想到两个解决方案,第一个就是尽量放大token限制,在大模型广泛采用32K的情况下,1k的token确实太少了。第二种方案就是首先保证一个chunk中不会包含不同语义块的文本,然后为每个chunk赋予一个额外的索引,这个索引可以是关键词,可以是主题等等,然后对索引进行embedding,进行相似度检索,类似下面的表格:
key embedding index text 1 [0.123,0.456……] 内科,急性痛风,治疗/急性痛风发作的治疗 急性痛风发作的治疗有以下要点…… 2 [0.789,0.098……] 外科,烧伤,术后护理/烧伤的术后护理 烧伤的术后护理有以下要点…… 如何设计合适的prompt综合利用检索到的文本和LLM本身具有的知识来进行输出。以ChatGLM为例,它似乎无法完全理解人类的指令,在通过相似度进行向量检索得到相关知识后,我希望它可以根据知识回答问题,但是它并未做到,如下图所示,反观ChatGPT可以做的相对令人满意一些,这或许是ChatGLM没有见识到足够多样的instruction,我们可能需要在这里进行额外的针对性训练。通过多次实验,我找到了一个prompt模板,它可以让chatglm完全遵循提供的知识回答问题,但是没有任何自己的补充等,明显不像正常的人类交流,只是在搜索答案,如下图所示:
对于多知识点聚合的场景,直接对问题和知识文本进行向量相似度检索似乎不是一个有效的方法。首先,本地知识建立索引时,通常对单个知识点进行 Embedding;不会也不可能,为不同知识点的排列组合分别制作索引。此外,原始问题直接 Embedding ,和单条知识点的向量相似度比较低。为了避免召回结果有遗漏,就需要 降低 相似度评分下限(similarity threshold),同时提高召回结果数量上限(top k),这回产生极大的负面影响:(1)召回结果有效信息密度大幅降低;threshold 过高或 top k 过低,会导致某些有效知识点无法命中;反之,很多无效知识点或噪声会被引入。且由于总 token 数量的限制,导致本地知识点被截断,遗漏相似度较低但有效的知识点。(2)召回结果的膨胀,增加了和 LLM 交互的 token 开销;增加了 LLM 处理的时间复杂度。(3)给 LLM 的分析处理带来额外噪声,影响最终答案的正确性。
针对这个问题,我能想到的解决方案有如下两种:(1)关键词或者是主题提取(2)建立多级索引(3)二者皆使用。前者提取一个chunk中的关键词,后者是用树形目录的节点路径作为多级索引。多级索引对于我们的指南应当是比较容易建立的,根据疾病可以获取其所属的科室,然后根据指南中的目录获得一个chunk的路径。对于关键词的提取,有两种方法:(1)基于传统NLP的提取(实测HanLP效果可以接受)(2)基于LLM的提取,对于简单提问效果不错,对于复杂提问效果不行。
任重而道远啊。。。。。。