Skip to content

eastboat2001/MCP_RAG

Repository files navigation

Modular RAG MCP Server

一个围绕“可讲清楚、可跑起来、可扩展”设计的模块化 RAG 项目。当前仓库的核心是 4 条能力链路:

  • 文档摄取:PDF -> Chunk -> Transform -> Embedding -> Chroma + BM25
  • 查询检索:Dense -> Sparse(BM25) -> RRF Fusion -> Optional Rerank
  • MCP 暴露:把检索能力包装成标准 MCP Server,供支持 MCP 的 AI 客户端调用
  • 可观测性:通过 Streamlit Dashboard 查看配置、数据、摄取轨迹、查询轨迹和评估结果

这份 README 不再沿用原来的教程式介绍,而是严格按当前代码的真实结构和真实运行方式说明,方便你自己跑项目,也方便面试时把项目讲清楚。

1. 项目定位

这个项目本质上不是“一个问答脚本”,而是一套模块化知识检索系统,包含:

  • src/ingestion/:负责离线数据摄取
  • src/core/query_engine/:负责在线检索与排序
  • src/mcp_server/:负责把检索能力暴露为 MCP 工具
  • src/observability/dashboard/:负责可视化管理与追踪
  • src/libs/:负责底层可插拔 Provider 实现

面试里可以直接这样概括:

我做的是一套模块化 RAG 系统,离线侧负责 PDF 文档解析、分块、元数据增强、向量化和双索引落库;在线侧负责 Dense + BM25 混合召回、RRF 融合和可选重排;然后通过 MCP Server 对外暴露标准工具接口,同时用 Streamlit Dashboard 做链路观测和数据管理。

2. 当前代码的真实能力边界

先说“现在代码真的支持什么”,这比泛泛而谈更重要。

  • 稳定的摄取入口是 PDF。scripts/ingest.py 当前只递归处理 .pdf 文件。
  • Dashboard 上传框允许 pdf/txt/md/docx,但当前 IngestionPipeline 实际绑定的是 PdfLoader,所以稳定可用的仍然是 PDF。
  • 检索链路已经完整:Dense 检索、Sparse(BM25) 检索、RRF 融合、可选 Rerank 都在代码里。
  • 默认配置下 rerankvision_llmevaluation、摄取阶段 LLM 增强都是关闭的,目的是先把主链路稳定跑通。
  • main.py 不是正式入口,实际入口是 scripts/*.pypython -m src.mcp_server.server

这几个边界在面试里反而是加分项,因为你能清楚说明“代码里已有扩展位”和“当前默认启用能力”的区别。

3. 目录结构

建议重点讲下面这棵目录树,而不是把整个仓库所有文件背下来:

MODULAR-RAG-MCP-SERVER/
├─ config/
│  ├─ settings.yaml                 # 主配置文件
│  └─ prompts/                      # 摄取增强与图片描述提示词
├─ scripts/
│  ├─ ingest.py                     # 文档摄取 CLI
│  ├─ query.py                      # 检索查询 CLI
│  ├─ evaluate.py                   # Golden Set 评估 CLI
│  └─ start_dashboard.py            # Streamlit 启动脚本
├─ src/
│  ├─ core/
│  │  ├─ settings.py                # 配置加载与校验
│  │  ├─ types.py                   # Document / Chunk / RetrievalResult 等核心类型
│  │  ├─ query_engine/              # 在线检索主链路
│  │  ├─ response/                  # 引用与响应封装
│  │  └─ trace/                     # Trace 记录结构
│  ├─ ingestion/
│  │  ├─ pipeline.py                # 摄取总编排
│  │  ├─ chunking/                  # 文档分块
│  │  ├─ transform/                 # chunk 精修 / metadata 增强 / 图片描述
│  │  ├─ embedding/                 # dense + sparse 编码
│  │  └─ storage/                   # Chroma / BM25 / image index 落库
│  ├─ libs/
│  │  ├─ llm/                       # OpenAI / Azure / Ollama / DeepSeek LLM
│  │  ├─ embedding/                 # OpenAI / Azure / Ollama Embedding
│  │  ├─ reranker/                  # none / cross_encoder / llm reranker
│  │  ├─ loader/                    # 当前默认 PdfLoader
│  │  ├─ splitter/                  # recursive / semantic / fixed_length 抽象
│  │  └─ vector_store/              # 当前默认 Chroma
│  ├─ mcp_server/
│  │  ├─ server.py                  # MCP stdio 入口
│  │  ├─ protocol_handler.py        # 工具注册与协议处理
│  │  └─ tools/                     # query_knowledge_hub / list_collections / get_document_summary
│  └─ observability/
│     ├─ dashboard/                 # Streamlit 多页面面板
│     └─ evaluation/                # 评估运行器与 evaluator
├─ tests/                           # unit / integration / e2e
├─ pyproject.toml                   # 依赖与 uv 脚本入口
└─ README.md

目录职责怎么讲

  • core 负责“协议和编排层”的公共能力,不关心具体厂商。
  • libs 负责“底层后端实现”,比如 OpenAI、Ollama、Chroma、CrossEncoder。
  • ingestion 负责离线建库。
  • query_engine 负责在线检索。
  • mcp_server 负责把在线检索封装成标准工具接口。
  • observability 负责让系统可视化和可追踪。

这种分层在面试里很好讲,因为它对应的是“业务编排层”和“可替换基础设施层”的分离。

4. 系统架构总览

4.1 离线摄取链路

PDF
 -> PdfLoader
 -> DocumentChunker
 -> ChunkRefiner
 -> MetadataEnricher
 -> ImageCaptioner
 -> DenseEncoder
 -> SparseEncoder
 -> VectorUpserter(Chroma)
 -> BM25Indexer
 -> ImageStorage

4.2 在线查询链路

User Query
 -> QueryProcessor
 -> DenseRetriever
 -> SparseRetriever
 -> RRFFusion
 -> Optional Reranker
 -> ResponseBuilder

4.3 服务暴露链路

MCP Client / CLI / Dashboard
 -> QueryKnowledgeHubTool / scripts/query.py / Dashboard page
 -> HybridSearch
 -> Vector Store + BM25 Index

5. 详细运行流程

5.1 环境准备

项目已经适配 uv,推荐统一用 uv 管理环境。

uv venv
# Windows PowerShell
.\.venv\Scripts\activate
uv sync

说明:

  • uv sync 会按 pyproject.toml 安装主依赖。
  • 当前依赖里已经包含 chromadbstreamlitmcpmarkitdown[pdf]pymupdfopenaihttpx 等运行所需包。
  • 如果你要本地向量化,需要确保本机 Ollama 服务已启动,并且已经拉好 embedding 模型。

5.2 配置模型

所有运行都依赖 settings.yaml

当前推荐的起步思路是:

  • 文本生成 llm:走阿里百炼的 OpenAI 兼容接口
  • 向量化 embedding:走本地 Ollama
  • 视觉模型 vision_llm:先关闭
  • rerank:先关闭
  • 摄取阶段 chunk_refiner.use_llmmetadata_enricher.use_llm:先关闭

这样做的原因很简单:

  • 先把主链路稳定跑通,比一上来堆功能更重要。
  • 检索质量的基础首先来自文档质量、chunk 质量、embedding 质量和双路召回,不是先靠重排或多模态。
  • 本地 8G 机器更适合承担 embedding,不适合再叠图像模型和大参数生成模型。
  • 生成模型走百炼,兼顾效果与资源;embedding 走本地,兼顾成本与控制力。

5.3 第一步:摄取文档

命令:

uv run python scripts/ingest.py --path tests/fixtures/sample_documents --collection demo

这里会发生 6 个阶段:

  1. 完整性检查
    计算文件 SHA256,查看是否已经处理过。未开启 --force 时,相同文件会跳过,避免重复建库。

  2. 文档加载
    PdfLoaderMarkItDown 提取文本,并在可用时用 PyMuPDF 提取图片,插入 [IMAGE: xxx] 占位符。

  3. 文档分块
    DocumentChunker 调用底层 SplitterFactory,按 chunk_sizechunk_overlap 生成 chunk,并补齐 chunk_idchunk_indexsource_ref 等元数据。

  4. Transform 阶段

    • ChunkRefiner:规则清洗,可选 LLM 精修
    • MetadataEnricher:抽标题、摘要、标签,可选 LLM 增强
    • ImageCaptioner:仅在 vision_llm.enabled=true 时对引用图片做描述并拼回文本
  5. 编码阶段

    • DenseEncoder:把 chunk 编成稠密向量,供语义检索使用
    • SparseEncoder:把 chunk 编成稀疏表示,本质上是统计分词后的词项频次和文档长度,供 BM25 检索使用
  6. 存储阶段

    • VectorUpserter 把 dense 向量写入 Chroma
    • BM25Indexer 把 sparse 信息写入 data/db/bm25/<collection>
    • ImageStorage 记录图片索引

摄取结果会落到哪里

  • Chroma:data/db/chroma
  • BM25 索引:data/db/bm25/<collection>
  • 图片:data/images/<collection>data/images/<doc_hash>
  • 摄取历史:data/db/ingestion_history.db
  • Trace:logs/traces.jsonl

5.4 第二步:执行查询

命令:

uv run python scripts/query.py --query "这个项目的核心能力是什么" --collection demo --verbose

查询时会经过下面几个阶段:

  1. QueryProcessor
    对问题做标准化、分词、停用词过滤,并支持 collection:xxx 这类轻量过滤语法。

  2. DenseRetriever
    把 query 做 embedding,在 Chroma 中做向量召回。

  3. SparseRetriever
    对 query 分词后,在 BM25 索引中做关键词召回。

  4. RRFFusion
    用 Reciprocal Rank Fusion 融合 dense 和 sparse 的排序结果。

  5. CoreReranker
    如果 rerank.enabled=true,再对融合结果精排;否则直接返回融合结果。

  6. 输出结果
    CLI 会打印 chunk 的分数、来源路径、页码、chunk 编号和文本片段;--verbose 会额外打印 dense、sparse、fusion、rerank 的中间结果。

为什么要做 Hybrid Search

  • Dense 检索擅长语义匹配,但对专有名词、缩写、错拼不稳定。
  • Sparse(BM25) 擅长关键词和术语精确匹配,但语义泛化弱。
  • RRF 融合的好处是不需要强依赖两边分数尺度统一,工程上更稳,解释成本也低。

面试里可以直接说:

我没有只做向量检索,而是做了 Dense + BM25 的混合召回。Dense 负责语义覆盖,Sparse 负责专有名词和术语命中,再用 RRF 做无监督融合,这样比单一路径更稳。

5.5 第三步:启动 Dashboard

命令:

uv run python scripts/start_dashboard.py --port 8501

Dashboard 当前有 6 个页面:

  • Overview:看当前组件配置、Chroma collection 统计、trace 数量
  • Data Browser:浏览库里的 chunk 和元数据
  • Ingestion Manager:上传文档并触发摄取,查看和删除文档
  • Ingestion Traces:查看每次摄取的阶段结果
  • Query Traces:查看每次查询的中间阶段
  • Evaluation Panel:查看评估相关信息

这个面板的作用不是“做产品前端”,而是帮助你解释和排查系统行为,属于工程观测能力。

5.6 第四步:启动 MCP Server

命令:

uv run mcp-server

这个服务启动后,会通过 stdio 暴露 3 个工具:

  • query_knowledge_hub
  • list_collections
  • get_document_summary

这部分的核心含义是:

  • RAG 是系统内部能力
  • MCP 是对外接口层

也就是说,这个项目不是“为了 MCP 才有 RAG”,而是“先有 RAG 能力,再通过 MCP 暴露给 Copilot、Claude Desktop、Cursor 等支持 MCP 的客户端”。

5.7 第五步:跑评估

命令:

uv run python scripts/evaluate.py --collection demo

默认会读取 tests/fixtures/golden_test_set.json,然后:

  • 用当前 Hybrid Search 对测试问题做召回
  • 调用配置里的 evaluator 计算指标
  • 输出 aggregate metrics 和 per-query metrics

当前默认配置下 evaluation.enabled=false,因为这一步更适合在主链路稳定后再开启。它的意义不是让项目“更完整”,而是让你能用数据驱动调参,而不是凭感觉调。

6. 配置项说明

本项目所有关键参数都在 settings.yaml

6.1 llm

作用:负责生成和摄取阶段可选的文本 LLM 增强。

关键字段:

  • provideropenai / azure / ollama / deepseek
  • model:具体模型名
  • base_url:OpenAI 兼容接口地址或 Ollama 地址
  • api_key:兼容接口密钥
  • temperature:生成稳定性
  • max_tokens:回答长度上限

为什么默认建议:

  • provider: openai + 百炼兼容接口,是因为代码里已经兼容 OpenAI SDK 调用方式,接入简单。
  • temperature: 0.0 适合知识问答和摄取增强,减少回答抖动,更利于复现和评估。
  • max_tokens: 4096 给问答和可能的元数据增强留足空间,但不会无限放大成本。

6.2 embedding

这一节描述的是“稠密编码”这一路的配置,也就是 Dense Retrieval 使用的 embedding 模型配置。

但如果从完整流程看,文档在上传并分块之后,编码阶段实际上会同时做两件事:

  • 稠密编码:DenseEncoder 调用 embedding 模型,把每个 chunk 转成 dense vector,写入 Chroma,后续 query 也会用同一个 embedding 模型转成 query vector 做语义召回。
  • 稀疏编码:SparseEncoder 对每个 chunk 做分词、词频统计和文档长度统计,再由 BM25Indexer 写入 BM25 索引,后续 query 也会先分词,再走 BM25 关键词召回。

所以更完整的说法不是“上传后只做 embedding”,而是:

文档分块后会同时完成稠密编码和稀疏编码。稠密编码服务于语义检索,稀疏编码服务于 BM25 关键词检索,查询阶段再把两条召回路径融合。

这里之所以单独叫 embedding,只是因为 settings.yaml 里的这个配置块只负责稠密编码模型本身的参数。

作用:控制 chunk 和 query 的稠密向量生成方式。

关键字段:

  • provideropenai / azure / ollama
  • model:embedding 模型名
  • dimensions:目标向量维度
  • base_url:本地 Ollama 或兼容接口地址

当前推荐配置:

  • provider: ollama
  • model: qwen3-embedding:4b
  • dimensions: 1536

为什么这样配:

  • qwen3-embedding:4b 在本地质量和资源之间比较平衡。
  • 1536D 是一个工程折中值,足够表达语义,又比更高原生维度更省存储和检索开销。
  • 项目已经支持把 dimensions 传给 Ollama /api/embed,所以这里不是“写着看”,而是真实生效。

注意:

  • 同一个 collection 内向量维度必须一致。
  • 切换 embedding 模型或维度后,要重新 ingest,不要混用旧索引。

6.3 稀疏编码与 BM25

这一部分没有单独的 bm25: 配置块,但它是当前项目检索链路里的另一半。

在摄取阶段:

  • SparseEncoder 会对每个 chunk 做分词
  • 统计每个 chunk 的词项频次、唯一词数、文档长度
  • 再由 BM25Indexer 写入 data/db/bm25/<collection>

在查询阶段:

  • QueryProcessor 会先对 query 做标准化、分词和停用词过滤
  • SparseRetriever 再基于 BM25 索引做关键词召回

为什么这条链路必须讲清楚:

  • 稠密向量更擅长语义相似
  • BM25 更擅长专有名词、缩写、版本号、术语和关键词精确匹配
  • 两条路径并行召回再做融合,才构成这个项目的 Hybrid Search

所以面试里讲“编码阶段”时,建议直接说完整:

文档分块后我没有只做 embedding,而是同时做了稠密编码和稀疏编码。稠密编码把 chunk 存进 Chroma 做语义召回,稀疏编码把词频统计写进 BM25 索引做关键词召回,查询时两条路径并行检索,再用 RRF 融合。

6.4 vision_llm

作用:给 PDF 里的图片做描述,把视觉信息转成文本,回到文本检索链路。

关键字段:

  • enabled
  • provider
  • model
  • base_url
  • max_image_size

为什么默认关闭:

  • 多模态会显著增加摄取成本和复杂度。
  • 8G 本地机不适合再叠一层图像推理。
  • 先把纯文本链路跑通,再开视觉链路,更容易定位问题。

6.5 vector_store

作用:dense 向量存储。

当前默认:

  • provider: chroma
  • persist_directory: ./data/db/chroma

为什么用 Chroma:

  • 本地可直接持久化,适合教程型和个人项目验证。
  • 对小中型知识库启动成本低。
  • 与当前代码集成已经完整。

6.6 retrieval

关键字段:

  • dense_top_k: 20
  • sparse_top_k: 20
  • fusion_top_k: 10
  • rrf_k: 60

为什么这样配:

  • 先从 dense 和 sparse 各召回更多候选,是为了给融合保留足够搜索空间。
  • fusion_top_k=10 适合大多数问答场景,既能覆盖答案,又不会把太多噪声带入后续阶段。
  • rrf_k=60 是很常见的经验值,排序衰减较平滑,工程上稳定。

这组参数的讲法可以是:

我把召回和最终返回做了分层,先各自多召回,再融合裁剪,这样能兼顾召回率和后续成本。

6.7 rerank

关键字段:

  • enabled
  • provider: none / cross_encoder / llm
  • model
  • top_k

为什么默认关闭:

  • 重排要么增加本地模型负担,要么增加额外 API 成本。
  • 在数据量不大、主链路还没稳定前,先看 Hybrid Search 的基础效果更合理。
  • 关闭重排后,系统更容易解释,也更适合初始调试。

什么时候开:

  • 你发现 Top20 里有答案,但 Top5 排不进去。
  • 你需要进一步优化查准率。

cross_encoderllm 的差异:

  • cross_encoder:更偏检索任务,速度通常比 LLM 重排更可控
  • llm:解释性更强,但成本更高、延迟更高

6.8 ingestion

关键字段:

  • chunk_size: 1000
  • chunk_overlap: 200
  • splitter: recursive
  • batch_size: 100
  • chunk_refiner.use_llm
  • metadata_enricher.use_llm

为什么这样配:

  • chunk_size=1000:避免 chunk 太碎导致上下文断裂,也避免太长导致检索粒度太粗。
  • chunk_overlap=200:让相邻 chunk 保留一定上下文,减少答案刚好落在边界上的问题。
  • recursive:是当前仓库最稳定的 splitter,适合先跑通。
  • batch_size=100:兼顾吞吐与内存占用。
  • chunk_refiner.use_llm=false:避免在初期引入额外不稳定因素。
  • metadata_enricher.use_llm=false:先依赖规则抽取标题/摘要/tag,成本更低、更可复现。

这部分在面试里可以这样讲:

我把 chunk 相关参数定在一个偏稳的区间,优先保证检索稳定性和工程成本,而不是一开始追求复杂策略。后续如果要继续优化,我会基于评估集再调整 chunk size、overlap 和是否启用 LLM 增强。

7. 推荐运行顺序

建议按下面顺序,不要一上来同时开所有功能:

  1. 配好 llmembedding
  2. 保持 vision_llm=false
  3. 保持 rerank=false
  4. 保持摄取阶段 LLM 增强为 false
  5. 先 ingest 样例 PDF
  6. 再 query 验证检索结果
  7. 再开 dashboard 看 traces
  8. 最后再尝试 rerank、evaluation、vision

推荐命令:

uv run python scripts/ingest.py --path tests/fixtures/sample_documents --collection demo
uv run python scripts/query.py --query "这个项目的核心能力是什么" --collection demo --verbose
uv run python scripts/start_dashboard.py
uv run mcp-server

8. 面试讲解模板

如果你要在面试里用 2 到 3 分钟介绍这个项目,可以按下面顺序讲:

8.1 一句话概括

我做的是一个模块化 RAG 系统,离线侧完成文档解析、分块、向量化和双索引建库,在线侧完成混合召回和排序,并通过 MCP 协议把检索能力暴露给外部 AI 客户端。

8.2 为什么这样设计

  • 我把 corelibs 分开,是为了把业务编排和底层 Provider 解耦。
  • 我做 Dense + BM25 混合召回,是为了兼顾语义匹配和术语匹配。
  • 我把 MCP 单独做成接口层,是为了让这套 RAG 可以复用到多个 AI 客户端,而不是绑死在一个前端页面里。
  • 我保留 Dashboard 和 Trace,是为了让整个链路可观察、可调试、可解释。

8.3 你做过哪些工程取舍

  • 默认先关闭 rerank、视觉模型和摄取 LLM 增强,先跑通主链路。
  • embedding 用本地 Ollama,生成用百炼兼容接口,平衡成本、效果和本地资源限制。
  • chunk 参数使用偏保守设置,优先保证稳定性。

8.4 如果继续优化,你会做什么

  • 基于 golden test set 调 chunk 参数和 rerank 策略
  • 扩展非 PDF loader
  • 对比不同 embedding 维度和召回指标
  • 把 MCP 接到真实 Agent 流程里

9. 常见问题

9.1 为什么 vector_store.collection_nameknowledge_hub,但脚本默认 collection 是 default

因为脚本层的 --collection 会显式指定运行时 collection。为了避免混淆,建议你在 ingest 和 query 时始终手动传同一个 --collection

9.2 为什么 Dashboard 可以上传 txt/md/docx,但 README 说当前稳定只支持 PDF

因为上传控件支持的格式比底层 IngestionPipeline 当前实际绑定的 loader 更宽。当前稳定链路仍是 PdfLoader

9.3 什么情况下需要重新 ingest

以下场景都建议重新 ingest:

  • 修改了 embedding 模型
  • 修改了 embedding 维度
  • 修改了 chunk 策略
  • 开启或关闭了摄取阶段增强并希望效果真实反映到索引

9.4 MCP 在这个项目里到底是什么

MCP 不是 RAG 本身,而是 RAG 的对外协议层。这个仓库做的是“把现有检索能力包装成 MCP 工具”,这样 Claude Desktop、Copilot、Cursor 之类的客户端就能直接调用。

10. 当前最推荐的使用方式

如果你的目标是“尽快跑通项目并能讲清楚”:

  • uv 管理环境
  • llm 走百炼兼容接口
  • embeddingqwen3-embedding:4b
  • dimensions1536
  • 先只 ingest PDF
  • 先关闭 rerank、vision、evaluation 和摄取 LLM 增强

这是当前最贴合这套代码、也最适合面试表达的一条路径。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages