彰甭妥缚基础原理对于?个企业专属的智能客服AI ?模型是必不可少例如 deepseek、chatGPT 等。 可模型本身并不知道公司的各种产品信息所以需要我们在给模型发送问题的时候将产品?册?同发送给模型。 可如果产品?册的内容?较多例如有上百?上千?会为该场景带来很多问题模型可能?法读取所有内容 ?语?模型只能存储?定量的信息通常成这个量为上下?窗???。如果产品?册内容超过上下?窗???模型就会读了后?内容忘记前?内容。前?所回答的准确率也?法得到保障。模型推理成本较? 模型推理成本取决于输?与输出的 token 数量。?般来说输?的 token数量越多推理成本就越?。模型推理慢 输?的内容越多模型需要消化的内容也就越多上百?的?册丢给模型会极?的拖慢模型推理的速度。既然直接将?册扔给模型不可?那么我们可以考虑将和问题相关的内容提取出来扔给模型。这时RAG 技术就派上?场了。RAG 的基本运?流程RAG 会将?档的内容切割为多个?段。当?户提出问题后 RAG 会根据问题的内容在所有的?段中寻找相关内容。 假设?户问题仅关联了 2 个?段那么 RAG 仅会将这 2 个?段发送给模型这样整个?册扔给模型的问题便迎刃?解了。上述仅是 RAG 流程的简化链路。每个环节都包含了很多实现细节。 ?般来说RAG 的基本流程包含两个部分准备阶段?户提问前需要将相关的?档都准备好并完成相应的预处理。其包含分? 与索引两个环节。回答阶段?户提问后需要根据?户的问题依次触发回答问题的各个环节包括召回 、重排 与?成。接下来我们逐步拆解看看这五个环节都是如何?作的。分片顾名思义分?就是将?档分成多个?段。分?的?式有很多种。可以按照字数来分?如 1000 字为?个?段。也可以按照段落来分每个段落是?个?段。 亦或者可以按照章节分按照?码分按照指定的字符分等等。?论选择何种?式最终?标是将?篇完整的?档切分为多份。?此该环节即可结束。索引索引是通过 Embedding 将?段?本转化为向量 然后将?段?本和对应的向量存储在向量数据库中的过程。这?存在?个重要概念需要理解向量数学上的?个概念既包含了??也包含了?向。通常我们会??个数组来表示它。 RAG 中使?的向量其维度可以包含数百个甚?上千个。维度越?向量所包含的信息也就越丰富使?这些向量做的?作内容可靠性也有越强。Embedding将?本转化为向量的?个过程。含义相近的?本在经历了 Embedding 之后其对应的向量也会?较接近。使??维向量来举例假设我们有两个?本?段分别是?本?段 1活字格是低代码平台?本?段 2活字格是低代码?具那么这两个?本?段在经历了 Embedding 之后会分别转化为两个?维向量分别是向量 1 3, 6向量 2 4, 6其在坐标轴上的可视化如图所示image可以看到这两个向量是?常接近。这时?户的问题内容是今天天?怎么样其对应的向量为 5, 3 。该向量位置如图所示image这说明前两个?本的内容在语义上是相似的?第三个问题内容和前两个?本内容毫不相关。这就是 Embedding 的?的。TIPEmbedding 这个操作需要借助专属的**向量模型**进?处理。关于向量模型的评估与选择可参考 [huggingface 的向量模型评估](https://huggingface.co/spaces/mteb/leaderboard)。向量数据库?来存储和查询向量的专?数据库。它为存储向量做了很多优化还提供了计算向量相似度等相关的函数?便我们查询与使? Embedding 后的向量。NOTE为确保向量和?本的对应关系我们需要在索引阶段务必同时存??本?段image向量仅仅是?个中间产物最终我们需要通过向量相似度检索出相似的向量并抽取原始?本?起发送给?模型?成最终的答案。?论是分?还是索引都是发?在?户提问题之前的阶段属于要提前准备的步骤。接下来我们来看看?户提问题之后的环节。召回召回就是搜索与?户问题相关?段的过程。这个环节从?户问题开始。?户提问活字格是什么?将?户的问题发送给 Embedding 模型。Embedding 模型会将问题转化为向量将问题向量发送?向量数据库中向量数据库会基于问题向量检索出库中与?户问题最为相关的 N 个?段内容N 为召回数量?般为 10 个可根据实际情况进?调整。在召回环节我们需要根据?户问题的向量与向量数据库中的向量进?相似度计算找到与?户问题最为相关的 N 个向量以及其对应的原始?本?段。TIP召回环节最重要的步骤就是基于 向量相似度 进?相关内容的检索。向量相似度的计算?式有很多种例如余弦相似度、欧?距离、点积等。 经过计算后的向量相似度是?个数字数字越?代表两个向量的相似度越?也就意味着向量对应的?本语义相关性越强。重排重排的全称是重新排序。其本质和召回是相同。召回是从所有的?段中找到相似度最?的 N 个?段。 ?重排则是在召回结果的基础上根据?段的相关性进?排序再选取出最相关的 K 个?段K 为最终输出数量?般为 3 个。TIP之所以在召回的基础上进?重拍的操作是因为召回环节使?的?本相似度计算逻辑在保证性能和效率的前提下获取到的结果可能并不是最优的。只适合做海量数据的初步筛选。 ?重排会采?准确率更好的相似度算法会占?更多的计算资源来进?排序从?获取到更相关的内容。因此重排更适合对于少量数据做精细化筛选。 该过程类似公司筛选?才。召回环节可类?为简历筛选?重排环节可类?为?试筛选。生成?成环节的唯??作就是?成最终答案。这?步我们已经获得了?户问题以及相关性最?的 K 个资料?段。我们可将这两部分?起发送给?模型让它根据?段内容来回答?户问题。 ?此整个 RAG 流程就结束了。imageRAG 实战 - 代码模式本章节将通过代码实战的?式帮助您快速搭建?个 RAG 系统。教程会使?三种不同的技术栈分别是Python(3.12 及以上), 使? Node.js(18.x 及以上), 使? Java(JDK 1.8 及以上), 使?作为包管理器。作为包管理器。作为包管理器。可以按需选择其中?种技术栈进?实战。环境准备向量数据库为保证多技术栈的依赖?致性我们选择Qdrant 作为向量数据库。TIPQdrant 暂不?持内存存储因此建议学习时可选择 docker 的?式进? Qdrant 服务的部署。Qdrant 服务部署完成后我们需要使? Qdrant 客户端与其进?交互。io.qdrantclient1.15.0模型服务教程会使?向量模型和标准?语?模型。为?便?户体验我们会使?阿?云百炼平台的模型。向量模型text-embedding-v4 ?于将?本转换为向量。?语?模型qwen-plus?于?成最终回复的内容。使?模型服务需要提前配置环境变量的 api key。配置?法可参考 阿?云百炼?档。TIP您也可以在对应的?程下创建配置?件从配置?件中读取环境变量。文本准备可??准备?于 RAG 的内容?档?户提问后RAG 系统会从该?档中进?答案搜索为?便学习建议准备?档格式为 Markdown。RAG 实现分片将?档?件按段落分割成?本块列表。该函数读取指定的?档?件并根据双换?符\n\n将?档内容分割成多个?本块。1/*** 将?档?件按段落分割成?本块列表** param docFile 要读取的?档?件路径* return 包含所有?空?本块的列表每个元素代表?档中的?个段落或?本块* throws IOException 当?件读取失败时抛出2*/9 public static List splitIntoChunksByParagraph(String docFile) throw10 String doc new String(Files.readAllBytes(Paths.get(docFile)));11 String[] chunks doc.split(\n\n);12 List result new ArrayList(); 1314 for (String chunk : chunks) {15 if (!chunk.trim().isEmpty()) {16 result.add(chunk); 17 }18 }19 return result; 20 }2122 // 分? List chunks TextUtil.splitIntoChunksByParagraph(src/main/resourc索引使? embedding 模型将第?步切割好的?本块依次转换为对应的?本向量。/*** 从配置?件中加载API Key*/private void loadApiKey() {try (InputStream input getClass().getClassLoader().getResourceAsStrea Properties prop new Properties();prop.load(input);apiKey prop.getProperty(dashscope.api-key);} catch (IOException e) {throw new RuntimeException(Failed to load API key from configurati}}/*** ?成?本嵌?向量* param textList ?本列表* return 向量结果*/20 public List textEmbedding(List textList) t21 TextEmbeddingParam param null;22 TextEmbedding textEmbedding null;23 try {24 param TextEmbeddingParam25 .builder()26 .apiKey(apiKey)27 .model(text-embedding-v4)28 .texts(textList)29 .parameter(dimension, 1024)30 .outputType(TextEmbeddingParam.OutputType.DENSE_AND_SPARSE)31 .build();32 textEmbedding new TextEmbedding();33 TextEmbeddingResult result textEmbedding.call(param);34 return result.getOutput().getEmbeddings();35 } catch (ApiException | NoApiKeyException e) {36 System.out.println(调?失败 e.getMessage());37 }38 return List.of(); 39 }4041 // ?本向量化 42 DashScopeClient client new DashScopeClient();43 List denseEmbeddings client.textEmbedding(chunks).stream()44 .map(TextEmbeddingResultItem::getEmbedding)45 .map(innerList - innerList.stream().map(Double::floatValue).to.toList();2.将?成的向量存?向量数据库中/*** 确保集合存在*/public void ensureCollectionExists() throws ExecutionException, Interrupted try {qdrantClient.getCollectionInfoAsync(COLLECTION_NAME).get();} catch (Exception e) {if (e.getCause() instanceof io.grpc.StatusRuntimeException statusEx9 if (statusException.getStatus().getCode() io.grpc.Status.Cod10 qdrantClient.createCollectionAsync(COLLECTION_NAME,11 Collections.VectorParams.newBuilder()12 .setDistance(Collections.Distance.C13 .setSize(1024)14 .build())15 .get();16 }17 }18 }19 }2021 /*** 保存向量** param chunks ?本块列表* param embeddings 嵌?向量列表*/public void saveEmbeddings(List chunks, List embedding28 // 构建 Points 列表29 List points IntStream.range(0, chunks.size())30 .mapToObj(i - {31 List vector embeddings.get(i);32 Map payload new HashMap();33 payload.put(text, ValueFactory.value(chunks.get(i)));3435 return Points.PointStruct.newBuilder()36 .setId(id(i))37 .setVectors(VectorsFactory.vectors(vector))38 .putAllPayload(payload)39 .build();40 })41 .collect(Collectors.toList());42 // 确保集合存在43 ensureCollectionExists();44 // 批量插? points45 qdrantClient.upsertAsync(46 COLLECTION_NAME,47 points48 ).get();49 System.out.println([INFO] 成功上传 points.size() 个向量到集合 50 }?此RAG 实现的索引部分就完成了。召回与重排/*** 查询向量** param queryEmbedding 查询向量* param topK* return 查询结果*/public List query(List queryEmbedding, int topK) throws Exec List queryResult qdrantClient.searchAsync(Points.SearchPoints.newBuilder().setCollectionName(COLLECTION_NAME).addAllVector(queryEmbedding).setLimit(topK).setWithPayload(Points.WithPayloadSelector.newBuilder().build()).get();return queryResult.stream().map(point - point.getPayloadMap().get(tex}// 召回 String query 请替换为?户的真实问题;List queryEmbeddings client.textEmbedding(List.of(query)).st.map(TextEmbeddingResultItem::getEmbedding).map(innerList - innerList.stream().map(Double::floatValue).toList.toList();List relatedChunks qdrantAgent.query(queryEmbeddings.getFirst(),5TIP由于我们使?的是阿?云百炼的专业向量模型其处理逻辑对于相关性提供了较好的?持。因此我们可以直接使?向量模型的输出结果进?召回??需进?额外的重排。如果您发现召回的?档与?户问题的相关性较低您可以尝试调整召回的?档数量 top_k 或者使?更专业的向量模型增加?次重排的操作。生成?成阶段需要?个标准的?本?成?模型将检索出的?档内容进?整理并输出最终答案。这?选择的是阿?百炼平台提供的?本?成模型 。/*** 调??本?成模型** param query ?户问题* param prompt 系统提示* return ?成结果*/public GenerationResult callWithMessage(String query, String prompt) throws Generation gen new Generation();Message systemMsg Message.builder().role(Role.SYSTEM.getValue()).content(prompt).build();Message userMsg Message.builder().role(Role.USER.getValue()).content(query).build();GenerationParam param GenerationParam.builder().apiKey(apiKey).model(qwen-plus).messages(Arrays.asList(systemMsg, userMsg)).resultFormat(GenerationParam.ResultFormat.MESSAGE).build(); return gen.call(param);}// ?成 String prompt String.format( 你是?位知识助?请根据?户的问题和下列?段?成准确的回答。?户问题%s相关?段%s请基于上述内容作答不要编造信息。如果相关?段中没有相关信息回答没有相关query, relatedChunks);GenerationResult result client.callWithMessage(query, prompt); System.out.println(result.getOutput().getChoices().getFirst().getMessage().总结?此我们使?代码开发的?式完成了?个基于 RAG 的问答系统的完整流程包括?档索引、问题召回和答案?成。在实际应?中你可能需要根据具体的业务场景和数据特征进?优化例如调整召回策略、优化?成模型的提示词等。接下来我们可以了解低代码?式的 RAG 系统构建。RAG 实战 - 低代码模式本章节将通过低代码的?式帮助您快速构建?个 RAG 系统。教程采?的低代码平台为环境准备设计器我们的构建过程旨在了解 RAG 的落地实现因此仅需安装设计器即可在本地测试实践。向量数据库为?便低代码模式下的实践我们选择使??个基于内存的向量数据库进?实践。您?需关?该数据的安装葡萄城市场已经提供了 插件您可以直接在设计器中进?安装。模型服务模型服务仍选择阿?云百炼平台。考虑到实现的通?性低代码平台的模型服务选择了通?的REST 接口您需要在设计器中安装 的插件。 在必要情况下您可能需要安装插件以及 。RAG 实现RAG 所有的实现逻辑均维护在活字格的「逻辑 服务端命令」中。