RAG 项目实战教程——小测验答案


1. 本项目使用哪个 Python Web 框架?它相比 Flask 的主要优势是什么?

本项目使用 FastAPI。相比 Flask 的主要优势包括:

  • 原生支持异步(async/await),适合 SSE 流式响应和高并发场景
  • 基于 Pydantic 的自动请求/响应验证和序列化
  • 自动生成 OpenAPI 文档
  • 类型提示驱动开发,代码更安全
  • 性能显著优于 Flask(基于 Starlette + Uvicorn)

2. docker-compose.ymldepends_on 的作用是什么?它能保证依赖服务完全就绪吗?

depends_on 控制容器的启动顺序,确保 swxy_apigsk_pges01redis 之后启动。但它不能保证依赖服务完全就绪——它只保证容器已启动,不保证容器内的服务(如 Elasticsearch)已完成初始化并准备好接受连接。如需等待服务就绪,应使用 healthcheck 配合 depends_on.condition: service_healthy。项目中 ES 配置了 healthcheck,但 depends_on 并未使用 condition 字段。


3. 在 retrieval.py 中,vector_similarity_weight=0.6 意味着什么?关键词检索权重是多少?

vector_similarity_weight=0.6 表示在 Rerank 精排阶段,向量语义相似度占 60% 权重。关键词检索权重为 1 - 0.6 = 0.4,即 40%。这在 Dealer.retrieval() 中传递给 rerank_by_model() 方法:tkweight = 1 - vector_similarity_weight = 0.4vtweight = vector_similarity_weight = 0.6


4. RagTokenizertokenize() 方法使用了哪两种匹配算法?当两种算法结果不一致时如何处理?

使用了正向最大匹配maxForward_)和逆向最大匹配maxBackward_)两种算法。当两种算法结果不一致时,对不一致的部分使用 DFS(深度优先搜索) 遍历所有可能的分词方案,然后通过评分函数 score_() 选出最优方案。评分综合考虑了分词数量(越少越好)、长词比例(越高越好)和词频(越高越好)。


5. fine_grained_tokenize() 为什么要取排序后的第二优分词方案而非最优方案?

因为最优分词方案已经存储在 content_ltks 字段中用于主检索。fine_grained_tokenize() 取第二优方案存储在 content_sm_ltks 字段中,目的是生成不同的词组合,增加检索时的命中机会。例如"数据分析"的最优分词是整词,而第二优分词可能是"数据"+“分析”,这样用户搜索"数据"时也能匹配到。


6. DeepDoc PDF 解析的 5 步流水线是什么?每一步的作用分别是什么?

  1. __images__():OCR 识别——将 PDF 每页转为图像,使用 OCR 提取文字,合并字符到文本块
  2. _layouts_rec():布局分析——使用深度学习模型识别每个文本块的类型(标题、正文、表格、图片等)
  3. _table_transformer_job():表格结构分析——识别表格内的行、列、表头、跨行跨列关系
  4. _text_merge():水平文本合并——将同一行内相邻且属于同一布局区域的文本块合并
  5. _concat_downward():垂直文本拼接——使用 XGBoost 模型判断上下相邻的文本块是否应该合并为一个段落

7. _updown_concat_features() 使用什么机器学习模型来判断文本块是否应该合并?为什么不用简单的规则?

使用 XGBoost 梯度提升树模型(self.updown_cnt_mdl,加载自 updown_concat_xgb.model)。不用简单规则的原因是文本拼接决策涉及 31 个特征的复杂组合,包括垂直距离、布局类型、标点符号、分词变化、跨页情况等。这些特征之间存在复杂的交互关系(如"虽然以句号结尾但下文是同一表格行"),很难用规则覆盖所有情况。机器学习模型通过训练数据自动学习特征组合的权重,鲁棒性远优于手写规则。


8. SSE(Server-Sent Events)和 WebSocket 的区别是什么?本项目为什么选择 SSE?

维度SSEWebSocket
方向单向(服务器→客户端)双向
协议HTTP独立协议(ws://)
复杂度
自动重连内建支持需要手动实现

本项目选择 SSE 因为:对话场景是单向流式的(服务器将 LLM 的 token 流推送给客户端),不需要双向通信。SSE 基于标准 HTTP,无需额外的协议协商,且 FastAPI 的 StreamingResponse 天然支持 SSE,实现更简单。


9. 在 chat.py 的 Prompt 中,引用标注格式 ##编号$$ 的设计目的是什么?

##编号$$ 是一种自定义的引用标注格式,目的是:

  1. 让 LLM 在回答中标注每段内容的来源编号,实现溯源可查
  2. 前端解析 ##数字$$ 模式后,可以将其渲染为可点击的引用链接,跳转到对应的文档段落
  3. 使用非常规符号组合(##$$)避免与 Markdown 语法或文档内容冲突
  4. 帮助用户判断回答的可信度——有引用的部分基于文档事实,无引用的部分可能是 LLM 补充的常识

10. 快速解析服务(QuickParseService)和知识库解析的适用场景分别是什么?它们的存储介质有何不同?

维度快速解析知识库解析
适用场景临时小文档问答(PDF<4页,TXT/DOCX<4000字)大型文档长期知识库
存储介质Redis(2小时过期)Elasticsearch(永久存储)
检索方式全文直接放入 LLM Prompt向量+关键词混合检索后取 Top-K
处理流程文本提取 → Redis 存储文本提取 → 分块 → 分词 → 向量化 → ES 索引

11. 前端使用 adapter: 'fetch' 的原因是什么?如果改为默认的 XMLHttpRequest 会怎样?

Axios 默认使用 XMLHttpRequest(XHR),而 XHR 不支持 ReadableStream,无法逐步读取服务器推送的数据——它只能在响应完全接收后才触发回调。adapter: 'fetch' 让 Axios 使用浏览器原生 Fetch API,Fetch API 返回 ReadableStream,前端可以通过 reader.read() 逐块读取 SSE 数据,实现真正的流式渲染。如果改为 XHR,用户必须等待 LLM 生成完毕才能看到回答,失去了流式输出的体验。


12. 在 Rerank 阶段,为什么标题的权重是 x2、关键词的权重是 x5、问答对的问题权重是 x6?

1
tks = content_ltks + title_tks * 2 + important_kwd * 5 + question_tks * 6

这是基于信息密度的领域经验设计:

  • 标题 x2:标题是内容的概括,匹配标题说明主题相关,但标题信息较少,适度加权
  • 关键词 x5important_kwd 是人工或算法标记的核心术语,命中说明高度相关,重加权
  • 问答对问题 x6:如果 chunk 本身是一个 FAQ 的问题部分(question_tks),用户问题与之匹配意味着该 chunk 的答案很可能就是用户需要的,因此给予最高权重

权重数值通过复制列表实现(如 title_tks * 2 表示标题分词出现两次),在 TF-IDF 或 BM25 计算中相当于提升了对应词的词频。


13. generate_recommended_questions() 使用 Qwen2.5-7B 而非 DeepSeek-R1 的原因是什么?

原因包括:

  1. 速度:推荐问题不是核心回答,用户不愿等待太久。Qwen2.5-7B 是轻量模型,推理速度远快于 DeepSeek-R1
  2. 成本:推荐问题的生成是额外开销,使用小模型降低 API 调用费用
  3. 稳定性:设置了 response_format={"type": "json_object"} 要求 JSON 输出,小模型在结构化输出方面已经足够稳定
  4. 超时控制:设置了 timeout=30 秒,确保不会因推荐问题生成过慢而阻塞主流程

14. Dealer.search()FusionExpr("weighted_sum", topk, {"weights": "0.05, 0.95"}) 的 0.05 和 0.95 分别代表什么?

这是 Elasticsearch 初步检索阶段的融合权重:

  • 0.05:全文关键词检索(matchText)的权重——仅占 5%
  • 0.95:向量语义检索(matchDense)的权重——占 95%

初步检索阶段极度偏重向量检索,目的是尽可能多地召回语义相关的候选文档(宁可多召回不相关的,也不漏掉相关的)。精确的相关性判断交给后续的 Rerank 阶段处理。

注意区分:这是 ES 层的融合权重,与 Rerank 阶段的 vector_similarity_weight(默认 0.6)是两个不同层次的权重。


15. 前端使用 Valtio 的 proxy()useSnapshot() 分别起什么作用?为什么在流式输出场景中这比 Redux 更合适?

  • proxy():创建一个可变的响应式代理对象。可以直接修改属性(如 target.content += "新token"chat.list.push(newItem)),Valtio 自动追踪变更
  • useSnapshot():创建代理对象的不可变快照,用于 React 渲染。只有当快照中实际使用的属性发生变化时才触发重渲染

在流式输出场景中,每收到一个 token(可能每秒几十次)就需要更新状态。如果使用 Redux:

  • 每次更新都需要 dispatch action → reducer 创建新状态对象 → 触发所有 connected 组件 re-render
  • 性能开销大,代码冗余

而 Valtio 只需 target.content += delta.content 一行代码,且只有依赖 content 属性的组件才会重渲染,性能更优、代码更简洁。