RAG 项目实战:MCP 协议 + GraphRAG + Cross-Encoder 精排从零搭建(附完整代码)
前言
大模型落地难,难在三个问题:知识滞后、幻觉严重、多跳推理弱。RAG(检索增强生成)是目前最主流的解法,但市面上多数 RAG 教程停在"向量检索 + LLM 生成"的玩具级别。
本篇记录我从零搭建一个工业级 RAG 智能体的完整过程,覆盖 2024-2025 主流技术栈,包含完整代码和我踩过的所有坑。适合做 RAG / Agent 方向项目、准备大模型岗位面试的同学。
你能学到:
-
混合检索(向量 + BM25 + RRF + HyDE + Cross-encoder 精排)
-
GraphRAG 知识图谱(三元组抽取 → 实体消歧 → text2cypher)
-
多 Agent 编排(路由 → 检索 → 推理 → 反思)
-
MCP 协议工具层
-
ragas 标准评估(LLM-as-judge)
一、系统架构
整个系统分 5 层,21 个核心脚本:

| 层 | 组件 | 说明 |
|---|---|---|
| 用户层 | Gradio / FastAPI / 图谱可视化 | 交互界面 |
| MCP 协议层 | FastMCP 工具服务器 | 标准化工具调用 |
| Agent 编排层 | 路由 → 检索 → 推理 → 反思 | Corrective RAG 范式 |
| 检索层 | 向量 + 图谱 + 层次摘要 + Reranker | 多路召回 + 两阶段精排 |
| 数据层 | ChromaDB / NetworkX / bge-m3 | 存储与模型 |
二、技术栈选型
| 组件 | 选型 | 为什么 |
|---|---|---|
| LLM | MiniMax-M3 | OpenAI 兼容,中文强 |
| Embedding | bge-m3(1024 维) | MTEB 排行榜前列,中英多语言 |
| Reranker | bge-reranker-v2-m3 | 2024 SOTA cross-encoder |
| 向量库 | ChromaDB | 轻量零运维 |
| 图谱 | NetworkX(可切 Neo4j) | 内存图降级 |
| 评估 | ragas 0.2.15 | GitHub RAG 评测标准 |
三、混合检索:从 70% 到 91%
这是技术含量最高的模块。逐步叠加策略,每加一层测一次准确率:

3.1 第一步:纯向量检索(Hit@5 = 70%)
# 04_embedder.py 核心
model = SentenceTransformer('./models/bge-m3')
# 文档入库不加前缀,查询加前缀(bge 官方推荐)
coll = chromadb.PersistentClient(path=DB_PATH).get_or_create_collection(
name="rag", metadata={"hnsw:space": "cosine"})
3.2 第二步:加 BM25 + RRF 融合(→ 82%)
向量检索对专有名词和数字不敏感。加 BM25 补精确匹配:
# RRF 融合:只看排名,不看绝对分数(解决量纲不一致)
def rrf_fuse(ranked_lists, k=60):
scores = {}
for lst in ranked_lists:
for rank, doc in enumerate(lst):
scores[doc] = scores.get(doc, 0) + 1 / (k + rank)
return sorted(scores, key=lambda d: -scores[d])
3.3 第三步:加 HyDE(→ 88%)
让 LLM 先生成"假设答案",用假设答案检索,拉平 query 和 chunk 的长度差距:
# HyDE:假设答案比短 query 更接近文档形态
hypothetical = llm.generate(f"假设答案:{query}")
results = vector_search(hypothetical)
3.4 第四步:Cross-encoder 精排(→ 91%)
# _reranker.py:bge-reranker-v2-m3 pairs = [[query, c["text"][:1000]] for c in candidates] inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors="pt") logits = model(**inputs).logits.squeeze(-1) # 用原始 logit 排序(sigmoid 后值域太窄,区分度差)
两阶段架构:双塔粗召回 top-50 → cross-encoder 精排 top-5。这是搜索引擎标配。
四、GraphRAG:多跳推理
4.1 三元组抽取(08_kg_extractor.py)
# LLM 抽三元组,三道防幻觉 # 1. 实体必须出现在原文 # 2. 关系长度 ≤ 15 字 # 3. 两次抽取取并集 + 频次过滤
4.2 实体消歧(09_kg_builder.py)
三层瀑布过滤,从便宜到贵:
# 层1:子串包含(免 API) # 层2:相似度 ≥ 0.88(免 API,difflib.SequenceMatcher) # 层3:0.75~0.88 灰度区调 LLM 仲裁(限量 20 对)
4.3 text2cypher(10_kg_query.py)
# LLM 把自然语言翻译成 Cypher
# MATCH (e:Entity {name:'MiniMax-M1'})-[:RELATES_TO]->(t) RETURN t
踩坑:text2cypher 的 RETURN 列每次都不同(F1 在 60%~100% 随机跳)。解法是 standardize_projection()——强制重写成固定 subject/predicate/object 三列投影。LLM 只定位子图,选列由服务端固定。
五、踩坑实录(重点)
坑 1:MiniMax-M3 的 <think> 块(系统性陷阱)
这个坑我踩了两次:
第一次(推理模块):用错误正则剥离 think 块,没剥掉,吃光 max_tokens,答案截断。
第二次(ragas 评测):Faithfulness 假低到 38%。think 块占满输出,ragas 的 JSON 被截断,解析失败记 0 分。
解法:
-
推理模块:用
re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)正确剥离 -
评测模块:
extra_body={"thinking":{"type":"disabled"}}关掉 thinking
修复后 Faithfulness:38% → 95.21%。
坑 2:双栏 PDF 文字错乱
pdfplumber 处理不了多栏(左右栏文字混在一起)。自写分栏检测 3 版全失败(标题"Scalable RL"被切成"Scal")。
错乱现象:解析结果左栏/右栏按行交错拼接,标题 "Scalable RL" 被切成 "Scal",整段读不通。
解法:换 PyMuPDF(fitz)的 get_text('text'),按视觉块排序天然支持多栏。10 分钟解决。
修复效果:换库后先读完整个左栏再读右栏,标题完整、段落连贯。
教训:别在一个库里死磕。先调研社区方案。
坑 3:反思重检索反而退步
设计了"反思 Agent",置信度低就重检索。实测准确率反而下降——重检索 query 从"无依据断言"抽,本身就偏。
解法:砍掉重检索,反思只打分、不干预。少做反而更好。
坑 4:ragas 和项目 numpy 版本冲突
ragas 依赖 numpy 1.26,项目用 numpy 2.x。
解法:把 ragas 装到独立 venv,数据中转 JSON,两个环境不混。
六、评估结果
6.1 ragas 标准指标(LLM-as-judge)

| 指标 | 总体 | bge_paper | MiniMax_M1 |
|---|---|---|---|
| Context Recall | 97.92% | 100.00% | 95.83% |
| Faithfulness | 95.21% | 96.93% | 93.49% |
6.2 4 篇论文端到端 F1(泛化验证)

| 论文 | 领域 | F1 |
|---|---|---|
| bge_paper | Embedding | 91~100% |
| MiniMax_M1 | LLM | 83.33% |
| Attention Is All You Need | NLP 经典 | 100% |
| ResNet | CV 经典 | 93.75% |
七、源码与总结
核心技术亮点:
-
混合检索 + Cross-encoder 两阶段(搜索引擎标准架构)
-
GraphRAG + 向量 RAG 融合(多跳推理 + 语义检索互补)
-
多 Agent 编排(路由 → 检索 → 推理 → 反思)
-
ragas 原版评估(LLM-as-judge 科学评测)
-
MCP 协议工具层(标准化工具调用)
完整代码开源在 Apageoflove/mcp-rag-agent,含 21 个核心脚本 + ragas 评测数据 + 中英双版 README。如果觉得有帮助,点个赞 + 收藏,后续会持续分享 RAG / Agent / 大模型落地的实战经验。
更多推荐


所有评论(0)