LangChain + Easysearch + MiMo 大模型——从零构建企业级 RAG 向量检索系统
不谈概念,只谈落地。一文讲清向量写入、kNN 语义检索、RAG 问答的完整链路,附可跑通代码。
一、背景:为什么是这个组合?
企业做知识库问答,绕不开三个核心问题:向量存哪、怎么检索、谁来回答。
| Easysearch | ||
| 小米 MiMo | ||
| Python 自研框架 |
三者配合,是目前国内开箱即用、成本可控的最优组合之一。
小米赠送的token正好用一下。
10 分钟可见效果。
二、架构:先理解数据流,再看代码
整个 RAG 链路分两个阶段,务必先理解这两条线。
阶段一:离线写入(Indexing)
原始文档(easysearch_docs.txt)
→ RecursiveCharacterTextSplitter 切块(500 字/块,50 字重叠)
→ MiMo LLM 语义编码(每块生成 256 维向量)
→ Easysearch kNN 索引(knn_dense_float_vector)
阶段二:在线检索 + 生成(Retrieval & Generation)
用户提问
→ MiMo LLM 向量化(同模型编码,保证语义空间一致)
→ Easysearch knn_nearest_neighbors 检索 Top-K
→ 拼接检索结果 + 用户问题 → MiMo LLM 生成回答
一条数据走完两条线,写进去的是什么向量,查出来就是同一套语义空间——这是检索质量的生命线。
三、Step 1:环境准备
前置条件
Easysearch 集群 已启动,HTTPS 开启,kNN 插件已安装并加载
MiMo API Key:小米开发者平台申请,Base URL 为 https://token-plan-cn.xiaomimimo.com/v1Python ≥ 3.10
项目结构
vectorPrj/
├── .env # 配置文件(API Key、连接参数等)
├── config.py # 配置加载模块
├── es_client.py # Easysearch REST API 封装(requests 直连)
├── mimo_embeddings.py # MiMo LLM 文本向量化
├── indexing.py # 文档切块 + 向量写入
├── retriever.py # kNN 向量检索
├── rag_qa.py # RAG 问答链
├── search_test.py # 检索效果验证
├── main.py # 命令行入口
├── easysearch_docs.txt # 知识库文档
└── requirements.txt # 依赖
安装依赖
pip install -r requirements.txt
核心依赖:requests、python-dotenv、langchain、langchain-community。
四、Step 2:用 MiMo 生成向量(Embedding)
关键认知
MiMo v2.5-pro 是推理模型,没有独立的 Embedding API。但我们可以利用其强大的语义理解能力,通过 Chat API "请"它输出文本的语义向量。
实现思路
文本 → 构造 Prompt:"你是文本语义编码器,请将文本映射为 256 维向量" → MiMo Chat API → 解析 JSON → 得到向量
核心代码 mimo_embeddings.py:
classMiMoLLMEmbeddings(Embeddings):
"""使用 MiMo Chat API 批量生成语义向量"""
def_batch_to_vectors(self, texts: List[str]) -> List[List[float]]:
# 构造 Prompt:要求输出 JSON 格式的向量数组
prompt = (
f"你是一个文本语义编码器。请将以下 {len(texts)} 段文本分别映射为"
f"{dims} 维向量。向量值在 -1.0 到 1.0 之间。"
f"只输出 JSON 格式,不要任何解释。\n"
f'格式: {{"vectors": [[0.1, -0.2, ...], [0.3, 0.5, ...], ...]}}\n'
)
# 调用 MiMo Chat API
msg = self._call_chat(prompt)
# 优先取 content,为空时取 reasoning_content(推理模型特性)
content = msg.get("content", "") or msg.get("reasoning_content", "")
return self._extract_vectors(content, len(texts), dims)
踩坑与调优
坑 1:max_tokens 不足导致向量被截断
256 维向量约需 3000+ token 输出。初始设置 max_tokens=4096 时,向量常被截断为 0。调大到 8192 后稳定。
坑 2:推理模型的 reasoning_content
MiMo 是推理模型,返回中 reasoning_content 是思考过程,content 是最终答案。向量可能在 content 中,也可能在 reasoning_content 中。两者都解析,取其一即可。
坑 3:批量处理导致向量混乱
初始 BATCH_SIZE=3 时,模型偶尔会输出错位或数量不对。改为 BATCH_SIZE=1 逐条处理,虽然慢但稳定。
向量解析的两套方案
def_extract_vectors(self, text, expected_count, dims):
# 方案1:解析 JSON 格式 {"vectors": [[...], ...]}
# 方案2:正则匹配所有数字数组 [0.1, -0.2, ...]
# 补齐/截断至指定维度
备选方案:本地模型
如果 MiMo 不稳定或网络受限,一行配置切换到本地 BGE 模型:
# .env 中设置
EMBEDDING_BACKEND=bge
自动加载 BAAI/bge-base-zh-v1.5,其余代码完全不用改——这正是抽象层的价值。
五、Step 3:写入文档到 Easysearch 向量索引
为什么不直接用 LangChain 的 Elasticsearch Store?
问题:ES 8.x 官方 Python 客户端会检测服务端是否为 Elasticsearch,非 ES 服务器直接拒绝连接。LangChain 的 ElasticsearchStore 底层依赖该客户端。
方案:直接用 requests 发送原生 ES REST API 请求,绕开客户端校验。
# es_client.py — 封装 HTTP 请求
defes_request(method, path, body=None):
url = f"{Config.ES_HOST}/{path}"
resp = session.request(method, url, data=json.dumps(body))
return resp.json()
创建 kNN 索引
# indexing.py
mapping = {
"mappings": {
"properties": {
"content": {"type": "text"}, # 原文,供 BM25 检索
"source": {"type": "keyword"}, # 来源文件名
"content_vector": { # 向量字段
"type": "knn_dense_float_vector",
"knn": {"dims": 256}
}
}
}
}
es_request("PUT", "rag-easysearch-docs", body=mapping)
关键参数说明:
knn_dense_float_vector:Easysearch kNN 插件要求的向量字段类型dims=256:与 MiMo 输出维度一致,维度不匹配写入直接报错content字段保留text类型:供混合检索中的 BM25 关键词匹配使用
文档切块 + 批量写入
defindex_documents():
# 1. 加载文档
docs = TextLoader("easysearch_docs.txt").load()
# 2. 切块:500 字一段,50 字重叠保留上下文
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)
chunks = splitter.split_documents(docs)
# 3. 向量化
embeddings = MiMoLLMEmbeddings()
vectors = embeddings.embed_documents([c.page_content for c in chunks])
# 4. 批量写入(bulk API)
for chunk, vec in zip(chunks, vectors):
actions.append({
"_index": "rag-easysearch-docs",
"_source": {
"content": chunk.page_content,
"source": chunk.metadata["source"],
"content_vector": vec
}
})
es_bulk(actions)
执行:
python main.py index
六、Step 4:验证向量检索效果
写入后必须先验证检索,再接入 LLM 生成。很多人跳过这步,最终回答差却不知道问题出在哪。
kNN 查询语法(Easysearch 原生)
# retriever.py
body = {
"query": {
"knn_nearest_neighbors": {
"field": "content_vector",
"vec": {"values": query_vector}, # 查询向量
"model": "exact", # exact=精确检索
"similarity": "cosine", # 余弦相似度
"candidates": 100, # 候选集大小
}
}
}
model 参数说明
exact | ||
lsh |
当前 mapping 使用默认配置,exact 兼容性最好。
混合检索:向量 + BM25
纯向量检索对专有名词(版本号、型号、人名)效果差,关键词检索则无法理解语义。混合检索结合两者:
"bool": {
"should": [
{"knn_nearest_neighbors": {...}}, # 向量语义匹配(加分)
{"match": {"content": query}} # BM25 关键词匹配(加分)
]
}
通过 .env 中的 HYBRID_SEARCH=true/false 一键切换。
验证检索
python main.py search "安全功能"
预期输出:
#1 相关度: 5.4621
来源: easysearch_docs.txt
内容: Easysearch 的安全功能包括:1. HTTPS 传输加密...
#2 相关度: 4.8903
来源: easysearch_docs.txt
内容: Easysearch 的 kNN 插件用于向量相似度检索...
#3 相关度: 4.2156
来源: easysearch_docs.txt
内容: Easysearch 默认启用 HTTPS,证书路径配置在...
判断标准:Top1 结果与查询高度相关,说明向量质量合格。如果 Top3 都不相关,通常是 Embedding 模型与文档语言不匹配,需换模型。
七、Step 5:构建 RAG 问答链
完整链路
用户问题 → MiMo 向量化 → Easysearch kNN 检索 Top-K → Prompt 拼接 → MiMo LLM 生成
自定义 Prompt(关键!)
PROMPT = """你是 Easysearch 技术专家。请严格根据以下文档内容回答问题,
若文档中没有答案请直接说"文档中未找到相关信息",不要自行编造。
文档内容:
{context}
用户问题:{question}
回答:"""
为什么这个 Prompt 如此重要?
不约束 LLM,模型会混入训练知识产生"幻觉"——回答听起来流畅但不可信。加上"文档中没有就说不知道",强制模型忠于检索结果。
问答执行
python main.py ask "Easysearch 的安全功能有哪些?"
输出示例:
回答: Easysearch 的安全功能包括以下 5 项:
1. HTTPS 传输加密:默认开启,保护数据传输安全
2. 用户认证:支持 Basic Auth 和 API Key 两种认证方式
3. 角色权限控制(RBAC):可创建不同角色,分配索引级别的读写权限
4. 审计日志:记录所有操作请求,便于安全审计
5. IP 白名单:限制允许访问的 IP 地址范围
来源文档:
[1] easysearch_docs.txt (相关度: 5.4621)
[2] easysearch_docs.txt (相关度: 4.8903)
[3] easysearch_docs.txt (相关度: 4.2156)
回答准确、来源可追溯——这正是 RAG 的核心价值。
八、交互式对话
python main.py chat
进入交互模式后,可以连续提问,每次自动检索 + 生成,体验完整的 RAG 对话流程。
RAG 问答系统已就绪(Easysearch kNN + MiMo)
索引: rag-easysearch-docs
检索模式: Hybrid(向量+BM25)
输入问题开始对话,输入 quit / exit 退出
============================================
你: Easysearch 怎么部署集群?
思考中...
助手: 根据文档,Easysearch 集群部署建议最少 3 个节点...
九、避坑指南:四个最容易踩的坑
坑 01:kNN 插件已安装但未加载
现象:创建索引时 type=knn_dense_float_vector 报 "No handler for type"。
原因:easysearch-plugin list 显示已安装,但 _nodes/plugins 显示未加载。
解决:重启 Easysearch。插件安装后必须重启才能生效。
坑 02:向量维度不匹配
现象:写入时报 mapping 错误。
原因:MiMo 输出 256 维,但索引 mapping 中 dims 设成了其他值。
解决:config.py 中 VECTOR_DIMS 必须与 Embedding 实际输出维度一致。
坑 03:lsh model 与 mapping 不兼容
现象:kNN 查询返回 400 错误 "query is not compatible with mapping"。
原因:model=lsh 需要 mapping 中有对应配置,默认 mapping 不包含。
解决:改用 model=exact,或创建索引时指定 lsh 参数。
坑 04:向量全部为零
现象:检索返回的结果与查询完全不相关。
原因:MiMo 输出被截断(max_tokens 不足),向量解析失败,全部补零。
解决:增大 max_tokens=8192,降低 BATCH_SIZE=1,增加 reasoning_content 备选解析。
十、落地清单
[x] Easysearch 安装 kNN 插件并确认已加载( _nodes/plugins)[x] 封装 MiMo LLM 为 LangChain Embeddings 接口 [x] 文档切块(500 字/块,50 字重叠)写入 Easysearch [x] 先验证检索相关度,再接入 LLM 生成 [x] 自定义 Prompt 约束 LLM 严格基于文档回答 [x] 生产环境开启混合检索(向量 + BM25),提升专有名词召回率 [x] 返回来源文档,让用户看到回答依据,增加可信度 [x] 支持交互式对话,体验完整 RAG 流程
十一、总结
本文从零搭建了一个完整的 RAG 向量检索系统,核心链路:
Easysearch(存储+检索) + MiMo(编码+生成) + 自研编排层
几个关键设计决策:
放弃 ES 8.x 客户端,改用 requests 直连: 绕开非 ES 服务器拒绝连接的问题,链路更透明 用 LLM Chat API 做 Embedding: MiMo 没有独立 Embedding API,但推理模型的语义理解能力完全够用 先验证检索再接入生成: 分步调试,问题定位清晰,避免"最终效果差但不知道哪环出问题" 混合检索作为默认策略: 向量解决语义,BM25 解决专有名词,两者互补
希望这篇文章能帮助你在自己的项目中快速落地 RAG。如果有任何问题,欢迎交流讨论。