Dify知识库检索与工作流召回配置详解

873 阅读7分钟

此文章来自海外技术部 - 钟杏锋

一、背景

dify中内置了知识库,知识库高质量索引模式下支持“检索设置”。当在工作流中使用该知识库时也可以对引用的知识库进行“召回设置”。知识库的检索方式配置和工作流中的召回设置比较相像。我们也常常会比较疑惑这两个配置之间是什么关系。了解这两种配置的区别和联系对于优化知识库检索效果至关重要。

核心概念澄清:

  • "检索设置":是知识库自身的配置,决定知识如何被索引和初步检索。
  • "召回设置":是工作流中使用知识库时的重排(Rerank)配置,依赖于检索设置提供的初步结果。

二、知识库检索配置

1. 索引模式

Dify知识库有两种索引模式:经济模式和高质量模式,这是知识库建立的基础。

2. 经济模式

经济模式,不会产生额外的服务费用的模式,不论在上传知识还是检索知识时都可以利用本地支持的关键词“倒排索引”来实现。

image.png

实现原理

# 检索代码实现: dify/api/core/rag/datasource/keyword/jieba/jieba.py
    def search(self, query: str, **kwargs: Any) -> list[Document]:
        # 获取所有倒排索引
        keyword_table = self._get_dataset_keyword_table()
         
        k = kwargs.get("top_k", 4)
        document_ids_filter = kwargs.get("document_ids_filter")
        sorted_chunk_indices = self._retrieve_ids_by_query(keyword_table or {}, query, k)
        ...
 
    def _retrieve_ids_by_query(self, keyword_table: dict, query: str, k: int = 4):
        # 提取句子关键字
        keyword_table_handler = JiebaKeywordTableHandler()
        keywords = keyword_table_handler.extract_keywords(query)
 
        # 遍历倒排索引,得出命中关键词最多的知识
        # go through text chunks in order of most matching keywords
        chunk_indices_count: dict[str, int] = defaultdict(int)
        keywords_list = [keyword for keyword in keywords if keyword in set(keyword_table.keys())]
        for keyword in keywords_list:
            for node_id in keyword_table[keyword]:
                chunk_indices_count[node_id] += 1
 
        sorted_chunk_indices = sorted(
            chunk_indices_count.keys(),
            key=lambda x: chunk_indices_count[x],
            reverse=True,
        )
 
        return sorted_chunk_indices[:k]
 
# 对检索出来的知识进行打分: dify/api/core/rag/retrieval/dataset_retrieval.py
def calculate_keyword_score(self, query: str, documents: list[Document], top_k: int) -> list[Document]:
    # TF-IDF算法实现
    # 计算所有关键词的IDF值
    keyword_idf = {}
    for keyword in all_keywords:
        doc_count_containing_keyword = sum(1 for doc_keywords in documents_keywords if keyword in doc_keywords)
        keyword_idf[keyword] = math.log((1 + total_documents) / (1 + doc_count_containing_keyword)) + 1
         
    # 计算相似度
    def cosine_similarity(vec1, vec2):
        intersection = set(vec1.keys()) & set(vec2.keys())
        numerator = sum(vec1[x] * vec2[x] for x in intersection)
        sum1 = sum(vec1[x] ** 2 for x in vec1)
        sum2 = sum(vec2[x] ** 2 for x in vec2)
        denominator = math.sqrt(sum1) * math.sqrt(sum2)
        if not denominator:
            return 0.0
        else:
            return float(numerator) / denominator

小结

  • 由于使用jieba分词,对非中文内容的检索效果较差
  • 经济模式切换到高质量模式时,必须重新建立索引
  • 检索出来的知识通过calculate_keyword_score函数进行余弦相似度打分,按分数排序输出

高质量模式

相对于经济模式,这种模式可能会产生额外的费用,但检索效果会远好于经济模式。高质量模式可使用向量检索和全文检索的方式,在上传知识库和检索知识库时需要先将文本进行“向量化”,再以向量的形式存到向量数据库或去向量数据库检索出相似的记录。所以需要配置Embedding模型(向量化)。

image.png

实现原理

# 以adb检索代码为例: dify/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_openapi.py
    def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
        from alibabacloud_gpdb20160503 import models as gpdb_20160503_models
 
        score_threshold = kwargs.get("score_threshold") or 0.0
 
        request = gpdb_20160503_models.QueryCollectionDataRequest(
            dbinstance_id=self.config.instance_id,
            region_id=self.config.region_id,
            namespace=self.config.namespace,
            namespace_password=self.config.namespace_password,
            collection=self._collection_name,
            include_values=kwargs.pop("include_values", True),
            metrics=self.config.metrics,
            vector=query_vector, # 传入当前检索的向量数据
            content=None,
            top_k=kwargs.get("top_k", 4),
            filter=None,
        )
        # 调用数据库api检索最相似的tok_k条数据
        response = self._client.query_collection_data(request)
        documents = []
        for match in response.body.matches.match:
            if match.score > score_threshold:
                metadata = json.loads(match.metadata.get("metadata_"))
                metadata["score"] = match.score # 检索结果包含分数,不需要重新打分
                doc = Document(
                    page_content=match.metadata.get("page_content"),
                    vector=match.values.value,
                    metadata=metadata,
                )
                documents.append(doc)
        documents = sorted(documents, key=lambda x: x.metadata["score"] if x.metadata else 0, reverse=True)
        return documents

检索设置

向量检索

在检索时需要先将文本进行向量化,再用向量化后的数据到向量数据库中进行检索,检索时可限制向量检索的的条数(topk)和检索的相似度阈值(Score阈值)。

在检索结果后也可选用“Rerank模型”对检索出来的结果再做一次打分重排,以得出最有结果。

image.png

全文检索

相比向量检索,全文检索时则不需要对文本进行向量化,而是直接进行文本搜索(大多由向量数据库提供)。针对检索的结果也可以限制条数(topk)和“Rerank模型”重排。

image.png

混合检索

混合检索可以并发执行“向量检索”和“全文检索”,在两边返回结果后再进行重新排序,以选出分数最高的topk条记录。因为需要对两个检索结果进行重新排序,所以Rerank是必选的,除了可选“Rerank模型”外也提供了“权重设置”这种内置的免费方式(:。

image.png

一般情况我们只需要选择“权重设置”即可,可以对“语义”和“关键词”配置权重。两者分数计算方式如下:

  • 语义分值:直接取向量检索返回的分值
  • 关键词分值:通过jieba分词分成多个关键词,再使用关键词与输入文本一起计算得出分值
  • 最终得分:语义权重*语义分值 + 关键词权重*关键词分值

小结

  • Embedding模型选定后就不能再变化,不同的Embedding模型向量化文本的结果不一致可能会影响检索效果。
  • 因为“混合检索”中计算关键词权重时使用到了jieba分词,所以理论上这个混合索引对中文支持会比较友好,其它语种慎用。

三、检索召回配置

在工作流中配置知识检索时,可以选择多个知识库作为检索条件。多个知识库的检索是并行的,等所有知识检索出来后再进行重排,返回分数最高的topk条记录,这也是知识库检索节点的“多路召回”功能。

image.png

注意到知识库检索召回设置中有一行标题“RERANK设置”,其实已经指明了它只是一个重排的配置,设置topk和阈值只是对重排生效。以下是一些例子:

  1. 当“知识库1”的检索配置中设置topk=1,检索节点只引用该知识库且topk=3时,最多只能召回1条知识。
  2. 当“知识库1”和“知识库2”的检索配置中设置topk=1,检索节点引用这两个知识库且topk=3时,最多只能召回2条知识。
  3. 当“知识库1”的检索配置中设置Score阈值=0.8,检索节点只引用该知识库且Score阈值=0.6(权重语义=1)时,只能召回Score阈值为0.8以上的知识。

所以“检索节点”的召回设置是在知识库召回配置的基础上进行重排过滤的。所以它受限于知识库的配置,并不能召回更多的知识,而是只能作为一个重排优化项。

四、总结

  1. 知识库使用“经济模式”的“倒排索引”时,需要注意知识库是否为中文,该方式对于非中文的检索不友好。
  2. 知识库索引模式由“经济模式”改为“高质量模式”时,需要重新建立向量索引,反之则不需要。
  3. 知识库使用“高质量模式”的“混合检索”时,同样需要考虑知识库是否为中文,在设置“关键词”权重时对非中文不友好。
  4. Embedding模型选定后不宜再修改,修改需要对整个知识库重新向量化。
  5. 工作流“知识库检索”节点中的“召回设置”不要于“知识库设置”中的“检索设置”搞混,“召回设置”只是对“检索设置”下召回的知识做重排过滤。