尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

RAG与LLM微调实战:本地知识库智能问答系统全链路解析

发布时间:2026/9/27 23:08:15

资讯中心
01
ARTICLE

RAG与LLM微调实战:本地知识库智能问答系统全链路解析

RAG与LLM微调实战:本地知识库智能问答系统全链路解析
简介本资源面向希望深入掌握检索增强生成技术的开发者与算法学习者提供一套基于本地知识库检索与LLM微调的智能问答系统完整实战方案帮助解决通用大模型在垂直领域回答不精准、知识更新滞后的问题。压缩包共61个文件约30.12MB以31个txt数据与说明文档、13个Python脚本、7个JSON配置、3个bin模型权重及3个ipynb实验笔记为主另含界面截图与PDF资料覆盖数据预处理、向量检索、模型微调与生成融合等环节。目前已有1829人学习下载。项目源码包含ChatGLM-6B的P-Tuning v2与LoRA微调脚本、基于SBert与Miracl的向量检索实现以及WebUI交互界面并附带金融研报等本地知识库样例便于读者快速复现检索与生成融合流程理解知识库构建、检索算法与微调策略的协同设计适合作为课程设计或技术选型的参考原型。1. 从一份「RAG LLM 微调」源码包说起它到底解决了什么很多人第一次接触 RAG是因为被通用大模型的「幻觉」坑过——问它公司内部的报销标准它能一本正经地编出一套不存在的流程。RAG检索增强生成加上本地知识库检索本质就是给模型外挂一个可查的资料库用户提问先检索本地文档把命中的片段塞进上下文再让 LLM 基于这些真实片段作答。而「LLM 微调」解决的是另一个问题——检索回来的内容格式五花八门通用模型不一定能稳定地按你的业务口径输出微调就是把这个输出习惯固化下来。这套「本地知识库检索 LLM 微调」的智能问答系统适合三类人手里有一堆 PDF/Word/Markdown 内部资料、想搭个能问答的助手的工程师想搞懂 RAG 全链路切分、向量化、检索、重排、生成到底怎么串起来的开发者以及准备拿一个完整项目源码练手、跑通再改造成自己业务的人。它不追求通用大模型那种「什么都懂」而是追求「只答我资料里有的答不出来就说不知道」。下面按落地顺序把这条链路拆开讲清楚。2. 本地知识库检索链路从文档切分到向量召回2.1 为什么先做检索而不是一上来就微调一个常见误区是效果不好就想着微调模型。但绝大多数「答非所问」的根因在检索环节——要么没检索到正确片段要么检索到的片段太碎、上下文不完整。微调只能改变模型的表达风格和输出格式改变不了「它压根没看到正确资料」这件事。所以正确的顺序是先把检索链路调到能稳定召回正确片段再考虑用微调去规范输出。检索链路的核心是四步文档加载 → 切分chunking→ 向量化embedding→ 向量库检索。每一步都有参数会直接影响最终效果下面逐个落到代码。2.2 文档切分chunk_size 和 overlap 怎么定切分是整条链路里最容易被忽视、又最影响召回质量的一步。切太大一个 chunk 里混了好几个主题检索命中后噪声多切太小一句话被拦腰截断语义不完整。常见做法是按语义边界切段落、标题再叠加固定长度兜底。from langchain.text_splitter import RecursiveCharacterTextSplitter # 中文场景下分隔符优先级段落 - 换行 - 句号 - 逗号 - 字符 splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个 chunk 目标字符数 chunk_overlap80, # 相邻 chunk 重叠字符数防止语义被切断 separators[\n\n, \n, 。, , , , , ], length_functionlen, ) docs splitter.split_text(raw_text) print(f切出 {len(docs)} 个 chunk平均长度 {sum(len(d) for d in docs)//len(docs)})逻辑说明RecursiveCharacterTextSplitter会按separators列表从前往后尝试优先在段落处切切不动再退到句号、逗号。参数上中文技术文档我一般把chunk_size定在 400600 字符chunk_overlap取 chunk_size 的 15% 左右这里 80/50016%。overlap 太小会导致跨 chunk 的答案丢上下文太大则检索结果重复、浪费上下文窗口。如果文档是结构化很强的 FAQ可以按「一问一答」直接切chunk_size 设成单条问答的长度即可。2.3 向量化与向量库embedding 模型和检索参数切分完就要把每个 chunk 转成向量存进向量库。embedding 模型的选择直接决定召回质量中文场景优先选在中文语料上训练过的模型。向量库本地跑一般用 FAISS 或 Chroma前者轻量、后者带持久化和元数据过滤。from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # 中文语义 embedding本地加载不依赖外部接口 embedding HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, # 归一化后可用内积算相似度 ) vectorstore FAISS.from_texts(docs, embedding) vectorstore.save_local(./faiss_index) # 持久化下次直接 load # 检索top_k 控制召回数量score_threshold 过滤低相关结果 retriever vectorstore.as_retriever( search_typesimilarity_score_threshold, search_kwargs{k: 5, score_threshold: 0.35}, ) hits retriever.invoke(报销标准是多少) for h in hits: print(h.page_content[:80], ---)逻辑说明normalize_embeddingsTrue让向量落到单位球面上相似度计算退化成内积速度快且数值稳定。k5表示召回 5 个片段实际项目里我一般先设 58再根据上下文窗口大小调整。score_threshold是过滤阈值低于它的结果直接丢弃——这一步很关键它让「知识库里没有的问题」能返回空从而触发「我不知道」的兜底逻辑而不是硬凑一个答案。阈值需要按你的 embedding 模型实测bge 系列一般 0.30.4 起步。2.4 重排为什么召回之后还要再排一次向量检索是「粗排」它按语义相似度召回但相似不等于相关。比如问「年假怎么算」可能召回一段讲「请假流程」的文字语义很近但答非所问。重排rerank用一个更精细的交叉编码器对「问题-chunk」逐对打分把真正相关的顶上来。from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-base, use_fp16True) def rerank(query, candidates, top_n3): pairs [[query, c.page_content] for c in candidates] scores reranker.compute_score(pairs, normalizeTrue) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [c for c, s in ranked[:top_n]] final_docs rerank(报销标准是多少, hits, top_n3)逻辑说明重排模型比 embedding 慢所以只对粗排召回的少量候选这里 5 个做精排取前 3 个送进 LLM。normalizeTrue把分数归一到 01方便设阈值。加了重排之后通常能把「答非所问」的比例明显压下来代价是每次查询多几十到几百毫秒。如果对延迟敏感可以只在召回分数接近时触发重排。3. LLM 微调让模型按你的业务口径稳定输出3.1 什么时候该微调什么时候不该微调不是万能药。如果问题是「模型不知道我的资料」那是检索的活如果问题是「模型知道但答得啰嗦、格式不对、不按模板输出」那才是微调的活。判断标准很简单把正确片段手动塞进 prompt模型能不能答对能答对但格式乱微调答不对回去修检索。微调的目标通常有三个固定输出格式比如强制 JSON、统一业务话术、让模型学会「资料里没有就说不知道」这个习惯。前两个靠 prompt 也能做但稳定性不如微调第三个尤其依赖训练数据里的负样本。3.2 构造微调数据集把 RAG 链路跑出来的样本喂回去微调数据最好来自真实链路。做法是用已经调好的检索链路对一批真实问题跑出「问题 检索片段 期望答案」的三元组人工校对后作为训练样本。这样训练分布和推理分布一致效果最稳。import json def build_sample(question, contexts, answer): # 用统一的 prompt 模板保证训练和推理时输入格式一致 ctx \n\n.join(contexts) prompt ( 你是企业知识库助手只能依据下面的资料回答资料中没有的信息回答「未收录」。\n f资料\n{ctx}\n\n问题{question}\n回答 ) return {instruction: prompt, output: answer} samples [] for q, ctxs, ans in raw_qa_pairs: samples.append(build_sample(q, ctxs, ans)) # 负样本资料里确实没有答案时期望输出「未收录」 samples.append(build_sample(公司食堂几点开饭, [年假制度..., 报销流程...], 未收录)) with open(train.jsonl, w, encodingutf-8) as f: for s in samples: f.write(json.dumps(s, ensure_asciiFalse) \n)逻辑说明instruction字段里把检索片段和问题拼成完整 promptoutput是期望答案。关键点是训练时的 prompt 模板必须和推理时一模一样否则模型学到的映射对不上。负样本不能省——没有负样本模型会倾向于「硬答」把「未收录」这个能力学废。样本量上格式对齐类任务几百到一两千条就能见效业务话术类建议 2000 条以上。3.3 LoRA 微调的关键参数rank、alpha、学习率全量微调成本高本地场景一般用 LoRA低秩适配只训练一小部分参数。核心参数是r秩、lora_alpha、learning_rate这三个直接决定微调是「没学到」还是「学过头」。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, # 秩越大容量越强8~16 是常见起点 lora_alpha16, # 缩放系数一般取 2*r target_modules[q_proj, v_proj], # 只挂到注意力投影层 lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) model get_peft_model(base_model, lora_config) model.print_trainable_parameters() # 确认可训练参数占比通常 1%逻辑说明r8是保守起点任务简单只改格式够用如果发现模型学不会业务话术可以加到 16 甚至 32但要注意过拟合。lora_alpha一般设成2*r它和r一起决定 LoRA 更新的幅度。target_modules只挂q_proj、v_proj是最省显存的常见做法想更强可以加上k_proj、o_proj。训练超参上学习率我一般从2e-4起跑 23 个 epoch观察验证集 loss 不再下降就停。3.4 微调后的模型怎么接回 RAG 链路微调产出的是 LoRA 权重推理时把它和基座模型合并替换掉原来 RAG 链路里的生成模型即可。检索部分完全不用动。from peft import PeftModel base load_base_model(your-base-model) model PeftModel.from_pretrained(base, ./lora_output) model model.merge_and_unload() # 合并权重推理更快 def rag_answer(question): docs rerank(question, retriever.invoke(question), top_n3) if not docs: return 未收录 ctx \n\n.join(d.page_content for d in docs) prompt f你是企业知识库助手只能依据资料回答。\n资料\n{ctx}\n\n问题{question}\n回答 return model.generate(prompt)逻辑说明merge_and_unload()把 LoRA 权重合并进基座省去推理时的额外计算。rag_answer里先检索、再判断是否为空、最后生成这就是完整的 RAG 微调闭环。注意 prompt 模板要和训练时严格一致否则微调白做。4. 避坑与排查这套链路最容易翻车的五个地方4.1 检索召回为空模型却硬答现象问一个知识库里没有的问题模型编了一个答案。原因检索返回了低相关片段或者score_threshold设得太低噪声片段被当成有效上下文。解决把score_threshold调高实测并在 prompt 里明确「资料中没有的信息回答未收录」同时用负样本微调强化这个行为。4.2 chunk 切太碎答案被拦腰截断现象明明资料里有答案检索也命中了但模型答得不完整。原因答案跨了两个 chunk只召回了其中一半。解决加大chunk_overlap或改用按语义/标题切分检索时把相邻 chunk 一起带进上下文。4.3 微调后模型「失忆」通用能力下降现象微调后格式是规范了但稍微换个问法就不会答了。原因学习率太大或 epoch 太多模型过拟合到训练样本的固定句式。解决降学习率到1e-4减少 epoch增加训练样本的问法多样性。4.4 训练和推理 prompt 不一致现象训练时 loss 降得很好上线效果却很差。原因训练用的 prompt 模板和推理时拼的不一样模型学到的映射对不上。解决把 prompt 模板抽成一个函数训练和推理共用同一份。4.5 向量库更新后检索结果错乱现象新增文档后老问题的检索结果变差。原因新旧文档用了不同的 embedding 模型或索引没重建。解决embedding 模型一旦确定就不要换新增文档后重建索引或确认增量写入用的是同一模型。5. 进阶技巧用「检索命中率」这个指标把链路调稳调 RAG 最怕凭感觉。我一般先建一个几十条的小评测集每条包含「问题 标准答案所在的 chunk」然后只测检索环节的命中率——正确 chunk 有没有进 top_k。这个指标比端到端效果更早暴露问题也更好定位。def hit_rate(eval_set, retriever, k5): hit 0 for q, gold_chunk_id in eval_set: docs retriever.invoke(q)[:k] if any(d.metadata.get(chunk_id) gold_chunk_id for d in docs): hit 1 return hit / len(eval_set) # 先调 chunk_size / overlap / k / threshold把命中率拉到 0.9 以上 for k in [3, 5, 8]: print(k, hit_rate(eval_set, retriever, k))逻辑说明gold_chunk_id是标准答案所在 chunk 的编号检索结果里只要包含它就记命中。命中率低于 0.9 时先别碰微调回去调切分和检索参数。命中率上去了再上重排和微调端到端效果才有保障。调优阶段关注指标典型目标切分chunk 平均长度、跨 chunk 答案比例平均 400600 字符检索top_k 命中率≥ 0.9重排精排后 top_3 命中率≥ 0.95微调格式合规率、未收录准确率格式 ≥ 0.98一个血泪经验别一上来就堆模型和参数先把检索命中率这个「黑匣子」打开看。我踩过最深的坑是花了两天调微调最后发现根因是 chunk 切太碎导致检索根本没召回对。从那以后我调任何 RAG 项目都先跑一遍命中率评测再动别的。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。