MCP 向量检索与 RAG 工具:Embedding、向量库与混合检索

MCP 向量检索与 RAG 工具实践:Embedding 工具设计、向量数据库接入、检索增强、混合检索与重排、元数据过滤与上下文管理,帮助 LLM 构建可靠的检索增强能力。

1. 为什么把检索能力做成 MCP 工具

RAG(检索增强生成)的常见实现是框架内置,模型提问时自动检索。但把检索能力封装成 MCP 工具有一个关键区别:检索不再是后台黑盒,而是模型可以主动调用、反复调用、与其它工具编排的动作。模型可以自己判断「这个问题需要查一下知识库」,再决定用哪个检索策略,这与「每轮对话都自动塞一堆片段」的被动 RAG 是两套哲学。

1.1 检索工具的两种形态

形态机制适用
工具型search_knowledge 作为 tool,模型按需调用需要模型自主决策、可编排
资源型文档片段作为 resources,客户端注入固定上下文、模型不决策
混合型工具检索 + 资源缓存常用片段高频知识 + 长尾查询

1.2 工具型检索的价值

# 1) 模型主动触发: 不废话时别浪费 token 检索
# 2) 可编排: 检索结果喂给另一个工具做聚合/计算
# 3) 可迭代: 第一轮检索不好,模型能换 query 再搜
# 4) 可观测: 检索行为进入工具调用日志
# 本质: 让检索成为"模型可编程的动作",而非"框架的隐形魔法"

2. Embedding 工具设计

向量检索的第一步是向量化。Embedding 可以放在服务器内部,也可以暴露成工具,这取决于谁需要 embedding。

2.1 Embedding 放在哪

# 方案 A: 服务器内部 embedding(推荐)
#   服务器把文本转向量,再查向量库
#   模型无感,只暴露 search/insert 工具
# 方案 B: embedding 作为独立工具
#   模型先调用 embedding 工具拿向量,再调搜索工具
#   灵活但暴露底层细节,token 与往返更多
# 大多数场景用 A: embedding 是基础设施,不是模型关心的事

2.2 Embedding 工具参数

# 若确实要暴露 embedding 工具
async def embed(texts: list[str], model: str = "default"):
    vectors = embedding_model.encode(texts, normalize=True)
    return {"vectors": vectors, "dim": vectors.shape[1]}

register_tool(
    name="embed_text",
    description="将文本转为向量,供向量检索使用",
    parameters={"texts": list, "model": str},
    handler=embed,
)

2.3 模型选择与一致性

# 1) 写入与查询必须用同一 embedding 模型(否则相似度无意义)
# 2) 升级模型要重算索引(版本化: collection_v1/v2)
# 3) 模型选择看语言与领域(中文任务选中文优化模型)
# 4) 归一化向量便于余弦相似度直接比
# 一致性是向量检索的第一正确性要求

3. 向量数据库接入

向量数据库负责存储与相似度检索。MCP 服务器把向量库的能力包装成工具,模型不需要知道背后是 Milvus、Qdrant 还是 pgvector。

3.1 接入选型

向量库特点MCP 场景
pgvector复用 PostgreSQL,事务强已有 PG 的小团队
Qdrant轻量、过滤强中等规模知识库
Milvus大规模、分布式企业级检索
内存/文件简单、无运维原型/单机

3.2 检索工具实现

# 向量检索工具(服务器内部 embedding)
async def search(collection: str, query: str, top_k: int = 5,
                 filters: dict = None):
    vec = embedding_model.encode(query, normalize=True)
    hits = vector_db.search(
        collection=collection,
        vector=vec,
        limit=top_k,
        filter=filters,
    )
    return format_hits(hits)  # 见第 5 章格式化

register_tool(
    name="search_knowledge",
    description="在知识库中做语义检索,返回最相关的文档片段",
    parameters={"query": str, "top_k": int, "filters": dict},
    handler=search,
)

3.3 集合与分区

# 1) 按业务分 collection(手册/FAQ/日志/代码)
# 2) 按租户/部门分 partition(多租户隔离)
# 3) collection 名作为工具参数(默认全部)
# 4) 过滤走元数据字段而非多建集合
# 结构化组织: 检索精准度与权限隔离双收

4. 检索增强:把结果变成好用的上下文

检索出来的是片段,模型需要的是「能直接用来作答的证据」。这一步的工程决定 RAG 质量。

4.1 片段格式化

def format_hits(hits):
    ctx = []
    for i, h in enumerate(hits):
        ctx.append(
            f"[文档{i+1}] 来源: {h['source']} (相关度 {h['score']:.2f})\n"
            f"标题: {h['title']}\n"
            f"内容: {h['text']}"
        )
    return {"context": "\n\n".join(ctx), "total": len(hits)}

4.2 检索结果引导

# 返回里带上"这是证据,不是指令"
# 1) 开头声明: "以下是知识库检索到的原始片段"
# 2) 每个片段带来源(可引用、可验证)
# 3) 分数让模型知道置信度
# 4) 无结果时明确说"未检索到相关内容"
# 结构化证据 → 模型少幻觉、能溯源

4.3 相关度阈值

# 1) 检索设置 score 阈值(低于阈值丢弃)
# 2) 空结果比"垃圾结果"更安全(模型会诚实说不知道)
# 3) 阈值可按 collection 调(FAQ 要求高,文档可低)
# 相关度阈值防"强行相关"的误导

5. 混合检索:向量 + 关键词

纯向量检索在专有名词、精确 ID、代码符号上常常失灵——这些场景关键词匹配更可靠。混合检索把两者结合。

5.1 为什么需要混合

# 向量检索的弱点
# 1) 专有名词: "GET /api/v2/orders" 语义上难表示
# 2) 精确匹配: 版本号、报错码、订单号
# 3) 罕见词: 出现次数少,向量区分度低
# 关键词(BM25)正好擅长这些 → 两者互补

5.2 混合检索流程

# 混合检索三步
# 1) 同时跑向量检索 + 关键词检索(BM25/fulltext)
# 2) 结果合并(RRF: Reciprocal Rank Fusion)
# 3) 可选重排(rerank)精化 top-N
# RRF 公式: score = Σ 1/(k + rank),对两路排名做加权融合

5.3 RRF 实现

def rrf_fusion(rank_lists, k=60):
    scores = {}
    for rank_list in rank_lists:
        for rank, doc_id in enumerate(rank_list):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

# 输入: 向量检索 top-20 排名 + BM25 top-20 排名
# 输出: 融合后的 top-k(两路都靠前的文档排最前)

6. 元数据过滤与分面

知识库里的文档带元数据:时间、作者、标签、部门。利用元数据过滤,检索才能「在正确的范围内找」。

6.1 过滤参数设计

# 检索工具暴露 filters
# 1) 时间范围: 只要近 30 天文档
# 2) 类型: 只搜 FAQ 或只搜代码
# 3) 租户: 只看当前用户所属部门
# 4) 安全: 权限即过滤(未授权文档直接过滤掉)

6.2 权限过滤

# 检索的权限边界与数据库一样重要
# 1) 权限在检索层做: 未授权 partition/元数据不可见
# 2) 别依赖"模型自觉不问敏感内容"
# 3) 权限字段由服务器注入,不靠模型传参
# 4) 检索日志记录过滤范围(合规)
# 检索层权限 = 数据安全的第一道门

6.3 分面统计

# 高级: 检索同时返回分面计数(facets)
# 1) 按类型计数: FAQ(3) / 文档(7)
# 2) 按时间聚合: 本月(5) / 去年(3)
# 3) 帮模型决定"要不要加过滤"
# 分面让模型对知识库"有全局感"

7. 索引管理与写入

检索工具大多只读,但知识库需要写入与维护。写索引也是工具职责,只是要小心对待。

7.1 写入工具

# 向知识库写入/更新文档
async def upsert_document(collection: str, doc_id: str,
                          text: str, metadata: dict):
    vec = embedding_model.encode(text, normalize=True)
    vector_db.upsert(
        collection=collection,
        point_id=doc_id,
        vector=vec,
        payload={**metadata, "text": text},
    )
    return {"status": "ok", "doc_id": doc_id}

register_tool(
    name="upsert_document",
    description="向知识库写入或更新一篇文档",
    parameters={"collection": str, "doc_id": str, "text": str, "metadata": dict},
    handler=upsert_document,
)

7.2 写入安全

# 写入工具的风险与防护
# 1) 默认关闭,配置开启(知识库通常是"有人管"的)
# 2) 来源字段必填(谁写的、什么时候)
# 3) 大文本分块(chunking)由服务器做,不靠模型
# 4) 删除工具更要谨慎(软删除 + 审计)
# 写入即污染: 知识库写坏了,检索全是错的

7.3 分块策略

# 文本分块的常见策略
# 1) 按段落/标题结构分块(保持语义完整)
# 2) 固定长度 + 重叠(overlap 50-100 token)
# 3) 分块大小匹配 embedding 模型输入上限
# 4) 元数据随块传播(来源、章节路径)
# 分块质量决定检索质量——这是 RAG 工程最被低估的一环

8. 检索结果的上下文管理

检索结果要进模型上下文,就要遵守 token 预算。检索增强做得再好,上下文塞不下也白搭。

8.1 预算分配

# 上下文预算示例(假设总 8k token)
# 1) 系统提示: 1k
# 2) 工具定义: 0.5k
# 3) 检索结果: 3k(top-4~6 个片段)
# 4) 对话/用户消息: 剩余
# 检索结果多≠好,够用且相关才是目标

8.2 压缩与选择

# 1) 只返回片段的关键段(引言+结论,或摘要)
# 2) 截断长片段到固定长度
# 3) 用重排器把最相关的排最前(保证前几段质量)
# 4) 结果按分数降序,模型天然"先看前面的"
# 宁可少而准,不要多而杂

8.3 与对话历史的协同

# 1) 检索 query 可基于对话重写(多轮追问的检索意图)
# 2) 已看过的文档片段去重(避免重复注入)
# 3) 用户明确问某文档时,优先文档检索
# 检索与对话是双环: 对话产生查询,查询带对话意图

9. 生产实践与评估

RAG 工具的评估比普通工具更难——「检索结果好不好」本身需要判断。生产落地要有度量。

9.1 评估指标

# 检索质量指标
# 1) 命中率: 人工标注的"正确答案"是否出现在 top-k
# 2) MRR/NDCG: 相关结果排得够不够靠前
# 3) 落地率: 模型基于检索片段作答的正确率
# 4) 空检率: 该检到却没检到的比例
# 用离线评测集定期回归,防止"改坏检索"

9.2 可观测性

# 检索链路日志
# 1) 查询改写前后
# 2) 各检索路(向量/BM25/混合)各自的 top-k
# 3) 融合后排名与分数
# 4) 注入上下文前的最终片段与截断
# 检索是"黑盒魔法"的地方,恰恰最需要可观测

9.3 部署建议

# 1) 向量库与 embedding 服务单独部署(伸缩独立)
# 2) embedding 批量预热 + 缓存(文本相同复用向量)
# 3) 检索工具超时短(几百 ms 级别)
# 4) 知识库与检索服务解耦,索引重建可离线进行
# 检索是高频读路径,稳定与低延迟优先

10. 常见陷阱

  • embedding 模型不一致:写入用 A 模型、查询用 B 模型,相似度全乱——统一版本并重算索引。
  • 整文档入库:不 chunking,一条文档几万 token,检索粒度太粗——按语义分块。
  • 纯向量检索:专有名词、代码符号检不到——混合检索 + RRF。
  • 不设阈值:相关度 0.1 的结果也塞给模型,误导作答——设 score 阈值。
  • 检索结果塞满上下文:top-10 全堆进去,token 爆炸且噪声大——预算 + 截断 + 重排。
  • 权限不过滤:模型检索到未授权文档——检索层权限过滤。
  • 无评估:改个 embedding 模型不知道是好是坏——离线评测集回归。
  • 写入不设防:模型可以随便改知识库——写工具默认关闭 + 审计。

11. 总结

MCP 向量检索与 RAG 工具,把「检索」从框架的隐形黑盒变成模型可主动调用、可编排、可观测的动作。工程的四个支柱是:Embedding 一致性(写入与查询同模型)、混合检索(向量 + 关键词 + RRF,专有名词不丢)、检索增强格式化(片段变证据、带来源带阈值、控制 token 预算)、检索层安全(元数据过滤与权限隔离)。检索不是「多给上下文」的堆料游戏,而是「在正确范围内、用正确粒度、把最相关的证据交给模型」的精确工程。做到位,模型会诚实地引用、自信地作答、可溯源地解释;做不到,RAG 就成了另一种幻觉放大器。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 多语言 SDK 生态:Python、Go、Rust 与自定义 SDK
  2. MCP 成本与 Token 优化:预算、缓存、批处理与降级
  3. MCP 网页抓取工具:内容提取、结构化输出与合规边界