第二阶段实践:Vibe Coding 实践¶
用 Codex 完成一个简易混合检索 RAG¶
上一章:Milvus 索引机制与基本操作
下一章:意图分类
本节放在第 04 章之后学习。前面已经学习了 Milvus、HNSW、Dense 向量、Sparse 向量和混合检索,本节把这些知识放进一个可以运行的 RAG 项目中,再使用 Vibe Coding 的方式完成代码、验证和修正。
本节统一使用 Codex 演示。下面的提示词可以直接复制到 Codex 对话中执行;代码修改、命令运行和结果检查都在同一个项目工作区完成。
这里的重点不是让 Codex 一次生成所有代码,而是练习一套可以重复使用的开发过程:
本节完成什么¶
完成本节后,你应该能够:
- 说清楚一个简易 RAG 的入库链路和在线问答链路;
- 让 Codex 根据结构化需求生成项目方案;
- 使用 Milvus 保存 Dense 和 Sparse 两类检索字段;
- 使用 Milvus 2.5+ 完成 Dense + BM25 混合检索;
- 理解为什么混合检索之后还需要 Reranker;
- 使用 BGE Reranker 对候选文档进行二次排序;
- 让 LLM 根据排序后的上下文生成答案和来源;
- 使用测试、接口响应、检索来源和日志判断代码是否正确;
- 遇到错误时,向 Codex 提供足够信息并完成最小修改。
本节完成的不是一个企业级平台,而是一条真实、可运行、可解释的核心 RAG 链路。
一、项目边界¶
1.1 本节主链路¶
本节只关注下面这条链路:
离线入库:
FAQ / 业务文档
→ 文件加载
→ 文本切分
→ BGE-M3 生成 Dense 向量
→ Milvus 内置 BM25 生成 Sparse 向量
→ 写入 Milvus
在线问答:
用户问题
→ Dense + BM25 混合检索
→ 合并候选结果
→ BGE Reranker 精排
→ 选择上下文
→ LLM 生成答案
→ 返回答案和来源
1.2 本节会用到的组件¶
| 组件 | 本节中的职责 |
|---|---|
| FastAPI | 提供 GET /、GET /health 和 POST /ask |
| Milvus 2.5+ | 保存文档并执行向量检索 |
| HNSW | 为 Dense 向量提供近似近邻索引 |
| BM25 | 根据关键词和词频执行 Sparse 检索 |
| BGE-M3 | 将问题和文档转换为 Dense 向量 |
| BGE Reranker Large | 对候选文档进行二次相关性排序 |
| OpenAI 兼容 LLM | 根据检索上下文生成答案 |
| MySQL | 保存本案例的 active 知识库版本 |
| Docker Compose | 启动基础设施和 API |
1.3 本节暂时不展开的内容¶
下面的能力属于后续企业级章节,本节只保留运行所需的最小接口,不展开其内部设计:
- 多场景意图识别和网关仲裁;
- Redis 缓存;
- 多租户权限治理;
- 知识库质量门禁和回滚;
- LangSmith Trace;
- WebSocket 流式输出;
- Agent、GraphRAG 和复杂任务编排。
现有 mini-rag/ 代码为了便于后续对照,保留了 active 版本、FAQ 直出、查询改写和查询变体等辅助能力。本节的主线仍然只追踪“入库、混合检索、Reranker、生成”四个核心环节。
二、先看懂最终效果¶
2.1 入库完成后的结果¶
执行入库脚本后,系统会得到两类 Milvus Collection:
每条记录的检索字段可以理解为:
Dense 和 Sparse 不是二选一:
- Dense 擅长理解“意思相近但用词不同”的问题;
- BM25 擅长命中明确的关键词、编号、制度名称和专有名词;
- 混合检索把两路结果合并,减少只依赖一种检索方式带来的漏召回。
2.2 在线提问后的结果¶
在 mini-rag/static/index.html 页面提问后,可以看到:
- 最终答案;
- 引用来源;
- 本次命中的路由;
- 改写后的问题;
- 查询变体;
- FAQ 和文档的命中数量;
- Reranker 是否运行;
- 每个阶段的耗时。
这些字段帮助你判断:问题有没有进入检索、检索命中了什么、Reranker 是否运行,以及慢在哪个阶段。
三、Vibe Coding 的工作方法¶
3.1 你和 Codex 分别负责什么¶
Vibe Coding 不是只写一条提示词,然后不再看代码。一个可控的分工是:
| 工作 | 主要责任 |
|---|---|
| 描述业务目标 | 你 |
| 确认技术边界 | 你和 Codex 一起确认 |
| 生成代码初稿 | Codex |
| 检查文件是否改对 | 你 |
| 运行测试和服务 | 你 |
| 根据报错提出修改方案 | Codex |
| 判断修改是否真正解决问题 | 你 |
Codex 可以提高编码速度,但不能替你判断结果是否正确。本节每完成一个阶段,都要留下三种证据:
3.2 提示词必须包含什么¶
一条有效的 Vibe Coding 请求至少包含:
不要只说“把 RAG 做出来”。应该明确说:
这一轮只实现 Milvus Collection 初始化。需要创建
text、dense和sparse字段,Dense 使用 HNSW,Sparse 使用 Milvus 2.5+ 的 BM25 内置函数。先阅读当前文件,再列出修改计划,暂时不要修改代码。完成后用一个不连接真实服务的测试验证索引参数。
3.3 每一轮只推进一个阶段¶
阶段 0:分析已有代码和目标
阶段 1:确认项目骨架和配置
阶段 2:完成文档加载、切分和入库
阶段 3:完成 Dense + BM25 混合检索
阶段 4:加入 BGE Reranker
阶段 5:连接 LLM 和问答接口
阶段 6:测试、调试和 Docker 验收
每个阶段结束后再进入下一阶段。这样出现问题时,可以知道问题是在哪一轮引入的。
四、实训一:让 Codex 先分析项目¶
打开一个新的工作目录,或者复制现有 mini-rag/ 作为练习目录。在 Codex 中打开这个工作区,第一轮只让它阅读需求和现有代码,不要求它立即生成大量代码。
请先阅读当前项目,不要修改任何文件。
我需要完成一个简易但真实可运行的 RAG:
1. 使用 FastAPI 提供 /、/health、/ask;
2. 使用 Milvus 2.5+ 保存文本、Dense 向量和 Sparse 向量;
3. Dense 使用本地 BGE-M3;
4. Sparse 使用 Milvus 服务端 BM25 内置函数;
5. Dense 索引使用 HNSW,Sparse 使用 BM25/AUTOINDEX;
6. 使用 Weighted Hybrid Search 融合两路召回结果;
7. 使用本地 BGE Reranker 对候选结果精排;
8. 使用 OpenAI 兼容接口调用 LLM 生成答案;
9. 返回 answer、sources、route、retrieval 等诊断字段;
10. 使用 Docker Compose 启动 MySQL、etcd、MinIO、Milvus 和 API。
请输出:
- 当前项目中已经存在的相关文件;
- 每个模块负责什么;
- 一次入库的执行顺序;
- 一次在线问答的执行顺序;
- 需要新增或修改的文件;
- 分阶段实施计划;
- 每个阶段的测试和验收方法。
暂时不要修改文件,也不要输出大段代码。
检查 Codex 的方案¶
重点检查:
- 入库和在线问答是否分开;
- Dense 和 Sparse 是否同时存在;
- 是否先混合召回,再 Reranker 精排;
- Reranker 是否被错误地放在入库阶段;
- 是否把 Reranker 当成向量索引;
- 是否明确了 LLM 只使用最终上下文;
- 是否给出了可执行的测试方法。
如果 Codex 把所有逻辑都塞进一个文件,先要求它重新拆分方案,不要马上接受代码。
五、实训二:按阶段生成代码¶
5.1 阶段 1:项目骨架和配置¶
请根据刚才确认的方案,只完成项目骨架和配置。
要求:
- 创建 app.py、qa_core/、scripts/、static/、tests/;
- 配置从 .env 读取,不把 API Key 写死在代码中;
- 先提供 GET /health;
- 为配置解析和健康检查添加纯逻辑测试;
- 不实现混合检索和 LLM 生成;
- 不修改与本阶段无关的文件。
完成后只报告:修改文件、启动命令、测试命令、测试结果和未完成内容。
本阶段的验收重点:
浏览器访问 http://127.0.0.1:8010/health,确认返回 JSON,而不是只确认进程没有退出。
5.2 阶段 2:文档加载、切分和入库¶
请只实现离线入库流程。
输入:scenarios/enterprise_knowledge/faq.csv 和 data/ 下的业务文档。
要求:
- FAQ 转换为包含 question、answer、source 的 Document;
- 支持 Markdown、TXT、PDF、DOCX、PPTX、CSV、XLSX;
- 文档使用 RecursiveCharacterTextSplitter 切分;
- 写入 FAQ Collection 和 Document Collection;
- Milvus 记录必须包含 text、dense、sparse;
- Dense 使用本地 BGE-M3;
- Sparse 使用 Milvus 2.5+ BM25BuiltInFunction;
- Dense 索引使用 HNSW,Sparse 使用 AUTOINDEX + BM25;
- 写入 MySQL 的知识库版本记录,并将版本设为 active;
- 添加不依赖真实服务的入库元数据和索引参数测试。
先阅读现有代码并列出会修改的文件,再执行修改。
本阶段要理解的顺序是:
5.3 阶段 3:Dense + BM25 混合检索¶
请在现有入库代码基础上,只实现在线混合检索。
要求:
- 接收一个 query 和 top_k;
- 同时执行 Dense 检索和 BM25 检索;
- 使用 Weighted Hybrid Search 合并结果;
- 结果必须保留原文和 source;
- 对重复 chunk 去重;
- 返回文档、分数和检索耗时;
- 不加入 Reranker,不加入 LLM,不修改 API 页面。
请说明:
1. Dense 和 Sparse 两路分别解决什么问题;
2. Weighted 融合发生在哪一步;
3. 为什么不能直接比较两路未经处理的原始分数;
4. 如何测试检索函数。
可以用下面的问题观察检索结果:
除了看最终答案,还要展开返回的 sources,确认来源内容确实与问题相关。
5.4 阶段 4:加入 BGE Reranker¶
请在混合检索之后加入 BGE Reranker,不要改动 Milvus 的索引和召回逻辑。
执行顺序必须是:
混合检索召回候选结果
→ 构造 (query, document) 对
→ CrossEncoder 批量打分
→ 按 Reranker 分数降序排列
→ 取最终 top_n
要求:
- 使用本地 BGE Reranker 模型;
- 模型只在在线检索阶段加载,不参与入库;
- 保留原文、source 和 Reranker 分数;
- 候选为空时返回空结果,不调用模型;
- 添加一个纯函数测试,验证排序和 top_n 截断;
- 记录 Reranker 阶段耗时。
完成后解释:
- Embedding 和 Reranker 的输入有什么区别;
- 为什么 Reranker 不能替代 Milvus 召回;
- 为什么候选数量不能无限增大。
5.5 阶段 5:连接 LLM 和问答接口¶
请把已经完成的检索和 Reranker 接到问答接口。
要求:
- POST /ask 接收 query、history 和 source_filter;
- 先执行混合检索,再执行 Reranker;
- 只把最终排序后的上下文交给 LLM;
- Prompt 要求答案只能依据上下文;
- 没有相关资料时明确说明资料不足;
- 返回 answer、sources、route、retrieval、timings;
- API Key 只从环境变量读取;
- 不缓存最终 LLM 答案。
本阶段的链路必须保持为:
POST /ask
→ pipeline.ask()
→ retrieval.search_many()
→ rerank()
→ select_context_docs()
→ generate_answer()
→ 返回 answer + sources
5.6 阶段 6:Docker 和说明文件¶
请只补齐 Docker Compose、Dockerfile、README 和运行测试。
要求:
- Docker Compose 启动 MySQL、etcd、MinIO、Milvus 和 API;
- API 通过服务名访问 Milvus 和 MySQL;
- 本地模型通过 volume 挂载到 /app/models;
- 不把 API Key 写进 Dockerfile 或 compose 文件;
- README 必须包含启动、入库、提问、测试和常见报错处理;
- 执行 docker compose config 验证配置;
- 不新增 Redis、Agent 或管理后台。
六、实训三:运行完整项目¶
6.1 准备模型和环境变量¶
当前仓库的参考代码位于:
需要准备以下本地模型目录:
复制配置文件:
确认 .env 至少包含:
DASHSCOPE_API_KEY=你的Key
EMBEDDING_MODEL_PATH=../models/bge-m3
RERANKER_MODEL_PATH=../models/bge-reranker-large
MILVUS_URI=http://127.0.0.1:19540
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3307
6.2 启动基础设施¶
确认 MySQL、etcd、MinIO 和 Milvus 已经运行后,再构建和启动 API:
6.3 初始化知识库¶
docker compose exec api python scripts/rebuild.py --reset-collections --description "vibe coding hybrid rag"
这一步会:
读取 FAQ 和业务文档
→ 文档切分
→ 创建或重建 Milvus Collection
→ 生成 Dense 向量
→ 由 Milvus 生成 Sparse BM25 数据
→ 写入 FAQ 和文档集合
→ 创建并激活 MySQL 知识库版本
6.4 发起问题¶
浏览器打开:
也可以使用命令行:
docker compose exec api python scripts/ask.py "新人入职需要完成哪些流程?"
docker compose exec api python scripts/ask.py "VPN 连不上怎么处理?" --source-filter it
检查返回结果时,按下面顺序看:
answer是否回答了当前问题;sources是否包含相关文档;retrieval.rerank是否为开启状态;retrieval.faq_hits和retrieval.doc_hits是否有命中;retrieval.timings中哪一个阶段耗时最高;retrieval.faq_rerank_ms/retrieval.doc_rerank_ms是否能反映 Reranker 精排耗时。
七、通过实验理解 Reranker¶
7.1 不要把三种分数混在一起¶
| 分数 | 来源 | 用途 |
|---|---|---|
| Dense 距离或相似度 | BGE-M3 + Milvus | 语义召回 |
| BM25 分数 | Milvus 服务端 | 关键词召回 |
| Reranker 分数 | CrossEncoder | 候选结果精排 |
这些分数的计算方式不同,不能简单地说“谁的分数最高谁就一定更相关”。混合检索阶段由 ranker 负责融合,Reranker 阶段再使用 CrossEncoder 对 query 和 document 联合判断。
7.2 用一句话理解 Reranker¶
可以把整个过程想成两次筛选:
Milvus 负责速度和召回范围,Reranker 负责候选集内的相关性排序。Reranker 不直接扫描整个知识库,所以不能替代向量数据库。
7.3 观察参数的作用¶
当前参考代码中的默认值是:
它们的含义是:
- 每路检索先召回一批候选;
- Reranker 对候选结果重新排序并保留前 8 条;
- 最后送给 LLM 的上下文最多保留 5 条。
这些是体验项目的默认实验参数,不代表所有业务都必须使用这些数值。参数调优要结合评测集和实际耗时,在后续质量评测章节再系统处理。
八、使用 Codex 修复报错¶
8.1 报错反馈的正确格式¶
遇到问题时,不要只把最后一行错误复制给 Codex。使用下面的结构:
当前目录:D:\workspace\knowforge-rag-platform\mini-rag
执行命令:
docker compose exec api python scripts/rebuild.py --reset-collections
期望结果:
FAQ 和业务文档写入 Milvus,并激活一个知识库版本。
完整报错:
粘贴从 Traceback 开始到最后一行的完整内容
请先判断错误发生在:配置、依赖、Milvus 连接、模型加载还是业务代码。
请先阅读相关文件,再给出根因和最小修改方案。
不要重写整个项目,也不要修改与这个错误无关的文件。
8.2 常见问题的定位顺序¶
| 现象 | 先检查什么 |
|---|---|
ModuleNotFoundError |
容器内依赖和 requirements.txt |
| 模型目录不存在 | volume 挂载和模型路径 |
| Milvus 连接失败 | docker compose ps、URI 和服务健康状态 |
| Collection 结构不匹配 | 索引参数和是否需要重建集合 |
| LLM 调用失败 | API Key、Base URL、网络和模型名 |
| 答案没有依据 | sources、上下文选择和 Prompt |
| 结果很慢 | retrieval.timings 中的检索和 Reranker 耗时 |
8.3 修复完成后的验收¶
“程序不再报错”只是第一步,还要确认业务结果正确。
九、只追一条代码线¶
不要第一次阅读就打开整个项目。先沿着下面的调用关系阅读:
9.1 入库链路¶
mini-rag/scripts/rebuild.py
→ mini-rag/qa_core/ingest.py
→ mini-rag/qa_core/loaders.py
→ mini-rag/qa_core/retrieval.py
→ MiniMilvusStore.add_documents()
→ Milvus
重点看:
load_faq_documents()如何把 FAQ 转为 Document;load_document_chunks()如何加载和切分文件;hybrid_index_params()如何声明 HNSW 和 BM25;MiniMilvusStore.store如何配置dense和sparse字段;add_documents()如何把文档写入 Milvus。
9.2 在线问答链路¶
mini-rag/app.py
→ qa_core/pipeline.py: ask()
→ qa_core/query.py: prepare_query()
→ qa_core/retrieval.py: search_many()
→ qa_core/retrieval.py: rerank()
→ qa_core/pipeline.py: select_context_docs()
→ qa_core/prompts.py: generate_answer()
→ 返回 AnswerResult
在线链路中先记住三件事:
- 混合检索负责召回候选;
- Reranker 负责候选集内的精排;
- LLM 只接收最终选出的上下文。
十、本节验收清单¶
10.1 运行验收¶
cd D:\workspace\knowforge-rag-platform\mini-rag
python -m pytest tests -q
docker compose config
docker compose ps
10.2 功能验收¶
- 能成功创建 FAQ 和文档 Collection;
- Dense 字段和 Sparse 字段同时存在;
- Dense 索引为 HNSW;
- Sparse 检索使用 BM25;
- 混合检索可以返回候选文档;
- Reranker 会重新排序候选结果;
- LLM 能根据检索上下文生成答案;
- 答案返回来源引用;
- 没有相关资料时不会编造确定答案。
10.3 Vibe Coding 验收¶
你还要能够回答:
- 本轮 Codex 修改了哪些文件;
- 为什么要先做混合召回,再做 Reranker;
- 为什么 Reranker 不放在入库阶段;
- 一次报错发生后,你给了 Codex 哪些上下文;
- 你用什么命令和结果确认修改有效;
- 如果答案不准确,应该先看
sources还是先改 Prompt。
十一、和后续章节的衔接¶
本节把后续项目的核心检索链路提前跑通,但有意把企业级复杂度留到后面:
| 本节 | 后续章节 |
|---|---|
单一 enterprise_knowledge 场景 |
第 05 章开始学习多场景意图分类和路由 |
直接调用 pipeline.ask() |
第 09、10 章学习 QAService 和完整 Pipeline |
| Dense + BM25 + Reranker | 第 06、07、08 章逐项拆解检索计划、改写、变体和混合检索 |
| 简单 active 版本 | 第 14、16 章学习版本管理和入库流程 |
| 基础测试 | 第 17、18 章学习评测、回归和接口验收 |
| Docker Compose 能运行 | 第 20 章学习离线交付、镜像和排障 |
本节的作用是把前面学过的 Milvus 操作变成一条完整的应用链路,也让你提前熟悉后续阅读企业级代码时最重要的方式:先找入口,再跟调用链,最后用运行结果验证理解。