本地RAG知识库实战:Ollama与FAISS搭建私有问答系统 📅 发布时间:2026/9/18 12:48:33 👁 浏览次数: 先说自己最近的经历。公司内部有上百份产品文档、售后FAQ、操作手册光靠人工翻资料效率太低但把数据传到云端API又过不了合规那一关。我最后落地的方案就是本地跑一套OllamaFAISSEmbeddingRerankLLM的私有大模型知识库。这个组合现在基本是本地RAG检索增强生成的标配适合有Python基础、想自己做私有知识库或者企业内部问答工具的开发者。整套链路拆开看并不复杂但真正部署的时候坑几乎都藏在细节里。这篇文章我按照从零到一的顺序把整套部署流程、每个组件的选型原因、参数调优经验还有我踩过的问题全部整理出来。涉及代码的部分我都给了可以直接跑的版本里面很多参数是我实际调过之后留下的不是网上抄来的默认值。1. 先搞清楚这套组合解决了什么问题1.1 为什么是这五个组件先说结论这五个组件正好覆盖了本地RAG的完整链路——Ollama管模型运行Embedding管文本向量化FAISS管相似度检索Rerank管精排LLM管最终生成答案。少了任何一个这条链路都会出现明显的短板。我之前也试过直接用LangChain一把梭文档加载、向量化、检索、对话全用框架自带组件。但LangChain封装得太狠出了问题很难定位比如检索结果明明是错的你也搞不清是embedding模型的问题还是切分参数的问题。所以后来我干脆把每个环节拆开自己控制每个步骤的输入输出调试起来爽得多。Ollama解决的是模型怎么在本地跑的问题。它把模型权重、运行环境、API服务打包在一起一条命令就能拉模型、起服务还支持GPU和CPU混合推理对普通开发机很友好。Embedding模型负责把文本转成向量这一步决定了后续检索的上限——如果向量化质量差后面FAISS再快、Rerank再准都是白搭。FAISS是向量检索库核心能力是在海量向量里快速找到与查询向量最相似的Top-K个结果。Rerank是很多人容易忽略的一环纯向量检索本质上是双塔粗排query和文档各自独立编码没有真正的交互所以经常出现语义相关但字面差异大的文档排在后面。Rerank模型把query和文档拼在一起做交叉编码精排效果有明显提升。最后LLM拿到Rerank筛出来的高质量片段结合用户问题生成有依据的回答。1.2 整套流程是怎么流转的整个系统分两个阶段。离线索引阶段把文档加载进来按一定策略切分成小块每条文本块通过Embedding模型转成向量存入FAISS索引文件。在线问答阶段用户提问后先把query做同样的向量化到FAISS里召回Top-K个候选片段再用Rerank模型精排把排名最高的几个片段拼进Prompt交给Ollama里跑的LLM生成最终答案。这两个阶段对应一条清晰的数据流离线文档 - 文本切分 - Embedding向量化 - FAISS索引文件 在线query - Embedding向量化 - FAISS召回Top-K - Rerank精排Top-N - 拼接Prompt - LLM回答理解这条链路最关键的一点是Embedding和Rerank各干各的活Embedding负责尽量别漏掉相关内容Rerank负责排序尽量精准。召回阶段多取一些候选精排阶段再缩小到适合塞进Prompt的数量这个粗召回精排序的设计是保证回答质量的底层逻辑。以我实际部署的经验一套纯本地问答系统单条query的响应时间大约在2到5秒取决于文档数量、检索量和LLM推理速度完全可以接受。下面我按实际部署顺序把每个环节拆开讲。2. 部署前的环境准备这一节先把基础环境搭好。整套部署我建议在Linux服务器或者Windows的WSL2里做Python版本用3.10性能稳定且兼容性好。2.1 Ollama的安装与模型下载Ollama支持Windows、macOS、Linux三类平台。Windows和macOS直接去官网下载安装包Linux用一行命令curl -fsSL https://ollama.com/install.sh | sh安装完验证一下ollama --version ollama serveollama serve是启动服务默认监听localhost:11434。如果是远程服务器需要设置环境变量OLLAMA_HOST0.0.0.0再启动这样其他机器才能访问到API。Windows下设置环境变量后要重启Ollama服务才生效这个我经常忘导致本地代码连不上。接下来拉模型。对话模型我推荐qwen2.5:7b-instruct中文能力强7B参数在消费级显卡上跑得很流畅如果你显存有限可以换qwen2.5:3b-instruct效果也还行就是推理深度差一些。Embedding模型用nomic-embed-text这是Ollama原生支持的嵌入模型768维对中文英文都有不错的效果。ollama pull qwen2.5:7b-instruct ollama pull nomic-embed-text注意如果你在纯CPU机器上跑建议选qwen2.5:7b-instruct-q4_K_M这类量化版本推理速度会快很多。显存8G以上的选默认量化版本就行。遇到下载慢的问题建议配置国内的镜像源或者提前下载GGUF格式的模型文件通过Modelfile导入本地。后面我在问题排查章节会详细讲这两种方式的操作步骤。另外非常建议把模型目录迁移到非系统盘。Windows下Ollama默认把模型安装在C盘动辄几个G装几个模型C盘就红了。设置环境变量OLLAMA_MODELSD:\ollama\modelsLinux/macOS下可以用软链接的方式mv ~/.ollama/models /data/ollama_models ln -s /data/ollama_models ~/.ollama/models2.2 Python运行环境准备这套方案的Python侧依赖不算多核心就四个库。用venv或conda建一个独立环境避免把系统环境搞乱python -m venv rag_env source rag_env/bin/activate pip install ollama faiss-cpu langchain-text-splitters FlagEmbedding这里我说明一下几个选择ollamaOllama官方Python客户端直接调本地API比用requests手写HTTP要省心。faiss-cpu普通文档量级用CPU版足够检索几十万条向量也就是几十毫秒的事没必要上GPU版。langchain-text-splitters我只用它的文本切分器LangChain全家桶太重但这个切分器是真好用值得单拎出来。FlagEmbeddingBGE系列Rerank模型的官方推理库支持BGE-reranker-base等模型的本地加载和推理。安装FlagEmbedding的时候如果遇到依赖冲突建议先装PyTorch CPU版或者对应CUDA版再装FlagEmbedding顺序反了容易搞出版本问题。这个坑我踩过不止一次。2.3 验证模型服务是否正常环境变量配好、模型拉完之后先用Python测试Ollama API是否正常import ollama # 测试对话模型 res ollama.chat( modelqwen2.5:7b-instruct, messages[{role: user, content: 你好用一句话介绍你自己}], ) print(res[message][content]) # 测试embedding模型 emb ollama.embed(modelnomic-embed-text, input[hello world]) print(len(emb[embeddings][0])) # 应该输出768如果对话模型有响应、embedding维度输出是768说明Ollama服务正常。到这一步环境就绪接下来开始构建知识库最核心的索引部分。3. Embedding与FAISS把知识变成可检索的仓库3.1 文本切分怎么切才合理很多人做RAG把注意力全放在模型选型上对文本切分策略完全不重视最后检索效果差找不到原因。文本切分直接影响两个环节一是向量化的质量LLM训练时见过的文本长度是有限的强行把几千字的文档塞给embedding模型向量表示会非常稀碎二是检索粒度的合理性切得太碎上下文割裂切得太长一坨内容里只有一个关键点检索命中率下降。我常用的配置是chunk_size500、chunk_overlap80按字符数切分同时指定按段落、句子、标点逐级切开。核心逻辑是优先保持语义完整的边界比如先按空行切再按句号切最后按空格兜底而不是硬生生在句子中间一刀切。from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, length_functionlen, separators[\n\n, \n, 。, , , , , ], ) with open(产品说明书.md, r, encodingutf-8) as f: text f.read() chunks splitter.split_text(text)选500这个值是我反复对比后的结果500字符大约对应中文几百字的内容足够承载一个完整的知识点又不至于让单个向量里的语义太杂。overlap80保证相邻片段之间有一定信息重叠避免一个知识点刚好被切分线劈成两半两个片段都缺失关键信息。如果你的文档有明确结构比如Markdown标题、PDF章节建议在切分前先把标题信息拼到每个片段前面。例如切分后把所在章节标题补到片段开头# 第三章 安装步骤\n\n具体安装流程...。这样检索时即使匹配到的是片段中段的句子向量也能感知到它属于哪个章节上下文信息更完整。3.2 向量化与索引构建文本切分好之后就可以调embedding模型做向量化然后写入FAISS索引。这里需要先强调一个非常重要的点Ollama的embedding接口是接受一个list的一次性传入所有chunks比for循环逐个调用要快很多。我在第一版代码里老老实实写了个循环500个chunks跑了好几分钟改成批量之后十几秒就完成了。import numpy as np import faiss import ollama def build_index(chunks, index_pathfaiss.index): # 批量向量化 res ollama.embed(modelnomic-embed-text, inputchunks) vectors np.array(res[embeddings]).astype(float32) # 归一化内积等价于余弦相似度 faiss.normalize_L2(vectors) # 建立索引向量维度来自embedding模型输出 dim vectors.shape[1] index faiss.IndexFlatIP(dim) index.add(vectors) # 保存索引和对应的文本块 faiss.write_index(index, index_path) with open(chunks.txt, w, encodingutf-8) as f: for i, c in enumerate(chunks): f.write(f{i}\t{c}\n) return index这里面的两个设计决策值得解释清楚。第一个是为什么用IndexFlatIP而不是IndexFlatL2。IndexFlatIP是内积距离在做向量检索之前对向量做了L2归一化那么内积计算的结果就等价于余弦相似度。余弦相似度的取值范围在-1到1之间越接近1越相似阈值语义清晰而L2距离是无界的不同场景下的绝对数值没有可比性你很难拍一个脑筋说距离小于多少算相关。所以实际部署中我统一用L2归一化内积的方式。第二个是为什么小规模场景用IndexFlatIP而不是IndexHNSWFlat。IndexFlatIP是暴力精确检索它会遍历所有向量计算量随数量线性增长IndexHNSWFlat是基于图的近似最近邻检索速度快但会有召回损失。对于十万条级别以下的数据IndexFlatIP的速度完全够快而且精确检索意味着没有召回损失布式调试时还能排除是不是HNSW参数没调好导致漏召回这个变量。等到文档量真的到了百万条级别再升级IndexHNSWFlat不迟。这里还要强调一点文本块列表一定要和向量索引保持同步因为FAISS索引里存的只是向量不存原文。我这边用chunks.txt按行编号存储索引里的position指向的就是chunks列表的下标。后续如果加了增量文档文本文件和索引文件要同步更新否则会出现检索到了某个位置但位置对应的文本对不上的错位问题。3.3 检索时的参数细节构建好索引后查询侧的逻辑不复杂但有几个参数需要特别注意。def search(query, index, chunks, top_k30): res ollama.embed(modelnomic-embed-text, input[query]) qvec np.array(res[embeddings]).astype(float32) faiss.normalize_L2(qvec) scores, positions index.search(qvec, top_k) results [] for score, pos in zip(scores[0], positions[0]): if score 0.4: continue results.append({ chunk: chunks[pos], score: float(score), position: int(pos), }) return results召回数量top_k我这里设置的是30也就是先从FAISS里拿30个候选片段再交给后面的Rerank精排。有同学可能会想既然后面有Rerank为什么不全量召回50个甚至100个因为Rerank模型是交叉编码计算复杂度远高于向量检索把它丢给太多候选会让整个链路的延迟显著上升。实际测试中30个候选是性能和效果比较平衡的点。如果文档库本身很大也可以适当调高到50但如果超过50效果也没有明显提升反而白白增加延迟。相似度阈值我设置的是0.4。具体值你可以在实际运行时把score打出来看看分布然后画一个分位数图选一个能过滤掉明显不相关结果的阈值。不要盲目照抄网上的0.6、0.7你的文档领域和embedding模型不一样得分分布差异很大。领域术语密集的文档得分普遍偏高阈值设得高反而把相关内容全过滤掉了。4. Rerank决定回答质量的隐藏瓶颈4.1 为什么纯向量检索不够很多人一开始没上Rerank只靠FAISS检索问答效果总感觉差口气。我自己在做一个企业内部运营文档的问答系统时做了个对比实验只用向量检索取Top5拼进Prompt准确率大约72%加上Rerank精排之后准确率直接到86%。这个14个百分点的提升就是交叉编码相对双塔编码带来的收益。为什么差距这么大因为embedding模型是双塔结构query和文档各自走一遍编码器变成两个独立的向量再用向量距离衡量相似度。这种结构为了检索效率牺牲了query和文档之间的深度交互——两个文本是否真正相关很多细粒度的语义信号在独立编码时丢掉了。比如query是退款流程需要多长时间文档里有用户在提交退款申请后财务审核周期为3-5个工作日这两个句子如果只用向量相似度很可能是中等偏上的分数但Rerank模型把两个句子拼在一起让注意力机制捕捉到退款申请和退款流程、3-5个工作日和多长时间之间的对应关系排序结果就精准多了。从架构上讲Rerank模型大多采用cross-encoder结构输入是[CLS] query [SEP] document [SEP]输出一个相关性的打分没办法像双塔模型那样预先计算好所有文档向量所以只能对少量候选重排。这也是粗召回精排序架构出现的原因FAISS负责从海量文档里快速捞回候选Rerank负责在候选里精准排序各司其职。4.2 本地化Rerank的落地方式Rerank模型最有名的开源系列是BGEBAAI General Embedding我用的是BAAI/bge-reranker-base中文效果不错模型体积适中CPU也能勉强跑。如果想更轻量可以用bge-reranker-small资源充足追求上限可以上bge-reranker-large。用FlagEmbedding库加载和推理非常直接from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-base, use_fp16True) def rerank(query, candidates, top_n5): pairs [[query, cand[chunk]] for cand in candidates] scores reranker.compute_score(pairs) # 按得分降序排列取前top_n scored sorted(zip(scores, candidates), keylambda x: x[0], reverseTrue) return [cand for _, cand in scored[:top_n]]第一次加载时会从HuggingFace下载模型权重大约1.1GB。如果网络条件不理想提前手动把模型文件下好放到~/.cache/huggingface/目录下即可。Rerank之后取多少条拼进Prompt我一般选3到5条。这个数字别贪多LLM的上下文窗口虽然越来越长但真正能在生成时有效参考的重点片段有限。如果塞进10条以上模型注意力容易被稀释反而回答得不如只给5条时精准。有次我把10条全塞进去模型开始东拉西扯把一些边缘内容也当成核心答案讲出来明显是上下文太杂导致的问题。4.3 没有GPU时怎么跑Rerank如果你的机器没有NVIDIA GPUuse_fp16True要关掉改成CPU推理。BGE-reranker-base在CPU上跑单条query精排30个候选大约需要1到3秒还能接受。但如果是并发请求量大的场景CPU跑Rerank会成为瓶颈。一个替代方案是用Ollama里的通用对话模型做伪Rerank让LLM对候选片段做相关性打分。比如给LLM一段提示词让它针对query给每个候选片段打1到5分再按分数排序。这种方式的优点是复用现有模型、零额外部署缺点是推理速度比专门的Rerank模型慢打分稳定性也不如专用模型。我建议小体量场景可以应急用生产环境还是老老实实部署一个BGE-reranker。5. 完整流程实现与参数调节前面两个阶段拆分完了这一节我给出一套能直接跑的完整代码然后详细讲每个关键参数背后的调优逻辑。整套代码的目录结构是这样的rag_project/ ├── build_index.py # 离线索引构建 ├── query_engine.py # 在线问答查询 ├── data/ # 放原始文档 └── index/ # 生成的索引和文本块5.1 索引构建脚本# build_index.py import os import glob import numpy as np import faiss import ollama from langchain_text_splitters import RecursiveCharacterTextSplitter def load_documents(data_dir): 加载目录下所有txt/md文档简单加个换行分隔 docs [] for ext in (*.txt, *.md): for path in glob.glob(os.path.join(data_dir, ext)): with open(path, r, encodingutf-8) as f: content f.read() filename os.path.basename(path) docs.append(f文档{filename}\n{content}) return \n\n.join(docs) def split_text(text): splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, length_functionlen, separators[\n\n, \n, 。, , , , , ], ) return splitter.split_text(text) def build_index(data_dirdata, index_dirindex): os.makedirs(index_dir, exist_okTrue) text load_documents(data_dir) chunks split_text(text) print(f切分完成共 {len(chunks)} 个文本块) res ollama.embed(modelnomic-embed-text, inputchunks) vectors np.array(res[embeddings]).astype(float32) faiss.normalize_L2(vectors) dim vectors.shape[1] index faiss.IndexFlatIP(dim) index.add(vectors) index_path os.path.join(index_dir, faiss.index) chunks_path os.path.join(index_dir, chunks.txt) faiss.write_index(index, index_path) with open(chunks_path, w, encodingutf-8) as f: for i, chunk in enumerate(chunks): f.write(f{i}\t{chunk}\n) print(f索引保存完成{index_path}) if __name__ __main__: build_index()这个脚本自己做了三件事加载文档、切分、向量化建索引。执行时注意如果文档是PDF、Word格式需要先转成纯文本或Markdown。我一般用pypdf或者markitdown这类工具批量转换转换质量直接影响后续切分效果值得花点时间做一下清洗比如去掉页眉页脚、自动拼接断行。5.2 问答查询脚本# query_engine.py import numpy as np import faiss import ollama from FlagEmbedding import FlagReranker class LocalRAG: def __init__(self, index_dirindex): self.index faiss.read_index(f{index_dir}/faiss.index) self.chunks [] with open(f{index_dir}/chunks.txt, r, encodingutf-8) as f: for line in f: pos, chunk line.strip().split(\t, 1) self.chunks.append(chunk) self.reranker FlagReranker(BAAI/bge-reranker-base, use_fp16True) def search(self, query, top_k30): res ollama.embed(modelnomic-embed-text, input[query]) qvec np.array(res[embeddings]).astype(float32) faiss.normalize_L2(qvec) scores, positions self.index.search(qvec, top_k) candidates [] for score, pos in zip(scores[0], positions[0]): if pos 0 or pos len(self.chunks): continue candidates.append({ chunk: self.chunks[pos], score: float(score), position: int(pos), }) return candidates def rerank(self, query, candidates, top_n5): pairs [[query, cand[chunk]] for cand in candidates] scores self.reranker.compute_score(pairs) scored sorted(zip(scores, candidates), keylambda x: x[0], reverseTrue) results [cand for _, cand in scored[:top_n]] # 这里可以打印一下精排结果方便调试 for i, cand in enumerate(results): print(f--- Rerank #{i1} ---) print(cand[chunk][:100].replace(\n, )) return results def ask(self, query, temperature0.2, num_ctx8192): candidates self.search(query, top_k30) top_related self.rerank(query, candidates, top_n5) context \n\n.join( f[{i1}]\n{cand[chunk]} for i, cand in enumerate(top_related) ) prompt f你是企业内部的知识库助手。请根据下面提供的参考资料用中文回答问题。 如果参考资料中没有相关内容请直接回答“资料中没有找到相关信息”不要编造。 参考资料 {context} 问题{query} 回答 res ollama.chat( modelqwen2.5:7b-instruct, messages[{role: user, content: prompt}], options{ temperature: temperature, num_ctx: num_ctx, }, ) return res[message][content] if __name__ __main__: rag LocalRAG() while True: query input(\n请输入问题输入quit退出) if query.strip().lower() quit: break answer rag.ask(query) print(\n回答, answer)这个脚本拼成了完整的问答服务。ask方法里那个prompt模板我特意加了如果没有相关内容就直说不知道这句约束对抑制大模型幻觉非常有效。别小看这句话实测下来回答的编造率明显下降。你还可以根据自己的需求把系统提示词改成你是客服专家、你是技术文档助手等角色设定。5.3 关键参数怎么调参数调优是最能体现实际操作经验的部分。我逐个讲讲我在生产环境里调过的参数和背后的原因。temperature回答知识类问题temperature设为0.2创意类、发散类问题可以调到0.7以上。temperature控制的是模型采样时的随机性值越大输出越跳跃。知识库问答要求稳定、忠实于资料温度越高越容易编造。0.2是我试过的一个平衡点既不会过于机械化也不会乱发挥。num_ctx这个参数非常关键代表模型上下文窗口的长度。Ollama默认只有2048也就是模型最多只能看到2K token的内容。当你把检索出来的5个文本块拼进prompt时如果总字数超过2048 token后边的内容会被直接截断模型根本看不到你的参考资料只能靠自己的训练记忆瞎答。我把num_ctx设置为8192同时确保单次问答的总token量在7K左右留出生成答案的余量。运行时会多占用一些显存但换来的是生成质量稳定值。top_k和top_nFAISS召回30、Rerank精排5这两个参数一粗一细。如果你的文档库特别大或者文档块比较碎可以适当把召回数提高到50但精排top_n尽量不要超过5到7。LLM能有效利用的黄金片段就那几个给太多反而让它抓不住重点。keep_alive第一次调Ollama请求时模型需要从磁盘加载到内存或显存这个加载时间可能长达几秒到几十秒。如果频繁问答建议设置keep_alive让模型常驻ollama.chat( modelqwen2.5:7b-instruct, messages[...], options{temperature: 0.2, num_ctx: 8192}, keep_alive30m, )keep_alive30m表示模型空转30分钟后再卸载。如果内存和显存都够也可以设24h甚至-1永久常驻。这招在体验优化上效果立竿见影问答延迟可以下降一个数量级。6. 常见问题与排查实录这一节把我在实际部署中遇到过的典型问题列出来每条都是真实踩过的坑附上解决思路。6.1 Ollama相关典型坑问题1模型下载速度慢、下载失败Ollama默认的模型源在国外网络条件不理想时经常拉到一半就断。这种情况我建议两个路子。第一配置国内镜像源设置环境变量指向可用的镜像地址重新启动Ollama后再ollama pull。第二如果镜像也不行直接用浏览器或下载工具从模型托管平台下载好GGUF格式文件再通过Modelfile导入。Modelfile导入的步骤很简单FROM /path/to/your/model.gguf然后执行ollama create my-model -f Modelfile ollama run my-model问题2报错file does not exist这个报错通常发生在ollama create或ollama run指定模型时Ollama找不到你指的文件或模型名。最常见的原因有两个一是写的模型名不对可以先执行ollama list看看本地到底有哪几个模型二是Modelfile里的FROM路径写错了Ollama不解析~符号需要写绝对路径比如FROM /home/user/models/xx.gguf不要写FROM ~/models/xx.gguf。问题3安装到了C盘导致磁盘不够模型文件动不动几个G装了三五个模型C盘直接爆掉。解决办法就是在安装Ollama之后、拉取模型之前先设置OLLAMA_MODELS环境变量指向其他盘符然后重启Ollama服务。已经下载了的模型直接把整个模型目录剪切到新位置再设置环境变量重启服务即可不需要重新下载。6.2 FAISS与Embedding的坑问题1维度不一致报错FAISS报维度不一致说明你建索引时用的embedding模型和查询时用的embedding模型不是同一个。比如建索引用nomic-embed-text768维查询时换了mxbai-embed-large1024维维度对不上检索必然报错。解决办法是统一模型如果非要换模型那就必须重建索引。这个坑在实际项目里出现频率很高尤其是多人协作用各自环境跑的时候。问题2相似度结果是负数如果你用了IndexFlatIP但向量没做归一化内积结果可能是负值看起来非常别扭。做查询之前把query向量也做一次L2归一化结果就是-1到1的余弦相似度语义更好解释。问题3faiss-cpu和faiss-gpu装混了FAISS的CPU和GPU版本不能同时装装了会冲突。普通场景装faiss-cpu就够用除非你确实需要在GPU上建索引、检索几千万条向量才考虑faiss-gpu。另外注意faiss这个包名已经被废弃维护统一使用faiss-cpu或faiss-gpu。6.3 Rerank和LLM输出的坑问题1Rerank模型推理太慢如果CPU跑bge-reranker-base速度不能忍优先换bge-reranker-small体积是base的三分之一左右速度更快、效果略降。另外可以把FAISS的top_k从30降到20减少Rerank的候选数延迟会明显下降。问题2LLM输出不稳定返回的JSON格式经常坏这个热搜词底下讨论很多我实际遇到过。最常见的原因是num_ctx太小导致输出被截断JSON写到一半就断了其次是temperature太高模型自由发挥导致格式错乱。解决办法是把num_ctx调到8192甚至更高把temperature调到0.1到0.2之间再不行就用JSON模式或写一个轻量的校验修复层。问题3回答内容明显没用到检索资料排查顺序是第一检查num_ctx是否足够prompt后半段被截断是最常见的原因第二打印出Rerank后的候选结果看看检索到的内容是否真的与问题相关第三检查prompt里参考下面资料回答问题的指令是否足够明确有些模型对弱指令的服从度不高要给足引导。7. 部署之后还能怎么扩展整套链路跑通、问答效果稳定之后还有几个方向可以继续做深。一是增量更新索引。目前索引是在一个大的build_index脚本里全量构建的文档一变多、一变频繁全量重建效率太低。可以把build_index改成增量模式新文档先切分、向量化然后index.add()追加到已有索引同时把文本块追加到chunks.txt这样索引规模可以持续增长。二是多路召回。现在只是用向量检索单路召回还可以加BM25关键词召回再把两路结果做融合。很多人反馈向量检索对问法很敏感换个说法就召回不到加上BM25之后关键词精确匹配能兜住向量检索的盲区。三是给检索结果加引用。在知识库问答场景里回答附上来源文档和原文片段非常重要。改法很简单在ask方法返回时把top_related里的文档名、原文一起返回前端展示的时候给每段回答挂上引用链接这个功能的信任感提升非常大。四是缓存与性能优化。高频相同问题可以直接缓存答案避免每次重新走完整链路。query向量也可以做缓存因为相同或相似query的向量计算结果是确定的省一次embedding调用的开销。最后分享一个我实际部署的心得这套组合最大的优势不是某个组件多厉害而是每一层都可以单独调试、单独替换。检索结果不准就调切分和embedding排序不准就换Rerank模型生成不准就调prompt和temperature。权责清晰排查方便。很多人一上来就想全自动端到端遇到问题抓瞎不如一开始就把每个环节的手动调试接口留好出问题的时候半小时就能定位。这套东西后续还可以往Agent方向扩展让模型自己决定什么时候检索、什么时候直接回答玩法就更多了。