深入 Dify 源码,洞察 Dify RAG 核心机制

易迟 2024-08-17 15:31:03 阅读 82

背景介绍

之前深入源码对 Dify 的 完整流程 进行了解读,基本上梳理了 Dify 的实现流程与主要组件。

但是在实际部署之后,发现 Dify 现有的 RAG 检索效果没有那么理想。因此个人结合前端页面,配置信息与实现流程,深入查看了私有化部署的 Dify 的技术细节。

将核心内容整理在这边,方便大家根据实际的业务场景调整 Dify 知识库的配置,或者根据需要进行二次开发调优。

技术细节

本次重点介绍的是 Dify docker 私有化部署 默认情况的技术方案,个人本地部署使用的 Dify + Xinference,感兴趣可以查看 Dify 与 Xinference 最佳组合 GPU 环境部署全流程。

RAG 涉及的核心流程如下所示:

请添加图片描述

下面对各个模块的机制与优化方案进行深入介绍。

Extractor

<code>Extractor 对应的 Dify 中的文件解析模块,用于从原始文件中提取文字内容。核心的文件解析流程在 api/core/rag/extractor/extract_processor.py 中实现。

从用户视角来看,目前存在两类解析方案:

基于 Unstructured 的文件解析方案,支持接入付费的 Unstructured 服务,部分的格式解析只有付费版本才支持,比如 .ppt;常规的 Dify 文件解析方案,基于开源库进行文件解析;

目前私有化部署时默认使用的是 Dify 文件解析方案,可以通过修改 .env 配置调整为 Unstructured 解析,但是使用 Unstructured 解析时需要配置对应的 Unstructured API 和 API Key,对应的配置如下所示:

// 文件解析类型,可以设置为 dify 或 Unstructured

ETL_TYPE=dify

UNSTRUCTURED_API_URL=

UNSTRUCTURED_API_KEY=

如果希望调整为 Unstructured 解析,可以修改 ETL_TYPE 字段。考虑到 Unstructured 解析可能需要付费,一般情况下可能直接使用 Dify 解析方案就好。可以看看默认 Dify 解析方案下的具体解析实现:

pdf 目前的解析是基于 pypdfium2 直接提取内容;html 目前的解析是基于 BeautifulSoup 简单提取内容;

都属于极其常规的开源方案,如果希望具备更好的效果,可以考虑针对特定类型实现自定义的解析方案。

Cleaner

Cleaner 对应的解析内容的清洗模块,可以去除无关的字符,减少后续 token 的消耗。最核心的清洗流程在 api/core/rag/cleaner/clean_processor.py 中,目前主要是基于特定规则进行过滤:

def clean(cls, text: str, process_rule: dict) -> str:

# 默认清洗,必然会执行

text = re.sub(r'<\|', '<', text)

text = re.sub(r'\|>', '>', text)

text = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\xEF\xBF\xBE]', '', text)

text = re.sub('\uFFFE', '', text)

rules = process_rule['rules'] if process_rule else None

# 基于预定义的规则选择是否执行

if 'pre_processing_rules' in rules:

pre_processing_rules = rules["pre_processing_rules"]

for pre_processing_rule in pre_processing_rules:

# 是否清理无关的空格和换行符

if pre_processing_rule["id"] == "remove_extra_spaces" and pre_processing_rule["enabled"] is True:

pattern = r'\n{3,}'

text = re.sub(pattern, '\n\n', text)

pattern = r'[\t\f\r\x20\u00a0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]{2,}'

text = re.sub(pattern, ' ', text)

# 是否清理 emails 和 urls

elif pre_processing_rule["id"] == "remove_urls_emails" and pre_processing_rule["enabled"] is True:

# 清理 email

pattern = r'([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)'

text = re.sub(pattern, '', text)

# 清理 URL

pattern = r'https?://[^\s]+'

text = re.sub(pattern, '', text)

return text

目前支持的内容清洗主要是清理空格换行符以及清理 Email 和 Url,默认情况下会清理空格换行符,但是不会开启清理 Email 和 Url。用户可以在前端页面进行调整:

请添加图片描述

Splitter

<code>Splitter 对应的是文件的分片机制,合适的分片可以避免文件块信息的不完整。目前 Dify 中实现的 Splitter 策略都在 api/core/rag/splitter 中。

实际 Dify 服务中主要使用下面两种策略分片:

EnhanceRecursiveCharacterTextSplitter, 递归查看字符来分割文本,支持基于特定的分隔符进行分割,类似 langchain 中的方案;FixedRecursiveCharacterTextSplitter, 同样是递归查看字符分割文本,但是可以支持用户指定分段标识符和分段最大长度等信息

两种分片机制其实差异不大,下面的这种灵活性更强一些。用户在前端分段设置中的两种分段选择对应的就是上面的不同机制:

请添加图片描述

自动分段与清洗对应的就是 <code>EnhanceRecursiveCharacterTextSplitter, 自定义对应的就是 FixedRecursiveCharacterTextSplitter

Retrieval

在前面的 定位 Dify 知识库检索的调用异常 文章中介绍过,目前 Dify 支持四种模式:

关键词检索向量检索全文检索混合检索

其中关键词检索对应的就是知识库检索页面的经济模式:

请添加图片描述

关键词检索是从原始文字中提取的关键词进行匹配,因此不需要需要模型进行处理,效率更高,但是一般情况下效果会更差,因为只能严格按照关键词进行匹配。

而其他三种检索方式对应的是页面中的高质量模式,用户可以在页面中选择一种:

请添加图片描述

这三种检索使用的都是向量数据库自身提供的能力,这种模式下分块的文本都是直接存入向量数据库。部分向量库不支持全文检索,这种情况下调用全文检索会直接返回空列表。

Dify 目前默认使用的向量数据库为 <code>weaviate, 如果需要可以修改 .env 配置:

# 可选 `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `chroma`, `opensearch`, `tidb_vector`, `oracle`, `tencent`

VECTOR_STORE=weaviate

目前默认的 weaviate 向量数据库同时支持向量检索和全文检索。如果希望调整为其他向量库,建议先确认下 Dify 中的实现是否支持了全文检索,不支持的情况下可能会导致检索效果很差。

截止 2024-7-17,Dify 中现有向量库支持全文检索的情况统计如下:

向量库 是否支持全文检索
weaviate
qdrant
milvus ×
myscale
relyt ×
pgvector ×
chroma ×
opensearch
tidb_vector ×
oracle ×
tencent ×
Rerank

Rerank 就是对检索的结果进行重新排序,重排序的效果与 Rerank 模型和对应的配置有关,用户可以在检索设置页进行设置:

请添加图片描述

可以看到除了模型设置之外,目前需要配置 Top K 和 score 阈值两个参数。

页面上的这两个参数事实上存在一些歧义,可以是检索阶段获取 Top K 和 score 过滤,也可能是 Rerank 后选择 Top K 和 score 过滤。从源码来看是哪种情况:

<code>def embedding_search(cls, flask_app: Flask, dataset_id: str, query: str,

top_k: int, score_threshold: Optional[float], reranking_model: Optional[dict],

all_documents: list, retrival_method: str, exceptions: list):

with flask_app.app_context():

try:

dataset = db.session.query(Dataset).filter(

Dataset.id == dataset_id

).first()

vector = Vector(

dataset=dataset

)

# 检索阶段基于 top_k 和 score_threshold 进行过滤

documents = vector.search_by_vector(

query,

search_type='similarity_score_threshold',code>

top_k=top_k,

score_threshold=score_threshold,

filter={

'group_id': [dataset.id]

}

)

if documents:

if reranking_model and retrival_method == RetrievalMethod.SEMANTIC_SEARCH.value:

data_post_processor = DataPostProcessor(str(dataset.tenant_id), reranking_model, False)

# 重排序阶段基于 score_threshold 进行过滤,top_n 与文档数量相等,不会用于过滤

all_documents.extend(data_post_processor.invoke(

query=query,

documents=documents,

score_threshold=score_threshold,

top_n=len(documents)

))

else:

all_documents.extend(documents)

except Exception as e:

exceptions.append(str(e))

根据上面的代码,可以确认用户设置的 top_k 是在检索阶段进行过滤,score 阈值会在检索和重排序阶段同时用于过滤。

从目前来看,我觉得这个设计是有明显问题的:

检索模型和重排序模型可能是不同厂商的模型,模型的分值分布差异可能会特别大,使用同样阈值过滤是不合适的。在 从《红楼梦》的视角看大模型知识库 RAG 服务的 Rerank 调优 文章中测试过,bge-reranker-large 和 bce-reranker-base_v1 模型的分值分布就差异很大,需要适配不同的阈值,强制检索和重排序使用同样阈值很有可能会导致效果不佳;重排序阶段不过滤 top K,只在检索阶段过滤同样是有问题的。因为检索阶段的排序是不准确,仅基于检索阶段过滤 top K 可能也是不准确的。事实上,如果 top K 设置较小,可能会导致选择错误的文档;如果设置较大的 top K,因为重排序后不过滤,很有可能上下文中文档过多导致效果不佳。Rerank 做得比较深入的有道 建议,检索阶段获取 top 50~100, 重排序获取 top5~10,也说明需要支持独立配置。

这一块可能需要 Dify 进行有针对性的改进。

总结

文章对 Dify 中使用 RAG 的技术细节进行梳理,整体而言,Dify 提供了一个基础版本的 RAG 服务,整体的设计和实现都比较清晰,但是效果上没有做过多的优化。

如果希望能获得更好的效果,可以通过部分配置进行调整。如果对 RAG 的效果要求较高,预期可能会需要进行有针对性的二次开发优化。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。