如何控制百炼 RAG 应用知识库检索范围

本文介绍了如何通过魔笔权限组实现细粒度的控制百炼 RAG 应用的知识库检索范围。

方案概览

image

要实现基于魔笔的细粒度知识库检索,可以分为6步:

  1. 获取用户权限组列表:在应用中通过{{mobi.currentUser.groups}}获取当前登录用户的权限组列表。

  2. 查询数据库权限组对应的知识库 ID:可以使用内置数据库或者您自己的数据库存储权限组 ID 和知识库 ID 的映射关系,通过这张映射表可以快速将第一步获取的权限组列表映射为知识库 ID 列表。

  3. 创建知识库:如果第二步中的表中没有当前权限组的知识库 ID,说明该权限组还未建立知识库,需要通过调用百炼的创建知识索引的接口创建一个新的知识库。

  4. 存储新的映射关系:将第三步中接口返回的新知识库 ID 与权限组的映射关系持久化存储到表中,方便下一次的检索。

  5. 限定 RAG 应用的检索范围:在百炼应用的集成操作中,将第二步获取的知识库 ID 列表(如果是新创建的知识库,则为第三步返回的知识库 ID)作为知识库检索范围的知识库 ID 列表参数。即可实现指定知识库范围进行检索。

  6. 限定知识库上传文档:在知识库管理页面,根据第二步获取的知识库 ID 列表(如果是新创建的知识库,则为第三步返回的知识库 ID)列出该用户可见的知识库列表和文档,用户上传文档的时候选择的知识库 ID 只能从这个列表中进行选择上传。

前期准备

  1. 搭建百炼应用,参考百炼文档知识检索增强(RAG),为了使用百炼应用的知识库检索范围参数,不能配置该应用的知识库。image

  2. 发布百炼应用后,获得 API-KEY、应用 ID。image

  3. 创建一个非结构化类目,用于存储所有用户的文档,记录该类目的 ID。image

步骤一:获取用户权限组列表

  1. 新建全局变量currentUserGroups

  2. 通过{{mobi.currentUser.groups}}获取用户的权限组列表,可以按需过滤指定的权限组。

  3. 将过滤后的权限组名称列表设置到变量currentUserGroups中。

    // 获取用户权限组名称
    const currentGroups = mobi.currentUser.groups.map(obj=>obj.name).filter(
        name => name !== "DEFAULT_END_USER" && name !== "END_USER"
    );
    currentUserGroups.setValue(currentGroups)
说明

{{mobi.currentUser.groups}}只有在发布的应用中才能获取登录用户的权限组,在应用的设计器中无法获取设计时用户的权限组。

步骤二:查询数据库权限组对应的知识库 ID

  1. 建立一张名为group_index_mapping的表记录权限组和知识库的映射关系。

    CREATE TABLE group_index_mapping (
     id INT AUTO_INCREMENT PRIMARY KEY,
     group_id VARCHAR(32) NOT NULL UNIQUE,
     index_id VARCHAR(32) NOT NULL UNIQUE,
     INDEX idx_group_id (group_id)
    );
  2. 创建集成操作根据group_id查询index_idimage.png

步骤三:创建知识库

  1. 创建 HTTP 集成操作或者阿里云 OpenAPI 集成操作调用阿里云百炼创建知识索引接口。阿里云百炼创建知识索引接口可以参考CreateIndex - 创建索引image.pngimage.png

  2. 调用创建知识库的集成操作globalCreatedIdx。使用权限组名称作为新知识库的名称。

    globalCreateIdx.trigger({
        "Name": groupId,
        "StructureType": "unstructured",
        "SinkType": "DEFAULT",
        "EmbeddingModelName": "text-embedding-v2",
        "RerankModelName": "gte-rerank-hybrid",
        "RerankMinScore": "0.2",
        "SourceType": "DATA_CENTER_CATEGORY",
        "CategoryIds": [""]
    })

步骤四:存储新的映射关系

  1. 创建集成操作将步骤三中返回的新知识库 ID 与权限组名称的映射关系插入到映射表中。image

  2. 汇总步骤一到步骤四的前端函数。

    const idxObjs = [];  // 存储当前用户的知识库对象数组
    const idxIds = [];  // 存储当前用户的知识库 id 数组
    let selectedKey = 0;
    const currentGroups = currentUserGroups.value;  //1.获取当前用户的权限组
    
    for (let i = 0; i < currentGroups.length; i++) {
        selectedKey = i + 1;
        const groupId = currentGroups[i]; 
        console.log(groupId);
    
        const indexId = await SelectIndexId.trigger({
            "groupId": groupId
        }); // 2.查询权限组对应的知识库 id
    
        if (indexId.data.length === 0) {
            // 3.如果不存在该权限组对应的知识库 id,则调用百炼接口创建知识库
            const currentIdx = await globalCreateIdx.trigger({
                "Name": groupId,
                "StructureType": "unstructured",
                "SinkType": "DEFAULT",
                "EmbeddingModelName": "text-embedding-v2",
                "RerankModelName": "gte-rerank-hybrid",
                "RerankMinScore": "0.2",
                "SourceType": "DATA_CENTER_CATEGORY",
                "CategoryIds": [""]
            });
    
            // 接口返回的新知识库 id
            const idxId = currentIdx.data.body.Data.Id;
    
            // 4.持久化存储新的映射关系
            await InsertIndexMapping.trigger({
                "groupId": groupId, "indexId": idxId,
            });
    
            idxObjs.push({
                "name": groupId, "id": idxId, "selectedKey": String(selectedKey)
            });
            idxIds.push(idxId);
        } else {
            // 如果存在,则直接获取该知识库 id
            const idxId = indexId.data[0].index_id;
            idxObjs.push({
                "name": groupId, "id": idxId, "selectedKey": String(selectedKey)
            });
            idxIds.push(idxId);
        }
    }
    
    // 存储到全局变量 currentIdxIds,作为后续知识库检索范围
    currentIdxIds.setValue(idxIds);
    
    // 返回对象数组,用于设置知识库管理页面的展示
    return idxObjs;

步骤五:限定 RAG 应用的检索范围

  1. 新增全局变量currentIDxIDs

  2. 将步骤二或步骤三中获取到的该用户可见的知识库 ID 数组存储到全局变量中currentIDxIDs

  3. 在对话页使用的百炼应用集成操作中,使用该变量设置知识库检索范围的知识库 ID 列表参数。image

步骤六:限定知识库上传文档

阿里云百炼的类目创建存在上限(1000个),详情请参考数据导入操作说明。因此对于大规模的用户类目隔离存在一定的限制,因此推荐使用单类目作为知识库的转存类目,并删除类目管理页面,屏蔽底层的类目管理细节。并且在知识库管理页面新增上传文档到指定知识库的逻辑:

image

  1. 新增上传按钮 uploadButton。文件上传至魔笔的系统文件存储,上传成功后执行前端函数。

  2. 新增 ApplyFileUploadLease 集成操作,实现申请文件上传租约接口,该接口可以参考ApplyFileUploadLease - 申请文档上传租约,配置如下:

    image

  3. 新增 UploadRemoteFile 集成操作,集成资源选择 file/系统内置集成/系统内置文件集成,该资源为系统内置资源,不需要创建。集成操作的配置如下:

    image.png

    image

  4. 新增 UploadFile 集成操作,实现申请文件上传到预置类目接口,该接口可以参考AddFile - 添加文档,配置如下:image

  5. 新增 SubmitIndexAddDocumentsJob 集成操作,实现将文档上传到指定知识库的接口,该接口可以参考SubmitIndexAddDocumentsJob - 提交索引追加任务,配置如下:image

  6. 创建前端函数 uploadBailianFile 绑定到 uploadButton 的上传系统成功事件,用来执行流程中的步骤2-6。

    // 1.点击上传按钮 uploadButton 触发前端函数
    // 2.计算MD5
    const base64String = uploadButton.values[0].base64Data;
    const wordArray = CryptoJS.enc.Base64.parse(base64String);
    const md5Hash = CryptoJS.MD5(wordArray);
    const md5Hex = md5Hash.toString(CryptoJS.enc.Hex);
    
    // 3.调用百炼接口申请文档上传租约,获取租约文档 ID
    const response = await ApplyFileUploadLease.trigger({
      "Md5": md5Hex,
      "FileName": uploadButton.values[0].name,
      "SizeInBytes": uploadButton.values[0].size
    });
    const fileUploadLeaseId = response.data.Data.FileUploadLeaseId;
    
    // 租约参数
    const url = response.data.Data.Param.Url;
    const contentType = response.data.Data.Param.Headers["Content-Type"];
    const bailianExtra = response.data.Data.Param.Headers["X-bailian-extra"];
    
    // 4.上传至百炼服务器临时存储
    const responseRemote = await UploadRemoteFile.trigger({
      "url": url,
      "contentType": contentType,
      "bailianExtra": bailianExtra,
      "fileId": uploadButton.values[0].fileId
    });
    
    // 5.调用百炼接口将文档上传到预置类目
    const respnseUpload = await UploadFile.trigger({
      "fileUploadLeaseId": fileUploadLeaseId
    });
    const fileId = respnseUpload.data.body.Data.FileId;
    
    // 6.上传文档到指定知识库
    const submitResp = await SubmitIndexAddDocumentsJob.trigger({
      "DocumentIds": [fileId]
    });
    
    // 刷新文档列表
    const responseListFile = await globalListFileByIdx.trigger();
    return responseListFile
说明

前端函数中 CryptoJS 库需要引入第三方库https://g.alicdn.com/code/lib/crypto-js/4.2.0/crypto-js.min.jsimage.png