我搭了 21 个脚本才搞明白:GraphRAG + MCP 比 LangChain 套壳强在哪

0 阅读7分钟

做 RAG 项目之前,我也看过不少"LangChain + Chroma + Gradio"的快速教程。三天能跑通一个 demo,看起来很美好。

但真要做工业级、要能讲清楚每一层为什么这么设计、要能在面试和生产里扛住追问——LangChain 的抽象反而是阻碍。

我这阵子做了一个完整的多模态 RAG 智能体,没有用 LangChain,而是用 MCP 协议从零搭了工具层。21 个脚本,从 PDF 解析到多 Agent 编排,每一层都自己写。

这篇不讲大道理,讲讲我在这 21 个脚本里搞明白的几件事,以及踩过的那些让我头秃的坑。


一、架构总览

5 层系统架构

5 层架构,每层职责清晰:

用户层(Gradio / FastAPI / 可视化)
    ↓
MCP 协议层(FastMCP 注册 4 工具)
    ↓
Agent 编排层(路由 → 检索 → 推理 → 反思)
    ↓
检索层(向量 + 图谱 + 层次摘要 + Reranker)
    ↓
数据层(ChromaDB / NetworkX / bge-m3 / bge-reranker)

为什么分层这么细? 因为每一层都可以独立测试、独立替换。比如图谱层我做了 Neo4j 和内存图双后端,Neo4j 连不上时自动降级到 NetworkX,本地也能跑通全链路。


二、检索层:两阶段架构是搜索引擎的标配

这是整个项目最核心的一块。Hit@5 从 70% 做到 91%:

01_retrieval_accuracy.png

纯向量           → 70%
+ BM25 + RRF     → 82%
+ HyDE           → 88%
+ Cross-encoder  → 91%

我想重点讲讲 Cross-encoder 精排这块,因为这是拉开和"玩具 RAG"差距的关键。

双塔 vs Cross-encoder

双塔(bi-encoder) :query 和 doc 分别编码成向量,算 cosine。

  • 优点:向量可以离线预计算,检索快
  • 缺点:两个向量独立编码,没有任何交互,精度有天花板

Cross-encoder:query 和 doc 拼一起喂入 transformer,做交叉注意力,输出一个分数。

  • 优点:精度高一个量级(交叉注意力捕获细粒度匹配)
  • 缺点:不能预计算,每次都要过模型,慢

所以工业界的标准做法是两阶段

双塔粗召回 top-50(快,预计算) → Cross-encoder 精排 top-5(精,实时打分)

Google、百度的搜索引擎都是这套架构。我用的是 bge-reranker-v2-m3(2024 MTEB reranker 排行榜前列)。

# _reranker.py
pairs = [[query, c["text"][:1000]] for c in candidates]
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors="pt")
with torch.no_grad():
    logits = model(**inputs).logits.squeeze(-1)
# 用原始 logit 排序,不用 sigmoid(sigmoid 后值域太窄,区分度差)

这里有个细节:用原始 logit 排序,不用 sigmoid。因为 sigmoid 后所有分数挤在 0.99 附近,排序区分度很差。logit 保留了排序信息。


三、GraphRAG:让 RAG 会多跳推理

向量检索只能找"相似片段",但跨文档的多跳推理它做不到。"A 用了什么技术,这技术还用在哪些地方"——这种答案分散在多个段落,靠相似度根本串不起来。

GraphRAG 的思路是把实体关系抽成图,用图遍历做多跳:

论文文本 → LLM 抽三元组 → 实体消歧 → 建图 → text2cypher 查询

实体消歧:三层瀑布

"MiniMax-M1"、"M1"、"the model" 可能都是同一个实体。不消歧图就碎了。

# 09_kg_builder.py
# 层1:子串包含(免 API)—— M1 ⊂ MiniMax-M1
# 层2:相似度 ≥ 0.88(免 API,difflib)
# 层3:0.75~0.88 灰度区调 LLM 仲裁(限量 20 对)

前两层免 API 解决大多数,LLM 只处理规则搞不定的少数模糊对。这是实体链接(Entity Linking)的经典范式——先用零成本规则解决大多数,LLM 只用于规则搞不定的少数,既准又省钱。

text2cypher 的 RETURN 抖动

这里踩了个特别折磨人的坑。text2cypher 让 LLM 把自然语言翻译成 Cypher,但 F1 在 60%~100% 之间随机跳。同一道题、同一个库,时对时错。temperature=0 都压不住。

查了很久才发现:不是采样随机,是 LLM 选列不稳定。LLM 每次生成的 RETURN a, b, c 列顺序都不一样,下游解析没法稳定消费。

解法是 standardize_projection()

# 不管 LLM 返回什么列,强制重写成固定三列
# LLM 只负责"定位子图",选列由服务端固定死
def standardize_projection(cypher, where_structure):
    # 从 MATCH 解析主语/关系/宾语变量
    # 从 WHERE 推断锚点(已知量)
    # 按锚点数投影成 subject/predicate/object
    ...

改完之后连续 6 次全 8/8 = 100%,抖动消失。

这个解法对齐了 text2cypher 业界共识:让 LLM 生成完整可执行 Cypher 本就脆弱,稳健做法是约束 LLM 只做意图定位,把投影交给固定模板(LangChain 的 GraphCypherQAChain、Neo4j 官方实践都强调这点)。


四、最戏剧性的坑:Faithfulness 38%

这个坑必须单独讲,因为它差点让我以为整个项目废了。

我用 ragas 0.2.15 跑标准评测,Faithfulness 只有 38% 。我以为我的答案有 62% 是幻觉。

查了半天,发现是 MiniMax-M3 的 <think> 推理块吃光了 max_tokens。ragas 让 M3 做 LLM-judge,M3 默认输出 <think>...</think> 推理块,把 token 预算全占了,要的 JSON 被截断,解析失败直接记 0 分。

不是答案有问题,是评测工具被 think 块搞炸了。

# 17b_ragas_official.py
class _MiniMaxClean(ChatOpenAI):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.extra_body = {"thinking": {"type": "disabled"}}  # 关掉 thinking

Faithfulness:38% → 95.21%

这个坑其实我踩了两次——推理模块也踩过一次(用错了正则,think 块没剥掉,答案被截断)。同一个坑踩两次,说明 MiniMax-M3 的 thinking 输出是个系统性陷阱,每个调它的模块都要处理。

02_ragas_metrics.png


五、反直觉:反思重检索,越反思越差

这是整个项目里我最想分享的一个领悟。

我设计了"反思 Agent"——答案生成后验证忠实度,置信度低就重新检索。听起来很合理,对吧?这是 Corrective RAG 论文里的标准范式。

实测发现:重检索之后,准确率反而下降了。

为什么?因为重检索的 query 是从"答案里没证据的部分"抽出来的,本身就偏,召回质量还不如第一次正常 query。等于越改越错。

最后我砍掉了重检索,反思只打分、不干预。完备性靠推理阶段"逐字引用 + Supporting evidence"兜底。

# 16_agent_orchestrator.py
# v1: 反思置信度 < 0.7 触发 _retry_queries()(重检索)
# v2: 反思只算置信度,不触发重检索
# 教训:少做反而更好。重检索的 query 本身就偏,越改越错

有时候少做反而更好。 "看起来高级"的设计,实测数据说话。这是这个项目里最反直觉、但最重要的一个领悟。


六、最终成果

4 篇论文端到端 F1(泛化验证)

03_paper_f1.png

论文领域F1
bge_paperEmbedding91~100%
MiniMax_M1LLM83.33%
Attention Is All You NeedNLP 经典100%
ResNetCV 经典93.75%

ragas 标准指标

指标
Context Recall97.92%
Faithfulness95.21%

说句实话:纯靠 LLM 生成(不做"贴原文片段"的兜底),F1 在 83~100% 是真实水平。少数失败是 LLM 释义(534,700534,700 → 0.53M)或漏术语。这是 LLM 生成的固有局限,不是 bug。


写在最后

这 21 个脚本里,我搞明白的最重要的事,其实不是某个具体技术,而是几个工程方法论:

  1. 调研优先,别造轮子:自写分栏检测 3 版全失败,换库 10 分钟解决。
  2. 指标异常低,先怀疑评测链路:Faithfulness 38% 不是答案的锅,是 think 块搞炸了评测。
  3. 少做有时候反而更好:反思重检索听起来高级,实测反而退步。
  4. 同一个坑踩两次,说明是系统性问题:think 块在推理和评测各踩一次。

RAG 这事看起来简单("不就是检索+生成吗"),做深了涉及检索算法、知识图谱、Agent 架构、评估方法——每一个都是独立领域。

完整代码开源在 Apageoflove/mcp-rag-agent,21 个核心脚本 + 评估数据 + 中英双版 README,欢迎来 issue 交流。

如果觉得这篇对你有启发,点个赞,后续会继续分享 RAG / Agent / 大模型落地的工程实战。