✅派聪明RAG面试题预测,31 道高频 AI 面试八股,1.2 万字 50 张手绘图,真吊打面试官 – 朝汐の小站
✅派聪明RAG面试题预测,31 道高频 AI 面试八股,1.2 万字 50 张手绘图,真吊打面试官
本文最后更新于 259 天前,如有错误请邮件至 zhiligyi222na@gmail.com

1.请详细描述完整的RAG系统架构,包括主要组件和数据流向?

RAG 系统本质上要解决一个问题:如何让 AI 能够基于企业内部的知识库来回答用户问题。所以整个架构设计围绕着”文件上传-文件存储-向量生成-答案生成”这条主线来展开。

当用户上传文档后,我们首先通过 Upload 接口来处理上传文件,并支持分片上传避免大文件传输问题。然后很关键的一点是,我们没有选择同步处理,而是把文件处理任务丢到 Kafka 的消息队列里,这样用户上传完就能立即得到响应,不用等待漫长的处理过程。

接下来是文档解析环节, FileProcessingConsumer 作为 Kafka 消费者会异步处理这些任务。我们使用 Apache Tika 来解析各种格式的文档,比如 PDF、Word、Excel 等,然后通过 ParseService 把文档内容切分成小段,这样做的好处是既能保持语义的完整性,又能控制向量化的粒度。

向量化这块是整个 RAG 系统的核心, VectorizationService 会调用豆包的 Embedding API 把文本转换成向量表示。我们选择把这些向量存储在 Elasticsearch 中,主要是因为 ES 在向量检索方面的性能比较好,而且支持混合检索。

说到检索,这是 RAG 系统能否准确回答问题的关键。我们实现了混合检索策略,既有基于向量相似度的语义检索,也有传统的关键词检索,这样能够在不同场景下都有比较好的召回效果。特别重要的是,我们在检索时加入了权限控制,确保用户只能检索到自己有权限访问的文档。

生成这块,我们集成了 DeepSeek 大语言模型,通过 DeepSeekClient 来调用 API。这里有个技术细节就是我们支持流式响应,用户不用等到整个回答生成完才能看到结果,而是可以实时看到 AI 的回答过程,体验会好很多。

整个对话流程是通过 ChatHandler 来协调的,它会先调用检索服务找到相关文档,然后把这些文档作为上下文传给大语言模型,最后把生成的回答通过 WebSocket 实时推送给用户。我们还在 Redis 中维护了对话历史,这样 AI 能够理解上下文,进行多轮对话。

权限控制方面,我们实现了基于组织标签的多租户架构。通过 OrgTagAuthorizationFilter 确保用户只能访问自己组织内的文档,实现数据的安全隔离。

总的来说,这套 RAG 架构的设计理念就是要在保证准确性的前提下,尽可能提升用户体验和系统性能,同时确保企业级的安全性。

2.在设计RAG系统时,如何选择合适的向量数据库?Elasticsearch、Pinecone等有什么区别?

首先说说我们为什么在派聪明中选择了 Elasticsearch。第一个原因是我们团队对 ES 比较熟悉,第二个原因是 ES 的混合检索能力很强,既支持传统的全文检索,也支持向量检索,这对 RAG 系统来说是个很大的优势。

但是说实话,ES 在纯向量检索性能上并不是最优的选择。如果系统主要是向量相似度搜索,专门的向量数据库会更合适。比如 Pinecone,它是专门为向量检索设计的云服务,性能确实很不错,而且使用起来很简单,基本上开箱即用。但是有个问题就是成本,特别是数据量大的时候,费用会比较高。而且作为云服务,数据安全和合规性可能是一些企业需要考虑的问题。

就我个人的体验来说,可以先用 ES 这样的通用方案快速验证业务价值,等业务稳定后再根据性能瓶颈考虑迁移到专门的向量数据库。这样既能快速上线,又能控制技术风险。毕竟 RAG 系统的核心价值还是在业务逻辑和数据质量上,选择合适的就行,不一定非要追求最新最炫的技术。

3.RAG系统中的混合检索是什么?如何实现?

混合检索简单来说就是把不同的检索方法结合起来,取长补短,提高检索的准确性和召回率。

在派聪明项目中,混合检索主要是结合了两种检索方式:语义检索和关键词检索。语义检索就是基于向量相似度的,它能够理解查询的语义含义,即使用词不完全匹配也能找到相关内容。比如用户问”如何提升工作效率”,它能找到包含”提高生产力”、”优化流程”这样语义相关的文档。而关键词检索就是传统的全文检索,它对精确匹配很有效,特别是一些专业术语、人名、地名这种。

在技术实现上,我们是这样做的。首先对用户查询同时执行向量检索和全文检索,然后把两个结果集合并。这里有个关键问题就是如何合并和排序。我们采用的是加权融合的方式,给语义检索和关键词检索分别设置权重,然后计算综合得分。

第一阶段:KNN 向量召回

// KNN 向量召回阶段
  s.knn(kn -> kn
      .field("vector")
      .queryVector(queryVector)  // 查询向量
      .k(recallK)               // 召回数量(topK * 30)
      .numCandidates(recallK)   // 候选数量
  );

第二阶段:关键词过滤

// 必须命中关键词 + 权限过滤
  s.query(q -> q.bool(b -> b
      .must(mst -> mst.match(m -> m.field("textContent").query(query)))  // 关键词匹配
      .filter(f -> f.bool(bf -> bf
          .should(s1 -> s1.term(t -> t.field("userId").value(userDbId)))  // 用户权限
          .should(s2 -> s2.term(t -> t.field("public").value(true)))      // 公开文档
          .should(s3 -> /* 组织权限 */)                                    // 组织权限
      ))
  ));

第三个阶段:BM25重排序

// BM25 rescore 重排序
  s.rescore(r -> r
      .windowSize(recallK)
      .query(rq -> rq
          .queryWeight(0.2d)          // KNN分数权重20%
          .rescoreQueryWeight(1.0d)   // BM25分数权重100%
          .query(rqq -> rqq.match(m -> m
              .field("textContent")
              .query(query)
              .operator(Operator.And)  // 严格关键词匹配
          ))
      )
  );

具体的算法是这样的:假设一个文档在语义检索中的得分是 0.8,在关键词检索中的得分是 0.6,我们可以设置语义检索权重为 0.7,关键词检索权重为 0.3,那么最终得分就是 0.8*0.7 + 0.6*0.3 = 0.74。当然这个权重可以根据实际效果来调整。

在此基础上,我们还加入了权限过滤的逻辑。用户只能检索到自己有权限访问的文档,这个过滤是在检索结果合并之后进行的。

  // 三种权限访问模式
  .should(s1 -> s1.term(t -> t.field("userId").value(userDbId)))     // 自己的文档
  .should(s2 -> s2.term(t -> t.field("public").value(true)))         // 公开文档  
  .should(s3 -> /* 组织层级权限 */)                                   // 组织文档

4.解释向量embedding的维度选择对系统性能的影响?为什么我们选择2048维而不是384维?

首先,从模型能力角度来看,我们使用的是火山引擎的 doubao-embedding-text-240515 模型。 根据官方文档,这个模型的最高维度向量是 2048 维,支持 512、1024 降维使用。我们选择 2048 维实际上是在使用这个模型的原生最高维度输出,这样能够最大程度地保留模型训练时学到的语义信息。

从语义表达能力来说,维度越高,向量能够表达的语义信息就越丰富。2048 维相比 384 维有着显著的优势。高维向量能够在语义空间中更精确地区分不同概念之间的细微差别,这对于 RAG 系统来说至关重要。

当然,高维度意味着成本更高。

从技术实现角度,我们在 ES 的 knowledge_base.json 中也配置了向量字段为 2048 维,使用 cosine 相似度计算。这个配置与豆包的 embedding 模型完全匹配。

5.如何解决向量检索中的”语义漂移”问题?

语义漂移是 RAG 系统中一个非常关键的问题,简单来说就是随着时间推移,向量表示的语义可能会发生偏移,导致检索效果下降。

语义漂移

在派聪明项目中,我们采用了多层次的解决策略。第一个层面是模型版本管理。我们在 EsDocument 中专门设计了 modelVersion 字段来记录每个向量是由哪个版本模型生成的。这样当我们升级向量模型时,可以识别出哪些向量需要重新生成。

第二个层面是增量更新策略。我们不是一次性替换所有历史向量,而是采用渐进式的方法。当检测到某些文档的检索效果明显下降时,我们会优先对这些文档进行重新向量化。这个过程可以通过用户反馈和检索点击率来触发。

第三个层面是混合检索的优势。我们结合了语义检索和关键词检索。即使语义向量出现漂移,关键词检索仍然能够提供稳定的基准效果。

6.在多租户RAG系统中,如何设计权限控制和数据隔离?

首先,我们采用了多层级的权限控制架构。在用户层面,系统支持普通用户和管理员两种角色,管理员拥有全局访问权限,可以管理所有用户的组织标签分配。在组织层面,我们设计了灵活的组织标签系统,每个用户可以属于多个组织,并且有一个主组织标签。特别值得一提的是,系统为每个用户自动创建私人组织标签(PRIVATE_用户名),确保用户有独立的私人空间。

在数据存储层面,我们设计了三个关键字段来实现数据隔离:userId 标识文档所有者,orgTag 标识文档所属组织,isPublic 标识是否为公开资源。

在文件上传时,系统会根据用户的主组织标签自动为文档分配组织标签,确保数据从源头就有正确的权限标识。同时,用户可以选择将文档设置为公开,这样其他用户也能访问。

我们还对请求加了过滤器,在每个请求到达控制器之前就进行权限验证:首先检查资源是否为公开资源,如果是则直接放行;然后验证用户是否为资源所有者,所有者拥有完全访问权限;接着检查用户是否为管理员,管理员拥有全局权限;最后进行组织标签匹配,只有用户的组织标签包含资源的组织标签时才允许访问。

在 RAG 系统的核心功能——混合检索中,我们还实现了权限感知的搜索。系统会根据用户的有效组织标签构建 Elasticsearch 查询条件,确保用户只能检索到有权限访问的文档。

7.当RAG检索到的知识与LLM预训练知识冲突时,你会如何处理?请提供具体的解决方案。

可以修改提示词,比如说设置优先级规则,让检索到的参考信息优先级高于预训练知识,在遇到冲突时优先采用检索信息,无法确定时明确告知“存在信息冲突,建议核实”。

ai:
    prompt:
      rules: |
        你是派聪明知识助手,处理知识冲突时须遵守:

        【优先级规则】
        1. 检索到的参考信息优先级 > 你的预训练知识
        2. 最新时间的信息 > 较旧的信息
        3. 企业内部规定 > 通用规则
        4. 具体场景规则 > 一般性原则

        【冲突处理】
        1. 发现冲突时,优先采用检索信息,并明确说明"根据最新检索信息"
        2. 如检索信息不完整,可补充预训练知识,但需标注"补充:基于通用知识"
        3. 存在明显矛盾时,同时呈现两种观点并标注来源
        4. 无法确定时,明确告知"存在信息冲突,建议核实"

        【回答格式】
        - 检索信息:(来源#编号)
        - 补充信息:(基于通用知识)
        - 冲突提示:(⚠️信息冲突)

8.评估RAG系统检索质量的关键指标有哪些?

准确性指标是最核心的。首先是召回率(Recall),也就是相关文档中有多少被成功检索出来了。比如用户问一个技术问题,实际上知识库中有 10 个相关文档,系统检索出了 7 个,那召回率就是 70%。

然后是精确率(Precision),检索出来的文档中有多少是真正相关的。如果我们检索出了 10 个文档,但只有 7 个是相关的,精确率就是 70%。

// 在 HybridSearchService 中添加评估逻辑
public EvaluationMetrics evaluateSearch(String query, List<String> relevantDocIds, int 
topK) {
  List<SearchResult> results = searchWithPermission(query, userId, topK);

  // 计算 Precision@K
  long relevantCount = results.stream()
      .limit(topK)
      .mapToLong(result -> relevantDocIds.contains(result.getFileMd5()) ? 1 : 0)
      .sum();
  double precision = (double) relevantCount / Math.min(topK, results.size());

  // 计算 Recall@K
  double recall = (double) relevantCount / relevantDocIds.size();

  // 计算 MRR
  double mrr = calculateMRR(results, relevantDocIds);

  return new EvaluationMetrics(precision, recall, mrr);
}

9.什么是RAG中的”幻觉”问题?如何预防?

RAG 中的”幻觉”是指大语言模型生成的内容与检索到的真实信息不符,或者模型编造了不存在的信息。在派聪明项目中,我们采用了多种策略来预防幻觉问题。

首先是提示词,我们明确要求模型严格基于检索到的文档内容回答,不要添加文档中没有的信息。

Color1

你的回答必须依据参考文献,若参考文献无法回答问题,则回复“无法回答”

:::

其次是检索质量,我们采用了最高维度的豆包向量模型,确保检索到的文档真正相关。同时,我还使用了混合检索策略,通过提高检索精度来减少无关信息的干扰。并通过设置相似度阈值,过滤掉相关性低的检索结果,确保只有高质量的上下文信息被传递给模型。

另外,我们为每个检索结果都添加了明确的来源标识和置信度信息,让模型清楚地知道信息的可靠性。

10.如何设计置信度评分机制来判断检索结果的可靠性?

置信度是 RAG 系统用来保证检索质量的一个重要指标。所以要综合多个维度来考虑。

第一个关键维度我认为是向量相似度。当用户搜索时,我们利用 Elasticsearch 的 KNN 算法计算查询向量和文档向量的余弦相似度,这个分数能反映语义层面的相关性——分数越高,说明文档和查询在语义上越接近。

第二个维度是文本关键词匹配度。我们会利用 ES 默认的 BM25 算法对关键词在文档中的出现频率、重要性进行打分。

然后综合计算出最后的得分 最终分数 = KNN分数 × 0.2 + BM25分数 × 1.0

11.比较固定长度分块和语义分块的优缺点?

固定长度分块和语义分块是 RAG 系统中两种主要的文档分割策略,**固定长度分块**是最简单直接的方式,派聪明采用的就是这种方式,每 512 个字符为一块,虽然可能会把语义完整的内容强行切断,但实现起来非常容易,对于计算资源有限的我们来说,是一个非常实用的选择。

语义分块会基于文档的语义结构来分割,比如按照段落、章节、主题来切分。这样能确保每个 chunk 包含相对完整的语义信息。我们打算在下一个版本中增加语义分块,对于结构化程度高的文档,比如技术手册、政策文件等,采用语义分块,充分利用文档的结构信息。对于结构化程度低的文档,比如聊天记录、邮件等,采用固定长度分块,确保处理的稳定性。

/**
   * 智能文本分割,保持语义完整性
   */
  private List<String> splitTextIntoChunks(String text, int chunkSize) {
      List<String> chunks = new ArrayList<>();

      // 按段落分割
      String[] paragraphs = text.split("\n\n+");

      StringBuilder currentChunk = new StringBuilder();

      for (String paragraph : paragraphs) {
          // 如果单个段落超过chunk大小,需要进一步分割
          if (paragraph.length() > chunkSize) {
              // 先保存当前chunk
              if (currentChunk.length() > 0) {
                  chunks.add(currentChunk.toString().trim());
                  currentChunk = new StringBuilder();
              }

              // 按句子分割长段落
              List<String> sentenceChunks = splitLongParagraph(paragraph, chunkSize);
              chunks.addAll(sentenceChunks);
          }
          // 如果添加这个段落会超过chunk大小
          else if (currentChunk.length() + paragraph.length() > chunkSize) {
              // 保存当前chunk
              if (currentChunk.length() > 0) {
                  chunks.add(currentChunk.toString().trim());
              }
              // 开始新chunk
              currentChunk = new StringBuilder(paragraph);
          }
          // 可以添加到当前chunk
          else {
              if (currentChunk.length() > 0) {
                  currentChunk.append("\n\n");
              }
              currentChunk.append(paragraph);
          }
      }

      // 添加最后一个chunk
      if (currentChunk.length() > 0) {
          chunks.add(currentChunk.toString().trim());
      }

      return chunks;
  }

/**
   * 分割长段落,按句子边界
   */
  private List<String> splitLongParagraph(String paragraph, int chunkSize) {
      List<String> chunks = new ArrayList<>();

      // 按句子分割
      String[] sentences = paragraph.split("(?<=[。!?;])|(?<=[.!?;])\\s+");

      StringBuilder currentChunk = new StringBuilder();

      for (String sentence : sentences) {
          if (currentChunk.length() + sentence.length() > chunkSize) {
              if (currentChunk.length() > 0) {
                  chunks.add(currentChunk.toString().trim());
                  currentChunk = new StringBuilder();
              }

              // 如果单个句子太长,按词分割
              if (sentence.length() > chunkSize) {
                  chunks.addAll(splitLongSentence(sentence, chunkSize));
              } else {
                  currentChunk.append(sentence);
              }
          } else {
              currentChunk.append(sentence);
          }
      }

      if (currentChunk.length() > 0) {
          chunks.add(currentChunk.toString().trim());
      }

      return chunks;
  }

/**
   * 分割超长句子,按词边界
   */
  private List<String> splitLongSentence(String sentence, int chunkSize) {
      List<String> chunks = new ArrayList<>();
      String[] words = sentence.split("\\s+");

      StringBuilder currentChunk = new StringBuilder();

      for (String word : words) {
          if (currentChunk.length() + word.length() + 1 > chunkSize) {
              if (currentChunk.length() > 0) {
                  chunks.add(currentChunk.toString().trim());
                  currentChunk = new StringBuilder();
              }
          }

          if (currentChunk.length() > 0) {
              currentChunk.append(" ");
          }
          currentChunk.append(word);
      }

      if (currentChunk.length() > 0) {
          chunks.add(currentChunk.toString().trim());
      }

      return chunks;
  }

12.如何处理跨chunk的信息完整性问题?

最直接有效的解决方案是引入滑动窗口机制,在相邻 chunk 之间保持一定的重叠区域。具体实现上,可以优化派聪明的分块逻辑,设置 20-30% 的重叠率。例如,如果 chunk 大小为 512 字符,则每次移动 350-400 字符,保留 100-150 字符的重叠。这样能够确保被切断的信息在相邻 chunk 中得到保留,提高信息检索的完整性。重叠策略虽然会增加存储空间,但能显著改善跨边界信息的连续性。

/**
   * 使用重叠窗口分割文本
   */
  private List<String> splitTextWithOverlap(String text, int chunkSize, int overlapSize) {
      List<String> chunks = new ArrayList<>();

      // 首先进行语义分割
      List<String> semanticChunks = splitTextIntoChunks(text, chunkSize);

      // 添加重叠内容
      for (int i = 0; i < semanticChunks.size(); i++) {
          StringBuilder chunkWithOverlap = new StringBuilder();

          // 添加前一个chunk的结尾部分作为重叠
          if (i > 0) {
              String prevChunk = semanticChunks.get(i - 1);
              String prevOverlap = getLastNChars(prevChunk, overlapSize / 2);
              chunkWithOverlap.append(prevOverlap).append(" ");
          }

          // 添加当前chunk
          chunkWithOverlap.append(semanticChunks.get(i));

          // 添加下一个chunk的开头部分作为重叠
          if (i < semanticChunks.size() - 1) {
              String nextChunk = semanticChunks.get(i + 1);
              String nextOverlap = getFirstNChars(nextChunk, overlapSize / 2);
              chunkWithOverlap.append(" ").append(nextOverlap);
          }

          chunks.add(chunkWithOverlap.toString());
      }

      return chunks;
  }

13.多模态内容如何在RAG中处理?

多模态内容包含文本、图像、表格等不同类型的信息载体,每种模态都有其独特的信息表达方式和语义特征。文本承载概念性和描述性信息,图像包含视觉和空间信息,表格则体现结构化的数据关系。

在 RAG 系统中处理这些内容的主要挑战在于:不同模态的信息密度差异巨大,语义表达方式各异,以及如何在统一的向量空间中表示和检索这些异构信息。

派聪明目前支持 txt、PDF、Word 等文本内容的处理,通过 Apache Tika 来完成,

Apache Tika 是一个开源的多格式内容分析工具,可以从超过 1000 种文件格式(如 PDF、Word、纯文本等)中提取文本内容和元数据(如作者、标题、创建时间等)。

对于下一版的派聪明,我们也打算追加图像、表格等内容的多模态支持。对于图像内容,可以集成 OCR 技术提取图像中的文字信息,同时使用图像描述模型(如 CLIP、BLIP 等)生成图像的文本描述。对于表格内容,需要将其转换为结构化的文本表示,如 CSV 格式或者带有行列标识的自然语言描述。

14.RAG系统的主要性能瓶颈在哪里?如何优化?

向量检索是最主要的瓶颈。当知识库规模达到几十万甚至上百万文档时,实时的向量相似度计算会成为明显的性能瓶颈。

优化方案包括:使用更高效的向量索引算法,如 HNSW,HNSW 算法构建了一个分层的小世界图,通过高效的导航和连接节点,实现快速的近似最近邻搜索。它利用图结构的优势,在多层次上进行跳跃和搜索,能显著减少检索时间。

Embedding 生成的延迟也是一个重要瓶颈。用户查询时需要实时生成 query 的 embedding,如果模型比较大,这个过程可能需要很长时间。派聪明目前调用的是豆包的向量 API,整体的体验我认为还是非常不错的。

大模型的推理时间也是一块大的性能开销,所以派聪明采用了流式输出的方式,让用户能够实时看到生成过程。

15.RAG怎么解决LLM上下文窗口有限的问题?

首先是知识入库阶段。我们不会在用户提问时才把所有文档都丢给模型。相反,我们会预先将所有的知识(比如公司的规章制度、产品手册、技术文档等)进行处理。这个处理过程包括:

  • 1)将长文档切分成更小的、逻辑完整的段落或“块”(Chunks);
  • 2)调用 Embedding 模型,将每个文本块都转换成一个数学向量,这个向量可以被认为是该文本块在多维空间中的“语义坐标”;
  • 3)将这些文本块和它们对应的向量存入向量数据库中,比如说 ElasticSearch。

其次是检索与生成阶段。当用户提出一个问题时,RAG 并不会直接把问题扔给 LLM。它会执行以下步骤:

  • 1)使用与入库时相同的 Embedding 模型,将用户的问题也转换成一个向量;
  • 2)用这个“问题向量”去向量数据库中进行“语义搜索”或“相似度查询”,找到与问题语义最相近的几个文本块;
  • 3)最后,也是最关键的一步,系统会将用户的原始问题和搜索到的这几个最相关的文本块一起打包,形成一个 Prompt,然后发送给 LLM。

通过这种方式,LLM 在回答问题时,它的上下文窗口里不再是海量的、不相关的原始文档,而是系统为它精心挑选的、与当前问题高度相关的几段“参考资料”。

16.在多轮对话中,如何管理和利用历史上下文?

派聪明当前采用了基于 Redis 的对话历史管理机制,每个用户都有一个唯一的会话 ID,所有的对话内容都按照时间顺序存在 Redis 中,并设置了 7 天的过期时间。

考虑到上下文会越来越长,我们打算在下一版实现一个滑动窗口,比如只保留最近 10 轮对话,或者根据 token 数量动态调整。

17.如何处理指代消解(如”它”、”这个”等)问题?

指代消解是多轮对话中的关键技术挑战,需要让 AI 理解”它”、”这个”、”那个”等指代词具体指向什么。

派聪明的策略是:

第一步,识别对话中的关键实体(人名、地名、概念等),建立实体库。比如用户问”什么是机器学习”,系统要记住”机器学习”这个实体。然后在后续对话中,当用户说”它有哪些应用”时,能够识别”它”指的是”机器学习”。

第二步,设计一个滑动窗口,重点关注最近 3-5 轮对话,因为指代关系通常不会跨越太远。

第三步,距离最近的同类型实体优先。

18.在不同领域(法律、医疗、金融)应用RAG时,需要注意什么特殊问题?

法律文本有很强的精确性要求,一个词汇的差异可能导致完全不同的法律后果。所以法律意见必须基于权威的法律文本,不能出现任何”创造性”的解释。我们在回答中强制要求引用具体的法条和案例,并且会标注信息来源的权威级别。对于模糊的问题,系统会明确建议咨询专业律师,而不是给出可能误导的回答。

医疗领域的挑战主要在于专业术语和安全性要求。RAG系统绝对不能给出具体的诊断建议或治疗方案,因为这涉及到医疗执业资格问题。我们在系统设计时就明确限制了回答范围,只提供医学知识科普,对于任何可能被理解为诊疗建议的内容,都会自动添加免责声明,建议用户咨询专业医生。

金融领域面临的主要挑战是数据的实时性和准确性要求。金融市场变化很快,昨天的数据今天可能就过时了。所以需要建立实时的数据更新机制,对于价格、汇率等高频变化的数据,要标注明确的时间戳,提醒用户数据的时效性。

另外要在回答中明确标注风险提示,说明这只是信息查询而非投资建议。对于一些敏感的金融产品信息,还会要求用户确认其合格投资者身份。

19.如何设计RESTful API来支持RAG系统的各种功能?

在设计派聪明 RAG 系统的 API 时,我首先会遵循几个核心的设计原则,确保 API 清晰。

第一,一切以资源为中心。我会把系统的核心功能抽象成资源,比如‘知识库’(Knowledge Bases)、‘文档’(Documents)、‘对话’(Conversations)等等。然后用标准的 HTTP 方法,像 GET、POST、DELETE,来对这些资源进行操作。

第二,我在 URL 里了加上版本号,比如 /api/v1。这样做的好处是,未来系统升级,推出新版 API 的时候,不会影响到正在使用旧版接口的用户,兼容性会非常好。

第三,无论是成功还是失败,API 的返回格式都应该是统一的 JSON 结构,比如都包含 code、message 和 data 这几个字段。这样前端或者其他调用方处理起来会非常方便。

20.RAG系统中如何保护敏感数据?

在派聪明中,我们通过 Spring Security+JWT 实现了基于 RBAC 的权限控制系统。当用户提问时,系统会根据用户的角色权限和组织标签,在搜索时自动过滤掉无权访问的数据。

第二,数据在流转中全程加密,比如说我们在将向量数据存入 ES 或者从 ES 取出时,采用 HTTPS 的加密方式,没有密钥是无法进行通信的。

21.如何进行A/B测试来优化RAG效果?

A/B 测试的策略可以分为三个核心部分: “测什么”、“怎么评”和“如何做” 。

测什么阶段我们需要明确测试的实验变量,比如说派聪明中有一个服务叫混合检索,融合了语义检索和关键词检索,那我们就可以测试不同的融合权重。比如,A 组是 0.5 * vector_score + 0.5 * bm25_score,B 组可以是 0.7 * vector_score + 0.3 * bm25_score,看看哪个组合更能命中用户的真实意图。

再比如说我们可以测试不同的文档分块策略,是 500 个字符还是 400 个字符还是有一部分重叠字符,看哪种策略能产生最恰当的上下文片段。

还有,我们可以对比不同的大模型,比如 A 组用 DeepSeek,B 组用通义千问/混元/豆包,看哪个模型的回答更流畅、更准确、更能遵循指令。

有了实验目标后,我们需要一套科学的评价体系来判断“谁优谁劣”。比如说我们可以在每个回答后面加上“顶/踩”按钮。A/B 测试的核心目标就是看哪个版本的“顶”率更高,“踩”率更低。

最后,我们需要通过技术手段来支撑整个 A/B 测试流程。比如说当一个请求进来时,系统会根据用户 ID 或会话 ID,通过哈希等方式,将用户稳定地分配到 A 组或 B 组。

一旦 B 组被验证为更优,我们再进行小范围的灰度发布(比如,先切 10% 的流量到 B 组),观察系统稳定性和核心指标。确认无误后,再逐步将所有流量切换到新版本,并最终下线旧版本。

22.GraphRAG与传统RAG有什么区别?

GraphRAG 是对 RAG 的增强,通过整合知识图谱中存储的结构化领域知识来增强检索,借助知识图谱中丰富的连接和语义关系,GraphRAG 可以克服纯向量 RAG 的局限性,并提供更准确、更易于解释的查询响应。

传统的 RAG 知识库本质上是一个“文档集合”。无论是 PDF、Word 还是网页,我们都将它们切割成一个个独立的文本块,然后对每个块进行向量化,存入向量数据库。

GraphRAG 的核心是“知识图谱”。在数据预处理阶段,我们不仅仅分块,还会利用大模型从文本中提取出实体(Entities)、关系(Relationships)和属性(Attributes),然后将它们构建成一个图。比如,从“沉默王二吹了一个牛逼”这句话中,我们可以抽取出 (实体:沉默王二)- [关系:吹了 ] ->(实体:牛逼) 这样的结构。这就把零散的知识点编织成了一张巨大的、相互连接的知识网络。

在检索阶段,传统的 RAG 是基于语义相似度的。就像我们在派聪明中做的那样,将用户问题向量化,然后在向量数据库中寻找与之最相似的文本块。

GraphRAG 的检索变成了在知识图谱上的图遍历或子图查询 。当用户问“沉默王二吹的牛逼是啥?”时,GraphRAG 可以:

  1. 第一跳 :在图谱中定位到“沉默王二”这个节点。
  2. 第二跳 :沿着“吹了”这条边,找到“牛逼”节点。
  3. 第三跳 :再从“牛逼”节点出发,找到“ 26 万订阅号读者”、“GitHub 14000+ star”等内容。

23.对RAG技术的未来发展有什么看法?

未来的 RAG 将是多模态的。图片、音频、视频、代码、表格、API……任何信息形态都可以被索引和检索。到时候,我们的提问可以是“这张图里穿蓝色衣服的人在做什么?”

未来的 RAG 将会向智能体方向发展,比如模型在检索后会先判断“我找到的知识足够回答问题吗?”如果不够,它可能会主动向用户追问,或者调用 function call 去外部查询,再根据前面的检索结果,生成新的、更精确的查询。

24.如果RAG系统返回0个检索结果,你会如何排查问题?

首先,我要确定这是特殊情况还是普遍现象。如果所有的提问都无法检索到结果,那很可能是系统级的故障,比如向量数据库连接失败了、索引被误删了、或者向量 API 服务宕机了。如果只是特殊情况,那么问题很可能出在数据处理、查询理解或召回策略上。比如,用户问了一个知识库里完全没有涉及的领域,或者查询的关键词过于生僻。

接着,我会检查召回的候选集数量 k 是否设置得过小,导致过滤条件叠加后没有结果。

权限也需要排查。可能是用户的权限不足,无法访问相关文档。

25.如何处理不同API服务(豆包 embedding、DeepSeek)的调用失败?

在调用豆包向量 API 失败时,我们会自动回退到纯文本搜索,实现服务降级,确保检索服务可用。

// 在 HybridSearchService 中添加更精细的错误处理
public List<Map<String, Object>> searchWithPermission(String query, String userId, int size) {
    try {
        // 尝试混合搜索
        return performHybridSearch(query, userId, size);
    } catch (EmbeddingServiceException e) {
        logger.warn("向量服务不可用,降级到文本搜索: {}", e.getMessage());
        return textOnlySearch(query, userId, size);
    } catch (ElasticsearchException e) {
        logger.error("搜索服务异常: {}", e.getMessage());
        return Collections.emptyList(); // 或返回缓存结果
    }
}

并且在调用豆包向量 API 时,我们采用了 Reactor 的重试机制,支持固定延迟重试 3 次,并设置了 30 秒的超时保护。

// 在 EmbeddingClient 中添加熔断器和更智能的重试
@Component
public class EmbeddingClient {
    private final CircuitBreaker circuitBreaker;
    
    private String callApiOnce(List<String> batch) {
        return circuitBreaker.executeSupplier(() -> {
            return webClient.post()
                .uri("/embeddings")
                .bodyValue(requestBody)
                .retrieve()
                .bodyToMono(String.class)
                .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
                    .maxBackoff(Duration.ofSeconds(10))
                    .filter(this::isRetryableException))
                .timeout(Duration.ofSeconds(30))
                .block();
        });
    }
    
    private boolean isRetryableException(Throwable ex) {
        return ex instanceof WebClientResponseException &&
               ((WebClientResponseException) ex).getStatusCode().is5xxServerError();
    }
}

下个版本中,我们打算接入更多的大模型 API,当 DeepSeek 不可用的时候,能够自动切换到豆包、文心一言、腾讯混元、阿里通义千问等。派聪明目前已经支持快速切换到本地的模型服务。

26.请说说你AIGC、RAG、Agent 的理解?

AIGC,全称为 AI Generated Content,意为“人工智能生成内容”。它指的是利用人工智能技术自动生成文本、图片、音频、视频等多种内容的过程。2022 年 11 月 30 日,OpenAI 基于 GPT-3.5 的 ChatGPT 正式上线,引爆了 AIGC 热潮。

RAG,是一种将信息检索(IR) 与大型语言模型(LLM) 的文本生成能力相结合的技术。其核心思想是:当 LLM 需要回答一个问题或生成文本时,不是仅依赖其内部训练时学到的知识,而是先从一个外部知识库中检索出相关的信息片段,然后将这些检索到的信息与原始问题/指令一起提供给 LLM,让 LLM 基于这些最新、最相关的上下文信息来生成更准确、更可靠、更少幻觉的答案。

Agent,也就是“智能体”,在计算机科学和人工智能领域指的是一个能够感知环境、自主决策并采取行动以实现特定目标的实体或系统。它可以是软件程序、机器人硬件,甚至是生物实体(如人类或动物),但在 AI 领域通常指软件智能体。

Agent 和 AIGC 最大的区别:

  1. AIGC 主要以生成式任务为主,而 Agent 是可以通过自主决策能力完成更多通用任务的智能系统。
  2. 常见的 AIGC 系统(文生文,文生图)的核心就是一个生成模型,而 Agent 是一个集 Function Call 模型、软件工程于一体的复杂系统,需要处理模型和外界的信息交互。
  3. Agent 可以集成 AIGC 能力完成某些特定的任务,也就是 AIGC 可以是 Agent 系统里面的一个子模块。

也就是说,Agent 最大的特点是,借助 Function Call 模型,可以自主决策使用外接的一些工具来完成特定的任务。

27.那什么是 function call 模型?

Function Calling,也就是函数调用, 是大型语言模型的关键技术。RAG技术是为了解决模型无法和外接数据交互的问题,但是 RAG 的局限在于只赋予了模型检索数据的能力,而 Function Calling 允许模型理解用户请求中的潜在意图,并自动生成结构化参数来调用外部任何函数/工具,从而突破纯文本生成的限制,实现与真实世界的交互,比如可以调用查天气、发邮件、数学计算等工具。

Function Call 模型最早由 OpenAI 在 2023 年 6 月 13 日提出并发布,首次在 GPT-4 模型上实现了 Function Calling 能力。

Function Call 需要先定义函数,向 LLM 描述函数的用途、输入参数格式(JSON Schema):

{
  "name": "get_current_weather",
  "description": "获取指定城市的天气",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称"
      },
      "unit": {
        "enum": ["celsius", "fahrenheit"]
      }
    },
    "required": ["city"]
  }
}

当用户提问“北京今天需要带伞吗?”

→ LLM 识别到意图需要调用 get_current_weather

→ 并生成结构化参数:{"city": "北京", "unit": "celsius"}

然后执行 get_current_weather 函数调用天气 API,获取真实数据:{"temp": 25, "rain_prob": 30%},然后将结果交回LLM,生成最终回复:“北京今天25°C,降水概率30%,建议带伞。”

那也就是 OpenAI 发布 Function Call 模型后,Agent 才开始迅速发展。Agent 真正进入到公众视野,被大家广泛关注的事件是 2025年4月 Manus 发布的通用智能体产品,引入了 Computer Use 和 Browser Use,首次展现出智能体的强大能力。

28.什么是 MCP?

MCP,是 Model Context Protocol 的缩写,也就是模型上下文协议,由人工智能公司 **Anthropic **于 2024 年 11 月 24 日正式发布并开源。

MCP 协议旨在解决大型语言模型(LLM)与外部数据源、工具间的集成难题,被比喻为“AI 应用的 USB-C 接口“。通过标准化通信协议,将传统的“M×N集成问题”(即多个模型与多个数据源的点对点连接)转化为“M+N模式”,大幅降低开发成本。

MCP 自 2024 年 11 月 24 日 发布以来,OpenAI、Google、微软、腾讯、阿里、百度等头部企业纷纷接入 MCP,推动其成为事实性行业标准

29.了解 A2A 吗?

A2A ,即 Agent-to-Agent ,指的是一种系统架构,其包含多个独立的、专门的 Agent 进行协同工作 ,以完成比单个 Agent 能处理的更复杂的任务。

单个 Agent 就像是一个全能的“通才”,他什么都懂一点,但可能没有哪个领域是顶尖的。

A2A 就像一个专家团队,有项目经理、数据分析师、文案专家、软件工程师等。项目经理负责拆解任务,然后分发给最合适的专家去执行。

一个 Agent 要能解决问题,首先需要获取准确的信息。RAG 可以作为这个 Agent 获取和理解信息的核心工具之一。

A2A 架构的优势在于每个 Agent 都可以专注于一个特定领域(如代码执行、数据库查询、API 调用、文案写作),使得开发、测试和维护更加简单。

30.了解Transformer 吗?

Transformer 是近年来深度学习领域,尤其是自然语言处理(NLP)中,最具革命性的模型架构。它奠定了所有现代大型语言模型(LLM),包括 GPT、BERT 等的基础。

Transformer 最初是为机器翻译任务设计的,所以它有一个经典的编码器-解码器(Encoder-Decoder)结构。

GPT 本质上就是把 Transformer 的 Decoder 部分拿出来,进行大量的预训练。

在 Transformer 出现之前,处理序列数据(如文本)的主流模型是 RNN,它的工作方式像人阅读一样,一个词一个词地顺序处理,并试图通过一个“记忆单元”来记住前面的信息。

RNN 的问题是必须处理完前一个词才能处理后一个词,这在硬件(GPU/TPU)飞速发展的当下阶段,极大地限制了训练速度。并且当句子很长时,RNN 很难记住最开始的信息,会出现“遗忘”现象

Transformer 完全抛弃了 RNN 的循环结构,提出了自注意力机制。

对于一句话中的每一个词,自注意力机制都会计算这句话中所有其他词对这个词的“重要性”或“相关性”得分。然后根据这个得分,将所有词的信息加权融合,生成这个词在当前上下文中的新表示。

比如说在“派聪明是一个企业级的 RAG 知识库,它是由沉默王二的团队研发的”这个句子中,自注意力机制能够识别出“它”指的是派聪明,而不是沉默王二。

Transformer 通过位置编码感知单词在句子中的位置顺序。位置编码是一个与词向量维度相同的向量,通过数学公式(正弦和余弦函数)生成,包含了单词在序列中的绝对或相对位置信息。在输入模型前,它会和词向量相加,让模型知道每个词的位置。

在 RAG 中,最后负责整合检索到的知识并生成答案的那个“生成”模块,通常就是一个基于 Transformer 的大型语言模型。而用于将文本块转换为向量的模型,也都是基于 Transformer 的 Encoder 结构训练出来的。

31.在做检索时,你是否尝试过或了解过其他的重排(Re-ranking)方法?

我们考虑过一种轻量级的重排方法—— 倒数排名融合(RRF)。它是 Milvus 混合搜索的一种重新排名策略,核心思想是,一个文档如果在多个不同的召回列表中都排名靠前,那么它应该更重要。

具体来说,我们会分别从向量检索和关键词检索拿到两个排好序的文档列表。对于任何一个文档,我们计算一个 RRF 分数,公式是 1 / (k + rank1) + 1 / (k + rank2),其中 rank1 和 rank2 是它在两个列表中的排名(如果不在某个列表中,则该项为0), k 是一个小的平滑常数(比如60)。最后,我们根据这个新的 RRF 分数对所有文档进行最终排序。

另外就是大模型重排,将召回的 Top N 个文档块的内容,连同原始查询一起,通过一个精心设计的提示词全部提交给 LLM。这个 Prompt 大致会是这样:

查询 :[用户的原始问题]

文档列表 :

[文档1]:[文档1的文本内容]

[文档2]:[文档2的文本内容]

任务 :请根据以上文档与查询的相关性,对文档进行重排,并以 JSON 格式输出排序后的文档索引列表。

然后,我们再解析 LLM 返回的 JSON 结果,得到最终的排序。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇