AI测试 LLMCase-V4 从策划文档到测试用例 - 第 6 章 generator·RAG:给大模型配上"参考书"

zhangbp · September 24, 2026 · 28 hits

本章目标:理解检索增强(RAG)在本工程的真实形态。学完你能回答:
嵌入和向量检索是什么直觉?本工程的"双库双路"指什么?一次检索从 query 到注入要经过哪些站?
few-shot 样例库怎么治理?

6.1 为什么需要 RAG:嵌入与向量检索的直觉

第 5 章的 prompt 里有两个检索块:document 上下文块和 few-shot 示范块。它们解决两个不同的问题:

  • document 检索解决"断章取义":生成"队伍人数上限"的要点时,文档里讲队伍规则的段落可能在三个 不同章节——把相关段落检索出来塞进 prompt,LLM 才看得全。
  • few-shot 检索解决"缺领域记忆":本项目以前给"组队类功能"写过的好用例长什么样? 检索 3 条同类历史样例作为示范,LLM 的输出风格和质量立刻有参照。

两者共用同一套底层技术——嵌入(embedding)+ 向量检索:

术语盒:嵌入与向量检索(30 秒版)。嵌入模型把一段文本变成一个定长数字向量
(本工程是 1024 维),语义相近的文本向量距离近。把所有文本块都变成向量存进向量库(FAISS),
查询时把查询文本也变成向量,找库里距离最近的 K 个块——就是"按语义搜文档"。
相比关键词搜索,它能搜到"说法不同但意思相近"的内容。

一个必须建立的量级直觉:bge-m3 的相似度分数封顶约 0.60-0.70(真实查询的实测分布)。
本工程曾按"0.85 才算相关"设阈值,结果每条查询都够不到阈值、检索永远返回空——
这是新人接手检索代码前必读的头号坑:阈值必须与分数尺度同源校准。

嵌入模型的选型与工程约束(新人常问"为什么是这个模型"):本工程用 BAAI/bge-m3 dense
(1024 维、最长 8192 token),v4 从 bge-small-zh-v1.5(512 维)BREAKING 升级——中文检索
质量与长序列是两个决定性理由(extractors/vector_config.py 文件头有完整决策记录)。工程面
三条同样重要:本地推理而非 API(HuggingFaceEmbeddings 加载权重);进程级单例
(~2.3GB/实例,曾因每实体 new × 5 并发把内存打爆——session#22 OOM 修复,双检锁全进程共享);
失败降级——模型缺失/实例化失败返回 None = "检索禁用",管线照跑不崩(实例化前先做
纯本地可用性预检,防 HuggingFace 网络探测挂起)。

6.2 双库双路:documents 与 few_shot

本工程的 RAG 是"双库双路"形态:

【documents 库】装策划文档原文块(需求上下文)
    入库:实体提取启动时自动 ingest 当前 SRC md(切块 → 嵌入 → FAISS 增量追加)
    检索:要点/用例生成期 retrieve_context_documents(top 3)

【few_shot_index 库】装历史样例(方法示范)
    入库:人工种子 / SVN 抽取 / badcase 回流,三通道治理后入库
    检索:要点/用例生成期 retrieve_few_shot(top 3,三级路由)

两库共用同一个嵌入入口(bge-m3 进程级单例)——向量空间一致才能谈相似度。
检索结果记进产物 metadata.retrieval_sources({document_context, few_shot_cases}),血缘可溯。

两库的"块形态"是分块方法的第一课(新人常问"convert2md 分块吗"——不分,见 3.7 边界):

  • documents 库按标题语义分块:MarkdownHeaderTextSplitter 以 H1–H4 标题为边界, 一个小节一块,header_path 元数据记录层级路径(如 系统设计 / 战斗 / 倍速)—— 语义完整性优先:命中后 LLM 仍知道这段话出自哪个章节语境;
  • few_shot 库不分块:一条历史要点/用例就是一条原子记录,整条入、整条检;
  • 两件"能力就位、默认不走"的储备(flag 默认 off 铁律的体现):其一是父子块 Small-to-Big(ParentChildChunker:父 1000 字/子 300 字/重叠 50,小块精检索、 命中展开回大块——检索侧展开钩子已接线,但当前入库的就是标题语义块,无父子元数据, 展开对旧命中自动放行);其二是 dense+BM25 双路 RRF 混合(query_hybrid:语义召回 与字面召回 RRF 融合——中文短查询下字面命中常比语义稳;代码默认 off,生产 .env 已开, 见 6.3 站 4)。稀疏嵌入(学习型 sparse/ColBERT)明确不用 (vector_config.py 文件头写死)。

向量检索在本工程其实有第三个应用点——对话式生成的实体定位(locate,8.5)用的是
"即抛型"内存向量:core/ask_locate.py 的 locate_candidates 在用户发起定位时,
把绑定项目 ENT 里的候选实体现算嵌入(embed_documents,"名称 + 类型 + 描述"拼文本),
与查询向量做纯 Python 余弦相似,取 top-8 回贴点选——不建 FAISS、不留索引,用完即弃。
匹配分三级(精确全等 → 子串包含 → 语义),语义档有 0.1 弱相关地板(全被滤掉就回零候选,
交互式定位不给无关答案);嵌入不可用时整体降级 difflib 字面相似(宁可弱匹配不挂)。
对照着记:沉淀型用法(历史知识,FAISS 落盘跨批次复用)与即抛型用法
(当次交互的临时匹配,内存现算)——同一套嵌入模型,两种工程形态。

6.3 从 query 到注入:检索链路逐站

以要点生成期的 few-shot 检索为例,一站站走:

站 1·query 构造(入检同构):查询文本不是随便拼的——与入库时构造文本块共用同一套格式模板
([TP] 实体: 名 | 类型: 型 / 描述: … 两行块)。格式对不齐,嵌入就不在一个语义空间里
(本工程 v3 时代录入/检索格式不对称导致检索失灵的根因修复)。

站 2·(可选)query 改写:HyDE(让 LLM 先生成一段假设性文档再检索)和 RRR(改写 query 补名词消歧)。
只在重试轮才启用(首轮便宜检索,重试才花钱升级)——成本分层;两 flag(ENABLE_HYDE/
ENABLE_RRR)代码默认 off 按需开。这是检索侧已落地的"查询重写"——构建/路由/澄清/
分解等查询技术在对话式生成一侧的完整对照见 6.5。

站 3·三级路由(tiered):为防"冷启动语义污染",检索按 provenance 阶梯降级:

tier1:同项目(Project_ID + Genre_ID 都锁)→ 命中且 sim≥0.60 就用
tier2:放宽项目维度,只按品类(Genre_ID,如"放置类")→ 命中就用
tier3:都空 → 返回空列表,生成走零样本(宁可没示范,不要不相干项目硬凑的示范)

project_id/genre_id 从 HTTP 入口一路穿透到 FAISS metadata 过滤(_matches_3d:
请求维度 present 但 mismatch 硬拒、missing 容忍)——隔离是全链路的,不是检索层的装饰。

站 4·质量后处理:FAISS 返回的是 L2 距离,必须归一化成相似度(sim = 1 - d/2,
归一化向量下 sim∈[0,1])再当分数用。生产链路是三段重排(few-shot 路):
dense+BM25 RRF 混合召回(query_hybrid)→ BgeReranker cross-encoder 精排
(宽召回 top-10 → 精排 → top-N)→ LLM 二次精排(ENABLE_LLM_RERANK,
对 top-N 用轻模型再排一次)。每一环都是 flag 接线、缺席自动降级:reranker 不可用
退回"0.70 地板 + 0.85 精度闸"的纯相似度闸门,RRF 关闭退回纯 dense。
最高分低于地板 → 弃全部(零样本兜底,不硬塞弱相关样例污染生成)。

站 5·注入:检索块渲染成中文标签的分节文本,按第 5 章 prompt 五层结构的位置插入。
few-shot 的注入措辞有讲究——"方法补充语 + approach 载体"而非整例照抄(提示模型学方法别学数值),
模型若吸收了 few-shot 的思路会记 [Absorb] 日志。

6.4 few-shot 样例库治理

样例库是"喂过的人工经验",治理面三通道:manual(人工录入/Excel 批量导入)、
svn_extract(从公司 SVN 历史测试资产抽取 + 结构门 + LLM 可迁移性过滤)、
badcase(评估打回的坏用例 → LLM 归纳通用规则回流,永不抛异常)。

三条治理纪律:

  1. jsonl 归档是真相源,FAISS 索引是派生物——两者双写;索引挂了可从归档一键重建 (scripts/rebuild_few_shot_index.py,跨机同步后就是靠它);
  2. quality_score 不自填——生成侧不给自己打分,分数只来自人工/过滤通道(防自我标榜);
  3. 双池扁平并存——品类池(带 Genre_ID)+ 通用池(无 Genre_ID)同库共存, 靠 metadata 过滤天然混合(missing 容忍),不需要递归继承代码。

6.5 实现全景:三个应用点、两份落盘库与一套嵌入基建

本章最后把"向量存储 / 嵌入"在本工程的全貌收拢成一张地图——新人在代码里搜 embedding
或 faiss 时,答案都在这张表里:

三个应用点(同一套 bge-m3 嵌入,三种工程形态):

应用点 形态 入库时机 检索方式 失败降级
documents 库(需求上下文) FAISS 落盘,全局累积 实体抽取工作流启动时非阻塞入库(core/workflow.py try/except 包裹,失败只告警) retrieve_context_documents,top_k=3,按 source_type 过滤 嵌入不可用 → VectorConfig.get_embeddings() 返 None → 检索禁用,主链照跑
few_shot 库(方法示范) FAISS 落盘 + jsonl 真相源双写 三通道治理入库(6.4) 三级路由 + 3D 元数据过滤 + 三段重排(6.3) 全空 → 零样本生成
locate(对话生成实体定位,8.5) 即抛型内存向量,不建 FAISS 不入库——现场 embed_documents 候选 + 查询 纯 Python 余弦相似度取 top-8 嵌入失败 → difflib 字符串匹配兜底

落盘形态:data/vector_store/documents/ 与 data/vector_store/few_shot_index/
各一对 index.faiss(向量索引,IndexFlatL2 精确搜索)+ index.pkl(langchain docstore,
原文与 metadata 的 pickle)。两句诚实注记:documents 库是全局累积的——无项目隔离、
append 不去重,同一文档重复跑批次会重复入库(体积换简单的取舍);few_shot 库则有
project/genre/feature_type 三维隔离,且 jsonl 归档才是真相源(索引可重建,6.4)。

嵌入基建(extractors/vector_config.py):BAAI/bge-m3 稠密向量,1024 维、
8192 token 上限,本地目录 models/embeddings/bge-m3、EMBEDDING_DEVICE=cuda。
两个工程要点:进程级单例(双检锁)——bge-m3 约 2.3GB,CaseRetriever 每实体、
多 worker 并发场景下若各持一份会内存爆炸,单例后全进程共享;CacheBackedEmbeddings
best-effort 缓存(InMemoryStore)避免重复文本重复编码。

后续优化思路(设计讨论,非现状):要真正支持多模态查询,路有三条——① 继续
文字中介但增强转写质量(最稳,VLM 升级即受益,改动最小);② 引多模态嵌入模型
(CLIP 系/中文 CLIP/视觉嵌入),把原图单独编码进独立 FAISS 索引,文图共享语义空间,
才谈得上"以文搜图/以图搜图"——代价是 bge-m3 无多模态版,须另起一套模型 + 索引 + 入库链;
③ 页面级嵌入(ColPali 系整页截图编码)适合文档 RAG 的整页召回。评估的锚点:
VLM 转写已捕获大部分语义,多模态嵌入的增量收益集中在转写有损的复杂图表上——
先量化转写损失再决定投入。

检索质量回归:向量化不是"建完就灵"——evaluator 侧 metrics/retrieval_metrics.py
提供 Recall@K / MRR / nDCG 纯函数,对着金标问题集给检索链路打分;
scripts/probe_retrieval.py 是命令行探针。6.3 站 4 说过的阈值(0.60 地板/0.85 闸)
就是这类实测校准出来的,改检索链路必须重跑。

生产姿态速查(代码默认 vs 生产 .env,改前先对表):

flag 代码默认 生产 .env 管什么
ENABLE_RRF_HYBRID off 开 dense+BM25 RRF 混合召回
ENABLE_BGE_RERANK off 开 bge-reranker-v2-m3 cross-encoder 精排
ENABLE_LLM_RERANK off 开 LLM 对 top-N 二次精排
USE_PHASE2_INGESTION on 开(默认即生效,.env 未显式设) ETT(Excel 表思考块)入库抽取;ImgThinking 恒抽不走此 flag
ENABLE_FLOWCHART_STRUCTURE off 开 FCT 流程图结构块

增删改查:两库姿态不同(都在 few_shot_data_manager.py / vector_store_writer.py):

操作 few_shot 库(全功能 CRUD) documents 库
增 三通道入库(manual/ingest_file/svn/badcase):FAISS add_documents + jsonl 双写 工作流启动时 ingest_markdown_document,append 累积
查 dense similarity_search_with_score / query_hybrid 混合;HTTP GET /api/few_shot/{list,stats,{id}} retrieve_context_documents(top_k=3)
删 DELETE /api/few_shot/{id}:扫 docstore 按 metadata['id'] 定位 → store.delete([...])(FAISS remove_ids + docstore 删)→ save_local 重落盘 → jsonl 同步删行 无(append-only,无清理 API)
改 PUT /api/few_shot/{id}:删除 + 重入库实现(向量不可原地改),version 自增;另有 upsert 无

两个底层细节:① IndexFlatL2 本身不支持按 id 删——langchain 封装用 ID 映射补齐
(代码注释原话"效果等价 IndexIDMap"),删完必须 save_local 整体重写双文件;
② BM25 那一路不是持久化稀疏索引——query_hybrid 每次在过滤后的候选集上现场建
BM25Okapi(few-shot 百条量级,建得起;documents 库量级大就不适用这个打法)。
documents 库当前没有重建/清理工具(scripts/rebuild_few_shot_index.py 只管 few_shot,
靠 jsonl 真相源重建)——全局累积的体积问题是有意接受的取舍,不是遗漏。

为什么是 FAISS、不是 Chroma/Milvus:先说诚实版——仓库里没有 chroma 的选型记录
(requirements/文档/openspec 零引用,FAISS 是 v3 沿革)。从工程约束倒推的理由:
FAISS 是嵌入式库不是服务——进程内调用、零额外部署,与本工程"4 服务单机 + NAS 落盘"
的形态严丝合缝;IndexFlatL2 精确搜索(全库扫描、无近似丢失)在万级以下 chunk 语料上
延迟毫秒级,用不到 ANN 近似;存储就是两个自描述文件(faiss + pickle),拷贝/备份/跨机
同步跟普通文件一样。对照 Chroma:内建 metadata 过滤、默认 HNSW、文档型存储更"数据库",
但要引入服务依赖(或嵌入式模式下多一层抽象)与自有存储格式——在本工程语料量级下,
这些优势换不来对应收益。升级思路(量变触发):语料涨 1~2 个量级 → 同库换
IndexIVFFlat/HNSW 索引(faiss 自带,改建索引一行);再涨或要多机 → 换服务型向量库
(Chroma/Milvus)。调用方只认 langchain VectorStore 接口(similarity_search_with_score),
这层接口天然隔离了底层替换;few_shot 还有 jsonl 真相源,整库导出重建即可迁移。

查询技术对照表(构建/翻译/路由/澄清/分解/扩展——谁有谁没有,一表看清):

No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up。