
做Agent应用最烦的不是写那个调LLM的循环而是“数据进得来、知识找得着”。做过RAG的兄弟应该都有同感文档切得稀碎、向量化维度对不上、检索结果一团糟——这些锅最后全得背在Agent身上。前阵子给一套Agent框架搭基础检索设施正好完整走了一遍Unstructured解析文档、FAISS建向量索引、BGE-M3做本地Embedding的链路从下载安装到参数调优都有不少体会整理出来给正要踩坑的朋友一个参考。这篇内容主要面向两类人一是自己在搭RAG或Agent记忆模块的技术人员二是想私有化部署知识库、但被文档解析和向量检索搞得头疼的同学。核心解决三件事让PDF、Word、网页等乱七八糟的文档格式变成干净文本让文本能算成可比较的向量让向量能被快速检索。这三步通了Agent的“长期记忆”底座基本就稳了。1. 整体设计三件套在Agent框架里各自扮演什么角色1.1 为什么偏偏是这三个组件接触Agent框架越深越会觉得底层其实就是“记忆规划工具”。记忆这块光靠对话上下文那点token根本不够用必须外挂一个能读文档、能存向量、能快速检索的子系统。而Unstructured、FAISS、BGE-M3这套组合恰好覆盖了记忆子系统最核心的三个环节。Unstructured负责的是“读文档”。它能把PDF、DOCX、HTML、PPT这些非结构化文件拆成有语义的区块而不是简单按字符硬切。FAISS负责“存和找”这是Meta开源的向量检索库在千万级向量下还能保持毫秒级响应本地部署没得挑。BGE-M3则是智源出的Embedding模型最大的卖点是支持中文和英文混合场景而且能把文档编码成1024维的稠密向量配合FAISS刚好契合。选这三个不是因为它们名气大而是因为它们是同类里最容易打通闭环的。BGE-M3和FAISS的组合维度匹配上不用做额外转换Unstructured解析出来的文本块转成向量之后直接丢进FAISS就算完事整个过程没有多余的胶水代码。1.2 数据流转链路我先画一下整套数据是怎么走的这样后面看安装和代码才不会懵原始文档 → Unstructured解析 → 文本分块 → BGE-M3编码 → FAISS索引存储 → 检索时Query经BGE-M3编码 → FAISS召回TopK → 送入LLM上下文这里有个容易被忽略的点Unstructured和BGE-M3之间的文本分块策略会直接影响最终检索效果。如果你解析完文档就直接整篇丢给BGE-M3编码长文档的语义会被稀释检索时命中率惨不忍睹。实际做法要结合文档结构做切片比如按段落、按标题层级切而不是无脑按字符数切。1.3 部署环境的准备清单动手之前先把环境列清楚避免装到一半发现版本冲突。我这次用的是Ubuntu 22.04Python 3.10显卡是RTX 3090驱动CUDA 11.8。如果你没有独立显卡CPU跑BGE-M3也能跑通只是首轮编码会慢一些后面FAISS检索本来就不吃GPU。需要提前装好的基础件有Python 3.10、pip、git、cmake编译FAISS某些扩展用、libmagicUnstructured检测文件类型用。建议直接用conda建一个独立环境避免和系统Python打架conda create -n agent_rag python3.10 conda activate agent_rag提示别用系统自带的Python环境直接装这些库Unstructured的依赖链非常长很容易把系统环境搞坏。独立环境是底线别贪方便。2. Unstructured安装与文档解析实操2.1 安装Unstructured的方式和坑Unstructured的安装分两种基础版和全功能版。基础版只支持纯文本和简单的HTML解析如果你想解析PDF、Word、图片必须装全功能扩展。刚开始我只装了基础版结果partition_pdf直接报错提示缺少detectron2和tesseract折腾了好一阵子。推荐直接用官方要求的全量安装方式pip install unstructured[all-docs]这一个命令会把检测文档类型的libmagic、处理PDF的pdf2image和detectron2、OCR用的tesseract等一并装好。但注意detectron2是个难啃的硬骨头pip直接装经常挂尤其在Windows上。如果你在Linux上装需要提前确认有没有编译好的wheel没有的话就用官方推荐的源码编译方式提前装好torch再编译detectron2会顺畅很多。注意Unstructured的依赖里涉及detectron2时花的时间最多耐心是关键。如果你不需要解析带复杂版式的PDF可以跳过detectron2用基础版配合tesseract做OCR也能凑合。2.2 用partition函数做文档分区安装完成后直接用partition系列函数就能干活。以最常用的PDF为例from unstructured.partition.pdf import partition_pdf elements partition_pdf( example.pdf, strategyhi_res, extract_images_in_pdfTrue, infer_table_structureTrue, )这个函数返回的不是纯文本列表而是一个个Element对象每个Element都带着类型信息比如Title、NarrativeText、Table、ListItem等。为什么要保留类型因为后续切分文本块时可以根据类型做不同的处理——标题可以单独作为索引块表格可以转成HTML或Markdown格式正文段落按语义合并这样向量化后的效果会比无脑切字符好得多。strategy参数值得单独说一下。默认的auto策略会根据文档类型自动选择解析方式但在表格多、版式复杂的PDF上我推荐直接用hi_res策略它能调用深度学习模型识别文档结构准确率高很多代价是解析耗时明显增加。实际用下来一份30页的PDFhi_res策略耗时是auto策略的5倍左右但提取的标题层级和表格结构确实更完整。2.3 表格提取与文本清洗PDF里的表格是文档解析的重灾区。直接用文本提取方式读表格往往会把表格内容挤成一坨完全没有行列概念。Unstructured的infer_table_structureTrue会调用表格结构识别模型把表格输出为HTML格式的Element。拿到Element之后需要把表格块单独处理。我的做法是将表格转成Markdown格式因为BGE-M3在Markdown格式下的表格语义理解效果更好。转换逻辑如下from unstructured.staging.base import elements_to_json # 提取所有文本类元素 for el in elements: if el.category Table: table_html el.metadata.text_as_html # 将HTML表格转成Markdown # 这里可以用html2markdown之类的库做个转换文本清洗这块我发现最关键的是去掉页眉页脚、页码、以及PDF里经常出现的重复水印。这些噪声如果不清除编码进向量以后检索时特别容易干扰相关性判断。Unstructured本身不做这个需要自己在拿到Element列表之后加一步过滤把文本长度过短、以及在每页都重复出现的片段直接丢掉。3. FAISS部署从安装到构建向量索引3.1 FAISS的安装与选择FAISS的安装相对简单没有Unstructured那么麻烦。CPU版本和GPU版本需要分开装# CPU版 pip install faiss-cpu # GPU版需要先有CUDA pip install faiss-gpu需要注意的是faiss-gpu的安装包版本要和你的CUDA版本匹配否则import的时候会报libcudart.so找不到之类的错误。我建议一开始就用CPU版调试逻辑等全部流程跑通之后再换GPU版加速索引构建。因为FAISS在构建千万级以下的索引时CPU版也就几秒钟的事完全够用。3.2 索引类型选型Flat、IVF还是HNSWFAISS里的索引类型非常多但实际做RAG场景用到最多的就三个IndexFlatIP、IndexIVFFlat、IndexHNSWFlat。IndexFlatIP是暴力检索原理是拿Query向量和库里的每个向量做内积计算取TopK。优点是召回率绝对高缺点也很明显——数据量大了以后内存和耗时线性增长。百万级向量以内这个索引完全能扛住。IndexIVFFlat是倒排索引的暴力版先把向量空间聚类成nlist个桶查询时只搜最近的几个桶大幅减少计算量。缺点是存在召回损失尤其当nlist设置不合理时效果下降明显。IndexHNSWFlat用的是HNSW图算法这几年做RAG最常用。它在召回率和查询速度之间平衡得最好内存占用比IVF稍大但索引质量更高。我这次用的是IndexHNSWFlat。import faiss dim 1024 # BGE-M3的向量维度 index faiss.IndexHNSWFlat(dim, 32) # M32这里的M参数是HNSW算法的核心表示每个节点的最大连接数。M越大图越稠密召回率越高但内存占用和构建耗时也越大。实测下来M32对于BGE-M3的1024维向量已经够用再往上提升不明显反而内存涨得快。3.3 索引构建的完整流程与内存计算把文本向量灌进FAISS之前有一个容易忽略的操作要确认向量的数据类型。FAISS默认使用float32存储而BGE-M3输出的向量也是float32所以可以直接add不需要额外转换。import numpy as np # embeddings: List[np.ndarray]每个元素是1024维的float32数组 embedding_matrix np.vstack(all_embeddings).astype(float32) index.add(embedding_matrix)内存方面有个公式可以先算一下总字节数 ≈ 4 × 维度 × 向量条数。如果是100万条BGE-M3向量1024维大约需要4GB内存。这个数字直接决定了你要不要考虑用IVFPQ这种压缩索引。我做的是几万条文档块级别的向量4GB完全没压力所以我直接用HNSWFlat不去折腾量化。保存和加载索引也很关键直接调接口就行faiss.write_index(index, doc_index.faiss) # 加载 index faiss.read_index(doc_index.faiss)提示FAISS索引文件和向量的元数据比如对应的是哪段文本需要分开保存。FAISS只管向量不管文本内容所以要把“向量→文本块”的映射关系单独存一份可以用JSON或者SQLite检索时根据FAISS返回的下标去查原文。4. BGE-M3本地部署与向量化4.1 为什么选择BGE-M3而不是其他Embedding模型Embedding模型选择这事团队里争论过好几次。有人用OpenAI的text-embedding-3-small方便是方便但数据出了内网合规上过不去。也有人试过别的开源中文模型中文还行一遇到中英混合的内容就拉胯。BGE-M3最打动我的有两个点一是支持8192 token的长文档不用分段就能编码这对“整段合同条款”“整页技术文档”这类场景太友好了二是它同时支持稠密检索、稀疏检索和多向量检索三种方式后续如果想升级成混合检索不需要换模型一套向量走天下。唯一需要注意的是BGE-M3对中文查询有一个官方建议在查询语句前加上“为这个句子生成表示以用于检索相关文章”这串指令能提升检索效果。实测确实有效加了以后Top1命中率能提升几个百分点。4.2 下载模型与本地加载BGE-M3的模型下载按照官方仓库的方式即可从Hugging Face或ModelScope下载权重。权重文件不算小大约2.2GB左右建议提前下载到本地后续加载不走网络避免环境受限。下载完目录结构大概是bge-m3/ ├── config.json ├── model.safetensors ├── tokenizer.json ├── tokenizer_config.json └── ...加载方式我用的是sentence-transformers库代码量最少效果也稳定pip install sentence-transformersfrom sentence_transformers import SentenceTransformer model SentenceTransformer(bge-m3目录路径, devicecuda)如果你是纯CPU环境把device改成cpu就行只是编码速度会慢不少。实测3090显卡上BGE-M3编码1000个文本块大约需要15秒CPU模式下可能需要两分钟。如果你的文档集达到百万级建议直接用GPU编码否则构建索引的时间会很煎熬。4.3 三种检索模式与FAISS的适配BGE-M3最强的地方在于它一个模型出了三种向量Dense稠密、Sparse稀疏、ColBERT多向量。对应到FAISS上Dense向量直接进FAISS做ANN检索就行Sparse向量和ColBERT向量则需要配合别的检索组件。我这次的方案只用了Dense向量因为和FAISS的配合最成熟能直接跑通整个RAG链路。Sparse和ColBERT虽然理论上召回效果上限更高但需要引入Elasticsearch或专门的延迟交互模块复杂度至少翻一倍。建议先跑通Dense这条线把Agent框架整体工作流调顺了再回头升级混合检索。5. 联调过程中的常见问题与排查记录5.1 环境与依赖报错速查表实际操作中遇到的坑比预想中多我把典型的几类问题整理出来了遇到相同报错的直接对照排查问题现象可能原因解决办法import unstructured报错找不到magic缺少libmagic系统库sudo apt install libmagic-devpartition_pdf提示detectron2缺失全量依赖没装全pip install unstructured[all-docs]重装FAISS读取索引时维度报错索引维度和Query维度不一致确认建索引和编码都用BGE-M3的1024维BGE-M3编码特别慢设备选择错误跑在CPU上检查device参数显存不够就压缩batch_size检索结果总是返回空集合HNSW的efSearch设置太小调大index.hnsw.efSearch参数通常设为128或256中文检索效果差没有加BGE-M3官方查询指令前缀在查询语句前拼上指定promptFAISS的efSearch参数这里多讲一句。HNSW索引在查询阶段有一个动态候选集大小叫efSearch默认值是16对于小规模索引问题不大但如果你有几十万条以上的向量efSearch16会明显影响召回率。我一般调到128速度影响很小但效果提升肉眼可见。5.2 检索质量不理想的调优经验有一次检索测试中我拿一份合同PDF里的一句话当QueryTop5返回的结果里只有一条沾边其他全是无关段落。排查了一圈问题出在文档分块策略上——Unstructured的hi_res把很大一段文字识别成了Title导致向量化时把包含标题和正文的大块混在一起语义混乱。后来我改了策略Title类型的Element单独作为一个小块后续跟着的正文段落再单独成块。这样一条文档能切出多个语义集中的小块检索命中率立刻提上来了。另外表格块建议直接单独处理不要让表格和正文混在一个块里否则表格的结构信息会被正文稀释。还有一个从实际使用中总结出来的技巧如果检索场景是按“问题找答案”建议把Query过一遍同义改写再编码让BGE-M3理解得更准确一些。这次虽然没在正式流程里加同义改写模块但在手动测试时明显感觉到原样Query和改写Query编码后的检索结果质量差距挺大。5.3 部署与模型文件管理经验模型文件和索引文件最好统一管理。我是把模型和索引都放在项目目录下的models/和indexes/里配合一个版本号方便回滚。BGE-M3重下了新版权重时需要重新构建一遍FAISS索引这个流程要自动化。你可以写一个简单的shell脚本每次更新完模型按顺序跑一遍“文档解析→向量化→索引构建”三步生成新版本的索引文件。不要图省事只在命令行手敲后面模型升级或者文档库更新时你一定会感谢当初写好的这个脚本。6. 从单点组件到Agent记忆系统的串联思考三件套各自部署完成后还剩最后一层“胶水”怎么把Unstructured解析出的文本块、BGE-M3的向量、FAISS的索引和上层Agent框架的编排逻辑无缝衔接。我的做法是把这三步封装成三个独立服务中间用消息队列串起来。文档解析服务收到新文档后解析出文本块发给向量化服务向量化服务调用BGE-M3生成向量写入FAISS索引索引写入成功后再把对应的元数据文档ID、文本块ID、摘要等保存到关系型数据库里。Agent查询时只用调用一个检索接口输入Query返回TopK的文本块和文档来源即可。这样拆的好处是每一环都可以独立升级。Unstructured版本更新了或者FAISS换成了别的向量数据库都不用动其他模块。而且后续想支持增量索引只要在文档解析服务里记录好每个文本块的唯一IDFAISS侧配合IDMap类型的索引就能做到新增、删除、更新而不重建全量索引。这套链路跑通之后其实你已经有了一个非常通用的Agent记忆基础设施。不只是Agent任何一个需要“理解文档、按语义检索”的系统都可以复用这套底座。如果在生产环境要进一步提高并发能力FAISS可以换成分布式版本BGE-M3也可以部署成独立的推理服务这些都是后话了。先把单机版跑稳比什么都实在。