QAnything源码学习
以下解读基于时间:20241218
概述
官方架构图如下:
该有的模块基本上都有了:
- Query理解
- 检索
-
- 召回
- 重排
- 大模型生成
- 数据入库
下面就从以上几个模块分别看看对应的源码
讲源码之前还是想先讲讲这个项目的目录结构,这样可能会更方便理解一点。主逻辑源码主要在qanything_kernel中
主逻辑源码分布 | 所有的配置都在这里 qanything_kernel/configs/model_config.py |
主要提供了一些必要的中间组件,不涉及主流程逻辑,但是为主流程提供了可用的各种组件。包括存储、向量化、大模型、重排功能 | 从目录名就可以看出来,这是核心模块,是RAG全流程的具象化。着重关注 |
一些独立的微服务,例如embedding和rerank服务分别在connector中被封装成具有相应功能的客户端 | 主服务,没啥好讲的,大部分用户在界面上需要用到的功能对应的路由都在这里 |
Query理解
query理解这块理论上能做的还是比较多的,包括意图识别、query改写等
从源码来看,qanything目前应该是只做了一次query改写。具体的代码在qanything_kernel/core/chains/condense_q_chain.py
,这里主要是将一个聊天中的用户问题基于聊天历史改写为一个独立且语义完整的query,这样做首先肯定能降低检索的难度,其次当聊天历史较长时即便丢弃早期的历史依然不会出现当前query不能很好理解的问题。具体的调用在qanything_kernel/core/local_doc_qa.py
(这个文件比较关键,其中还包含了检索、重排等核心组件的调用)
定义 qanything_kernel/core/chains/condense_q_chain.py | 调用 qanything_kernel/core/local_doc_qa.py |
检索
召回
主要用了ES基于BM25的字面相似度以及使用Milvus的Embedding相似度做召回也就是架构图中的1st Retrieval,一般来说这两路召回作为一个baseline已经够了,后续可以基于业务场景添加更多路召回
具体的实现和调用如下,ES这块比较简单,langchain有直接可用的组件,直接用就完事了。
定义 qanything_kernel/core/retriever/elasticsearchstore.py | 调用 qanything_kernel/core/retriever/parent_retriever.py |
Milvus的话,qanything_kernel/core/retriever/vectorstore.py
中实现了一个供retrieval使用的客户端,以及继承自langchain_community的Milvus类复写了embedding化文本添加到数据库的aadd_texts方法。并且依赖于一个YouDaoEmbeddings类 (qanything_kernel/connector/embedding/embedding_for_online_client.py
) ,这个类依赖于独立启动的embedding微服务 (qanything_kernel/dependent_server/embedding_server/embedding_server.py
) 并提供了一些embedding文本的方法。
Milvus客户端 qanything_kernel/core/retriever/vectorstore.py | Milvus的复写 qanything_kernel/core/retriever/vectorstore.py |
给整个服务提供embedding能力 qanything_kernel/connector/embedding/embedding_for_online_client.py | 独立的embedding服务 qanything_kernel/dependent_server/embedding_server/embedding_server.py |
retrieval中的调用 qanything_kernel/core/retriever/parent_retriever.py | 合并es和milvus向量检索的结果 qanything_kernel/core/retriever/parent_retriever.py |
由于还要做模型层面的重排,所以这里就是直接将两路召回的结果放到一个列表里,并没有做任何的简单的排序。作为baseline的话,其实rerank也可以先不做,可以直接基于一些规则做个简单的得分加权排个序就行,或者使用RRF也行
重排
重排这块相对来说比较简单清晰,首先定义定义了一个YouDaoRerank (qanything_kernel/connector/rerank/rerank_for_online_client.py
),然后这个client依赖于一个独立的rerank服务 (qanything_kernel/dependent_server/rerank_server/rerank_server.py
),这个服务的能力来源于部署的onnx模型(qanything_kernel/dependent_server/rerank_server/rerank_onnx_backend.py
),这个rerank模型不同于召回的模型,它应该是交互式的不同于召回的双塔分别编码。
rerank客户端 | rerank独立服务 |
rerank模型后端 | retrieval中的调用 |
大模型生成
这块就整体来说感觉是最简单的,将前面排序好的文档放进来结合prompt给出结果就行。来看看qanything具体还有哪些细节,步骤如下:
- 看看query有没有和faq匹配上,如果匹配上了直接返回faq里的答案,不经过大模型了
- 如果没有匹配上faq,再看有没有检索到文档,对应着不同的prompt模版
- 完了就是调用大模型生成答案了
数据入库
这一步和在线问答服务是解耦的,当你上传文件时,服务只是将文件的元信息存到数据库,并将文件保存到本地。然后由这个数据结构化入库服务将本地文件进行chunk,依次保存到milvus和es中
数据chunk入库 qanything_kernel/core/retriever/parent_retriever.py |
小结
总的来说,感觉qanything还是比较好读的,各模块之间比较解耦,魔改复用应该也比较方便,唯一不太好读的就是检索部分,因为用了langchain,我对langchain非常不熟悉,所以它的一些类的怎么使用,要复写哪些方法都不是很清楚,而且有些具体的逻辑层层追溯看起来有点晕。不过这些细节不影响整体的理解和阅读。rerank部分就比较友好了,都是纯手敲的,没有借助三方工具。