MindRoute RAG 面试准备|企业知识库 Agentic RAG 项目面经
MindRoute RAG|企业知识库 Agentic RAG 面试准备
项目别名:nanchat
面试岗位:Agent / Python 后端开发
使用方式:先背“1 分钟项目介绍”和“核心链路”,再按面试官追问深度展开。所有效果指标只填写真实数据,不要临时编造。
一、1 分钟项目介绍(可直接说)
我负责的是 MindRoute RAG,一个面向企业文档知识库的问答系统,核心目标是让模型能够基于企业内部资料回答问题,并返回可定位的来源和页码,降低幻觉和人工查文档的成本。
项目同时支持 Standard RAG 和 Agentic RAG 两种模式。Standard RAG 作为稳定基线,执行查询改写、混合召回、重排和答案生成;对于需要多步骤推理、跨章节查找或要求完整覆盖的问题,Agentic RAG 会先由 Planner 分解任务,再由 Executor 并行执行多个检索子任务,根据证据完整度决定是否通过 Seed Query 定向补检,必要时进行一次 Scope Exploration 和 Replan,最后统一合成答案。
在知识库侧,我们采用 Parent-Child 两级切分:Child 负责提高召回精度,Parent 保留完整上下文。检索阶段结合 BGE-M3 向量检索和 BM25 关键词检索,用 RRF 融合后,再使用 BGE Reranker 对候选 Parent 进行精排。数据层使用 Qdrant 存储向量和检索元数据,MySQL 存储文档、Parent 片段和会话等结构化数据,文档解析使用 MinerU,服务层使用 Python、FastAPI、LangChain 和 LangGraph,前端通过 Vue/TypeScript 接收 SSE 流式结果。
我主要负责后端 Agent 流程、RAG 检索链路、异步并发、SSE 流式协议以及异常和证据约束设计。
这段话的四个关键词
- 为什么做:企业文档多、检索成本高、通用大模型容易幻觉。
- 怎么检索:Parent-Child + dense/sparse hybrid + RRF + rerank。
- 为什么 Agentic:复杂问题需要拆解、并行检索、缺口补检和重规划。
- 工程怎么落地:FastAPI 异步服务、LangGraph 状态图、SSE 流式输出、引用可追溯。
为什么需要 RAG:以公司年报问答为例
面试时不要只说“RAG 可以减少幻觉”,最好马上落到一个具体业务问题上。比如企业知识库里有某上市公司近几年的年报,用户问:
“请根据这家公司 2024 年年报,比较 2024 年和 2023 年的营业收入、毛利率、研发费用变化,解释主要原因,并列出每个结论对应的页码;如果年报中还披露了相关风险,也一并说明。”
这个问题看似只是查数字,实际上同时包含了时间范围、表格取数、指标计算、跨章节解释、风险交叉验证和来源追溯。
1. 为什么不能只依赖大模型记忆
- 知识可能过时:模型参数中的知识不一定包含最新年报,企业内部报告更不可能天然存在于通用模型中。
- 数字和单位容易错:年报里可能同时出现“万元、百万元、亿元”,还会区分合并口径、母公司口径和持续经营口径。
- 信息分散:营业收入和净利润可能在合并利润表,变化原因在“管理层讨论与分析”,风险又在“重大风险提示”或附注里。
- 长文档难以完整关注:几十到上百页的年报不能简单粗暴地全部塞进上下文,模型容易遗漏脚注、表格列名和限定条件。
- 回答需要可审计:财务、人事、法务场景不能只给一个看起来合理的答案,用户需要知道结论来自哪份报告、哪一页、哪一段。
- 企业资料有权限边界:内部年报、经营分析和制度文件不能被模型当成公开常识处理,还需要按用户和租户做权限过滤。
所以 RAG 的价值不是“让模型多背一点内容”,而是把当前、私有、可更新、可引用的企业资料在回答时动态提供给模型,并要求答案受证据约束。
2. 这个问题怎样走 Standard RAG
如果用户只问一个明确事实,例如“2024 年营业收入是多少”,Standard RAG 就足够:
1 | 问题:2024 年营业收入是多少? |
这里特别要保留表头、单位、合并范围和年份列,不能只把包含数字的一行切出来。对于“同比增长率”这类计算,也应先抽取原始数字,再用确定性代码计算,最后让模型负责解释,避免模型心算出错。
3. 这个问题为什么适合 Agentic RAG
上面的完整问题至少可以拆成三个相互独立、可以并行的任务:
1 | 原问题:比较 2024/2023 经营指标,解释原因,并补充相关风险 |
这就是 Agentic RAG 和普通“召回几段文本再生成”的区别:它会根据问题结构决定要查哪些地方,并且能发现“有数字但没有解释”这种证据缺口,而不是拿一段不完整的上下文直接回答。
4. 面试官问“为什么你的项目需要 RAG”,可以直接这样回答
以公司年报为例,用户问的不只是一个静态事实,而可能要求比较多个年度指标、解释变化原因,并给出页码来源。相关信息分散在财务报表、管理层讨论与分析、附注和风险章节中,而且年报会持续更新,里面还有单位、合并口径和脚注等限定条件。只依赖大模型参数容易遇到知识过时、数字幻觉和无法追溯来源的问题。
我们通过 RAG 在回答时动态检索企业最新文档,把 Child 作为召回单元、Parent 作为上下文单元,再用 Hybrid Search 和 Reranker 找到可靠证据,并把文件名和页码一起传给模型。简单事实走 Standard RAG;涉及跨章节、多指标比较或完整覆盖的问题,使用 Agentic RAG 拆分任务、并行检索、针对缺口补检,最后基于证据合成答案。
5. RAG 也不是万能的
如果 PDF 解析错了表格、年份列丢失、页码映射错误,后面的向量检索再准确也没用。因此年报场景除了 RAG,还要重视 MinerU 的版面和表格解析、Parent-Child 切分、单位/年份元数据、引用页码、数值计算工具和离线评测。一个完整的系统应该能在证据不足时明确拒答,而不是为了“回答得像”而补全一个数字。
二、项目整体链路(必须讲顺)
1. 离线入库链路
- 读取 PDF、Word、Markdown 等企业文档。
- 使用 MinerU 做版面解析,尽量保留标题、段落、表格和页码等结构信息。
- 对文档做清洗、标准化和结构化切分。
- 先切出较大的 Parent,再在 Parent 内切出带 overlap 的 Child。
- 使用 BGE-M3 为 Child 生成 dense embedding,同时建立 BM25 sparse 索引。
- Child 向量和元数据写入 Qdrant;Parent 正文、文档信息和页码写入 MySQL。
- 为文档和片段生成稳定 ID,支持重复构建、增量更新和来源追踪。
2. Standard RAG 在线链路
1 | 用户问题 |
3. Agentic RAG 在线链路
1 | 初始宽检索 |
三、最高频问题与参考答案
Q1:为什么要做 Agentic RAG,而不是普通 RAG?
参考答案:
普通 RAG 通常是“一次 query、一次召回、一次生成”,对于事实明确、范围较小的问题已经够用。但企业问题经常包含多个子问题、跨文档比较、时间顺序、完整枚举等要求,一次召回很容易只命中其中一部分。
Agentic RAG 的价值不是让模型无边界地自由发挥,而是给检索过程增加了受控的决策能力:先判断问题是否需要拆解,再并行执行子任务,根据证据是否完整决定是否补检,最后统一合成。简单问题可以少走步骤,复杂问题才增加检索深度,因此在效果和成本之间做动态平衡。
追问:Agentic RAG 一定比 Standard RAG 好吗?
不一定。Agentic RAG 会增加 Planner、子任务回答和补检的模型调用,延迟和成本更高,也可能引入规划错误。所以项目保留 Standard RAG 作为低延迟基线,并通过任务数、并发数、最大补检次数和上下文长度做硬限制,避免所有问题都走复杂流程。
Q2:请完整介绍一次 Agentic RAG 请求。
参考答案:
请求进入后,先对原始问题做一次初始检索,得到一批代表性证据。Planner 结合问题、历史对话和初始证据,输出结构化的 RetrievalPlan,包括解析后的问题、是否需要范围探索、子任务列表和合成指令。
如果计划为空,说明初始证据已经足够,可以直接进入合成;如果是 Scope Exploration,就先用探索任务查清知识库中相关类别、章节或条款范围,再把探索结果交给 Planner 做一次 Replan;普通任务则直接交给 Executor 并行执行。
每个任务先利用共享证据池判断现有资料是否已经完整。如果是 partial,Seed Query 只针对缺失信息生成新的检索词,然后重新检索并合并证据。最后 Synthesis 只基于初始证据和任务结果生成答案,并统一返回去重后的 Parent 引用。
Q3:Planner、Executor、Synthesis 分别负责什么?
参考答案:
- Planner:负责理解问题、判断是否需要拆解、生成任务以及检索 query。它不直接回答最终问题。
- Executor:负责执行任务,包括检索、基于证据回答、判断证据是否完整,以及必要时生成 Seed Query 补检。
- Synthesis:负责把初始证据和各任务结果整合成面向用户的最终答案,不向用户暴露内部规划术语。
这种职责拆分可以避免一个大 Prompt 同时承担规划、检索和回答,便于限制每一步的输入输出,也更容易观测和测试。
追问:为什么要用 LangGraph?
因为 Agentic RAG 本质上是有状态的流程图,而不是简单的一次函数调用。LangGraph 可以把初始检索、规划、范围探索、重规划、执行、合成建模成明确节点和边,状态通过 Typed State 在节点间传递,条件边负责路由。相比把所有逻辑写在一个 while 循环里,流程更容易调试、扩展和观察。
Q4:Scope Exploration 和普通任务拆分有什么区别?
参考答案:
普通任务拆分是已经知道要回答哪些具体问题,例如“适用条件是什么”和“例外情况是什么”,任务本身就是最终答案的一部分。
Scope Exploration 用在“列出所有类型”“完整覆盖所有情形”这类问题。初始检索只能说明部分语料样本,不能因为没检索到某个类别就断言它不存在。因此先用探索任务查清相关类别、章节或条款范围,再根据探索结果重新生成最终任务。项目中把范围探索限制为一轮,Replan 时关闭再次 Scope Exploration,避免 Agent 无限扩张。
Q5:什么是 Seed Query?它和 Query Rewrite 有什么区别?
参考答案:
Query Rewrite 是在检索开始前,根据历史对话补全省略的主语、对象和指代,把用户问题改写成适合检索的独立 query。它主要解决多轮对话中的“它”“上面那个规定”等指代问题。
Seed Query 是任务执行过程中发现证据不完整后生成的补充检索 query。它不是简单同义改写,而是根据任务的 missing 字段,从不同术语或检索角度定向寻找缺失证据。例如第一次已经找到“适用条件”,但缺少“例外情况”,Seed Query 应该围绕例外情况检索,而不是重复搜索原问题。
二者的触发时机不同:Query Rewrite 在初次检索前,Seed Query 在证据评估为 partial 之后。
Q6:Parent-Child 为什么比只切一种 Chunk 更好?
参考答案:
只使用大 Chunk,虽然上下文完整,但向量表示容易被多个主题稀释,召回精度下降;只使用小 Chunk,召回可能更准确,但生成阶段上下文碎片化,模型缺少条款前后关系。
Parent-Child 把两个目标分开:Child 作为检索单元,尺寸较小、主题更集中;Parent 作为生成单元,保留相对完整的段落或章节上下文。检索时先命中 Child,再通过 parent_id 找回 Parent,并按 Parent 去重。这样既提高召回的精确性,又保证最终回答有足够上下文。
切分参数不是越小越好,需要结合文档结构、模型上下文、召回效果和延迟,通过离线评测调优。中文文档还要注意字符长度和 token 长度并不完全等价。
Q7:为什么采用 dense + BM25 的混合检索?
参考答案:
Dense 检索擅长语义相似和同义表达,例如用户说“员工离职后的竞业限制”,文档写的是“劳动关系解除后的竞业约束”;BM25 擅长精确匹配关键词、编号、专有名词、产品名和条款号。企业文档里往往同时存在自然语言问题和强关键词约束,只使用一种检索容易漏召回。
所以我们分别召回 dense 和 BM25 结果,再用 RRF 按排名融合,而不是直接把两个原始分数相加。因为两个检索器的分数空间和分布不同,直接相加需要额外归一化,容易让某一个检索器的分数尺度主导结果。
Q8:RRF 是什么?为什么适合这里?
参考答案:
RRF,即 Reciprocal Rank Fusion,核心思想是只关心文档在不同检索结果中的名次:
1 | def rrf_fuse(result_lists, k=60): |
同一个文档如果同时出现在 dense 和 BM25 的前列,会得到更高的融合分数;只在一个列表中出现的文档也不会被完全丢弃。RRF 不需要把不同检索器的原始分数强行校准,工程上比较稳健。
追问:RRF 后为什么还要 Rerank?
RRF 仍然是基于检索器的粗粒度排序,目标是提高召回覆盖率;Reranker 会把 query 和候选 Parent 一起输入交叉编码模型,做更精细的相关性判断。典型流程是先用较大的候选池召回,再用 Reranker 精排,最后按阈值和 top-k 截断,控制模型上下文长度。
Q9:Reranker 和 Embedding 有什么区别?
参考答案:
Embedding 模型通常把 query 和文档分别编码成向量,适合在大规模向量库中高效近似检索;Reranker 通常把 query-document pair 一起输入模型,进行更细粒度的交互建模,相关性判断更准,但计算成本更高。
因此不能把 Reranker 直接用于全库检索。项目先用 BGE-M3 和 BM25 召回候选,再用 BGE Reranker 对有限候选进行精排。这样把高精度模型放在小候选集上,在效果和延迟之间取得平衡。
Q10:Qdrant 和 MySQL 分别存什么?为什么不全部放一个数据库?
参考答案:
Qdrant 负责向量检索相关数据,主要存 Child 的 dense 向量、BM25 sparse 索引以及 parent_id、document_id、页码等元数据;MySQL 负责结构化数据和完整文本,包括文档记录、Parent 内容、来源信息、会话和消息等。
向量数据库擅长近邻检索和混合召回,关系型数据库擅长事务、关联查询和结构化持久化。分开存储可以让两类系统各自发挥优势:Qdrant 返回命中的 Child ID,再由 MySQL 批量取回完整 Parent,避免把完整长文本重复塞进向量库。
追问:两个存储如何保证一致性?
入库时使用文档内容 hash 生成稳定 ID;更新文档时先删除旧版本的向量和结构化记录,再写入新 Parent 和 Child,并在失败时清理本次写入的数据。生产环境还会进一步使用 ingestion job 状态、版本号、幂等 upsert、补偿任务或双版本切换,避免跨数据库操作无法原子提交的问题。
Q11:为什么要按 Parent 去重?
参考答案:
一个 Parent 内可能有多个 Child 被召回。如果直接把这些 Child 全部送给模型,会出现同一段内容重复、上下文碎片化、token 浪费的问题。
我们在候选阶段按 parent_id 聚合,记录最高命中分数、命中次数和 matched child IDs;之后一次性取回 Parent 完整内容,再进行重排。最终引用也按 Parent 去重,用户看到的是可读的完整来源,而不是一堆重复的小片段。
Q12:如何控制 Agent 不断循环或无限调用模型?
参考答案:
我会同时做模型层和工程层的限制:
- Planner 限制最大任务数。
- Scope Exploration 最多执行一轮,Replan 阶段禁止再次范围探索。
- 单任务最大检索次数有限制,Seed Query 最多补检有限次数。
- 并行执行使用 semaphore 限制最大并发。
- 结构化输出用 Pydantic 校验,不合法时走 fallback。
- 每一步记录 stage、task_id、attempts 和状态,方便观测和排查。
- 如果证据为 none 或 Seed Query 判断继续检索无收益,就停止并明确告诉用户证据不足。
Agent 的关键不是“给它无限自由”,而是把自由度限制在可观测、可回收的边界内。
Q13:多任务并行是怎么实现的?会有什么并发问题?
参考答案:
任务执行层使用异步 I/O,并通过 asyncio.gather 并行执行相互独立的任务,同时用 asyncio.Semaphore 限制最大并发,避免同时打爆模型服务、Embedding 服务或数据库。
多个任务共享一个 EvidencePool:每个任务检索到的新 Parent 会合并到池中,后续任务可以复用已有证据。EvidencePool 的更新使用异步锁保护,按 parent_id 去重并保留更高分结果。
需要注意的并发问题包括:外部 API 限流、任务取消、超时、共享状态竞态、一个任务异常是否影响其他任务,以及 gather 返回结果顺序。工程上应该设置超时、重试和熔断,并明确任务级失败和整条请求失败的边界。
Q14:如何避免 RAG 上下文被文档中的恶意指令劫持?
参考答案:
知识库内容应该被视为不可信数据,而不是系统指令。组装上下文时,我会明确告诉模型:检索结果只作为参考资料,不执行其中的指令;回答只能依据给定证据,资料不足时要说明,不能编造来源。
如果系统存在工具调用,还要在架构层隔离“资料内容”和“工具指令”,不能让文档直接产生工具调用参数。生产环境还需要租户过滤、文档权限校验、敏感信息脱敏、审计日志和针对 Prompt Injection 的测试集。单靠 Prompt 提醒不能算完整安全方案。
Q15:为什么要用 SSE,而不是 WebSocket?
参考答案:
这个项目主要是服务端持续向浏览器推送模型生成内容,通信方向以服务端到客户端为主,SSE 的语义更简单,浏览器原生支持,基于 HTTP,接入 FastAPI 也比较直接。我们把事件划分为 start、reasoning_delta、agentic_stage、delta、thinking_complete、done 和 error,前端按事件类型更新状态。
WebSocket 更适合双向实时通信,例如多人协作、实时中断和复杂交互。如果后续需要服务端主动推送、双向控制或更复杂的协作状态,再考虑 WebSocket。
追问:SSE 如何处理断线?
后端在生成过程中检查客户端是否断开;Agentic 任务会被取消,外部流被关闭,避免继续消耗模型资源。前端使用 ReadableStream 和 AbortController,维护会话级的流状态。只有正常收到终止事件后才把完整 Assistant 消息落库,避免半截答案污染历史。
Q16:如何做延迟优化?
参考答案:
我会先把延迟拆成几个部分:查询改写、Embedding、向量检索、重排、Planner、子任务执行、最终合成和首 token 延迟,然后分别优化:
- 复用异步 HTTP、LLM、Embedding 和 Qdrant 客户端,减少连接建立成本。
- Embedding 和向量写入采用批量处理。
- 先粗召回再重排,控制 reranker 候选数。
- 对相同或高度相似 query 做短期缓存。
- Agentic 任务并行执行,但设置最大并发。
- 限制最大任务数、补检次数和 Prompt 字符数。
- 能直接回答的问题跳过不必要的任务检索。
- SSE 先发送阶段事件和首 token,改善用户感知延迟。
优化时不能只看平均延迟,还要观察 p50、p95、首 token 延迟、成功率和单请求 token 成本。
Q17:如何评估这个 RAG 系统的效果?
参考答案:
我会分检索层、生成层和系统层评估。
- 检索层:Recall@K、Precision@K、MRR、nDCG,重点看正确 Parent 是否进入候选集以及页码引用是否准确。
- 重排层:观察 reranker 后正确证据的排名变化和阈值过滤误杀率。
- 生成层:答案正确性、Faithfulness、Context Recall、引用覆盖率、拒答准确率。
- Agent 层:任务完成率、平均任务数、补检成功率、无效循环率、复杂问题覆盖率。
- 系统层:首 token 延迟、完整响应 p50/p95、失败率、token 成本和并发吞吐。
测试集要覆盖简单事实、多跳问题、跨文档比较、需要完整枚举的问题、无答案问题、歧义问题和 Prompt Injection 文档。没有真实实验数据时,我不会直接声称“提升了多少”,而是先说明评估方案和下一步实验。
Q18:如果 RAG 答错了,你会怎么定位?
参考答案:
我会按链路逐层定位:
- 解析问题:原文是否被正确解析,表格、标题、页码有没有丢失。
- 切分问题:正确内容是否被切散,Parent 是否包含完整上下文。
- 召回问题:正确 Child 是否进入 dense/BM25 候选集。
- 融合问题:RRF 是否让正确结果排名过低。
- 重排问题:reranker 是否把正确 Parent 过滤掉,阈值是否过高。
- 上下文问题:最终 Prompt 是否截断、重复或混入无关内容。
- 生成问题:模型是否忽略证据、误解证据或受到上下文注入影响。
- Agent 问题:Planner 是否拆错任务,Seed Query 是否没有针对 missing 信息。
每个阶段都记录 query、候选 ID、分数、页码、任务状态、attempts 和最终引用,才能把“模型答错”定位为具体环节的问题。
Q19:如果模型输出不是合法 JSON,Agent 怎么办?
参考答案:
不能直接相信模型输出。我会先尝试去除 Markdown code fence,再用 JSON 解析,并使用 Pydantic 对字段、枚举值和长度做校验。解析失败时,不让异常扩散到整个服务,而是采用安全 fallback:例如把当前问题作为 resolved_query、跳过任务拆分,直接使用初始证据走合成;或者把该任务标记为 error 并继续其他独立任务。
生产环境还可以使用模型原生 structured output、重试一次带格式约束的请求、记录原始输出用于观测,以及给每种失败定义明确的降级路径。
Q20:这个项目中你认为最难的工程问题是什么?
参考答案:
我认为最难的不是把一次 RAG 调通,而是把多阶段 Agent 流程做成稳定、可控、可流式展示的服务。
一方面,Planner、任务执行、补检和 Synthesis 之间有共享状态和异步并发,需要防止重复检索、无限循环、任务失败互相影响;另一方面,前端还要实时看到 Agent 阶段、思考内容和最终 token,后端不能因为执行内部流程就阻塞 SSE。因此我把 Agent 图、任务执行器、证据池和 SSE 队列拆开:Agent 通过 callback 推送阶段和 token,聊天服务负责转发,EvidencePool 负责受控共享,最终只持久化完整结果。
这个设计让算法流程和传输层解耦,也方便后续增加新的 Agent 节点或观测指标。
四、工程与 Python 后端追问
Q21:FastAPI 中为什么使用 async?
参考答案:
这个系统的大部分耗时来自模型、Embedding、Reranker、Qdrant 和数据库等 I/O。使用 async 可以在等待外部服务时让出事件循环,从而支持多个请求并发处理。多任务 Agent 也可以使用 asyncio 并行执行。
但 async 不是万能的。如果引入 CPU 密集型 PDF 解析、大量本地向量计算或同步 SDK,就需要放到线程池/进程池,或者使用异步客户端,否则会阻塞事件循环。
Q22:如何设计异常处理和降级?
参考答案:
我会区分配置错误、基础设施错误、模型错误和业务错误:
- 配置错误:启动或请求时校验 API Key、URL、模型和 top-k,返回明确错误。
- Qdrant/数据库不可用:知识库请求返回可识别错误,不让异常堆栈泄漏给用户。
- 模型调用失败:有限重试,必要时返回错误事件;不能假装生成成功。
- SSE 中途断开:取消未完成任务、关闭流和客户端连接。
- Agent 单任务失败:如果任务相互独立,可以标记该任务失败,让其他任务继续;最终答案明确证据缺口。
日志中记录 request/thread/task 相关 ID 和阶段,但不记录 API Key、完整敏感文档或用户隐私内容。
Q23:如何设计会话历史和上下文窗口?
参考答案:
短期记忆是当前会话消息,发送给模型前按需要截取或压缩;知识库检索上下文只作为本次请求的临时上下文,不应该把所有 Parent 原文写进聊天历史,否则会导致历史膨胀和上下文污染。
查询改写只使用最近几轮对话补全当前问题,最终回答仍保留用户的原始问题。对于长会话,可以采用滑动窗口、历史摘要和重要事实提取,并分别控制历史 token、检索 token、思考 token 和答案 token。
Q24:如何保证接口幂等和知识库可重复构建?
参考答案:
文档使用内容 hash 生成稳定 document_id,Parent 和 Child 的 ID 由 document_id、chunk_index 等稳定字段派生。重复构建相同文件不会产生随机的新片段 ID,更新文件时只替换对应版本。
入库任务还需要做到批次可重试、upsert 幂等、旧版本清理和失败补偿。在线聊天的消息 ID 由服务端生成,流式生成只有在正常结束时落库,避免客户端重试造成重复 Assistant 消息。
五、容易被问到的取舍题
1. 为什么不所有问题都用 Agentic?
复杂度和收益不匹配。简单问题走 Standard RAG 更快、更便宜、更容易稳定;Agentic 只对多跳、比较、完整枚举和证据不足需要补检的问题启用。
2. 为什么不把所有候选 Parent 都塞进 Prompt?
会增加 token、延迟和噪声,反而可能降低答案质量。需要通过 reranker、分数阈值、Parent 去重、最大数量和最大字符数控制上下文。
3. 为什么不只用向量检索?
企业文档中有大量条款号、产品名、专有名词和数字,关键词检索对精确匹配更可靠;dense 和 sparse 互补。
4. 为什么不让 Planner 直接调用所有工具?
工具调用越自由,越难控制成本、权限和失败边界。项目把规划、检索、证据判断和合成拆成固定节点,并对任务数、重试数、并发数设上限。
5. 为什么不把 Reranker 放在全库第一步?
交叉编码需要同时处理 query 和文档,计算复杂度高,无法承受全库扫描,所以只能对召回候选做精排。
6. 如何处理没有答案的问题?
保留检索证据和分数阈值;当没有通过阈值的可靠 Parent,明确回答知识库中没有找到足够依据,不用外部常识补全,更不能伪造引用。
六、手撕代码高频题
1. RRF 融合
1 | def reciprocal_rank_fusion(result_lists, k=60): |
复杂度:设总候选数为 N,打分是 O(N),排序是 O(M log M),M 为去重后的文档数。
2. 限制并发执行任务
1 | import asyncio |
要点:Semaphore 限并发,gather 并行等待;是否吞掉异常要根据业务决定,任务级错误和整条请求错误要区分。
3. Parent 去重保留最高分
1 | def merge_parent_hits(items): |
4. 一个简单的受控 Agent Loop
1 | async def agent_loop(question, max_rounds=2): |
面试时要主动说出:真实项目还需要超时、取消、结构化输出校验、错误状态和引用合并,不能只写一个无限 while。
七、面试时不要说过头
- 没有真实实验数据时,不要编造“准确率提升 30%”“延迟降低 50%”。可以说评估指标、实验设计和下一步优化。
- 不要把 Agent 说成“模型自己想做什么都可以”,要强调任务上限、重试上限、并发限制和证据约束。
- 不要说“有引用就一定真实”,引用只能说明答案使用了哪些资料,还要评估引用是否真正支持结论。
- 不要把 Query Rewrite、Seed Query、Replan 混成一个概念,要讲清触发时机。
- 不要把 RRF 分数和 Reranker 分数直接比较,它们来自不同阶段、不同分数空间。
- 被问到效果时,先说明测试集构成,再说明指标和结果。
- 被问到线上规模时,如果没有实际规模,就明确说当前项目重点是流程验证和工程闭环,并给出扩展方案:缓存、队列、水平扩展、连接池、限流、异步索引和多租户隔离。
八、最后 30 秒项目总结
这个项目的核心不是简单接一个大模型,而是把企业知识库问答拆成了可观测、可约束的检索与决策流程:用 Parent-Child 和 Hybrid Search 提升召回,用 Reranker 控制上下文质量,用 LangGraph 编排 Planner-Executor 和 Replan,用证据完整度和 Seed Query 做定向补检,再通过 SSE 把中间阶段和最终答案稳定地交付给前端。我的工作重点是 Agent 流程、RAG 检索、异步并发、流式协议和异常边界。
