本期是概述,主要就是概述整个项目,我这里直接给出本期的目录吧:
为什么要讲解这个项目
相比于论文中的对RAG的探索,实践更讲求实用性和全链路的完整性,类似文档处理、精排等逻辑在现实中的使用,一个很快的学习方式就是看开源项目,随着RAG项目的逐渐成熟,完整的开源项目也逐渐变多,包括langchain在内,类似Qanything、RAGflow、fastRAG都是一些被人们所提及比较好的项目了,本次选择了Qanything单纯是看了一段时间了(早在),所以来补坑。
Qanything是一个比较综合完整的开源RAG项目,其完整程度在我的视角是在这几个方面:
- RAG全流程都具备,从文件上传、处理到在线推理、排序等,关键模块都是有的,而且中文注释,注释和文档也比较完善,所以很适合学习。
- 包括完整的前后端体系(源码里有看到npm、webpack之类的关键词,相信这块做的也并不含糊)。
- 部署支持多环境和多种功能,从他多个文档里可以看到,支持linux、windows、Mac等多个环境,而且也有很丰富的教程。
因此,我的评价还是挺高的,特此给大家分享一下。也希望大家在我讲解的帮助下,能快速了解这个项目。
项目内代码结构概述
因为整个项目要适配多平台,而且RAG本身是一个完整的系统,所以有大量算法外围的工作,如好几个中间件如milvus,es,mysql,同时有docker、windows等环境的配置教程,还是比较完善的,先来看一眼根目录中的内容。
|--Dockerfile#docker构造文件
|--FAQ.md#常见问题回答
|--FAQ_zh.md#常见问题回答
|--LICENSE#证书
|--README.md#README文档
|--README_zh.md#README文档
|--assets#一些模型之类的文件会放在这里
|--close.sh#关闭脚本
|--docker-compose-linux.yaml#docker构造所需要的yaml文件,linux专用
|--docker-compose-windows.yaml#docker构造所需要的yaml文件,windows专用
|--docs#各种使用文档
|--etc#其他文件,此处有一个prompt
|--qanything_kernel#qanything的核心带代码,基本是python,包括服务、算法等算法
|--requirements.txt#python依赖
|--run.sh#启动脚本
|--scripts#其他脚本
|--third_party#三方库
`--volumes#数据库容器的外部映射地址
这里可以看到:
- 根目录下的内容主要集中在服务层面的内容,如docker、服务的启停(run/close)、各种文档(README、docs)等。
- 各种三方的资源似乎都有,可以借此学习一下各种相关组件的知识。
- 算法的核心代码应该是在qanything_kernel里面。
然后来看qanything_kernel中的文件是什么样的。
|--__init__.py#空的
|--configs#这里是模型和服务的配置,还有prompt之类的
|--connector#中间件的连接工具
|--core#milvus、es、mysql的都在里面,大家其实都能参考着用的
|--dependent_server#独立服务,这里主要是放大模型服务、ocr服务(图片文字抽取)、重排模型服务的
|--qanything_server# qanything的核心服务,也是对外的服务,另外还有一些js文件和图片素材,应该是给前端用的。
`--utils#零散的代码工具。这里可以说是宝藏了,里面挺多工具函数自己平时都能用的,类似safe_get等,另外很多文件处理的工具,类似文件加载、切片等,都在里面。
这一层就基本到服务内部了,挺多核心的东西都在这里的。
服务结构
服务结构单独开一章节,详细聊聊里面的内容,通过这个也可以看出完整的RAG系统可能包含什么部分,这个从服务启动的脚本里面能逐步找到的。
首先是服务启动,找到run_for_local_option.sh这个东西,因为本身是python服务,所以启动的时候大都避不开命令python,排除掉一些在线请求的服务,只考虑local模式,并过滤一些大概可以找到这几个命令:
compute_capability=$(python3scripts/get_cuda_capability.py$gpu_id1)
nohuppython3-ullm_server_entrypoint.py--host="0.0.0.0"--port=36001--model-path="tokenizer_assets"--model-url="0.0.0.0:10001">/workspace/qanything_local/logs/debug_logs/llm_server_entrypoint.log2>&1&
nohuppython3-uqanything_kernel/dependent_server/rerank_for_local_serve/rerank_server.py>/workspace/qanything_local/logs/debug_logs/rerank_server.log2>&1&
CUDA_VISIBLE_DEVICES=$gpu_id2nohuppython3-uqanything_kernel/dependent_server/ocr_serve/ocr_server.py>/workspace/qanything_local/logs/debug_logs/ocr_server.log2>&1&
nohuppython3-uqanything_kernel/qanything_server/sanic_api.py--mode"local">/workspace/qanything_local/logs/debug_logs/sanic_api.log2>&1&
这里每行的动作,是干了这些事:
scripts/get_cuda_capability.py:检测cuda可用性llm_server_entrypoint.py:启动大模型服务(模型中转,即调接口的),如果要本地部署,还需要用fastchat启动大模型服务。ependent_server/rerank_for_local_serve/rerank_server.py:重排序服务dependent_server/ocr_serve/ocr_server.py:ocr服务qanything_server/sanic_api.py:核心sanic服务
这里看起来,服务的拆分还是比较简单的,但从接口层面,可能就会比较复杂,我们进一步看qanything_kernel\qanything_server\sanic_api.py,直接看这段代码:
app.add_route(document,"/api/docs",methods=['GET'])
app.add_route(new_knowledge_base,"/api/local_doc_qa/new_knowledge_base",methods=['
OST'])#tags=["新建知识库"]
app.add_route(upload_weblink,"/api/local_doc_qa/upload_weblink",methods=['
OST'])#tags=["上传网页链接"]
app.add_route(upload_files,"/api/local_doc_qa/upload_files",methods=['
OST'])#tags=["上传文件"]
app.add_route(local_doc_chat,"/api/local_doc_qa/local_doc_chat",methods=['
OST'])#tags=["问答接口"]
app.add_route(list_kbs,"/api/local_doc_qa/list_knowledge_base",methods=['
OST'])#tags=["知识库列表"]
app.add_route(list_docs,"/api/local_doc_qa/list_files",methods=['
OST'])#tags=["文件列表"]
app.add_route(get_total_status,"/api/local_doc_qa/get_total_status",methods=['
OST'])#tags=["获取所有知识库状态"]
app.add_route(clean_files_by_status,"/api/local_doc_qa/clean_files_by_status",methods=['
OST'])#tags=["清理数据库"]
app.add_route(delete_docs,"/api/local_doc_qa/delete_files",methods=['
OST'])#tags=["删除文件"]
app.add_route(delete_knowledge_base,"/api/local_doc_qa/delete_knowledge_base",methods=['
OST'])#tags=["删除知识库"]
app.add_route(rename_knowledge_base,"/api/local_doc_qa/rename_knowledge_base",methods=['POST'])#tags=["重命名知识库"]
可以看到,这里的服务接口远比实际想的还要多,这里绝大部分是文档相关的接口,包括文档多个类型的加载、查询、删除,至于这里类似document、new_knowledge_base的定义,都在隔壁的qanything_kernel\qanything_server\handler.py里面(说实话我个人并不喜欢from XXX import *的模式,写起来简单,但是不利于阅读和后续的开发,包括后面可能重名、文件重复之类的,非常不好找,不过见仁见智因地制宜各有取舍吧)。
__all__=["new_knowledge_base","upload_files","list_kbs","list_docs","delete_knowledge_base","delete_docs",
"rename_knowledge_base","get_total_status","clean_files_by_status","upload_weblink","local_doc_chat",
"document"]
这就是所有的接口。
而这里比较重要的,当然就是/api/local_doc_qa/local_doc_chat这个问答接口了,在线推理主要就是请求这个来实现的。当然了,这里有关上传文件、链接之类的功能,也非常重要,在后续的篇章里,我会谈及,重点分别在“文件处理”、“在线推理”这两步上,敬请期待。
算法之外的细节
本期还是重点聚焦在算法以外的事吧,通过对整体项目多个细节的学习,能让我们的技术栈更加完整吧。
算法服务sanic
sanic似乎不是一个新东西了,qanything用的版本,根据requirements使用的是sanic==23.6.0。
- 官方文档:https://sanic.readthedocs.io/en/stable/
通过这玩意,就能把一个模块包装成服务,供别人随时请求使用了,就这个功能而言,和fastapi(这个的推荐度也比较高)、tornado之类的还是比较类似的。
这里以一个最简单的接口来解释一下。首先是需要定义一个接口,在qanything项目内就是一个handler,写在qanything_kernel\qanything_server\handler.py里面。这是其中一个最简单的,展示基本文档的接口。
fromsanicimportrequest
fromsanic.responseimporttextassanic_text
asyncdefdocument(req:request):
description="""
#QAnything介绍
...
"""
returnsanic_text(description)
有了这个接口后,在服务的启动中,增加这个接口即可。
fromsanicimportSanic
app=Sanic("QAnything")
app.add_route(document,"/api/docs",methods=['GET'])
app.run(host='0.0.0.0',port=8777,workers=10,access_log=False)
这样就完成了一个接口的编写,还是比较简单的。
具体详细的使用方法,包括POST请求、流式等,具体可以详细看看项目,这些都是有的。
大模型部署
大模型服务这里使用了两种方案,一种是直接用sanic做中转站的方案,一种是使用fastchat来进行。
先把整块部署代码的启动脚本搬出来,比较长,可以直接翻到最后面看解析。
if["$runtime_backend"="default"];then
echo"ExecutingdefaultFastTransformerruntime_backend"
#startllmserver
#判断一下,如果gpu_id1和gpu_id2相同,则只启动一个triton_server
if[$gpu_id1-eq$gpu_id2];then
echo"Thetritonserverwillstarton$gpu_id1GPU"
CUDA_VISIBLE_DEVICES=$gpu_id1nohup/opt/tritonserver/bin/tritonserver--model-store=/model_repos/QAEnsemble--http-port=10000--grpc-port=10001--metrics-port=10002--log-verbose=1>/workspace/qanything_local/logs/debug_logs/llm_embed_rerank_tritonserver.log2>&1&
update_or_append_to_env"RERANK_PORT""10001"
update_or_append_to_env"EMBED_PORT""10001"
else
echo"Thetritonserverwillstarton$gpu_id1and$gpu_id2GPUs"
CUDA_VISIBLE_DEVICES=$gpu_id1nohup/opt/tritonserver/bin/tritonserver--model-store=/model_repos/QAEnsemble_base--http-port=10000--grpc-port=10001--metrics-port=10002--log-verbose=1>/workspace/qanything_local/logs/debug_logs/llm_tritonserver.log2>&1&
CUDA_VISIBLE_DEVICES=$gpu_id2nohup/opt/tritonserver/bin/tritonserver--model-store=/model_repos/QAEnsemble_embed_rerank--http-port=9000--grpc-port=9001--metrics-port=9002--log-verbose=1>/workspace/qanything_local/logs/debug_logs/embed_rerank_tritonserver.log2>&1&
update_or_append_to_env"RERANK_PORT""9001"
update_or_append_to_env"EMBED_PORT""9001"
fi
cd/workspace/qanything_local/qanything_kernel/dependent_server/llm_for_local_serve||exit
nohuppython3-ullm_server_entrypoint.py--host="0.0.0.0"--port=36001--model-path="tokenizer_assets"--model-url="0.0.0.0:10001">/workspace/qanything_local/logs/debug_logs/llm_server_entrypoint.log2>&1&
echo"Thellmtransferserviceisready!(1/8)"
echo"大模型中转服务已就绪!(1/8)"
else
echo"Thetritonserverforembeddingandrerankerwillstarton$gpu_id2GPUs"
CUDA_VISIBLE_DEVICES=$gpu_id2nohup/opt/tritonserver/bin/tritonserver--model-store=/model_repos/QAEnsemble_embed_rerank--http-port=9000--grpc-port=9001--metrics-port=9002--log-verbose=1>/workspace/qanything_local/logs/debug_logs/embed_rerank_tritonserver.log2>&1&
update_or_append_to_env"RERANK_PORT""9001"
update_or_append_to_env"EMBED_PORT""9001"
LLM_API_SERVE_CONV_TEMPLATE="$conv_template"
LLM_API_SERVE_MODEL="$model_name"
check_folder_existence"$LLM_API_SERVE_MODEL"
update_or_append_to_env"LLM_API_SERVE_PORT""7802"
update_or_append_to_env"LLM_API_SERVE_MODEL""$LLM_API_SERVE_MODEL"
update_or_append_to_env"LLM_API_SERVE_CONV_TEMPLATE""$LLM_API_SERVE_CONV_TEMPLATE"
mkdir-p/workspace/qanything_local/logs/debug_logs/fastchat_logs&&cd/workspace/qanything_local/logs/debug_logs/fastchat_logs
nohuppython3-mfastchat.serve.controller--host0.0.0.0--port7800>/workspace/qanything_local/logs/debug_logs/fastchat_logs/fschat_controller_7800.log2>&1&
nohuppython3-mfastchat.serve.openai_api_server--host0.0.0.0--port7802--controller-addresshttp://0.0.0.0:7800>/workspace/qanything_local/logs/debug_logs/fastchat_logs/fschat_openai_api_server_7802.log2>&1&
gpus=$tensor_parallel
if[$tensor_parallel-eq2];then
gpus="$gpu_id1,$gpu_id2"
else
gpus="$gpu_id1"
fi
case$runtime_backendin
"hf")
echo"Executinghfruntime_backend"
CUDA_VISIBLE_DEVICES=$gpusnohuppython3-mfastchat.serve.model_worker--host0.0.0.0--port7801\
--controller-addresshttp://0.0.0.0:7800--worker-addresshttp://0.0.0.0:7801\
--model-path/model_repos/CustomLLM/$LLM_API_SERVE_MODEL--load-8bit\
--gpus$gpus--num-gpus$tensor_parallel--dtypebfloat16--conv-template$LLM_API_SERVE_CONV_TEMPLATE>/workspace/qanything_local/logs/debug_logs/fastchat_logs/fschat_model_worker_7801.log2>&1&
;;
"vllm")
echo"Executingvllmruntime_backend"
CUDA_VISIBLE_DEVICES=$gpusnohuppython3-mfastchat.serve.vllm_worker--host0.0.0.0--port7801\
--controller-addresshttp://0.0.0.0:7800--worker-addresshttp://0.0.0.0:7801\
--model-path/model_repos/CustomLLM/$LLM_API_SERVE_MODEL--trust-remote-code--block-size32--tensor-parallel-size$tensor_parallel\
--max-model-len4096--gpu-memory-utilization$gpu_memory_utilization--dtypebfloat16--conv-template$LLM_API_SERVE_CONV_TEMPLATE>/workspace/qanything_local/logs/debug_logs/fastchat_logs/fschat_model_worker_7801.log2>&1&
;;
"sglang")
echo"Executingsglangruntime_backend"
;;
*)
echo"Invalidruntime_backendoption";exit1
;;
esac
fi
先忽略这里有关rank之类模型部署的内容。
首先是sanic方案,qanything_kernel\dependent_server\llm_for_local_serve\llm_server_entrypoint.py里面,这只是一个中转服务,内部只是在请求QwenTritonModel,调用的逻辑在qanything_kernel\dependent_server\llm_for_local_serve\modeling_qwen.py,不过在代码内没有找到具体模型部署的代码,从请求代码来看,像是有一个grpc接口。
然后是fastchat方案,在else下,代码比较详尽的是runtime_backend为hf和vllm的模式。具体的部署,都是围绕着fastchat进行的(https://github.com/lm-sys/FastChat),这个工具目前看还比较方便的,直接命令式就能做了。
小模型部署
这里涉及的小模型还不少。
首先是向量模型、排序模型,基本的模式都是triton启服务,然后再启一个服务做中转,至少向量模型和排序模型都是,类似下面这样。
CUDA_VISIBLE_DEVICES=$gpu_id2nohup/opt/tritonserver/bin/tritonserver--model-store=/model_repos/QAEnsemble_embed_rerank--http-port=9000--grpc-port=9001--metrics-port=9002--log-verbose=1>/workspace/qanything_local/logs/debug_logs/embed_rerank_tritonserver.log2>&1&
nohuppython3-uqanything_kernel/dependent_server/rerank_for_local_serve/rerank_server.py>/workspace/qanything_local/logs/debug_logs/rerank_server.log2>&1&
rerank_server后面会请求grpc服务,具体的请求则写在了qanything_kernel\dependent_server\rerank_for_local_serve\rerank_server_backend.py里面。
比较特殊的是OCR模型,内部用的是PaddleOCR,这个直接包了层sanic服务来实现的。
CUDA_VISIBLE_DEVICES=$gpu_id2nohuppython3-uqanything_kernel/dependent_server/ocr_serve/ocr_server.py>/workspace/qanything_local/logs/debug_logs/ocr_server.log2>&1&
中间件
首先是向量库milvus(https://milvus.io/),也支持python(pymilvus==2.3.4)。在qanything的代码中,给出了一个比较完整的客户端使用组件(qanything_kernel\connector\database\milvus\milvus_client.py),这个工具写的很完整,健壮性也很高,仔细研读和使用的可靠性都很高。
至于ES,也就是elasticsearch(项目中用的版本应该是8.11.4),应该是做搜索引擎的鼻祖了,这已经非常成熟,同样支持python客户端的(pip install elasticsearch),qanything_kernel\connector\database\milvus\es_client.py是一个不错的工具,此处ES主要用于字面的检索,在milvus_client.py中有提及字面和向量混合搜索的功能,默认是只支持向量检索,如果要混合检索,就会请求一次ES。
def__search_emb_sync(self,embs,expr='',top_k=None,client_timeout=None,queries=None):
ifnottop_k:
top_k=self.top_k
milvus_records=self.sess.search(data=embs,partition_names=self.kb_ids,anns_field="embedding",
param=self.search_params,limit=top_k,
output_fields=self.output_fields,expr=expr,timeout=client_timeout)
milvus_records_proc=self.parse_batch_result(milvus_records)
#debug_logger.info(milvus_records)
#混合检索
ifself.hybrid_search:
es_records=self.client.search(queries)#补一个client的定义:self.client = ElasticsearchClient(index_name=self.index_name)
es_records_proc=self.parse_es_batch_result(es_records,milvus_records)
milvus_records_proc.extend(es_records_proc)
returnmilvus_records_proc
值得提醒的是,ES在比较新的版本其实也支持构造向量索引了(也就是支持向量召回了),此处还使用milvus应该有别的原因吧。
最后就是mysql了,mysql在这里主要是起到了记录文档、知识管理、用户管理等作用,保持数据的一致性,核心代码在qanything_kernel\connector\database\mysql\mysql_client.py,这里的mysql没有做一个通用组件,而是直接定制化了一个类KnowledgeBaseManager,直接做文档、知识等信息的总结。
小结
qanything的内容太多,一篇文章不足以讲述,本期第一篇先给大家介绍这个项目,同时对里面的服务架构进行详细讲解,另外把非得算法,但是值得学习的内容,如算法服务、关键的中间件等,也进行了一定的解析,希望对大家有所帮助。