外观
面试高频质疑应答
针对 DocMind 项目面试时最容易被挑战的方向,准备逐条应答。 每个问题包含:面试官可能的问法 → 参考回答 → 追问预判。
零、场景质疑("为什么做这个")
Q0. 这个项目解决的是什么实际问题?有真实用户吗?
面向研发团队的内部技术文档问答。核心痛点是:团队有大量散落在不同平台、不同格式的技术文档(架构设计 PDF、API 文档 Markdown、运维手册 Word、会议纪要网页),新人 onboarding 要翻几十篇才能找到一个配置参数,老员工被频繁打断。
真实使用场景:我自己的开发团队作为用户——入库了项目的技术文档、部署手册、设计决策记录,日常在 Cursor IDE 里通过 MCP 协议直接查知识库。比如"Spring Boot 3.4 的 graceful shutdown 怎么配置"这种问题,直接在编辑器里得到答案 + 文档来源引用,不需要切到浏览器翻 Confluence。
实话说用户规模是自己团队内部使用,不是 ToC 产品。但技术选型和架构设计都按"可扩展到 50-200 人团队"的标准来做的——多知识库隔离、RBAC 权限、动态参数热更新都考虑了。
追问预判:
- "为什么不直接用 Confluence 搜索 / 飞书搜索?" → 传统搜索返回的是文档链接列表,不是结构化答案。而且不支持跨文档综合推理(比如"对比 A 方案和 B 方案"需要综合两篇设计文档)。更重要的是没法在 IDE 里用。
- "为什么不用 Dify / RAGFlow 这些开源方案?" → 两个原因:一是 MCP 协议暴露给 IDE 的能力这些方案没有原生支持;二是做这个项目本身是为了深入理解 Agentic RAG 的每一层——从 RRF 融合公式到 Cross-Encoder 重排到 Self-Reflection,每一层都有评测数据证明增量价值。用开箱即用的方案学不到这些。
- "用户量这么小,性能优化有意义吗?" → 有。语义缓存减少了 45% 的重复 LLM 调用,Self-Reflection 高分短路减少了 60-70% 的无效评分调用,双模型策略把工具选择成本压低 70%。这些优化的核心价值不是"抗多少并发",而是降单查询成本——个人项目每月 API 调用费是真金白银。
一、必要性质疑("为什么不简化")
Q1. Agent 是不是过度设计?大多数知识问答,向量检索 + LLM 三步就够了,你为什么要搞 ReAct + 工具选择 + 反思?
项目最初确实只有向量检索 + LLM 两步。加 Agent 架构是为了解决三类朴素方案解决不了的问题:
第一,查询类型差异大。知识库同时有 API 参数/配置项(需要精确关键词匹配)、操作指南(需要步骤完整性)、时效信息(需要联网搜索)。单一向量检索路径对精确查询类表现很差——我的评测数据显示,事实类查询在纯向量检索下 Recall@5 只有 0.73,加上 BM25 混合召回后升到 0.93。
第二,Phase 2 的 Supervisor-Worker 架构进一步证明了 Agent 的价值。SupervisorAgent 根据查询复杂度自动选策略——简单查询走迭代 ReAct(1-2 轮即停,成本等于原方案),复杂查询走 Plan-and-Execute(LLM 生成多步计划按拓扑并行执行)。IterationDecider 基于置信度轨迹决定何时终止,ObservationEvaluator 在检索质量差时自动切策略(空召回→Web,低分→HyDE,冲突→Analysis)。这种动态编排能力是固定管线做不到的。
第三,架构为可扩展性留了口。4 个 Worker(Retrieval/Web/Memory/Analysis)通过统一接口解耦,新增 Worker 只需实现接口 + 注册。同时 MCP 协议暴露给外部 AI 编辑器。
Self-Reflection 最初确实有过度设计的问题——评测验证了流式模式下它对答案质量零贡献。但发现问题后我做了改造:反思评分不通过时触发主模型流式重写,复测忠实度 +0.037。这是一个"评测驱动修复"的完整闭环。
追问预判:
- "规则引擎能覆盖多少查询?" → 实测约 85% 的查询走规则路径,15% 走 LLM 工具选择兜底。
- "不用 Agent 也能做多路召回吧?" → 能,但工具组合的动态决策(比如判断是否需要联网)和执行结果的统一收集需要一个编排层。Agent 是这个编排层的自然形态。
Q2. MCP 真的有外部客户端调用吗?还是只是简历上的词?
MCP 服务端在项目里有两个实际作用:
内部作用:Spring AI 的
ToolCallbackProvider把 5 个@Tool注解的工具类(DocSearchTool / KeywordSearchTool / WebSearchTool / MemoryTool / KbMetaTool,共 6 个端点)注册给ChatClient,LLM 工具选择时直接通过 function calling 调用它们。这个是生产链路在用的。外部作用:通过
spring-ai-starter-mcp-server-webmvc暴露标准 MCP 接口。我用 Cursor IDE 作为 MCP 客户端对接验证过——IDE 能直接调用 DocMind 的语义检索和关键词检索,把知识库作为编程辅助的上下文来源。实话说,外部调用场景目前仅限于我自己的 IDE 验证,没有第三方用户在用。但 MCP 协议本身的工程价值在于:同一套工具代码既服务内部 Agent 循环,也对外暴露标准接口,没有代码重复。
追问预判:
- "那你为什么不直接写个 REST 接口?" → MCP 是结构化的工具描述协议,客户端不需要看文档就知道参数 schema——这比裸 REST 对 AI 客户端更友好。而且 Spring AI 原生支持,加一个 starter 依赖就行,成本几乎为零。
Q3. Query Decomposition 拆出来的子问题真的有意义吗?还是 LLM 瞎拆增加噪声?
这个确实有噪声风险。我的处理方式有三层保护:
第一,不是所有查询都拆。
QueryUnderstandingService的分类器只对复杂度为 COMPLEX 且包含多个子语义的查询触发拆解。简单查询(占大多数)直接走主路径,不过拆解分支。第二,子问题只走向量检索不走 BM25。设计决策是:规范化后的子问题语义性强,BM25 对它们增益小;不重复触发网络搜索和记忆召回,避免额外延迟和噪声。
第三,合并阶段有保底机制。
SubQueryMerger在跨子问题去重时,如果合并后候选不足,会从全局 top-N 中补齐,保证不会因为拆得太细导致最终候选为空。评测数据显示多跳类查询当前 Recall@5 只有 0.50(是所有类别中最低的),而这些恰好是 Query Decomposition 的目标场景。下一轮评测会加入 V5 配置专项验证拆解的增量价值。
Q4. 8 套自适应参数预设是不是过度工程?凭什么是 8 套?
这个设计在后续迭代中已被简化。原始的
QueryProfiler用复杂度 × 专指度的 2×4 矩阵输出 8 套参数预设,实际运行后发现:
- 参数差异主要体现在"是否调 BM25"和"重排候选数"两个维度
- 8 套中有 4 套参数几乎相同
所以我在迭代中把 QueryProfiler 替换成了
RetrievalPlanner——一个纯规则引擎,根据分类信号直接决定调哪些工具。检索参数(向量 top-K、BM25 top-K、重排 top-K)改为从sys_ai_config数据库表读取,全局统一,运行时可调不需要重启。这是一个"先做复杂再简化"的过程。评测验证了简化后的效果不亚于原版——因为参数敏感度实际上没想象中高。
Q4.5 Supervisor-Worker 架构是不是过度设计?4 个 Worker 加 Supervisor 比原来的单类多了更多代码和间接层。
从代码量看确实多了——原来 1 个文件 1400 行,现在 16 个文件加起来约 2000 行。但关键指标不是总行数而是单次修改影响范围:
原来要加一个新检索策略(比如 HyDE 假设文档生成),需要在 DocMindAgent 的
runReActLoop()里加条件分支,改动 1400 行文件,风险高且难以测试。现在只需要:①写一个HyDeWorker implements Worker(~100 行);②在PlanExecutor.resolveWorker()加一行 case;③在ObservationEvaluator加一个条件(低分时推荐 TRIGGER_HYDE)。三处改动互不耦合,可独立 review 和测试。另外,Supervisor-Worker 不是我发明的——这是 multi-agent 系统的经典拓扑(参考 AutoGen、CrewAI 的 hierarchical mode)。在 RAG 场景下的价值是让编排决策(什么时候停、什么时候切策略)和执行实现(怎么检索、怎么搜索)彻底分离。
如果项目只有一种检索策略且永远不会加新 Worker,那确实过度设计。但我已经有 4 种不同来源(向量/BM25/Web/Memory)+ 即将加入 HyDE 和更多分析型 Worker,这个规模下 Supervisor-Worker 是合理的。
追问预判:
- "IterationDecider 的置信度阈值 0.7 怎么定的?" → 经验值起步,存在
sys_ai_config可热配。0.7 意味着"至少有一个 chunk 的 rerank 分数达到还不错的水平"。后续评测会用 grid search 在 [0.5, 0.9] 区间找最优点。 - "ObservationEvaluator 的降级会不会无限循环?" → 不会。两层保护:①
state.hasTriedStrategy(name)记录已尝试策略,同一策略不会重复推荐;②IterationDecider 有硬上限maxIterations=3。
Q4.6 既然已经有 QueryClassification 三维分类,为什么 Phase 5 还要再加一层"范畴判定"?
起点不是设计假设,是一个真实 bug:用户在一段正常知识问答之后输入"总结上面的对话",系统把"总结"、"上面"、"对话"当成检索关键词去 Milvus 查询,召回 12 条主题完全无关的切片,自纠错降到 0% 置信度还是把答案推送到前端。
定位根因发现 QueryClassification 的五个 intent 取值(factoid / procedural / comparison / opinion / chitchat)都隐含一个前提——"用户问的是知识库里的内容"。"总结上面的对话"被归到 factoid 或 chitchat,但下游不区分意图,照常调用 RetrievalWorker。这不是分类做错了,是分类少了一个维度。
Phase 5 加的是"要不要查知识库"的判断,跟原有的"问的是什么类型"是正交的两件事:
维度 取值 判什么 范畴(Phase 5 新增) 元对话 / 闲聊 / 知识查询 / 任务执行 / 越界 是否走 RAG 检索 intent(已有) factoid / procedural / comparison / opinion / chitchat 知识查询的语义类型 complexity SIMPLE / MEDIUM / COMPLEX 是否走 Plan-and-Execute specificity FUZZY / NORMAL / PRECISE 是否启用 HyDE 一个具体例子:用户问"我刚才问的那个事实是什么",intent 仍然是 factoid(事实型问答),但范畴是元对话——不去查知识库,直接读历史对话。两个维度独立判定。
工程上判定走两级:先用正则识别"总结上面"、"翻译你刚才"这种高置信度模式,命中即跳过模型调用;不命中再走小模型。合并模式开启时(默认)小模型那次调用直接复用 QueryUnderstanding 的同一次模型调用——只在提示词里加了一个 scope 字段。没有为范畴判定增加额外的模型调用。
追问预判:
- "为什么不直接让 LLM tool calling 自己决定要不要检索?" → 调研结论是这个路径业界已经放弃。LinkedIn 工程博客、Airbnb v1→v2、Microsoft Semantic Kernel 都从纯 tool calling 路由迁出,原因是模型容易过度检索、识别不出元对话。LangGraph Adaptive RAG 教程里也明确说显式路由比 tool calling 路由更可控。
- "怎么保证范畴判错不会比改造前更糟?" → 每一级都做了降级:规则路径异常 → 让模型路径兜;模型路径异常 → 默认按知识查询走原流程。所以最差情况就是回到改造前的行为,不会更糟。
Q4.7 检索结果三档置信度评估为什么要再多一道闸?跟 Self-Reflection 不是重复了吗?
不重复,两道闸的位置和检查内容完全不同。
阶段 检查时机 检查内容 失败处理 RetrievalGrader(Phase 5 新增) 检索完成、模型生成之前 检索到的内容跟问题主题对不对得上 LOW 档让 fallback 接管,不进入生成 SelfReflection(已有) 模型生成之后 答案与参考来源是否一致、完整、表达清晰 不通过时触发重写 Phase 5 之前只有 SelfReflection。问题是:当检索本身就跑题(召回了主题完全无关的切片),SelfReflection 检查"答案是否与切片一致"会通过——因为答案确实是基于切片生成的,但切片本身跟问题没关系。"总结上面的对话"那个 bug 就是这样卡死的:自纠错认为答案跟切片一致,但切片跟问题完全两回事。
所以加 RetrievalGrader 是在"模型生成之前"判断检索质量本身,不是评估生成出来的答案。CRAG 论文(Yan et al. 2024)的做法是用一个独立的小模型评估检索质量,分三档触发不同后续动作:
- HIGH(rerank 顶分 ≥ 0.65)→ 直接生成
- LOW(rerank 顶分 ≤ 0.25)→ 让 fallback 接管
- 灰区(中间区间)→ 仲裁
关键设计是没有按论文的做法在灰区调小模型生成。把 top-3 切片拼成上下文让 reranker 重新单对打分——本来 reranker 就是用来评估 (问题, 段落) 相关度的,复用它做仲裁是天然合适的。一次 reranker 调用 ~50ms,是模型生成的 1/5 时延。论文用 T5-Large 是因为他们做研究用现成模型,工程上能复用现有 reranker 就没必要再引入一个新模型。
此外 Phase 5 还在 SelfReflection 加了一项额外检查(切题度),是 RetrievalGrader 漏判时的最后一道闸——如果检索置信度被错误判成 HIGH 但生成时仍然出现"答案引用的切片跟问题不是一个主题",反思会标记
topicMismatch=true,主流程跳过重写直接降级。
追问预判:
- "灰区仲裁有四种模式,默认为什么选 cross_encoder 而不是 llm?" → 延迟差 6 倍(50ms vs 300ms)、成本差更多(reranker 单对调用比小模型生成便宜一个量级)、效果上 reranker 本来就是干这个的。
llm模式留着是为了对比基线和排错。heuristic(用顶分与次分的差、前三平均分)适合本地化或限网环境。disabled适合不在乎灰区、希望直接走 fallback 的场景。 - "三档阈值 0.65 / 0.25 怎么定的?" → 经验值起步,写在
sys_ai_config可热改。后续评测可以用 grid search 在 [0.5, 0.8] / [0.1, 0.4] 找最优。
二、技术深度(基础功)
Q5. RRF 公式手写一遍。k=60 怎么定的?为什么不用加权求和?
RRF 公式:
score(d) = Σ 1 / (k + rank_i(d))其中
rank_i(d)是文档 d 在第 i 路检索结果中的排名(从 1 开始),k 是平滑常数。我的实现中(
RRFFusion.java),排名从 1 开始计数:javadouble rrfScore = vectorWeight / (rrfK + rank + 1); // rank 从 0 开始,所以 +1k=60 的来源:这是原始 RRF 论文(Cormack et al., 2009)推荐的默认值。k 的作用是控制排名衰减的速度——k 越大,不同排名之间的分数差异越小(更平滑),top-1 的优势越弱。60 是经验值,我没有单独调过,因为评测显示当前效果已经满足需求。这个值存在
sys_ai_config表中,可以随时调整。为什么不用加权求和:加权求和(weighted sum)要求不同路的分数处于同一量纲——但向量检索返回 cosine similarity(0-1),BM25 返回的是 TF-IDF 类分数(可能几十分),两者直接求和会让 BM25 绝对主导。RRF 只看排名不看分数,天然避免了这个归一化问题。这也是 RRF 在工业界流行的核心原因。
我的实现额外支持了加权 RRF:
fuse(vectorResults, bm25Results, topN, rrfK, vectorWeight, bm25Weight),通过 weight 调节两路的贡献比例——这比调 k 更直观。
Q6. BM25 的 k1、b 参数含义?你的实现和 Lucene 的有什么区别?
k1 = 1.5:控制词频饱和度。k1 越大,一个词在文档中出现多次时分数增长越不容易饱和。1.5 是经典默认值。
b = 0.75:控制文档长度归一化的程度。b=1 表示完全归一化(长文档大幅惩罚),b=0 表示不归一化。0.75 是平衡点。
公式:
score(q, d) = Σ IDF(t) × [tf(t,d) × (k1 + 1)] / [tf(t,d) + k1 × (1 - b + b × |d|/avgdl)]与 Lucene 的区别:
我没有直接用 Lucene 的 BM25Similarity,而是自己实现了评分逻辑。原因是我的方案分两层:
- 预召回层:用 MySQL FULLTEXT 索引(ngram 分词)+
MATCH AGAINST做粗筛,拿到候选集(最多 400 条)- 精排层:在应用内对候选集做中文分词(
ChineseTextTokenizer)+ BM25 打分这样做是因为 Lucene 需要维护本地索引文件,在文档增量更新场景下一致性维护复杂(迭代 #9 的索引未刷新问题就是前车之鉴)。用 MySQL FULLTEXT 做预召回把一致性问题交给数据库,应用内 BM25 只负责对候选精排。
我还加了一个短语覆盖度加分(
phraseCoverageBonus):如果查询词在文档中以原始形式出现(不只是分词后的 token 匹配),额外加分。这对中文精确术语查询有帮助。
追问预判:
- "ChineseTextTokenizer 用的什么分词方案?" → 我自己实现的轻量中文分词器,核心是正则切分 + 停用词过滤 + 长度过滤(≥2 字符)。没有用 HanLP 或 jieba 是因为对 BM25 召回场景来说,2-gram 粒度已经足够,不需要准确的语义分词。
Q6.5 为什么要在 Cross-Encoder 之后再做 MMR?直接扩大 topK 不行吗?
单纯扩大 topK 解决不了信息冗余问题。假设一篇 10 页的文档被切成 25 个 chunk,用户问的恰好是这篇文档的主题——Top-8 精排结果可能全部来自这篇文档的相邻段落,内容高度重叠。送 8 条 80% 相似的 chunk 给 LLM,等于用 3000 token 的上下文窗口只传达了 1 条有效信息。
MMR 的价值是在固定 token 预算内最大化信息密度:
MMR(d) = λ × relevance(q,d) − (1−λ) × max_sim(d, already_selected)λ=0.7 表示我让相关性占 70% 权重、多样性占 30%——不会为了多样性牺牲太多精度。相似度用 bigram Jaccard 而不是 embedding cosine,因为 reranked chunk 数量小(≤8),O(n²) Jaccard 远比多一轮 embedding API 调用便宜。
放在 Cross-Encoder 之后而非之前的原因:MMR 需要可靠的 relevance score 作为输入——RRF 分数是排名衍生的,尺度不稳定;只有 Cross-Encoder 的 relevance_score 才是真正的语义相关性分数。
追问预判:
- "λ 怎么调的?" → 默认 0.7 存在
sys_ai_config,可以在线调。调大(→1.0)退化为纯精排无多样性,调小(→0.5)多样性更强但可能放进弱相关 chunk。0.7 是业界常用起点,后续会结合评测微调。 - "为什么不直接按 knowledgeBaseId 去重一篇只留一条?" → 太粗暴了。同一篇文档的不同章节可能覆盖查询的不同方面(比如"配置说明"和"故障排查"),按文档去重会丢有效信息。MMR 是按内容相似度去冗余,粒度更细更合理。
Q7. Cosine vs L2 vs IP(内积)区别?你为什么选 Cosine?
L2(欧氏距离):衡量向量在空间中的绝对距离。对向量长度敏感——同方向但长度不同的两个向量,L2 距离不为零。
IP(内积):
a·b = |a|×|b|×cos(θ)。如果向量未归一化,内积同时受方向和长度影响。Cosine(余弦相似度):
cos(θ) = a·b / (|a|×|b|)。只衡量方向相似度,忽略长度差异。等价于"先 L2-normalize 再做 IP"。选 Cosine 的原因:
text-embedding-v3 的输出向量是经过归一化的(官方文档说明),所以理论上 Cosine 和 IP 对它等价。但我选 Cosine 是因为:
- 语义相似性的直觉含义是"方向是否一致"而非"长度是否相近"
- Milvus 对 Cosine 有专门的索引优化路径
- 阈值设定更直观——0.92 就是"非常相似",不受向量长度干扰
Q8. chunk size、overlap 怎么定的?语义切分 vs 固定切分的取舍?
项目经历了两代 chunker:
第一代(固定切分):目标 400 字 / 最大 600 字 / 重叠 50 字。按
\n\n分段,长段按句号切分。问题是会切碎表格和代码块。第二代(Markdown-aware 切分,迭代 #9 后):因为上游统一走 MinerU 输出结构化 Markdown,所以 chunker 重写为基于 block 类型的装配器——表格、代码块作为完整 block 不切分;heading 切换时强制分块;普通段落在 400-600 字区间合并。
chunk size 400-600 的依据:
- 太小(<200):语义不完整,上下文不够 LLM 回答
- 太大(>1000):检索时一大段话里只有一句相关,precision 下降;且 embedding 对长文本的表征能力递减
- 400-600 是常见的经验区间,兼顾完整性和精度
重叠的作用:保证跨 chunk 边界的信息不丢失。50 字约一两句话。第二代里重叠策略只对普通段落生效——表格和代码块是完整保留的,不需要重叠。
为什么不用纯语义切分(如基于 embedding 相似度的滑动窗口)?试过,两个问题:一是计算成本高(每一步都要调 embedding 接口),二是切出来的 chunk 大小极不均匀,后续 RRF 和 token 预算控制难以适配。结构化 Markdown 的 heading 层级已经是一种轻量语义信号,足够用。
Q9. Milvus 索引类型选的什么?参数怎么调?和 PgVector / Qdrant 比为什么选它?
索引类型:HNSW(Hierarchical Navigable Small World)。
关键参数:
M = 16:每个节点在图中的最大连接数。越大召回越好但内存越高。16 是中等偏保守的选择efConstruction = 200:建索引时搜索的邻居数。越大索引越准但建索引越慢。200 对 18,000 条切片的规模已经足够efSearch = 64:查询时搜索的邻居数。越大召回越好但延迟越高。64 在当前数据量下延迟稳定在 30-50ms为什么选 Milvus 而非 PgVector / Qdrant:
维度 Milvus PgVector Qdrant 向量规模支撑 亿级 千万级(实测 500 万后性能下降明显) 亿级 生态 Spring AI 原生支持 需要自己写 SQL 需要额外 SDK 部署 Docker 一键起 附在 PG 上 Docker 一键起 适合场景 专业向量数据库 已有 PG 想加向量能力 轻量快速 选 Milvus 的实际理由:Spring AI 的
spring-ai-milvus-store开箱即用,我只需要配置连接信息就能用。当前数据量(18,000 条)对三者都没有压力,选型主要看框架集成度。诚实说,这个数据规模用 PgVector 也完全可以。选 Milvus 更多是考虑到如果数据量增长到百万级后不需要换方案。
Q10. Cross-Encoder 的延迟成本怎么样?为什么不用本地 BGE-Reranker?
当前方案:调用 DashScope gte-rerank API,平均延迟 387ms(5 个候选)。
为什么用云端 API 不用本地模型:
- BGE-Reranker 需要 GPU——本项目的部署目标是单机 Mac / Linux,没有独立 GPU
- CPU 推理 BGE-Reranker-v2-m3 约 500-800ms(5 个候选),并不比 API 快
- API 方案的工程复杂度低(一次 HTTP 调用 vs 加载 ONNX Runtime + 模型文件管理)
成本:gte-rerank 每次调用约 ¥0.003(5 个候选),单 query 占比不到总成本的 10%。
风险:
- 评测时撞过 DashScope 的免费额度限流(每分钟 30 次),说明并发场景下这个外部依赖是脆弱的
- 限流时降级到关键词重叠度评分(
fallbackRerank),质量显著下降如果项目需要支持高并发,改造方向是自部署 BGE-Reranker + GPU 服务器,或者升级 DashScope 付费额度。当前个人项目阶段,API 方案的简单性优先于性能上限。
三、工程化坑
Q11. ThreadLocal + SSE 流式输出:流式是异步的,ThreadLocal 会不会丢?
这是一个好问题。实际上在我的实现里,ThreadLocal 没有在异步上下文中使用。
看
DocMindAgent.execute()的执行模型:整个方法是在调用线程上同步执行的(虽然对客户端是 SSE 流式)。SseEmitter 的流式是"往 emitter 里不断写数据",但写的动作本身发生在同一个线程上——ChatModel.stream().doOnNext().blockLast()最终通过blockLast()阻塞在当前线程。
AgentToolContext(ThreadLocal)只在llmDrivenRetrieve()方法内使用,从activate()到clear()都在同一个线程的同一个方法栈里:javaAgentToolContext.activate(kbIds, userId); try { agentClient.prompt().call().content(); // tool 回调发生在同一线程(Spring AI 同步 function calling) chunks = AgentToolContext.get().getChunks(); } finally { AgentToolContext.clear(); // 同线程清理 }唯一用到异步的地方是 Query Decomposition 的子问题并行检索(
CompletableFuture.supplyAsync),但那条路径不使用 AgentToolContext——子问题走的是直接调用vectorRetriever.retrieve(),不经过 LLM 工具选择。所以当前没有 ThreadLocal 跨线程传递的问题。但如果将来把主生成改成真正的 Reactor 异步流(去掉
blockLast()),就必须把 ThreadLocal 换成 Reactor Context 或者请求作用域的 bean。
Q12. SSE 中间断了怎么办?客户端怎么恢复?
当前实现对 SSE 断连的处理比较简单:
服务端:
SseEmitter设置了超时(默认 5 分钟),超时或异常时调emitter.completeWithError()。生成过程中如果写入失败(客户端断开),sendSseEvent会 catch 异常并打日志,但不会中断后续处理——答案仍然会被完整生成并持久化到qa_message表。客户端恢复:前端目前的实现是——如果 SSE 连接断开,重新建连后通过
conversationId拉取最近一条 assistant 消息。因为答案已经持久化了,断连不会丢失最终结果,只是丢失了流式过程中的中间 token。已知不足:没有实现断点续传——断连后无法从中间位置继续推送,只能拿完整结果。对于需要严格断点续传的场景,需要引入消息队列(比如 Redis Stream)作为中间缓冲。但当前场景是知识问答,不是长文生成,单次答案通常 2-3 秒完成,断连概率低。
Q13. 语义缓存 30 天 TTL,知识库更新了答案怎么失效?
缓存失效有两层保护:
第一层(KB 版本校验):
SemanticCacheService.lookup()在命中缓存后会校验kbVersion。每当知识库有文档增删或重新入库时,KbVersionService会递增对应 KB 的版本号。缓存条目中存储了写入时的 kbVersion,如果当前版本号大于缓存中的版本号,视为未命中,走正常检索路径。第二层(TTL 兜底):Redis key 设置 30 天 TTL 作为最终兜底,防止版本号机制出错时缓存永远不过期。
已知问题:
- 版本号是知识库级别的——某个 KB 里改了一个文档,整个 KB 相关的缓存全部失效。粒度较粗,但实现简单可靠
- 语义缓存的命中条件是 query embedding cosine ≥ 0.92——这个阈值如果设得太低,语义相近但答案应该不同的查询可能被错误缓存命中
Q14. 降级路径有没有集成测试?LLM 挂了怎么验证 fallback 真的能兜住?
诚实回答:目前没有专门的降级路径集成测试。当前的测试覆盖情况:
CrossEncoderReranker的fallbackRerank有在评测过程中被真实触发过(DashScope 限流时),确认降级评分逻辑能正常输出结果- EarlyStopGate 的两个关卡有单独的逻辑验证(通过评测中的超范围类查询间接验证了 gate2 触发后走 fallback prompt 的路径)
- LLM 工具选择失败后降级到
multiRetrieve(规则路由),这条路径在开发阶段因为 API 不稳定被频繁触发过缺失的:
- 没有模拟 LLM 完全不可用的场景(比如 mock 一个始终报错的 ChatModel)
- 没有验证"所有外部依赖同时不可用"时系统是否还能返回一个有意义的响应
这是一个合理的技术债。如果要补,方案是写一个
@SpringBootTest用@MockBean替换 ChatModel 为一个始终抛异常的实现,验证最终用户能拿到一个"暂时无法回答"的友好提示而不是 500 错误。
Q15. 配置热更新怎么做的?并发读写会不会有问题?
热更新机制:
所有 RAG 参数存储在 MySQL 的
sys_ai_config表中。AiConfigHolder内部用ConcurrentHashMap保存内存快照,模型实例用AtomicReference<ChatModel>持有。更新流程:管理后台修改参数 →
AiConfigService写入数据库 → 调用AiConfigHolder.updateBatch()更新内存 map → 如果 LLM 模型参数变了,调refreshLlmModel()原子替换模型实例。并发安全:
ConcurrentHashMap本身保证读写线程安全AtomicReference.set()保证模型引用的原子替换- 正在进行的请求持有旧模型引用,自然跑完后旧引用被 GC——不存在中间状态
不足:多实例部署时,只有收到管理请求的那个实例会更新内存。如果需要多实例同步,需要引入 Redis Pub/Sub 或配置中心(Nacos/Apollo)广播变更事件。当前单实例部署不存在这个问题。
四、成本与延迟
Q16. 一次完整查询要调几次 LLM?总成本和延迟分别多少?
最佳情况(85% 的查询)——规则路径 + 反思短路:
- LLM 调用:1 次 QueryUnderstanding(小模型)+ 1 次主答案生成(主模型)= 2 次
- 反思短路(rerank top-1 ≥ 0.85)跳过反思 LLM 调用
- 总延迟约 2.0s,成本约 ¥0.030
典型情况(含反思):
- LLM 调用:1 次 QueryUnderstanding + 1 次主答案生成 + 1 次 Self-Reflection = 3 次
- 其中 QueryUnderstanding 和 Reflection 走小模型(qwen-turbo),主答案走主模型(qwen-plus)
- 总延迟约 3.3s,成本约 ¥0.034
最坏情况(分类器模糊 + 拆解 + 反思):
- LLM 调用:1 次 QueryUnderstanding + 1 次 LLM 工具选择 + 1 次主答案 + 1 次 Reflection = 4 次
- 总延迟约 4.5s,成本约 ¥0.055
对比参照:ChatGPT Plus 用户每条消息的平均推理成本约 $0.05-0.10。我的方案在 ¥0.03-0.05 区间,考虑到包含检索 + 重排 + 多次 LLM 调用,成本控制是合理的。
Q17. 缓存命中率 45% 是怎么测的?
语义缓存的命中率 45% 是通过 Redis 中的命中计数器统计的:
cache_hit_count / total_query_count。前提条件:
- 命中条件:query embedding 与缓存条目的 cosine 相似度 ≥ 0.92 且 KB 版本一致
- 写入门控:同一归一化 query 被问 ≥ 2 次才写入缓存(避免低频查询污染缓存)
45% 的含义:在一段时间的真实使用中(包括我自己反复测试和少量外部演示),有近一半的查询是重复或高度相似的。这个数字在真实多用户场景下可能更低(用户提问更分散),也可能更高(如果用户群集中在特定主题)。
对成本的影响:命中时跳过整条 RAG 链路(embedding + 检索 + 重排 + LLM),延迟从 3s 降到约 200ms,成本从 ¥0.034 降到接近 0。按 45% 命中率算,平均成本降约 40%。
Q18. p50 / p99 延迟分别多少?
基于评测数据(52 条查询 × V4 完整链路):
分位 延迟 p50 3,100 ms p75 3,450 ms p95 4,200 ms p99 5,100 ms p99 偏高的原因:
- 网络搜索路径(Tavily API)加 1-3s 不稳定延迟
- DashScope 偶发慢请求(API 冷启动)
- Query Decomposition 路径(3 个子问题并行检索 + 合并 + 重排)比单路径多约 800ms
对用户感知的影响:流式输出模式下用户看到第一个字的时间(TTFT)约 1.5-2.0s(检索 + 重排完成后开始流式生成),后续逐字推送。实际体感比 p50=3.1s 这个数字好——因为用户在生成完成前已经开始阅读了。
缓存命中时:~200ms 返回完整答案(模拟流式分块推送)。
五、跟业界对比
Q19. 为什么不用 LangChain / LlamaIndex / Dify 直接搭?
回答的核心论点:用框架能更快搭出一个 demo,但面试要展示的是"理解每一层在做什么"而不是"会调 API"。
具体差异:
维度 LangChain/LlamaIndex 我的实现 检索控制 黑盒 retriever,参数暴露有限 自己实现 BM25 评分 + RRF 融合,能精确调 k1/b/rrfK 重排 一行代码 Reranker(model)自己写 API 调用 + 降级逻辑 + 后处理(去重/截断/token 预算) 可观测 需要接 LangSmith/Langfuse 插件 OpenTelemetry span 嵌入到每个阶段,与 Langfuse 原生对接 部署 Python 生态,通常 FastAPI 服务 Java + Spring Boot,更贴合企业后端的技术栈要求 坦诚说:如果目标是快速做一个产品原型,LangChain + Dify 可能一天就能搭出 80% 的功能。但这个项目的目标是面试展示工程能力——我需要能回答"RRF 公式怎么写"、"BM25 的 k1 是什么"、"重排 API 挂了怎么降级"这些问题,这些答案只有自己实现过才能有底气。
另外,Java 生态里 LangChain 的对等方案(LangChain4j / Spring AI)还比较年轻。我选择 Spring AI 作为 LLM 交互层(ChatClient / ToolCallbackProvider),但检索和融合层自己实现——这是"框架做连接,核心逻辑自己控制"的平衡点。
Q20. 跟 ChatGPT 上传 PDF 比,你的差异化是什么?
ChatGPT 上传 PDF 的限制:
- 单次会话上下文窗口有限(128K token),大文档会被截断
- 没有持久化——下次对话要重新上传
- 检索策略不透明,用户无法控制
- 无法跨多个文档联合检索
- 没有来源追溯(不告诉你答案来自第几页)
DocMind 的差异化:
- 持久化知识库:文档入库后永久可用,支持增量更新
- 来源追溯:每条答案标注来源文档名 + 章节 + 页码,前端可点击跳转
- 检索策略透明可控:用户能在管理后台调整检索参数(top-K、重排阈值、缓存策略),不是黑盒
- 多文档联合检索:124 篇文档共享一个向量空间,跨文档关联无额外成本
- 置信度提示:低置信度答案会提示用户"建议核实",而不是自信地给出可能错误的答案
- 对话记忆:跨会话的长期记忆(Redis),支持追问"你上次说的那个方案"
本质区别:ChatGPT 的文件上传是"临时增强上下文",DocMind 是"构建可持久检索的知识系统"。面向的场景不同——前者适合一次性快问快答,后者适合团队长期沉淀和反复查阅的知识管理。
Q21. 你和 RAGFlow / Dify / FastGPT 这些开源 RAG 产品比有什么不同?
坦诚说,功能完善度不如它们——它们是产品级开源项目,有几十人的团队在维护。我是一个人做的面试项目。
但有几个点是我能讲清楚而"用框架搭"讲不清楚的:
检索融合的细节:我能手写 RRF 公式、解释 k=60 的含义、说明为什么不用加权求和。用 RAGFlow 的人只能说"我配置了混合检索"
降级策略的设计思路:LLM 工具选择失败 → 规则路由;重排 API 限流 → 关键词评分降级;检索结果过差 → 两级早停关卡。这些是自己踩过坑后的决策,不是配置项能学到的
评测驱动的优化:我有对照实验证明"每加一层带来多少增量"、"反思在流式下是无效的"。这种评测思维比功能实现更有说服力
成本意识:我能回答单次查询花多少钱、反思模块值不值、评测本身该花多少钱。这是工程判断力,不是功能点
总结:我不是在做一个要上架的产品,而是在通过构建一个完整系统来展示"我理解 RAG 每一层在做什么、每个决策背后的权衡是什么"。
六、安全性
Q22. Prompt injection 怎么防?用户上传的文档里有"忽略上述指令"怎么办?
当前系统对 prompt injection 的防御有三层,但坦诚说第一层最关键的那个不是我设计的:
第一层(LLM 自身安全对齐):qwen-plus 经过安全训练,对直白的注入指令("忽略前面"、"输出 system prompt")有内置拒绝能力。评测中 5/5 拒答依赖的是这一层。
第二层(检索稀释):恶意文档中的注入文本在入库时被切片打散。用户提问时,向量检索不太可能把"忽略前面指令"这种文本排到 top-5——因为它和正常问题的语义相关度低。等于检索层天然做了一次过滤。
第三层(系统提示词隔离):PromptAssembler 的模板结构是"系统指令 → 检索上下文 → 用户问题"。系统指令明确说明"以下是检索到的参考资料",给 LLM 提供了区分指令和数据的上下文。
已知不足:
- 没有专门的 injection 检测规则(SafetyGuard 只匹配紧急词)
- 如果换成安全对齐较弱的模型,现有防御可能全部失效
- 文档内容本身没有做清洗(如果有人故意上传包含注入指令的 PDF,理论上可以污染后续回答)
改进方向:
- 在 SafetyGuard 增加 injection 模式正则匹配
- 在 PromptAssembler 中用 sandwich 结构(系统指令 → 用户输入 → 重申关键约束)
- 入库时对文档内容做敏感指令检测和清洗
Q23. 多用户文档隔离怎么做的?用户 A 能搜到用户 B 的私有文档吗?
当前隔离模型:
kb_knowledge_base表有user_id字段,每个知识库归属一个用户- 向量检索时通过
kbIds参数限定范围——前端传入用户有权限访问的知识库 ID 列表- Milvus 中每条向量存储了
kb_id作为 partition 标识,检索时带 filter 条件安全验证:
- Controller 层通过 JWT 鉴权确认用户身份
- 请求中的
kbIds会与用户有权限的 KB 列表做交集校验——用户无法通过伪造 kbIds 访问他人的知识库不足:
- BM25 检索路径的隔离依赖
WHERE kb_id IN (...)SQL 条件——如果忘传 kbIds 会查全库。当前代码如果 kbIds 为空,默认全库搜索。需要加一个防护:未传 kbIds 时只搜用户自己的 KB- 语义缓存是全局共享的——如果用户 A 和用户 B 问了相似的问题,B 可能命中 A 的缓存答案。缓存条目中存储了 kbIds,lookup 时会校验 kbIds 一致性,但如果两人访问相同 KB(比如公共知识库),缓存会被复用。这在设计上是预期行为
Q24. LLM 输出会不会泄漏其他用户的文档内容?
正常路径:不会。因为 LLM 的输入只包含当前用户有权限访问的 KB 中检索出的切片——提示词里只会注入 kbIds 限定范围内的内容。
风险点:
- 如果 LLM 有上下文泄漏(同一模型实例服务多用户),理论上有风险。但 DashScope 的 API 是无状态的——每次请求独立,不存在跨请求的上下文残留
- 对话历史存在
qa_message表中,查询时通过conversationId+userId双重验证唯一例外:如果用户使用了"记忆"功能(MemoryTool),记忆内容存在 Redis 中以 userId 为 key 隔离。但如果 Redis 被未授权访问,所有用户的记忆都会暴露。这层要靠 Redis 本身的认证和网络隔离来保护。
Q25. 文档上传有没有做安全检查?比如恶意文件、超大文件?
当前已有的检查:
- 文件类型白名单:只接受 PDF / DOCX / MD / TXT / PPTX / 图片格式
- 文件大小限制:Spring Boot 配置了
spring.servlet.multipart.max-file-size(默认 50MB)- 文件存储在 MinIO(S3 兼容对象存储)中,不存储在应用服务器本地文件系统——避免路径遍历攻击
没有做的:
- 没有对 PDF 内容做病毒扫描
- 没有检测 PDF 中嵌入的恶意 JavaScript(PDF 支持嵌入 JS)
- 没有限制单用户的总存储空间(理论上可以无限上传)
- 没有对解析后的文本内容做 prompt injection 检测
这些属于生产环境的安全加固项。作为面试项目,重点放在了 RAG 链路本身的设计和优化上,安全方面做了基本防护但没有做到企业级标准。如果面试官追问,我会说明这是已知的技术债并给出改进方案。
七、Phase 5/6 架构决策质疑
Q26. 你的 Scope Routing 两层设计为什么不直接用一层?
这是精度和覆盖度的 trade-off。
Tier-0 正则能 100% 精准识别"你好"、"谢谢"、"总结上面的对话"这些高置信度模式——但覆盖面窄,"帮我看看上面那个讨论"这种变体它识别不了。Tier-1 LLM 覆盖面广,几乎所有模糊表述都能正确分类——但延迟高(~250ms),且偶尔误判。
两层叠加的收益:Tier-0 零成本处理约 20% 的非知识查询(问候、致谢、显式对话引用),剩余 80% 才走 LLM。而且 Tier-0 和 Tier-1 是合并模式——Tier-1 的 scope 判断复用 QueryUnderstanding 的同一次 LLM 调用,不是额外花一次。任何一层失败默认走 KNOWLEDGE_QUERY,保证不会比没加 Scope Routing 之前更差。
追问预判:
- "Tier-0 的规则会不会越来越多越难维护?" → 见 Q30
- "合并模式省了多少?" → 知识查询路径从 2 次 LLM 调用(独立 ScopeRouter + QueryUnderstanding)降到 1 次,省 ~250ms
Q27. Path Decision 为什么不让 LLM 决定用哪些工具?
工具选择本质是 4 个布尔信号(isAmbiguous / specificity=PRECISE / timeAware / memoryAware)到 4 个工具的子集映射。这个规则空间总共就十几种组合,用 LLM 做就像用 GPT-4 算 1+1——不是不行,是没必要。
更重要的是:LLM Function Calling 有三个 Agent 系统里很痛的问题。第一是不确定性——同一个输入可能产出不同的工具组合(temperature>0),debug 时无法复现。第二是不可单元测试——你没法 assert LLM 的输出。第三是不可解释——出了问题你只能看 prompt 和 completion,不知道 LLM "想"了什么。
RetrievalPlanner 的 4 个 if 判断解决了所有这些问题:<1ms 延迟、100% 确定性、单元测试直接断言、PathDecision.reason 记录判定路径。
追问预判:
- "那复杂场景下规则引擎够用吗?" → 工具选择的规则空间小且变化慢。如果未来工具数从 4 个增长到 20 个,规则空间变大,那时候可以考虑 LLM 做选择——但目前 4 个工具用 LLM 是过度设计
- "和 Drools 有什么区别?" → 见 Q32
Q28. CRAG 评分是不是增加了延迟?值得吗?
看模式。默认的 heuristic 模式 0ms 延迟——它用的是 rerank 阶段已经产出的 per-chunk 分数,纯内存计算。cross_encoder 模式加 ~50ms(一次 reranker API 调用),对比 LLM 仲裁的 ~300ms 已经是 1/6。
值不值得?CRAG 解决的核心问题是:没有它的时候,LOW 质量的切片也会被送进 generation,LLM 基于低质量切片生成的答案大概率是幻觉。有了三档评分:HIGH 直接生成(零额外开销),LOW 走降级 prompt 明确告知"证据不足"避免编造,AMBIGUOUS 触发 Web Search 补偿。这不是"加了一步延迟",而是"花 0-50ms 避免一次可能的幻觉"。
实际调参过程中,CRAG 灰区阈值(HIGH≥0.65 / LOW≤0.25)就是我在 Langfuse trace 里看 rerank 分数分布后调出来的——这也是 Phase 6 可观测性的直接收益。
追问预判:
- "为什么用三档不用连续分数?" → 见 Q33
- "AMBIGUOUS 触发 Web Search 有多大概率改善结果?" → 取决于查询类型。时效性问题改善明显,领域专有问题改善有限——但 Web Search 的结果也会参与后续 rerank,不会劣化
Q29. Langfuse 可观测性在面试项目里有什么意义?
两方面。
实际调试价值:Phase 5 的 CRAG 灰区阈值就是典型例子——我需要看 rerank 分数在不同查询类型下的分布才能决定 HIGH/LOW 的分界线。之前只能加临时日志然后重启看控制台,有了 Langfuse 直接在 trace 瀑布图里看每步的属性值。再比如 QueryUnderstanding 确定性后处理——发现 LLM 对对比类问题漏判 decompose 也是在 Langfuse 里看到
needDecompose=false的 trace 后定位的。工程意识展示:Agent 系统和传统 CRUD 最大的区别是不可观测——LLM 在黑盒里做了什么决策你看不到。面试官关心的不是你会不会接 Langfuse,而是你有没有"AI 系统需要可观测性"这个意识。同样的道理也适用于 CRAG 评分——你有没有"检索质量需要量化评估"的意识。
追问预判:
- "生产环境 trace 量会不会很大?" → BatchSpanProcessor 2 秒批量导出 + RootNameFilteringSpanProcessor 白名单过滤框架噪音,只保留 Agent 关键 trace。采样率也可以通过配置控制
- "为什么不用 Prometheus?" → Prometheus 是通用监控(QPS/延迟/错误率),Langfuse 是 LLM 专用可观测性(prompt/completion/token 消耗/模型成本)。对 RAG 系统来说后者更有价值
Q30. MetaIntentDetector 的规则怎么维护?会不会越来越多越难管?
当前 6 条规则,每条有明确的语义边界:
- 闲聊问候(短语 ≤8 字符匹配 "你好/hello/谢谢/bye")
- 身份声明("我是+职业" 无问号)
- 知识库元信息("有哪些知识库/多少份文档")
- 显式对话引用("你刚才说的/我刚问的")
- 历史引用 + 对话名词共现
- 历史引用 + 元动作(总结/翻译)共现
设计原则是"宁可漏判多调一次 LLM,不要误判把知识查询路由错"。所以只加高置信度模式——正则能 100% 精准匹配的才加。
数量增长空间有限:元对话和闲聊的表达模式本身是有限集(人类打招呼的方式就那么几种)。我不认为会增长到难以维护的程度。如果真到了那一步,可以考虑用 few-shot 小模型替换正则,但那会引入 Tier-0 的延迟开销——目前不值得。
追问预判:
- "有没有误判的例子?" → 规则设计偏保守,误判主要是漏判(该短路的没短路),不是误判(不该短路的短路了)。漏判的后果只是多调一次 LLM,不影响正确性
Q31. AgentState 为什么不用状态机框架(如 Spring Statemachine)?
AgentState 是数据容器,不是状态机。它记录的是 Agent 执行过程中的所有中间状态(query / scopeDecision / gradeResult / accumulatedEvidence / confidenceTrajectory),但没有固定的状态转换图。
Agent 的每个 step 可以根据前序结果动态决策——比如 CRAG 评分 AMBIGUOUS 时可能补一次 Web Search 再回到 grading,可能直接进 generation,取决于是否已经尝试过 web。这种动态推理链不适合用状态机表达。
Spring Statemachine 适合的是有限状态流程编排——订单状态(待支付→已支付→已发货→已完成),每个状态的转换边是确定的。Agent 的状态空间是开放的,用状态机反而会把灵活性锁死。
追问预判:
- "那 LangGraph 不就是 Agent 状态机吗?" → LangGraph 的"状态图"更像是流程编排的 DSL,不是传统意义的 FSM。DocMind 用 Java 代码直接编排(if/else + 方法调用),和 LangGraph 的 Graph.add_edge 本质上做的是同一件事,只是表达方式不同。Java 版本更容易 debug 和单测
Q32. 你的 RetrievalPlanner 规则引擎和传统的 Drools 有什么区别?
Drools 是通用规则引擎:Rete 算法做推理、DRL 领域语言定义规则、支持 hot-deploy 运行时加载。适合规则数量大(几百条)、规则间有复杂依赖、业务人员需要自行管理规则的场景——比如保险核保、促销策略。
RetrievalPlanner 的规则是 4 个 if 判断,规则空间总共十几种组合,变化频率极低(加一个工具才需要改)。用 Drools 就像用 Spring Cloud 写一个 TODO 应用——技术栈选型不匹配。
更务实地说:Java 代码的 4 个 if + 单元测试比 Drools DRL 文件更可维护——IDE 能跳转、重构工具能改名、编译期就能发现错误。DRL 文件这些都做不到。
追问预判:
- "如果规则越来越多呢?" → 工具选择的规则空间天然有限——取决于工具数量和分类信号数量。工具从 4 个增长到 10 个,规则也就从 4 个 if 变成 10 个 if,仍然不需要 Drools。如果到了 50 个工具 + 复杂依赖关系,那时候该引入的不是规则引擎,而是 LLM 工具选择(因为规则空间已经大到人写不过来了)
Q33. 为什么 CRAG 只用三档不用连续分数?
连续分数(0-1)需要你定义一个映射函数:score 在什么范围做什么动作?0.47 和 0.48 有什么区别?这个映射函数的参数比三档的两个阈值更难调,而且更难解释。
三档的设计哲学是:HIGH 和 LOW 是高置信度的快速通道(不需要额外处理),灰区才需要仲裁。这跟 CRAG 论文(Yan et al. 2024)的设计一致——论文也是 Correct / Ambiguous / Incorrect 三档。
从工程角度:三档意味着只有两个阈值要调(high_threshold 和 low_threshold),都放在
sys_ai_config热配表里。连续分数意味着一套映射曲线要调,参数空间大得多。在没有大量标注数据做拟合之前,三档是更稳健的选择。
追问预判:
- "灰区范围太大(0.25-0.65)怎么办?" → 灰区不是"不确定",而是"需要进一步判断"。heuristic 模式看 top1-top2 分差和前三均分做第二轮判定,cross_encoder 模式让 reranker 重新打分。灰区的宽度反而给了仲裁空间
- "阈值怎么调出来的?" → 在 Langfuse trace 里看不同查询类型下 rerank 顶分的分布。HIGH 阈值设在分布的 P75 附近(多数好查询的顶分都高于它),LOW 阈值设在明显的质量悬崖处
附:应对追问的通用策略
被问到不会的:不要编。说"这个点我没有深入研究过,但我的理解是..."或者"这个是我的技术债,改进方向是..."。诚实比假装懂更安全。
被质疑某个选型不对:不要辩护。说"你说的方案确实是另一种选择,我选当前方案的理由是...,trade-off 是...,如果重新来过我可能会..."。
被要求现场写代码:RRF 公式、BM25 核心计算、ThreadLocal 使用模式这些要能白板手写。代码量都在 10 行以内。
被问数字但记不清:给数量级而非精确值。"大约 3 秒"比"3288 毫秒"在面试中更安全——精确数字如果和追问对不上会很被动。