本地知识库问答机器人实战:LlamaIndex + Qwen1.5 搭建 RAG 系统
发布时间:2026/10/27 19:53:08 锦皓数字建站

本地知识库问答机器人这个方向我从去年开始断断续续折腾了好几轮从最早的纯向量检索方案到后面加上重排序、查询改写踩过的坑基本能写一本小册子。这次拿 LlamaIndex 配 Qwen1.5 重新搭了一套跑下来效果比预期稳不少推理全部落在本地文档不出内网响应速度也能接受。这篇就把整个搭建过程、参数取舍和实际踩坑记录整理出来给同样想在自己机器上跑一套私有问答系统的朋友做个参考。先说清楚这套东西是什么它本质上是一个 RAG检索增强生成系统把本地的一批文档切碎、向量化、存进索引用户提问时先检索出最相关的片段再连同问题一起喂给本地部署的 Qwen1.5 生成答案。能做什么企业内部文档问答、个人笔记检索、产品手册查询这类场景都能覆盖。适合谁看有一定 Python 基础、手里有台带独立显卡的机器、想自己掌控数据流向的开发者。如果你只是想快速体验一下 RAG 效果这套方案也能跑但硬件门槛得先掂量清楚。1. 整体方案设计与选型考量1.1 为什么是 LlamaIndex 加 Qwen1.5 这个组合选型这件事我一开始也纠结过。LangChain 生态更大、教程更多但它的抽象层太厚一个简单的检索问答要套好几层 Chain调试的时候堆栈信息看得人头皮发麻。LlamaIndex 的定位更聚焦它就是冲着索引 检索 生成这条链路设计的核心概念清晰Document、Node、Index、QueryEngine一层层往下走出问题容易定位。对于知识库问答这种单一目标场景LlamaIndex 的路径更短代码量也更少。Qwen1.5 这边选它的理由很实际。第一是中文能力强本地知识库大概率是中文文档用英文为主的模型做中文问答效果会打折扣。第二是尺寸档位全0.5B 到 72B 都有我手头是一张 24G 显存的卡跑 7B 的量化版本绰绰有余14B 的 4bit 量化也能塞进去。第三是社区支持好Hugging Face 上直接能拉到配合 transformers 或 llama.cpp 都能跑部署路径成熟。提示模型尺寸不是越大越好。7B 在知识库问答场景下只要检索质量过关生成质量通常够用盲目上大模型反而拖慢响应交互体验变差。1.2 检索链路的分层设计思路整套系统的核心链路我拆成四层文档解析层、索引构建层、检索召回层、生成回答层。这么拆不是为了好看是为了每一层都能单独调、单独测。文档解析层负责把 PDF、Word、Markdown 这些异构格式统一成纯文本这里最容易出问题的是 PDF 的表格和分栏排版解析出来经常是乱的。索引构建层做切块和向量化切块大小直接决定检索粒度这个参数后面细说。检索召回层是效果的关键我用了向量检索加关键词检索的混合策略再叠一层重排序。生成回答层就是把检索结果拼进 prompt交给 Qwen1.5 输出。为什么要在检索层做混合纯向量检索对语义相近的表达很友好但对专有名词、编号、型号这类精确匹配的东西不敏感。比如你问XX-2000 型号的参数向量检索可能召回一堆语义相关但型号不对的段落。加上关键词检索BM25 那类做互补召回率能明显提升。这个设计是我踩过坑之后加的后面问题排查那节会具体讲。1.3 本地部署的硬件与依赖盘点硬件这块给个实测参考7B 模型 4bit 量化显存占用大概 5 到 6G加上嵌入模型和重排序模型总共 8G 左右能跑起来。嵌入模型我用的是 bge-small-zh几百兆的体量CPU 也能跑但慢建议放 GPU。重排序模型 bge-reranker-base 也是几百兆级别。如果你的文档量在几千页以内16G 内存加 8G 显存的机器足够。依赖方面核心就几个llama-index 主包、llama-index-llms-huggingface、llama-index-embeddings-huggingface、transformers、torch、sentence-transformers。版本上我建议锁一下LlamaIndex 迭代很快不同版本 API 有 breaking change我用的 0.10.x 系列接口相对稳定。torch 记得装对应 CUDA 版本的不然 GPU 用不上白瞎硬件。2. 核心环节拆解与关键参数2.1 文档切块Chunk Size 到底怎么定切块是 RAG 里最容易被低估的环节。切太大一个块里混了好几个主题检索出来噪声多模型容易被无关内容带偏切太小上下文不完整模型拿到的信息碎片化答非所问。我的经验值是中文文档 512 到 768 个 token 之间overlap 设 50 到 100 个 token。为什么留 overlap因为切块是按固定长度硬切的一个完整的句子或段落很可能被切在边界上overlap 能让边界附近的内容在两个块里都出现避免信息断裂。这个道理跟剪视频留重叠帧一样防止关键画面被切掉。具体到 LlamaIndex 的配置用 SentenceSplitter参数这样设from llama_index.core.node_parser import SentenceSplitter splitter SentenceSplitter( chunk_size600, chunk_overlap80, paragraph_separator\n\n, secondary_chunking_regex[^,.;。][,.;。]? )secondary_chunking_regex这个参数值得说一下它让切块尽量在标点处断开而不是硬切在句子中间。中文的句号、问号、感叹号都覆盖进去切出来的块读起来更完整。实测下来加了这层标点感知检索命中率能提升几个百分点尤其是问答类文档。注意不同文档类型要区别对待。技术手册这种条目化的内容切块可以小一点400 左右就够叙述性的长文档600 到 800 更合适。一刀切的参数在混合文档库里效果会打折。2.2 嵌入模型的选择与向量维度权衡嵌入模型决定了语义相似这件事算得准不准。中文场景我优先推荐 bge 系列bge-small-zh-v1.5 是性价比之选512 维向量速度快效果在中小规模知识库上够用。如果追求更高精度bge-large-zh 是 1024 维检索质量更好但索引构建慢、存储占用大。维度这个事要算笔账假设你有 1 万个文档块512 维用 float32 存储大概是 10000 × 512 × 4 字节约 20MB1024 维就是 40MB。单看不大但向量检索时的距离计算量是跟维度成正比的维度翻倍检索延迟也差不多翻倍。所以不是维度越高越好得在精度和速度之间找平衡。LlamaIndex 里接 HuggingFace 嵌入模型from llama_index.embeddings.huggingface import HuggingFaceEmbedding embed_model HuggingFaceEmbedding( model_nameBAAI/bge-small-zh-v1.5, max_length512, embed_batch_size32 )embed_batch_size这个参数跟显存挂钩32 是个稳妥值显存富裕可以往上调能加快索引构建速度。max_length要和切块大小匹配切块 600 token 但嵌入模型只吃 512超出的部分会被截断等于白切。这个细节很多人忽略导致长块的信息实际没进向量。2.3 混合检索与重排序的叠加策略前面提了混合检索的必要性这里说具体实现。LlamaIndex 支持 VectorIndexRetriever 和 BM25Retriever 组合用 QueryFusionRetriever 做融合。融合策略有两种相对分数融合和倒数排名融合我用的是后者它对不同检索器的分数尺度不敏感更稳。from llama_index.core.retrievers import VectorIndexRetriever, BM25Retriever from llama_index.core.retrievers import QueryFusionRetriever from llama_index.core.retrievers import RecursiveRetriever vector_retriever VectorIndexRetriever(indexvector_index, similarity_top_k10) bm25_retriever BM25Retriever.from_defaults(indexvector_index, similarity_top_k10) fusion_retriever QueryFusionRetriever( [vector_retriever, bm25_retriever], similarity_top_k8, num_queries1, modereciprocal_rerank )similarity_top_k这里设 10 是召回阶段融合后取 8 个进重排序。为什么召回要多、进重排要少召回阶段宁可多捞一些把可能相关的都拿进来重排序阶段再用更精细的模型筛。重排序模型 bge-reranker-base 是交叉编码器它把问题和每个候选块拼在一起算相关性分数比向量点积准得多但计算量大所以只对少量候选做。重排序的接入from llama_index.postprocessor.flag_embedding_reranker import FlagEmbeddingReranker reranker FlagEmbeddingReranker( modelBAAI/bge-reranker-base, top_n4 )top_n4意味着最终只把 4 个最相关的块喂给生成模型。这个数字不是拍脑袋定的块太多会稀释关键信息还可能超出模型上下文窗口块太少又可能漏掉答案。4 到 5 个是我实测下来比较均衡的值。2.4 Qwen1.5 的加载与推理参数调优模型加载用 transformers 直接拉7B 的 Chat 版本 4bit 量化from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig import torch bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_compute_dtypetorch.float16, bnb_4bit_quant_typenf4, bnb_4bit_use_double_quantTrue ) tokenizer AutoTokenizer.from_pretrained(Qwen/Qwen1.5-7B-Chat) model AutoModelForCausalLM.from_pretrained( Qwen/Qwen1.5-7B-Chat, quantization_configbnb_config, device_mapauto, trust_remote_codeTrue )nf4量化类型配合双重量化能在几乎不掉效果的前提下把显存压下来。device_mapauto让 transformers 自己分配层到 GPU单卡场景够用。推理参数上知识库问答场景我建议 temperature 设 0.1 到 0.3低温度让输出更确定、更贴事实减少胡编。top_p 设 0.8max_new_tokens 设 512 到 768够回答大多数问题又不至于生成冗长废话。repetition_penalty 设 1.05 到 1.1防止模型陷入重复循环。提示知识库问答最怕模型自由发挥。prompt 里一定要明确约束仅根据提供的上下文回答上下文没有的信息就说不知道这比调温度更管用。3. 完整搭建流程与实操记录3.1 环境准备与依赖安装先建虚拟环境Python 3.10 是我验证过最稳的版本3.11 也能跑但个别包有兼容问题。依赖安装顺序有讲究torch 要先装且要装 CUDA 版本后面装 transformers 和 sentence-transformers 时它们会检测已有的 torch不会重复拉 CPU 版。conda create -n rag-qa python3.10 -y conda activate rag-qa pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install llama-index llama-index-llms-huggingface \ llama-index-embeddings-huggingface \ llama-index-postprocessor-flag-embedding-reranker \ transformers accelerate bitsandbytes sentence-transformers \ pypdf docx2txtaccelerate和bitsandbytes是量化加载的依赖别漏。pypdf和docx2txt是文档解析用的按你实际文档格式增减。装完跑一句torch.cuda.is_available()确认 GPU 能用返回 False 就说明 CUDA 版本对不上得重装。3.2 文档加载与预处理文档加载用 SimpleDirectoryReader它能自动识别目录下的多种格式from llama_index.core import SimpleDirectoryReader documents SimpleDirectoryReader( input_dir./knowledge_base, required_exts[.pdf, .docx, .md, .txt], recursiveTrue ).load_data()recursiveTrue会递归子目录适合文档按主题分文件夹存放的情况。加载完建议先打印几段看看解析质量PDF 尤其要检查扫描版 PDF 解析出来可能是空的那种得先做 OCR这套流程不覆盖。预处理这一步我加了个小动作清洗掉多余的空行和页眉页脚。很多 PDF 每页都有重复的页眉不清掉会污染检索结果模型可能把页眉当成正文内容。简单用正则处理一下import re def clean_text(text): text re.sub(r\n{3,}, \n\n, text) text re.sub(r^\s*第\s*\d\s*页.*$, , text, flagsre.MULTILINE) return text.strip() for doc in documents: doc.text clean_text(doc.text)这个清洗逻辑是按常见页眉格式写的你实际文档的页眉样式不同就改正则。别小看这一步我第一版没做清洗检索出来的结果里混着页眉回答质量明显受影响。3.3 索引构建与持久化索引构建是把文档块向量化并建索引的过程用 VectorStoreIndexfrom llama_index.core import VectorStoreIndex, Settings, StorageContext from llama_index.core import load_index_from_storage Settings.embed_model embed_model Settings.node_parser splitter Settings.llm None # 构建阶段不需要 LLM index VectorStoreIndex.from_documents(documents, show_progressTrue) index.storage_context.persist(persist_dir./index_storage)Settings.llm None这行是个小技巧构建索引时用不到生成模型设成 None 能避免它去加载模型省显存也省时间。构建完持久化到磁盘下次启动直接加载不用重新算向量storage_context StorageContext.from_defaults(persist_dir./index_storage) index load_index_from_storage(storage_context)持久化目录里会生成几个 json 和向量文件文档量大的话这个目录可能几百兆正常。索引构建时间跟文档量和硬件相关我这边 2000 页左右的文档7B 卡上大概跑了十几分钟主要耗在向量化上。3.4 查询引擎组装与 Prompt 设计把检索器、重排序器、生成模型串起来组成查询引擎from llama_index.core.query_engine import RetrieverQueryEngine from llama_index.core import PromptTemplate qa_prompt PromptTemplate( 你是一个严谨的知识库问答助手。请仅根据下面提供的上下文回答问题。\n 如果上下文中没有相关信息直接回答\根据现有资料无法回答该问题\不要编造。\n 回答时尽量引用上下文中的原文表述。\n\n 上下文\n{context_str}\n\n 问题{query_str}\n 回答 ) query_engine RetrieverQueryEngine.from_args( retrieverfusion_retriever, node_postprocessors[reranker], text_qa_templateqa_prompt, llmllm )prompt 里那几句约束是效果的关键。仅根据上下文防止模型用预训练知识瞎答无法回答就说不知道降低幻觉率引用原文让答案更可追溯。这三条我反复调过措辞现在的版本在测试集上幻觉率明显低于不加约束的版本。查询调用很简单response query_engine.query(你们产品的保修期是多久) print(response)返回的 response 对象里还带 source_nodes能看到检索到了哪些块调试时很有用。3.5 流式输出与交互体验优化生成模型出字慢一次性等完整答案体验差改成流式输出response query_engine.query(保修政策是什么) response.print_response_stream()流式输出让用户看到字一个个蹦出来感知延迟大幅降低。如果要做成 Web 服务把流式接口接到前端 SSE 上就行LlamaIndex 的 response 对象支持迭代取 token。另外我加了个简单的多轮对话支持用 CondenseQuestionChatEngine它会把历史对话压缩成一个独立问题再检索避免指代消解问题。比如用户先问保修期多久再问那延保呢第二个问题单独看没有主语压缩后会变成延保的保修期是多久检索才准。4. 常见问题排查与避坑实录4.1 检索不准的典型原因与排查路径检索不准是最高频的问题表现是答非所问或者答无法回答。排查我按这个顺序走先看 source_nodes 里召回了什么如果召回的内容本身就不相关问题在检索层如果召回相关但答案没生成对问题在生成层。检索层的问题又分几种。切块太大导致噪声多就调小 chunk_size嵌入模型对领域词汇不敏感考虑换更大模型或做微调专有名词匹配不上检查 BM25 检索器有没有正常工作。我遇到过一次 BM25 没生效原因是中文分词没配好BM25 默认按空格分词中文整句被当成一个词等于没检索。解决办法是给 BM25 配个中文分词器或者用 jieba 预处理。生成层的问题多半是 prompt 没约束好或者 top_n 太大把无关内容喂进去了。把 top_n 从 6 降到 4答案聚焦度会明显提升。4.2 显存溢出与推理速度优化显存溢出常见于三个环节模型加载、索引构建、并发查询。模型加载溢出就降量化位数或者换小模型索引构建溢出就把 embed_batch_size 调小并发查询溢出就得做请求队列别让多个查询同时占显存。推理速度优化我试过几个手段。一是开 flash attentiontransformers 里加attn_implementationflash_attention_2速度有提升但要求显卡架构支持。二是用 vLLM 替代 transformers 做推理后端吞吐量提升明显但部署复杂度高一些单用户场景收益不大。三是把 max_new_tokens 压到实际需要的最小值很多回答 300 token 就够设 768 是浪费。下面这张表是我实测的几个配置对比供参考配置项保守配置均衡配置性能配置模型Qwen1.5-7B 4bitQwen1.5-7B 4bitQwen1.5-14B 4bit嵌入模型bge-small-zhbge-small-zhbge-large-zhchunk_size400600600top_k 召回81012top_n 重排345显存占用约 6G约 8G约 14G单次响应2-3 秒3-5 秒6-10 秒4.3 模型幻觉的抑制手段幻觉就是模型编造上下文里没有的信息这在知识库问答里是致命的。抑制手段我总结了四条按有效性排序prompt 约束最直接明确说不知道就说不知道降低 temperature 减少随机性提高检索质量让上下文里真有答案加后处理校验对生成的答案做一次事实性检查。第四条实现起来麻烦我目前用的是轻量版让模型在回答末尾标注引用的上下文片段编号人工抽检时能快速定位答案来源。如果答案引用的片段跟内容对不上基本就是幻觉。注意不要指望完全消除幻觉。7B 级别的模型在复杂问题上仍会出错关键场景建议保留人工复核环节或者把答案置信度低的情况标记出来。4.4 中文文档解析的坑PDF 解析的坑最多。分栏排版的 PDF解析出来文字顺序是乱的左右栏内容交错在一起。表格解析出来是一堆错位的文本。扫描版 PDF 直接是空白。这几种情况我的处理方式是分栏文档用 pdfplumber 按坐标重排表格用 camelot 单独抽扫描版走 OCR 流程。这些都不在 LlamaIndex 默认能力里得自己补。Word 文档相对好处理docx2txt 基本够用但要注意图片里的文字抽不出来如果文档关键信息在图里得单独处理。Markdown 最省心本身就是结构化文本解析几乎无损。还有一个隐蔽的坑编码问题。有些老文档是 GBK 编码直接读会乱码加载时指定 encoding 或者先转码。这个坑我踩过一次索引建完了发现全是乱码白跑一遍。4.5 常见问题速查表现象可能原因排查动作解决方向答无法回答检索没召回相关内容看 source_nodes调切块/换嵌入模型/开混合检索答非所问召回噪声多检查 top_n 和 chunk_size降 top_n/调小切块答案编造prompt 约束不足检查 prompt 模板加强约束/降温度中文检索失效BM25 分词问题测 BM25 单独召回配中文分词器显存溢出批量太大/模型太大看报错环节降 batch/降量化/换小模型响应太慢生成 token 太多看输出长度压 max_new_tokens/换推理后端解析乱码编码不匹配检查原始编码指定 encoding 转码索引加载失败版本不兼容看报错信息锁依赖版本/重建索引这套系统我前后调了大概两周最大的体会是RAG 的效果上限由检索质量决定生成模型只是把检索到的信息组织成通顺的话。很多人一上来就换更大的模型其实先把切块、嵌入、重排序这三步调好7B 模型也能给出很靠谱的答案。另外索引一定要持久化每次重建既费时又费显存调参阶段尤其明显。最后分享一个小技巧建一个几十条的测试问答对每次改参数都跑一遍用命中率量化效果比凭感觉调靠谱得多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。