09 口袋速背卡片(考前 10 分钟看这一页)
只背这一页。长文都在这页之外的 01–08 篇,考前不要再看长文。 顺序:① 主线口述 → ② 数字 → ③ 四条链路一句话 → ④ 十个必答 → ⑤ 六个缺陷 → ⑥ 三个反问。
① 主线口述(必须形成条件反射,约 40 秒)
"BinRag 是企业文档知识库的 RAG 问答系统,我用 Go 写的。 入库是异步管道:多格式解析 → 三种分块策略 → Embedding 存 Qdrant,同时灌一份我自己实现的 BM25 内存倒排索引,worker 池失败重试、重启续跑。 问答时向量和 BM25 双路并行召回,加权 RRF 融合(k=60,0.7/0.3),再过 cross-encoder 重排,按 2048 token 预算组装上下文,SSE 流式输出回答和可定位的引用来源。 部署上是一个 Go 二进制:Web 版 go:embed 前端,桌面版 Wails 内嵌同一套后端;对外另开一个 MCP Server,用 6 个只读 Tool 把 RAG 能力开放给外部 Agent。 开发方式我直说:AI 辅助编码,但我先写 spec 和可验证的 checklist,做了一次全量代码 review 修掉 6 个 P0 和十几项 P1——所以我清楚它哪里是坏的。"
② 数字卡(被追问就报,报完补一句来源)
| 项 | 值 |
|---|---|
| 两路召回 / 最终引用 | 各 Top-10 / 最多 5 条 |
| RRF | k=60,向量 0.7 / BM25 0.3(校验强制和为 1) |
| BM25 | k1=1.2、b=0.75(硬编码);IDF=log((N−df+0.5)/(df+0.5)+1) |
| 中文分词 | bigram(生产额外开 unigram 补单字) |
| 分块 | chunk_size 512(部署配置 500)、overlap 50 |
| 上下文预算 | 2048 token、最多 5 条、历史 10 条 |
| 入库 | worker 5(代码默认 2)、轮询 500ms、重试 3 次、退避 2s/4s/8s(区别于外部调用的 1s/2s/4s) |
| 外部依赖 | Embedding 批 100(部署 10)、重排 top_n 5 / 超时 30s / 重试 3 / QPS 10 |
| 安全 | 扫描件阈值 20 字符、MCP 6 Tool、审计截断 2000 字符、JWT 1200 分钟 |
| 规模 | Go 63 文件 / 405 测试函数、前端 vitest 37、Python pytest 86、30+ 条 REST 路由 |
③ 四条链路一句话
| 链路 | 一句话 |
|---|---|
| 入库 | 上传只做"校验+落盘+建任务"立刻返回 task_id;worker 用 UPDATE…WHERE id IN (SELECT…pending) RETURNING 原子认领;Load→可读性校验(≥20字符)→Chunk→Embed→Qdrant.Upsert + BM25.Add;失败退避重试写在 updated_at 里;重启把 processing 重置为 pending。 |
| 检索 | 向量(Qdrant gRPC,Must+kb_id 下推过滤)∥ BM25(打分前按 kb 过滤)→ 加权 RRF → 截断 → cross-encoder 重排(失败降级原序)。 |
| 问答 | 历史(≤10) → Query 改写(低温 LLM,失败降级原问题)→ 检索 → buildContext(2048 token / 5 条,[编号](来源)正文)→ 生成;SSE 顺序 sources → chunk×N → done / error。 |
| MCP | 同进程 /mcp,streamable HTTP;Bearer API Key SHA-256 认证(失败 HTTP 401);越权与不存在统一返回 -32001(不泄露资源存在性);异步审计。 |
④ 十个必答题(一句话答案)
- 为什么混合检索? 向量对专有名词/编号/错误码会漏,BM25 补精确匹配;混合的目的是降方差,不是普涨。
- BM25 自己实现的? 是,200 行倒排索引;IDF 用非负变体(末尾 +1 保证不为负),k1 控制词频饱和、b 控制长度归一化。
- 中文怎么分词? bigram,无词典;代价是单字查询漏召回,所以生产额外开 unigram。
- 为什么用 RRF 不用加权求和? 余弦 0~1 与 BM25 无上界量纲不可比;RRF 只看排名,天然免疫量纲,且双路命中自动上位;k=60 起平滑作用(削弱头部)。
- cross-encoder 和向量检索区别? 双塔独立编码所以能预计算(负责召回);cross-encoder 把 query 和文档拼一起过模型、能建模交互,精度高但不能预计算(负责精排)。
- 重排挂了怎么办? 降级返回融合原序 + warn 日志,不阻塞主链路;副作用是
score字段语义从相关性分变成 RRF 分。 - 为什么单集合不用一库一集合? 集合数随租户线性爆炸;单集合 + payload
kb_id过滤,过滤下推到检索里而不是检索完再筛。 - Token 预算怎么裁? 自研估算(中文 2 token/字、英文按词),按分数序累加,放不下就停;最多 5 条。
- 怎么防幻觉? 三层:prompt 强约束"资料未覆盖就说未覆盖"、引用编号与 sources 一一对应可溯源、检索为空直接返回"未找到相关资料"不调 LLM。(没做 NLI 校验,主动认)
- 你怎么知道效果变好了?
cmd/eval:数据集驱动 Recall@K + LLM-as-Judge(准确性 0-10 / 忠实度二值,temperature=0);另有独立 Python RAGAS 服务(faithfulness/answer_relevancy/context_precision/context_recall)。基线还没跑完,我不报没有统计意义的数字。
⑤ 六个必须主动交代的缺陷(每题一句:缺陷 + 影响 + 修法)
| # | 缺陷 | 一句话说法 |
|---|---|---|
| 1 | BM25 无持久化、启动无回灌 | "重启后混合检索静默退化成纯向量(DocCount()>0 门控跳过,无告警,只有 trace 的 method 字段能看出来)。修法:启动从 payload/DB 回灌 + 就绪状态 + 告警;前置依赖是先把 Rebuild 改用 AddWithDocID。" |
| 2 | BM25 独有命中正文为空 | "allDocs 只由向量结果构建,所以 BM25 独有的 chunk 只剩 ID,空内容进上下文。修法:BM25 索引存正文,或融合后批量 Get 补齐。" |
| 3 | 重排无 oversample | "候选池 = topK,重排只能改排序、救不了召回。修法:召回放大 3~5 倍再重排取 topK。" |
| 4 | score 语义不稳定 | "重排成功是 relevance_score,降级是 RRF 分,同字段两套量纲。修法:拆成 fusion_score / rerank_score。" |
| 5 | 安全类:MCP owner 解析失败 fail-open、REST 层把用户凭据当系统级 Key、无 metrics/trace | "权限收敛应该从凭据 owner 派生而不是从凭据类型派生;检索范围解析失败必须 fail-closed。可观测性只有 slog 日志,没有 metrics 和 trace。" |
| 6 | Embedding 不校验返回条数 + 无 payload 索引 / 无 score 阈值 / 无超时预算 | "少返回条目会产生 nil 向量并真的写进 Qdrant;kb_id 没建 payload 索引;检索层没有超时预算,靠上游 ctx 兜。" |
说缺陷的姿势:一句缺陷 → 一句影响 → 一句修法 → 停住。不要道歉、不要过度自我否定。
⑥ 三个反问(挑 2–3 个问)
- "你们线上向量检索和关键词检索怎么融合的?RRF 还是加权?召回和重排的条数比例大概多少?"
- "团队现在有 RAG 效果的评估基线吗?人工标注还是模型判分?"
- "如果我进来做这块,第一件该做的是补评估基线,还是先解决某个具体线上问题?"
⑦ 最关键的三句"保命话术"
- 被问效果数字:"没有可信基线,我不编 Recall。唯一真实的一组是 RAGAS 在 7 条样本上的四个指标——faith 1.0 / relevancy 0.78 / precision 0.83 / recall 0.95,样本量我先讲清楚,它只证明链路通。另外
docs/15-rag优化计划.md里那句'预期 Recall 提升 10%+'是计划里的预期,不是实测。" - 被质疑 AI 写的:"是 AI 辅助。我先写 spec 和可验证 checklist,做过一次全量 review 修掉 6 个 P0 + 十几项 P1;有些模块我只做到行为验证,比如 ffmpeg 抽帧和 Wails 打包。"
- 被问不会的:"这一层我没做到源码级——我的理解是 X(说出你知道的边界),回去我会先看 Y(说出你会从哪入手)。" (永远不要硬编。会说"不知道边界在哪"比假装懂安全一百倍。)
⑧ 架构图(考前照着默画一遍)
上传 → 落盘 + task(pending) → 返回 task_id
↓ 500ms 轮询 / 原子认领
worker×5 → Load → 校验(≥20字符) → Chunk(3策略) → Embed(批) → Qdrant.Upsert ─┬→ BM25 索引 Add
└→ PG 状态更新
失败 → pending, retry++ (退避写 updated_at, 2s/4s/8s) → 超 3 次 failed
问答 → KB 范围校验(防越权) → 历史(≤10) → Query 改写(LLM)
→ 检索[向量 ∥ BM25 → 加权 RRF(k=60, 0.7/0.3) → 重排(/v1/rerank)]
→ buildContext(≤2048 token, ≤5 条) → 生成
→ SSE: thinking×N → sources → chunk×N → done / error
出口:REST / SSE / MCP(6 只读 Tool) 共享:internal/app 装配 + 认证 + 权限⑨ 上场前 60 秒自检
- [ ] 我能不看稿说出 ② 里的 6 个数字吗?
- [ ] 我能用一句话说清"RRF 为什么不用加权求和"吗?(关键词:量纲不可比)
- [ ] 我能主动说出 ⑤ 里的第 1 条(BM25 静默失效)吗?
- [ ] 我的"效果数字"答法是否已经背成肌肉记忆?
- [ ] 反问准备好了吗?
- [ ] 最后一个心理准备:被问到答不上来的细节是正常的,这个项目有 6 万行代码,我只需要把主干链路、每个取舍的理由、以及我知道的缺陷讲清楚——这三样我都有。