找回密码
 立即注册
首页 业界区 业界 AI 智能体 RAG 入门教程

AI 智能体 RAG 入门教程

全叶农 昨天 15:45
AI 智能体 RAG 入门教程

简介

您是否正在寻找⼀种可靠的⽅法来构建智能知识客服或强⼤的知识库?检索增强生成 (RAG) 技术正是您实现这些⽬标的理想选择。
RAG,全称为 Retrieval-Augmented Generation,中⽂译为检索增强⽣成。这项技术的核⼼在于整合两⼤关键功能:

  • 检索:根据⽤户的提问,从现有的知识库中精准地找出最相关的⽂档或信息。
  • ⽣成:依据检索到的⽂档内容,智能地⽣成准确、连贯的答案。
RAG 是当前最主流的 AI 问答解决⽅案之⼀,已被⼴泛应⽤于企业级知识助⼿和智能客服系统的搭建,帮助众多企业提升客户服务效率和知识管理⽔平。
1.png

教程目标

本教程将深⼊浅出地阐述 RAG 的实现原理,并详细指导您如何从零开始搭建⼀个完整的 RAG 系统。 通过学习本教程,您不仅能透彻理解⾼质量智能客服和知识库的构建逻辑, 还能进⼀步探索
葡萄城开源的企业级 RAG 系统框架 GC QA RAG, 从⽽为⽣产环境的部署打下坚实基础。
基础原理

对于⼀个企业专属的智能客服,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
其在坐标轴上的可视化如图所示:
2.png

可以看到,这两个向量是⾮常接近。这时,⽤户的问题内容是:"今天天⽓怎么样?",其对应的向量为: 5, 3 。该向量位置如图所示:
3.png

这说明前两个⽂本的内容在语义上是相似的,⽽第三个问题内容和前两个⽂本内容毫不相关。这就是 Embedding 的⽬的。
  1. TIP
  2. Embedding 这个操作需要借助专属的**向量模型**进⾏处理。
  3. 关于向量模型的评估与选择,可参考 [huggingface 的向量模型评估](https://huggingface.co/spaces/mteb/leaderboard)。
复制代码

  • 向量数据库:⽤来存储和查询向量的专⽤数据库。它为存储向量做了很多优化,还提供了计算向量相似度等相关的函数,⽅便我们查询与使⽤ Embedding 后的向量。
    NOTE
    为确保向量和⽂本的对应关系,我们需要在索引阶段,务必同时存⼊⽂本⽚段!
4.png
  1. 向量仅仅是⼀个中间产物,最终我们需要通过向量相似度检索出相似的向量,并抽取原始⽂本,⼀起发送给⼤模型,⽣成最终的答案。
复制代码
⽆论是分⽚,还是索引,都是发⽣在⽤户提问题之前的阶段,属于要提前准备的步骤。接下来,我们来看看⽤户提问题之后的环节。
召回

召回就是搜索与⽤户问题相关⽚段的过程。这个环节从⽤户问题开始。

  • ⽤户提问:"活字格是什么?"
  • 将⽤户的问题发送给 Embedding 模型。
  • Embedding 模型会将问题转化为向量;
  • 将问题向量发送⾄向量数据库中;
  • 向量数据库会基于问题向量检索出库中与⽤户问题最为相关的 N 个⽚段内容(N 为召回数量,
    ⼀般为 10 个,可根据实际情况进⾏调整)。
    在召回环节,我们需要根据⽤户问题的向量,与向量数据库中的向量进⾏相似度计算,找到与⽤户问题最为相关的 N 个向量以及其对应的原始⽂本⽚段。
    1. TIP
    2. 召回环节最重要的步骤就是基于 向量相似度 进⾏相关内容的检索。
    3. 向量相似度的计算⽅式有很多种,例如余弦相似度、欧⽒距离、点积等。 经过计算后的向量相似度是⼀个数字,数字越⼤,代表两个向量的相似度越⾼,也就意味着向量对应的⽂本语义相关性越强。
    复制代码
重排

重排的全称是重新排序。其本质和召回是相同。召回是从所有的⽚段中找到相似度最⾼的 N 个⽚段。 ⽽重排则是在召回结果的基础上,根据⽚段的相关性进⾏排序,再选取出最相关的 K 个⽚段
(K 为最终输出数量,⼀般为 3 个)。
  1. TIP
  2. 之所以在召回的基础上进⾏重拍的操作,是因为召回环节使⽤的⽂本相似度计算逻辑,在保证性能和效率的前提下,获取到的结果可能并不是最优的。只适合做海量数据的初步筛选。 ⽽重排会采⽤准确率更好的相似度算法(会占⽤更多的计算资源)来进⾏排序,从⽽获取到更相关的内容。因此重排更适合对于少量数据做精细化筛选。 该过程类似公司筛选⼈才。召回环节可类⽐为简历筛选,⽽重排环节可类⽐为⾯试筛选。
复制代码
生成

⽣成环节的唯⼀⼯作就是⽣成最终答案。
这⼀步我们已经获得了⽤户问题以及相关性最⾼的 K 个资料⽚段。我们可将这两部分⼀起发送给
⼤模型,让它根据⽚段内容来回答⽤户问题。 ⾄此,整个 RAG 流程就结束了。
5.png

RAG 实战 - 代码模式

本章节将通过代码实战的⽅式,帮助您快速搭建⼀个 RAG 系统。教程会使⽤三种不同的技术栈,分别是:
Python(3.12 及以上), 使⽤ Node.js(18.x 及以上), 使⽤ Java(JDK 1.8 及以上), 使⽤
作为包管理器。作为包管理器。
作为包管理器。
可以按需选择其中⼀种技术栈进⾏实战。
环境准备

向量数据库

为保证多技术栈的依赖⼀致性,我们选择Qdrant 作为向量数据库。
  1. TIP
  2. Qdrant 暂不⽀持内存存储,因此建议学习时可选择 docker 的⽅式进⾏ Qdrant 服务的部署。
复制代码
Qdrant 服务部署完成后,我们需要使⽤ Qdrant 客户端与其进⾏交互。
  1. <dependency>
  2. <groupId>io.qdrant</groupId>
  3. client</artifactId>
  4. <version>1.15.0</version>
  5. </dependency>
复制代码
模型服务

教程会使⽤向量模型和标准⼤语⾔模型。为⽅便⽤户体验,我们会使⽤阿⾥云百炼平台的模型。
向量模型:text-embedding-v4 ⽤于将⽂本转换为向量。
⼤语⾔模型:qwen-plus⽤于⽣成最终回复的内容。
使⽤模型服务,需要提前配置环境变量的 api key。配置⽅法可参考 阿⾥云百炼⽂档。
  1. TIP
  2. 您也可以在对应的⼯程下创建配置⽂件,从配置⽂件中读取环境变量。
复制代码
文本准备

可⾃⾏准备⽤于 RAG 的内容⽂档,⽤户提问后,RAG 系统会从该⽂档中进⾏答案搜索,为⽅便学习,建议准备⽂档格式为 Markdown。
RAG 实现

分片

将⽂档⽂件按段落分割成⽂本块列表。
该函数读取指定的⽂档⽂件,并根据双换⾏符\n\n将⽂档内容分割成多个⽂本块。
  1. 1       
  2. /**
  3. *        将⽂档⽂件按段落分割成⽂本块列表
  4. *
  5. *        @param docFile 要读取的⽂档⽂件路径
  6. *        @return 包含所有⾮空⽂本块的列表,每个元素代表⽂档中的⼀个段落或⽂本块
  7. *        @throws IOException 当⽂件读取失败时抛出       
  8. 2*/
  9. 9        public static List<String> splitIntoChunksByParagraph(String docFile) throw
  10. 10        String doc = new String(Files.readAllBytes(Paths.get(docFile)));
  11. 11        String[] chunks = doc.split("\n\n");
  12. 12        List<String> result = new ArrayList<>(); 13
  13. 14        for (String chunk : chunks) {
  14. 15        if (!chunk.trim().isEmpty()) {
  15. 16        result.add(chunk); 17        }
  16. 18        }
  17. 19        return result; 20        }
  18. 21
  19. 22        // ======== 分⽚ ========
  20. List<String> chunks = TextUtil.splitIntoChunksByParagraph("src/main/resourc
复制代码
索引


  • 使⽤ embedding 模型将第⼀步切割好的⽂本块依次转换为对应的⽂本向量。
  1. /**
  2. * 从配置⽂件中加载API Key
  3. */
  4. private void loadApiKey() {
  5. try (InputStream input = getClass().getClassLoader().getResourceAsStrea Properties prop = new Properties();
  6. prop.load(input);
  7. apiKey = prop.getProperty("dashscope.api-key");
  8. } catch (IOException e) {
  9. throw new RuntimeException("Failed to load API key from configurati
  10. }
  11. }
  12. /**
  13. *        ⽣成⽂本嵌⼊向量
  14. *        @param textList ⽂本列表
  15. *        @return 向量结果
  16. */
  17. 20        public List<TextEmbeddingResultItem> textEmbedding(List<String> textList) t
  18. 21        TextEmbeddingParam param = null;
  19. 22        TextEmbedding textEmbedding = null;
  20. 23        try {
  21. 24        param = TextEmbeddingParam
  22. 25        .builder()
  23. 26        .apiKey(apiKey)
  24. 27        .model("text-embedding-v4")
  25. 28        .texts(textList)
  26. 29        .parameter("dimension", 1024)
  27. 30        .outputType(TextEmbeddingParam.OutputType.DENSE_AND_SPARSE)
  28. 31        .build();
  29. 32        textEmbedding = new TextEmbedding();
  30. 33        TextEmbeddingResult result = textEmbedding.call(param);
  31. 34        return result.getOutput().getEmbeddings();
  32. 35        } catch (ApiException | NoApiKeyException e) {
  33. 36        System.out.println("调⽤失败:" + e.getMessage());
  34. 37        }
  35. 38                return List.of(); 39        }
  36. 40
  37. 41        // ======== ⽂本向量化 ========
  38. 42        DashScopeClient client = new DashScopeClient();
  39. 43        List<List<Float>> denseEmbeddings = client.textEmbedding(chunks).stream()
  40. 44        .map(TextEmbeddingResultItem::getEmbedding)
  41. 45        .map(innerList -> innerList.stream().map(Double::floatValue).to
  42. .toList();
  43.                   
复制代码
2.将⽣成的向量存⼊向量数据库中
  1. /**
  2. * 确保集合存在
  3. */
  4. public void ensureCollectionExists() throws ExecutionException, Interrupted try {
  5. qdrantClient.getCollectionInfoAsync(COLLECTION_NAME).get();
  6. } catch (Exception e) {
  7. if (e.getCause() instanceof io.grpc.StatusRuntimeException statusEx
  8. 9                                if        (statusException.getStatus().getCode() == io.grpc.Status.Cod
  9. 10                                        qdrantClient.createCollectionAsync(COLLECTION_NAME,
  10. 11                                        Collections.VectorParams.newBuilder()
  11. 12                                        .setDistance(Collections.Distance.C
  12. 13                                        .setSize(1024)
  13. 14                                        .build())
  14. 15                                        .get();
  15. 16                                }       
  16. 17                        }               
  17. 18                }                       
  18. 19        }                               
  19. 20                                       
  20. 21        /***        保存向量
  21. *
  22. *        @param chunks        ⽂本块列表
  23. *        @param embeddings 嵌⼊向量列表
  24. */
  25. public void saveEmbeddings(List<String> chunks, List<List<Float>> embedding
  26. 28                // 构建 Points 列表
  27. 29                List<Points.PointStruct> points = IntStream.range(0, chunks.size())
  28. 30                .mapToObj(i -> {
  29. 31                List<Float> vector = embeddings.get(i);
  30. 32                Map<String, JsonWithInt.Value> payload = new HashMap<>();
  31. 33                payload.put("text", ValueFactory.value(chunks.get(i)));
  32. 34               
  33. 35                return Points.PointStruct.newBuilder()
  34. 36                .setId(id(i))
  35. 37                .setVectors(VectorsFactory.vectors(vector))
  36. 38                .putAllPayload(payload)
  37. 39                .build();
  38. 40                })
  39. 41                .collect(Collectors.toList());
  40. 42                // 确保集合存在
  41. 43                ensureCollectionExists();
  42. 44                // 批量插⼊ points
  43. 45                qdrantClient.upsertAsync(
  44. 46                COLLECTION_NAME,
  45. 47                points
  46. 48                ).get();
  47. 49                System.out.println("[INFO] 成功上传 " + points.size() + " 个向量到集合 '"
  48. 50        }       
复制代码
⾄此,RAG 实现的索引部分就完成了。
召回与重排
  1. /**
  2. *        查询向量
  3. *
  4. *        @param queryEmbedding 查询向量
  5. *        @param topK
  6. *        @return 查询结果
  7. */
  8. public List<String> query(List<Float> queryEmbedding, int topK) throws Exec List<Points.ScoredPoint> queryResult = qdrantClient.searchAsync(
  9. Points.SearchPoints.newBuilder()
  10. .setCollectionName(COLLECTION_NAME)
  11. .addAllVector(queryEmbedding)
  12. .setLimit(topK)
  13. .setWithPayload(Points.WithPayloadSelector.newBuilder()
  14. .build()
  15. ).get();
  16. return queryResult.stream().map(point -> point.getPayloadMap().get("tex
  17. }
  18. // ======== 召回 ========
  19. String query = "请替换为⽤户的真实问题";
  20. List<List<Float>> queryEmbeddings = client.textEmbedding(List.of(query)).st
  21. .map(TextEmbeddingResultItem::getEmbedding)
  22. .map(innerList -> innerList.stream().map(Double::floatValue).toList
  23. .toList();
  24. List<String> relatedChunks = qdrantAgent.query(queryEmbeddings.getFirst(),5
复制代码
  1. TIP
  2. 由于我们使⽤的是阿⾥云百炼的专业向量模型,其处理逻辑对于相关性提供了较好的⽀持。因此,我们可以直接使⽤向量模型的输出结果进⾏召回,⽽⽆需进⾏额外的重排。
  3. 如果您发现召回的⽂档与⽤户问题的相关性较低,您可以尝试调整召回的⽂档数量 top_k ,或者使⽤更专业的向量模型增加⼆次重排的操作。
复制代码
生成

⽣成阶段需要⼀个标准的⽂本⽣成⼤模型,将检索出的⽂档内容进⾏整理,并输出最终答案。这⾥选择的是阿⾥百炼平台提供的⽂本⽣成模型 。
  1. /**
  2. *        调⽤⽂本⽣成模型
  3. *
  4. *        @param query ⽤户问题
  5. *        @param prompt 系统提示
  6. *        @return ⽣成结果
  7. */
  8. public GenerationResult callWithMessage(String query, String prompt) throws Generation gen = new Generation();
  9. Message systemMsg = Message.builder()
  10. .role(Role.SYSTEM.getValue())
  11. .content(prompt)
  12. .build();
  13. Message userMsg = Message.builder()
  14. .role(Role.USER.getValue())
  15. .content(query)
  16. .build();
  17. GenerationParam param = GenerationParam.builder()
  18. .apiKey(apiKey)
  19. .model("qwen-plus")
  20. .messages(Arrays.asList(systemMsg, userMsg))
  21. .resultFormat(GenerationParam.ResultFormat.MESSAGE)
  22. .build(); return gen.call(param);
  23. }
  24. // ======== ⽣成 ========
  25. String prompt = String.format( """
  26. 你是⼀位知识助⼿,请根据⽤户的问题和下列⽚段⽣成准确的回答。
  27. ⽤户问题:%s
  28. 相关⽚段:
  29. %s
  30. 请基于上述内容作答,不要编造信息。如果相关⽚段中没有相关信息,回答"没有相关
  31. query, relatedChunks
  32. );
  33. GenerationResult result = client.callWithMessage(query, prompt); System.out.println(result.getOutput().getChoices().getFirst().getMessage().
复制代码
总结

⾄此,我们使⽤代码开发的⽅式,完成了⼀个基于 RAG 的问答系统的完整流程,包括⽂档索引、问题召回和答案⽣成。在实际应⽤中,你可能需要根据具体的业务场景和数据特征进⾏优化,例如调整召回策略、优化⽣成模型的提示词等。
接下来,我们可以了解低代码⽅式的 RAG 系统构建。
RAG 实战 - 低代码模式

本章节将通过低代码的⽅式,帮助您快速构建⼀个 RAG 系统。教程采⽤的低代码平台为
环境准备

设计器

我们的构建过程旨在了解 RAG 的落地实现,因此,仅需安装设计器,即可在本地测试实践。
向量数据库

为⽅便低代码模式下的实践,我们选择使⽤⼀个基于内存的向量数据库进⾏实践。您⽆需关⼼该数据的安装,葡萄城市场已经提供了 插件,您可以直接在设计器中进⾏安装。
模型服务

模型服务仍选择阿⾥云百炼平台。考虑到实现的通⽤性,低代码平台的模型服务选择了通⽤的
REST 接口,您需要在设计器中安装 的插件。 在必要情况下,您可能需要安装插件以及 。
RAG 实现
RAG 所有的实现逻辑均维护在活字格的「逻辑 服务端命令」中。
6.png

在低代码模式下,分⽚的动作也交由 AI 模型来实现,这样⽆需过分关注⽂本格式,仅通过语义即可实现效果较好的切⽚。
7.png

索引


  • 向量⽣成。与 embedding 模型通信,加⼊了知识领域维度参数,⽅便后续只是维护时,可按照知识领域进⾏区分,不过其实现思路与代码完全⼀致。
8.png

2.向量存储。将向量与原始⽂本存储内存数据库 Falss 中。
9.png

召回与重排

低代码实现中,提供了召回与重排两个步骤的实现。其中,重排调⽤了阿⾥云百炼平台的⽂本排序模型 gte-rerank。
10.png

生成

低代码实现中,⽣成的环节可视为应⽤层的⼀部分,因此可以将其逻辑按照知识问答模板的形式进⾏设计。
11.png

总结

⾄此,低代码的 RAG 实践完成。您可以在葡萄城市场中下载教程的⼯程⽂件。 在打开⼯程⽂件时,系统会⾃动安装其依赖的所有插件。
GC-QA-RAG
GC QA RAG 是⼀款⾯向葡萄城产品⽣态(包括 活字格、WYN、SpreadJS 和 GCExcel 等)的检索增强⽣成(RAG)系统。是将 RAG 场景应⽤在⽣产环境中的最佳实践。
该框架完全开源,提供了 RAG 的全套解决⽅案,包括⽂档分⽚、⽂档索引、⽂档检索、⽂档⽣成等,开箱即⽤。
github: https://github.com/GrapeCity-AI/gc-qa-rag wiki: https://grapecity-ai.github.io/gc-qa-rag
12.png

框架引导

分⽚

GC QA RAG 基于⽂档内容的情况,提供了多种分⽚策略:短⽂档的处理策略:基于句⼦技术的动态控制;
⻓⽂档的处理策略:两阶段记忆 - 聚焦对话机制。
框架提出了专属的 QA 机制,将⽂本的切⽚内容结合问答的形式,构建出更契合⽤户问答场景的切
⽚结果。
➡ 访问详情
索引

基于 QA 的切⽚结果,可通过多个维度进⾏ embedding,从⽽提供更多维度下,更精准的向量索引。
问题稀疏向量问题稠密向量答案稀疏向量答案稠密向量
➡ 访问详情
召回与重排

GC QA RAG 在向量数据库检索时,选择使⽤多路混合检索,兼顾关键词和语义理解,并将检索结果采⽤ RRF 算法进⾏融合排序。⽀持通过类别标识进⾏筛选,避免语义混乱。并将最终的结果进⾏去重,确保知识点唯⼀。
➡ 访问详情
⽣成

GC QA RAG 提供了结构化的 Prompt 设计,并⽀持多轮对话⽀持,有效提升复杂场景下的问答能⼒。
➡ 访问详情
扩展链接

敏捷构建企业级应用及AI智能体

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册