本地免费知识库搭建:Ollama+FAISS+MoreLogic RAG实战
发布时间:2026/10/2 3:47:31来源:尧图网络
1. 为什么我要自己搭一个知识库1.1 从“收藏夹吃灰”说起我电脑里有个文件夹叫“待读”里面塞了大概四百多个网页存档、PDF 和 Markdown 笔记。每次想找某个技术细节比如“FAISS 的 IndexIVFFlat 怎么调 nprobe”我得先回忆这东西是去年几月看的然后翻聊天记录、翻浏览器书签、翻笔记软件最后大概率还是重新搜一遍。这种体验就像你家里有个巨大的仓库但所有东西都堆在地上没有货架也没有标签找一件东西得把整个仓库翻一遍。后来我试过几个在线笔记和知识管理工具问题也很明显要么数据在别人服务器上要么免费版有各种限制要么搜索能力太弱只能匹配关键词没法理解语义。比如我搜“怎么让模型记住我上传的文档”它只会去找包含“模型”“文档”这些词的文章而不会理解我其实想问的是 RAG 的检索增强生成流程。所以我就想能不能在自己电脑上搭一个完全本地的、免费的、支持语义搜索的知识库。核心需求就三条数据不出本机、不花钱、能理解自然语言提问。折腾了一圈最后落到了 MoreLogic RAG 个人免费版这个方案上配合 Ollama 做本地模型推理FAISS 做向量检索Python 做胶水层。整套东西跑起来之后我那个“待读”文件夹终于变成了一个能对话的知识库。1.2 这套方案到底适合谁先说清楚这不是那种企业级、高并发、多租户的知识库系统。它更适合下面这几类人个人开发者手里有一堆技术文档、API 说明、项目笔记想快速检索。学生和研究者需要管理论文、实验记录、参考文献并且想用自然语言提问。小团队内部使用三五个人共享一批资料不想把数据传到第三方平台。对数据隐私敏感的人比如律师、医生、财务人员文档不能离开本地环境。如果你需要的是支持上千人同时在线、有复杂权限管理、能对接企业微信和钉钉的那种系统那这套方案不适合你你应该去看 Dify 或者 FastGPT 这类更重的平台。但如果你只是想要一个“自己用的、能搜能问的文档库”那这套组合拳的性价比非常高。1.3 整体技术选型背后的逻辑这套方案的核心链路其实不复杂文档进来切块转向量存进 FAISS提问时把问题也转向量去 FAISS 里找最相似的块再把问题和这些块一起塞给 Ollama 里跑的大模型让它生成回答。为什么选 Ollama 而不是直接调云端 API因为免费版的核心诉求就是“不花钱”和“数据不出门”。Ollama 可以在本地跑 Qwen、Llama、Gemma 这些开源模型虽然效果比不上 GPT-4但对于“从我的文档里找答案”这种任务7B 到 14B 的模型已经够用了。而且 Ollama 的安装和管理非常简单一条ollama run qwen2.5:7b就能跑起来。为什么选 FAISS 而不是 Chroma 或 Milvus因为 FAISS 是 Facebook 开源的向量检索库纯 Python 调用不需要额外起服务对于个人知识库这种数据量几千到几万条 chunk来说FAISS 的本地索引完全够用而且速度极快。Chroma 虽然更方便但它的持久化机制有时候会出一些奇怪的问题Milvus 则是为大规模设计的个人用属于杀鸡用牛刀。MoreLogic RAG 个人免费版在这里的角色其实是把上面这些组件串起来的“脚手架”。它提供了文档解析、切块、向量化、检索、生成这一整套流程的封装让你不用从零写代码。但它的免费版功能有限所以我在实际使用中做了一些替换和补充下面会详细说。2. 环境准备与核心组件安装2.1 Python 环境的干净搭建我强烈建议不要用系统自带的 Python也不要在一个已经装了几百个包的环境里折腾。用 conda 或者 venv 建一个独立环境这是血泪教训。我之前图省事直接在 base 环境里装结果 FAISS 和某个科学计算库的版本冲突排查了一下午。# 用 conda 创建环境指定 Python 3.10 conda create -n myrag python3.10 conda activate myrag # 或者用 venv python -m venv myrag_env source myrag_env/bin/activate # Linux/Mac # myrag_env\Scripts\activate # Windows为什么选 Python 3.10因为 3.11 和 3.12 在某些深度学习库的兼容性上还有问题3.10 是目前最稳的版本。装完之后先升级 pip然后装核心依赖pip install --upgrade pip pip install faiss-cpu # 如果你有 NVIDIA 显卡并且想用 GPU 加速可以装 faiss-gpu pip install sentence-transformers pip install ollama pip install pypdf markdown beautifulsoup4 pip install numpy pandas tqdm这里解释一下每个包的作用。faiss-cpu是向量检索的核心它负责存储和快速查找相似向量。sentence-transformers用来把文本转成向量我后面会讲为什么不用 OpenAI 的 embedding API。ollama是 Python 客户端用来和本地 Ollama 服务通信。pypdf和markdown用来解析不同格式的文档。tqdm用来显示进度条处理大量文档时没有进度条你会很焦虑。注意如果你在 Windows 上装 faiss-cpu 报错大概率是缺少 Visual C 运行库去微软官网下载最新的 VC redistributable 装上就行。另外faiss-cpu 在 Python 3.10 上的 wheel 包很全基本不会遇到编译问题。2.2 Ollama 的安装与模型选择Ollama 的安装本身很简单官网下载对应系统的安装包一路下一步就行。但国内用户会遇到两个问题下载慢和模型拉取慢。下载慢的问题可以找国内的镜像源。Ollama 的模型仓库是放在 registry.ollama.ai 上的国内访问确实不稳定。我的做法是先用迅雷或者 IDM 把模型文件下载下来然后手动导入。具体操作是在能正常访问的机器上执行ollama pull qwen2.5:7b然后去~/.ollama/models目录下把对应的 blob 文件拷贝出来放到目标机器的同样目录下。Ollama 的模型存储结构是models/blobs/sha256-xxx和models/manifests/registry.ollama.ai/library/qwen2.5/7b把这两部分都拷过去就行。模型选择方面我试过好几个。Qwen2.5:7b 在中文理解和指令遵循上表现最好而且对硬件要求不高16GB 内存的机器就能跑。Llama3.1:8b 英文能力强但中文问答有时候会答非所问。Gemma2:9b 也不错但推理速度比 Qwen 慢一些。如果你机器配置高可以上 Qwen2.5:14b效果明显更好。如果只有 8GB 内存那就用 Qwen2.5:3b 或者 Phi3:mini虽然能力有限但做简单的文档检索问答也够用。# 拉取模型 ollama pull qwen2.5:7b # 测试模型是否正常 ollama run qwen2.5:7b 你好请用一句话介绍你自己如果遇到ollama run qwen3.5:2b error: 500 internal server error: llama-server process这种报错通常是模型文件损坏或者内存不足。先检查内存占用然后尝试重新拉取模型。还有一个常见原因是 Ollama 的版本太老去官网下载最新版覆盖安装即可。2.3 FAISS 的索引类型选择FAISS 提供了多种索引类型选错了会导致检索速度慢或者精度下降。对于个人知识库这种规模我推荐用IndexFlatL2或者IndexIVFFlat。IndexFlatL2是最简单的暴力检索它会把查询向量和库里所有向量逐一计算 L2 距离然后返回最近的 k 个。优点是精度 100%缺点是数据量大了之后速度线性下降。对于一万条以下的 chunk用这个完全没问题检索时间在毫秒级。IndexIVFFlat是倒排索引它先把向量聚类成 nlist 个簇检索时只查最近的 nprobe 个簇。这样速度更快但会损失一点精度。当你的 chunk 数量超过五万时建议用这个。nlist 一般设为sqrt(N)nprobe 设为nlist/10左右。import faiss import numpy as np # 假设向量维度是 768 dimension 768 # 暴力检索索引 index_flat faiss.IndexFlatL2(dimension) # 倒排索引 nlist 100 # 聚类中心数量 quantizer faiss.IndexFlatL2(dimension) index_ivf faiss.IndexIVFFlat(quantizer, dimension, nlist, faiss.METRIC_L2) # 需要先训练 index_ivf.train(vectors) index_ivf.add(vectors) index_ivf.nprobe 10 # 检索时查 10 个簇我实测下来对于个人知识库直接用IndexFlatL2最省心。等你真的攒了十万条以上的 chunk再考虑换 IVF。而且 FAISS 支持保存和加载索引faiss.write_index(index, my_index.faiss)和faiss.read_index(my_index.faiss)这样每次启动不用重新计算所有向量。3. 文档处理与向量化实操3.1 文档解析的坑与解决方案知识库的第一步是把各种格式的文档变成纯文本。听起来简单但实际操作中坑很多。PDF 里的表格、扫描件里的图片、Markdown 里的代码块处理不好都会丢失信息。对于 PDF我用pypdf做基础提取但它对复杂排版的 PDF 支持很差。如果 PDF 是扫描件那pypdf提取出来全是空白。这时候需要用 OCR我试过pytesseract配合pdf2image效果还行但速度慢。对于个人知识库我建议先把扫描件用其他工具转成文字版 PDF再入库。对于 Markdown 和纯文本直接读取就行。对于 Word 文档用python-docx。对于网页存档用BeautifulSoup提取正文去掉导航栏和广告。from pypdf import PdfReader import markdown from bs4 import BeautifulSoup def parse_pdf(file_path): reader PdfReader(file_path) text for page in reader.pages: text page.extract_text() \n return text def parse_markdown(file_path): with open(file_path, r, encodingutf-8) as f: md_text f.read() html markdown.markdown(md_text) soup BeautifulSoup(html, html.parser) return soup.get_text() def parse_html(file_path): with open(file_path, r, encodingutf-8) as f: html f.read() soup BeautifulSoup(html, html.parser) # 去掉 script 和 style for tag in soup([script, style, nav, footer, header]): tag.decompose() return soup.get_text()注意解析 PDF 时一定要加异常处理。有些 PDF 是加密的有些是损坏的直接抛异常会让整个批处理中断。用 try-except 包起来记录失败的文件名后面单独处理。3.2 文本切块策略不是越细越好切块是 RAG 里最容易被忽视但影响最大的环节。切得太粗检索出来的块包含太多无关信息模型容易被干扰切得太细一个完整的语义单元被拆散检索出来的是碎片模型看不懂。我的经验是按语义边界切而不是按固定字数切。具体来说优先按段落切如果段落太长超过 800 字再按句子切。句子边界用中文的句号、问号、感叹号以及英文的句号、问号、感叹号来识别。import re def split_text(text, max_chunk_size500, overlap50): # 先按段落分 paragraphs re.split(r\n\s*\n, text) chunks [] current_chunk for para in paragraphs: para para.strip() if not para: continue # 如果单个段落就超过 max_chunk_size按句子切 if len(para) max_chunk_size: sentences re.split(r(?[。.!?])\s*, para) for sent in sentences: if len(current_chunk) len(sent) max_chunk_size: current_chunk sent else: if current_chunk: chunks.append(current_chunk) current_chunk sent else: if len(current_chunk) len(para) max_chunk_size: current_chunk para \n else: if current_chunk: chunks.append(current_chunk) current_chunk para \n if current_chunk: chunks.append(current_chunk) # 加 overlap final_chunks [] for i, chunk in enumerate(chunks): if i 0: prev_tail chunks[i-1][-overlap:] chunk prev_tail chunk final_chunks.append(chunk) return final_chunksoverlap 的作用是防止关键信息刚好落在切分边界上。比如一句话被切成两半前半句在 chunk A后半句在 chunk B检索时可能只召回 A模型就看不到完整信息。加 50 到 100 字的 overlap 能缓解这个问题。3.3 向量化模型的选择与本地部署向量化就是把文本变成一串数字向量让语义相似的文本在向量空间里距离更近。OpenAI 的 text-embedding-ada-002 效果很好但要花钱而且数据要传到云端。所以我用sentence-transformers里的开源模型。中文场景我推荐BAAI/bge-large-zh-v1.5或者BAAI/bge-base-zh-v1.5。BGE 系列在中文语义相似度任务上表现很好而且模型大小适中。large 版是 1024 维base 版是 768 维。如果机器内存够用 large否则用 base效果差距不大。from sentence_transformers import SentenceTransformer # 加载模型第一次会自动下载 model SentenceTransformer(BAAI/bge-base-zh-v1.5) # 编码 texts [这是第一段文本, 这是第二段文本] embeddings model.encode(texts, normalize_embeddingsTrue) print(embeddings.shape) # (2, 768)normalize_embeddingsTrue很重要它把向量归一化到单位长度这样用内积计算相似度就等价于余弦相似度。FAISS 的IndexFlatIP就是内积索引配合归一化向量使用效果最好。注意第一次运行SentenceTransformer会从 HuggingFace 下载模型国内可能很慢。可以设置环境变量HF_ENDPOINThttps://hf-mirror.com来加速。或者提前用huggingface-cli download把模型下载到本地然后加载时指定本地路径。4. 检索与生成链路的完整实现4.1 构建 FAISS 索引并持久化有了向量之后就可以建索引了。我把所有 chunk 的向量存成一个 numpy 数组然后加到 FAISS 索引里。同时用一个列表保存每个 chunk 的原始文本和元数据来源文件、页码等这样检索到向量后能找回对应的文本。import faiss import numpy as np import pickle import os class VectorStore: def __init__(self, dimension768): self.dimension dimension self.index faiss.IndexFlatIP(dimension) # 内积索引 self.chunks [] # 存储原始文本 self.metadata [] # 存储元数据 def add(self, embeddings, chunks, metadata_list): # embeddings 是 numpy 数组shape (N, dimension) self.index.add(embeddings.astype(float32)) self.chunks.extend(chunks) self.metadata.extend(metadata_list) def search(self, query_embedding, top_k5): query_embedding query_embedding.astype(float32).reshape(1, -1) distances, indices self.index.search(query_embedding, top_k) results [] for dist, idx in zip(distances[0], indices[0]): if idx -1: continue results.append({ score: float(dist), text: self.chunks[idx], metadata: self.metadata[idx] }) return results def save(self, path): faiss.write_index(self.index, os.path.join(path, index.faiss)) with open(os.path.join(path, chunks.pkl), wb) as f: pickle.dump({chunks: self.chunks, metadata: self.metadata}, f) def load(self, path): self.index faiss.read_index(os.path.join(path, index.faiss)) with open(os.path.join(path, chunks.pkl), rb) as f: data pickle.load(f) self.chunks data[chunks] self.metadata data[metadata]用IndexFlatIP而不是IndexFlatL2是因为归一化后的向量用内积计算相似度更直接分数越高越相似。如果用 L2 距离分数越低越相似容易搞混。4.2 检索策略不只是向量相似度单纯的向量检索有时候会漏掉关键词精确匹配的情况。比如你搜“FAISS IndexIVFFlat”向量检索可能返回一堆讲“向量索引”的通用文章但真正包含这个具体术语的文档反而排在后面。这时候需要混合检索向量检索 关键词检索。我的做法是用rank_bm25做一个简单的 BM25 关键词检索然后把两路结果融合。融合算法用 Reciprocal Rank FusionRRF它对不同检索系统的分数尺度不敏感效果很稳。from rank_bm25 import BM25Okapi import jieba class HybridRetriever: def __init__(self, vector_store): self.vector_store vector_store # 构建 BM25 索引 tokenized_chunks [list(jieba.cut(chunk)) for chunk in vector_store.chunks] self.bm25 BM25Okapi(tokenized_chunks) def search(self, query, query_embedding, top_k5): # 向量检索 vector_results self.vector_store.search(query_embedding, top_k * 2) # BM25 检索 tokenized_query list(jieba.cut(query)) bm25_scores self.bm25.get_scores(tokenized_query) bm25_top_indices np.argsort(bm25_scores)[::-1][:top_k * 2] # RRF 融合 rrf_scores {} for rank, result in enumerate(vector_results): idx self.vector_store.chunks.index(result[text]) rrf_scores[idx] rrf_scores.get(idx, 0) 1 / (60 rank 1) for rank, idx in enumerate(bm25_top_indices): rrf_scores[idx] rrf_scores.get(idx, 0) 1 / (60 rank 1) # 排序取 top_k sorted_indices sorted(rrf_scores.keys(), keylambda x: rrf_scores[x], reverseTrue)[:top_k] results [] for idx in sorted_indices: results.append({ text: self.vector_store.chunks[idx], metadata: self.vector_store.metadata[idx], rrf_score: rrf_scores[idx] }) return resultsRRF 里的常数 60 是经验值来自原论文。它的作用是平滑排名差异让两路检索的贡献更均衡。我实测下来混合检索比纯向量检索的召回率高 15% 到 20%尤其是对于包含专有名词和技术术语的查询。4.3 提示词工程让模型好好说话检索到相关文档块之后要把它们和用户问题一起塞给 Ollama 里的模型。提示词的设计直接决定了回答质量。我试过很多版本最后稳定下来的模板是这样的PROMPT_TEMPLATE 你是一个知识库助手。请根据下面提供的参考资料回答用户的问题。 规则 1. 只使用参考资料中的信息回答不要编造。 2. 如果参考资料中没有相关信息直接说“根据现有资料无法回答这个问题”。 3. 回答要简洁、准确引用具体内容时注明来源。 4. 如果用户的问题涉及多个方面分点回答。 参考资料 {context} 用户问题{question} 回答这个模板的关键在于“只使用参考资料”和“无法回答时直接说”。如果不加这两条模型会倾向于用自己的知识编造答案这在知识库场景里是致命的。我踩过这个坑问一个文档里没有的问题模型编了一段看起来很像真的回答差点把我误导。context 的拼接也有讲究。不要把检索到的所有块一股脑塞进去那样会超出模型的上下文窗口而且无关信息会干扰模型。我一般取 top 3 到 top 5每个块截断到 500 字左右总 context 控制在 2000 字以内。def build_context(search_results, max_length2000): context_parts [] current_length 0 for result in search_results: text result[text][:500] source result[metadata].get(source, 未知来源) part f[来源{source}]\n{text}\n if current_length len(part) max_length: break context_parts.append(part) current_length len(part) return \n---\n.join(context_parts)4.4 调用 Ollama 生成回答Ollama 的 Python 客户端用起来很简单但有几个参数需要调。temperature设低一点0.1 到 0.3 之间这样回答更稳定、更贴近原文。num_ctx要设大一些默认是 2048如果你的 context 比较长设成 4096 或 8192。top_p设 0.9 就行。import ollama def generate_answer(question, context, modelqwen2.5:7b): prompt PROMPT_TEMPLATE.format(contextcontext, questionquestion) response ollama.chat( modelmodel, messages[{role: user, content: prompt}], options{ temperature: 0.2, num_ctx: 4096, top_p: 0.9, } ) return response[message][content]注意如果你的文档很多检索出来的 context 经常超过 2000 字建议把num_ctx调到 8192。但要注意num_ctx越大显存和内存占用越高。7B 模型在 8192 上下文下大概需要 8GB 到 10GB 显存。如果显存不够Ollama 会自动回退到 CPU 推理速度会慢很多。5. 常见问题排查与性能优化5.1 检索结果不相关怎么办这是最常见的问题。你问“怎么配置 FAISS 的 nprobe”检索出来的却是“FAISS 安装教程”。原因通常有三个切块太粗、embedding 模型不适合、检索策略单一。首先检查切块。如果每个 chunk 有 1000 字以上那检索出来的块肯定包含大量无关信息。把 max_chunk_size 降到 300 到 500 字重新建索引。其次检查 embedding 模型。如果你用的是英文模型处理中文文档效果肯定差。换成 BGE 中文模型。最后加上 BM25 混合检索前面已经讲过怎么做了。还有一个容易被忽视的点查询改写。用户的问题往往很短比如“nprobe 怎么调”直接拿这个去检索向量可能不够明确。可以用 Ollama 先把问题改写成更完整的查询比如“FAISS IndexIVFFlat 的 nprobe 参数应该如何设置”再去检索。这一步能明显提升召回质量。def rewrite_query(question, modelqwen2.5:7b): prompt f请把下面的问题改写成更适合检索的查询语句保持原意但补充必要的上下文关键词。只输出改写后的查询不要解释。 原问题{question} 改写后 response ollama.chat( modelmodel, messages[{role: user, content: prompt}], options{temperature: 0.1} ) return response[message][content].strip()5.2 Ollama 响应慢或超时Ollama 第一次加载模型会慢因为要把模型权重从磁盘读到内存。之后每次推理如果模型已经在内存里速度就正常了。如果你发现每次都很慢可能是内存不足模型被反复换入换出。检查方法跑ollama ps看模型是否在运行。如果不在说明被卸载了。解决办法是增加系统内存或者换更小的模型。另外num_ctx设太大也会拖慢速度因为注意力计算量随上下文长度平方增长。如果不需要长上下文设成 2048 就行。还有一个坑Ollama 默认只保留一个模型在内存里。如果你同时用了 embedding 模型和生成模型而且 embedding 模型也是通过 Ollama 跑的那两者会互相挤占内存。我的做法是 embedding 用 sentence-transformers 单独跑不走 Ollama这样两者互不干扰。5.3 常见问题速查表问题现象可能原因排查方法解决方案检索结果完全不相关切块太粗或 embedding 模型不匹配打印 chunk 内容检查长度和语言减小 chunk size换中文 embedding 模型Ollama 报 500 错误模型文件损坏或内存不足查看 Ollama 日志检查内存占用重新拉取模型或换更小模型回答编造内容提示词没有限制检查 prompt 模板加上“只使用参考资料”和“无法回答时直接说”索引加载失败FAISS 版本不匹配检查 faiss 版本和保存时的版本重新建索引或统一版本中文检索效果差用了英文 embedding 模型检查模型名称换成 BAAI/bge-base-zh-v1.5处理 PDF 时卡住PDF 加密或损坏单独测试该 PDF跳过损坏文件记录日志内存占用过高num_ctx 太大或模型太大用ollama ps查看减小 num_ctx换 3B 模型5.4 性能优化的几个实用技巧第一个技巧是批量编码。SentenceTransformer.encode支持传入列表一次性编码多个文本比循环单条编码快很多。我处理 1000 个 chunk批量编码只要十几秒单条循环要几分钟。第二个技巧是索引分片。如果你的文档按主题分得很清楚可以建多个 FAISS 索引检索时先判断问题属于哪个主题再去对应的索引里搜。这样每个索引更小检索更快而且精度更高。第三个技巧是缓存。相同的查询没必要每次都重新编码和检索。用一个简单的字典缓存 query 到结果的映射命中缓存直接返回。对于个人知识库查询重复率其实挺高的。from functools import lru_cache lru_cache(maxsize100) def cached_search(query): # 注意lru_cache 不能直接用于带 numpy 数组参数的函数 # 这里只是示意实际需要把 query 转成字符串再缓存 pass第四个技巧是异步处理。文档解析和向量化可以并行做用concurrent.futures.ThreadPoolExecutor开几个线程同时处理多个文件。但要注意FAISS 的 add 操作不是线程安全的需要加锁。6. 从零到一的完整操作流程6.1 目录结构规划在开始写代码之前先把目录结构定好。我习惯这样组织my_knowledge_base/ ├── documents/ # 原始文档 │ ├── tech/ │ ├── papers/ │ └── notes/ ├── data/ # 处理后的数据 │ ├── index.faiss # FAISS 索引 │ ├── chunks.pkl # chunk 文本和元数据 │ └── bm25.pkl # BM25 索引 ├── scripts/ │ ├── ingest.py # 文档入库脚本 │ ├── query.py # 查询脚本 │ └── config.py # 配置文件 └── requirements.txt把原始文档和处理后的数据分开这样重新建索引时不会误删原始文件。config.py 里放模型名称、chunk 大小、路径这些配置改起来方便。6.2 入库脚本的完整实现入库脚本做四件事遍历文档、解析、切块、向量化、存索引。我把它写成一个可重复执行的脚本每次有新文档就重新跑一遍。为了增量更新可以记录已处理的文件列表跳过没变化的文件。import os import hashlib from pathlib import Path from tqdm import tqdm def get_file_hash(file_path): with open(file_path, rb) as f: return hashlib.md5(f.read()).hexdigest() def ingest_documents(doc_dir, vector_store, embed_model, processed_filesNone): if processed_files is None: processed_files {} all_chunks [] all_metadata [] for root, dirs, files in os.walk(doc_dir): for file in tqdm(files, desc处理文档): file_path os.path.join(root, file) file_hash get_file_hash(file_path) # 跳过未变化的文件 if file_path in processed_files and processed_files[file_path] file_hash: continue # 根据扩展名选择解析器 ext Path(file_path).suffix.lower() try: if ext .pdf: text parse_pdf(file_path) elif ext .md: text parse_markdown(file_path) elif ext in [.txt, .py, .js]: with open(file_path, r, encodingutf-8) as f: text f.read() elif ext in [.html, .htm]: text parse_html(file_path) else: continue except Exception as e: print(f解析失败 {file_path}: {e}) continue # 切块 chunks split_text(text) for i, chunk in enumerate(chunks): all_chunks.append(chunk) all_metadata.append({ source: file_path, chunk_index: i, file_hash: file_hash }) processed_files[file_path] file_hash if not all_chunks: print(没有新文档需要处理) return processed_files # 批量向量化 print(f正在向量化 {len(all_chunks)} 个 chunk...) embeddings embed_model.encode( all_chunks, batch_size32, show_progress_barTrue, normalize_embeddingsTrue ) # 加入索引 vector_store.add(embeddings, all_chunks, all_metadata) print(f入库完成当前索引共 {vector_store.index.ntotal} 条向量) return processed_files这个脚本的关键点是增量更新。通过文件哈希判断文件是否变化只处理新文件或修改过的文件。对于个人知识库每次全量重建索引太浪费时间增量更新能省很多事。6.3 查询脚本的完整实现查询脚本把前面所有模块串起来改写查询、编码、混合检索、构建 context、调用 Ollama 生成回答。def query_knowledge_base(question, vector_store, retriever, embed_model, modelqwen2.5:7b): # 1. 查询改写 rewritten rewrite_query(question, model) print(f改写后的查询{rewritten}) # 2. 编码 query_embedding embed_model.encode([rewritten], normalize_embeddingsTrue)[0] # 3. 混合检索 results retriever.search(rewritten, query_embedding, top_k5) # 4. 构建 context context build_context(results) # 5. 生成回答 answer generate_answer(question, context, model) return answer, results # 使用示例 if __name__ __main__: # 加载索引 store VectorStore() store.load(data/) # 加载 embedding 模型 embed_model SentenceTransformer(BAAI/bge-base-zh-v1.5) # 构建混合检索器 retriever HybridRetriever(store) # 查询 question FAISS 的 nprobe 参数怎么调 answer, sources query_knowledge_base(question, store, retriever, embed_model) print(\n回答) print(answer) print(\n参考来源) for s in sources: print(f- {s[metadata][source]} (相关度: {s[rrf_score]:.4f}))6.4 日常使用与维护建议这套东西搭好之后日常使用就是往 documents 目录里丢新文件然后跑一次 ingest.py。查询用 query.py或者你可以用 Streamlit 做个简单的 Web 界面这样不用每次开终端。维护方面我建议每个月检查一次索引大小和检索质量。如果发现检索变慢可能是 chunk 太多了考虑换 IVF 索引。如果发现回答质量下降可能是模型需要更新或者提示词需要调整。还有一个实用技巧给文档打标签。在 metadata 里加一个category字段检索时可以按类别过滤。比如你问“Python 的装饰器怎么用”可以只在categorypython的 chunk 里检索这样精度更高。# 带过滤的检索 def search_with_filter(self, query_embedding, category, top_k5): # 先检索更多结果再过滤 results self.search(query_embedding, top_k * 5) filtered [r for r in results if r[metadata].get(category) category] return filtered[:top_k]这套方案我从去年用到现在管理了大概两千多份文档检索响应时间在 1 秒以内生成回答大概 3 到 5 秒。硬件是一台 32GB 内存、RTX 3060 12GB 的台式机。如果你用笔记本8GB 内存跑 3B 模型也能用就是慢一点。关键是整套东西完全免费数据完全本地想怎么改就怎么改。
网站建设高端定制企业官网