面试知识库

M5 · RAG 品类知识库 —— 开发文档(面试向)#

讲为什么这么做、做了哪些取舍,不讲代码。读完能用大白话讲出来。

一句话概括#

M5 把 category_insight(品类洞察工具)从 M4 的”占位桩”升级成一条真 RAG 链路:用户要买某个品类的东西之前,Agent 先去一个品类知识库里查一查”这类货大概长什么样”——典型爆款是哪些、评分大盘怎样、便宜/中档/高端各在什么价位。查的方式是两路并行召回(语义 + 关键词)融合 → cross-encoder 精排 → 把多张卡片压成一段结构化结论。这套东西的意义是:让后面的精挑环节”懂行”,而不是对着商品标题瞎猜。

打个比方:以前 Agent 像个第一天上岗的导购,你说”旅行三件套”它都不知道标配是哪三件;M5 给它配了一本”行业小抄”,先翻一眼再给你推荐。


1. 为什么需要”品类常识”这一层#

把候选商品直接丢给精挑,模型只能看商品标题和几个表面字段做判断,会犯这种错:

  • 你说”旅行三件套,不要塑料”——它不知道”三件套”标配哪几件,可能漏选。
  • 你说”中性气质的咖啡杯”——它不知道这品类陶瓷/玻璃/不锈钢各占多少、口碑差在哪。

让模型每条 query 都”现场学”这种垂类知识,既不靠谱(训练数据未必覆盖)又贵。RAG 的思路是:把这些”行家常识”提前沉淀成知识库,用的时候按需查回来。 这就是 category_insight 的定位——它不是又一个”搜商品”的工具,而是搜商品和精挑之前的”前置认知层”。

面试怎么讲:这一层解决的是”Agent 不懂行”。常识不该让模型临场编,而该从一个可维护的知识库里查。这也是 RAG 的本质——不靠模型记忆,靠外部检索喂结论。


2. RAG 不是”向量检索”,是”召回 → 提炼 → 摘要”三步#

一个常见误解是 RAG = 向量搜一把丢给模型。真正能用的 RAG 是三步:

  1. 召回:把品类名当 query,在知识库里捞回 Top-N 张相关卡片。
  2. 精排:把粗排结果重新排一遍,把”相关但跑题”的卡剔掉。
  3. 提炼:把多张卡片压成结构化字段——给上层的是结论(“这品类典型价位 budget $3–35 / mid 35–123 / premium 123+”),不是把 5 篇原文整段塞回去

知识库里也不放博主长文,只放结构化卡片。最初三类(爆款卡、属性卡-评分分布、价格档位卡),后来补到五类:① bestseller(月销头部热卖)、② attribute-评分分布(真实数据聚合)、③ attribute-LLM 多维度(LLM 生成的材质/连接方式等选购维度分布)、④ price_range(p5–p95 分位切档)、⑤ attribute_schema(Shopify 分类法的维度名+取值列表)。247 个品类共 2,044 张卡。每张卡都是”已经提炼好的一句结论”。

面试怎么讲:RAG 的价值在”提炼”那一步。把原文塞回模型是偷懒——又占上下文又容易跑偏。我让工具内部把原始证据消化掉,只回传结论。这也呼应了项目的上下文治理:给主流程的永远是压缩过的结构化数据。


3. 选型:为什么知识库用 OpenSearch,而召回层还是 Faiss#

项目里”用到向量”的地方不止一处,但它们性质不同,不该一刀切用同一个库:

  • 商品召回(M3):千级以上商品、纯算力、对延迟极敏感、过滤主要是”按平台分库”。→ 用 Faiss,直接吃向量数组,单机就快。
  • 品类知识库(M5):千级卡片、要”语义召回 + 关键词匹配 + 按字段过滤”三件事一起做,还要权重可调。→ 用 OpenSearch

为什么知识库非 OpenSearch 不可?因为它要的是混合检索(Hybrid):语义那一路(KNN 向量)和关键词那一路(BM25 全文)并行跑、各自打分、再加权融合。这件事的关键好处是权重在引擎层就能调——今天想更看重字面就把 BM25 权重调高,明天想更看重语义就调低,不用改代码不用重启。纯向量库(Faiss)做不到这个,过滤和融合逻辑只能塞回业务代码越写越乱。

为什么不是”先过滤再向量”或”先向量再过滤”? 前者过滤太狠时向量索引就近乎失效(候选太少近邻没意义);后者向量起手丢掉的、过滤阶段再也捞不回来。只有”两路并行 + 加权融合”能让两边互相补位。

面试怎么讲:召回层和应用层是两类需求。召回层吃算力,Faiss 最省;应用层要”语义+关键词+标量”揉在一次请求里还要权重可调,这是 OpenSearch 的独门能力。这不是哪个库更强,是各用其长的分层。


4. 一个对教材的主动更正:中文教材 vs 英文数据#

参考资料假设知识库是中文卡片,用中文分词器(ik)。但我手上的真实数据(data/rag)是英文 Amazon 商品。照抄就会出错——中文分词器分英文是错的。

我的处理:BM25 那一路改用 OpenSearch 内置的 english 分词器(匹配真实语料)。那中文 query(比如用户搜”旅行收纳”)怎么命中英文卡片?靠语义那一路(KNN 多语言向量)兜底——这恰恰是 Hybrid 双路设计的意图:一路(关键词)命不中,另一路(语义)补上。再加一张”品类别名表”把”旅行收纳”先归一到标准品类名”travel accessories”,召回就稳了。

面试怎么讲:教材是参考不是圣经。它的语料假设和我的真实数据对不上,我就按真实数据改——分词器换英文,跨语言交给向量。这正是项目从头贯彻的原则:对齐思想,按实情改实现。


5. 又一次”没有外部服务也得能跑”#

跟 M3 编码器一个道理:OpenSearch 是个外部常驻服务,但开发、测试、CI 时它常常不在。如果代码”非它不可”,那连跑个单测都得先起 docker。

我的处理:知识库检索做成双后端。配了 OPENSEARCH_HOST 就走真 OpenSearch(引擎层 Hybrid);没配就退回一个进程内的本地 Hybrid——用同一份卡片、在本进程里算同一个”归一化 + 加权融合”的公式,只是规模小。两条路算的是同一个融合公式,所以离线/CI 不依赖 docker 也能把整条 RAG 链路跑通、被测试断言;docker 一起来,自动切到真服务。我两条路都实测过,召回指标几乎一致。

精排(cross-encoder)也是同样套路:配了远程就走真 BGE-Reranker 服务,没配就退到一个确定性的本地打分。远程挂了也不让整条链路崩,自动降级。

面试怎么讲:每引入一个外部依赖,我都配一个”离线替身”。不是为了偷懒,是为了让 CI 不被外部服务绑架、让链路有兜底。真服务和替身走同一接口,切换零成本。


6. 评测:给”升级”装一个客观刹车#

RAG 链路最怕的是”改了一行召回代码,case-by-case 看着变好了,整体其实退步了”。所以我搭了一套召回评测:用一小份”金标集”(哪个 query 应该召回哪些卡片)算三个指标——Recall@K(该召回的找回了多少)、MRR(第一条对的排第几)、NDCG(排序质量好不好)。每次改权重、改精排,先跑一遍评测过门禁再合代码。

金标集怎么来?关系是确定性构造的,不靠模型拍脑袋:同一个品类下的三张卡天然互为”相关”,所以”该召回什么”是确定的。模型只负责把品类名改写成更自然的购物口语(覆盖名词类、口语类等不同 query 形态),让评测不只测”字面同名召回”。没配模型时直接用品类名也能跑。

一个诚实的边界:因为我用的是 M3 那个”确定性本地替身”编码器(语义不强),离线评测的召回数字到不了教材里真 BGE 的高位——但这正是替身的定位。指标门禁我定的是宽松基线(冒烟级),目标是”防整体塌方”,不是上线级硬指标。换上真 embedding 后这套评测和门禁原样适用、数字会更高。

面试怎么讲:评测不是为了刷分,是为了让”改进”可证伪。我每次动召回都有一个客观数字对照,避免自我感觉良好。门禁定宽是因为底层编码器是离线替身——我把它定位成冒烟刹车,诚实标注它不是上线级阈值。


7. 评分细节里踩到的两个坑(值得一讲)#

  • 归一化的”无信号”该归零,不是归一:两路分数融合前要各自归一到 0–1。但当某一路完全没有信号时(比如中文 query 对英文卡片,关键词那路全是 0),如果给所有候选都打满分,等于凭空给每个候选注入一个常数——既抬高了绝对分,又抹平了”精排短路”要用的首尾差距。正确做法是:这一路没信号就归零、不贡献,把排序权交给另一路。这个细节直接决定了跨语言召回时行为对不对。
  • NDCG 的”重要性顺序”不能是字母序:NDCG 衡量”高价值的卡有没有排在前面”,它按金标列表的顺序给递减权重。我一开始把金标里的卡按 card_id 字母序排——那 NDCG 就退化成”奖励 card_id 字母靠前的”,毫无意义。改成按重要性排(爆款 > 属性 > 价格),NDCG 才真的在衡量排序质量。

面试怎么讲:这俩都是”看着对、其实悄悄错”的坑。归一化把无信号当满分、NDCG 拿字母序当重要性——都不会报错,但会让指标骗你。这种 bug 只能靠想清楚每个数字的语义来抓,是 code review 里我自己复盘出来的。


8. 这一关交付了什么、边界在哪#

交付category_insight 走通完整 RAG(召回→精排→结构化);OpenSearch docker 环境 + 索引 mapping + Hybrid 管道;从真实 Amazon 数据离线建知识库的 ETL(聚合→入库门禁);cross-encoder 精排客户端;召回评测三指标 + 金标集生成 + 跑分门禁。真 OpenSearch 和本地替身两条路都实测跑通,指标一致。

后续增强(已落地)

  • LLM 属性分布卡scripts/etl/llm_attributes.py):补上了 M5 原始交付只有”评分分布”这一维属性卡的缺口。用 LLM 按品类生成多维度属性分布(如 "Material: Nylon 40% / Polyester 25% / Canvas 15%"),confidence 固定 0.6(低于真实数据卡的 0.5–0.95),raw_evidence 标注 "LLM-generated"。卡类型仍为 attribute,与评分分布卡共存。知识库从最初的 741 卡(248 品类 × 3 类)扩展到 2,044 卡(247 品类 × 5 类),新增的 1,062 张 LLM 属性卡把 refdocs 要求的「材质:尼龙 60% / 帆布 25%」级别的多维度分布补齐了。
  • Shopify 分类法属性模式卡scripts/etl/shopify_attrs.py):246 张 attribute_schema 卡,提供每品类的选购维度名 + 典型取值列表(无分布比例),作为 item_picker 精挑时的锚点。
  • 语义缓存(ENH-E):category_insight 的两级缓存(精确 + 语义),不再每次查同一品类都走 RAG。
  • 工具思考结果摘要:category_insight 的 report_tool_end 现在带 result 字段,包含爆款名、价位档、Top 选购维度及其典型取值的人读摘要(供前端展开看)。

诚实的边界(更新)

  • 属性卡目前只有”评分分布”一个维度 → LLM 属性卡已补齐多维度(材质/连接方式/…),但数值是近似的(directional accuracy),不是精确统计。
  • 离线替身编码器语义偏弱,召回质量的天花板在真 embedding 那边;这一关把架构、链路、评测都搭对了,换引擎即提质。
  • 冷启动(新品类无卡片)走 WebSearch 兜底——已在主链路里接通。
  • 精排和知识库的”上线级”调参(权重按 query 形态自适应、人工标注集扩到几百条)属于持续迭代,不在这一关。