二哥项目资料

派聪明 RAG 面试题预测:31道高频 AI 面试八股,1.2 万字 50 张手绘图

正文 55,869 字 #11 原文
本文目录 · 63 节

星球:Java进阶之路&二哥编程星球 | 专栏:派聪明RAG知识库 | 作者:沉默王二 | 发布:2025-10-08

派聪明 RAG 面试题预测:31道高频 AI 面试八股,1.2 万字 50 张手绘图

派聪明 RAG 面试题预测:31道高频 AI 面试八股,1.2 万字 50 张手绘图

技术派阅读体验会比星球更好: https://paicoding.com/column/10/19 支持微信扫码直接登录了。

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 relevantDocIds, int

topK) {

List 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 splitTextIntoChunks(String text, int chunkSize) {

List 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 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 splitLongParagraph(String paragraph, int chunkSize) {

List chunks = new ArrayList<>();

// 按句子分割

String[] sentences = paragraph.split("(? 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 splitLongSentence(String sentence, int chunkSize) {

List 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 splitTextWithOverlap(String text, int chunkSize, int overlapSize) {

List chunks = new ArrayList<>();

// 首先进行语义分割

List semanticChunks = splitTextIntoChunks(text, chunkSize);

// 添加重叠内容

for (int i = 0; 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 (实体:牛逼) 这样的结构。这就把零散的知识点编织成了一张巨大的、相互连接的知识网络。

在检索阶段,传统的 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> 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 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 结果,得到最终的排序。

技术派阅读体验会比星球更好: https://paicoding.com/column/10/19 支持微信扫码直接登录了。

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 relevantDocIds, int

topK) {

List 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 splitTextIntoChunks(String text, int chunkSize) {

List 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 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 splitLongParagraph(String paragraph, int chunkSize) {

List chunks = new ArrayList<>();

// 按句子分割

String[] sentences = paragraph.split("(? 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 splitLongSentence(String sentence, int chunkSize) {

List 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 splitTextWithOverlap(String text, int chunkSize, int overlapSize) {

List chunks = new ArrayList<>();

// 首先进行语义分割

List semanticChunks = splitTextIntoChunks(text, chunkSize);

// 添加重叠内容

for (int i = 0; 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 (实体:牛逼) 这样的结构。这就把零散的知识点编织成了一张巨大的、相互连接的知识网络。

在检索阶段,传统的 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> 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 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 结果,得到最终的排序。

技术派阅读体验会比星球更好: https://paicoding.com/column/10/19 支持微信扫码直接登录了。

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 relevantDocIds, int

topK) {

List 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 splitTextIntoChunks(String text, int chunkSize) {

List 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 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 splitLongParagraph(String paragraph, int chunkSize) {

List chunks = new ArrayList<>();

// 按句子分割

String[] sentences = paragraph.split("(? 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 splitLongSentence(String sentence, int chunkSize) {

List 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 splitTextWithOverlap(String text, int chunkSize, int overlapSize) {

List chunks = new ArrayList<>();

// 首先进行语义分割

List semanticChunks = splitTextIntoChunks(text, chunkSize);

// 添加重叠内容

for (int i = 0; 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 (实体:牛逼) 这样的结构。这就把零散的知识点编织成了一张巨大的、相互连接的知识网络。

在检索阶段,传统的 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> 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 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 结果,得到最终的排序。