从零搭建本地 RAG 系统:200 行 Python 让你的模型“带着资料回答问题“

从零搭建本地 RAG 系统:200 行 Python 让你的模型“带着资料回答问题“
副标题省 token 不只是省钱——在你的 6GB 显卡上RAG 就是一剂显存解药一、引子模型很聪明但它会忘在上一篇 KV Cache 优化中我们花了很大力气去压缩显存占用。量化、Flash Attention、GQA 架构对比——所有手段的核心都是在有限显存里塞进更长的上下文。但有没有想过这个问题为什么一定要把整篇文档塞进去用户问一个问题往往只需要文档中的两三段相关信息。你把整本手册 50 页全塞进上下文KV Cache 自然爆炸。这就是 RAG 的切入点——检索增强生成。纯模型方案 用户问问题 → 凭训练数据里的记忆回答 → 知识截止、幻觉 ❌ RAG 方案 用户问问题 → 从知识库里检索相关段落 → 带着资料推理 → 准确、可溯源 ✅省 token的两种含义如果你用云 APIGPT-4o 约 $2.5/百万 token省 token 省钱。每次多塞 1000 token一万次调用就是 $25。如果你像我一样用本地模型GTX 1660 Ti 6GB省 token 省显存。1000 token 的 prompt 对应约 144 MiB 的 KV CacheQwen3-8B 数据省出来的是能不能跑起来的区别。RAG 同时做成了这两件事。它不只是提高回答质量更从源头减少了需要喂给模型的 token 量。本文的目标用大约 60 行 Python在我们的本地环境上搭出一个完整的 RAG Pipeline。二、环境准备依赖只有四个包pipinstallchromadb sentence-transformers langchain-text-splitters requests包用途chromadb向量数据库零部署数据存本地磁盘sentence-transformers加载 embedding 模型把文字变成向量langchain-text-splitters文本分块只用这一个子模块requests调 llama.cpp 的 HTTP APIEmbedding 模型我们选 BAAI/bge-small-zh-v1.5只有 33MBCPU 上跑单条只需 ~5ms。它不是大模型是专门为语义相似度训练的小模型——术业有专攻。验证环境# 验证 embeddingpython-cfrom sentence_transformers import SentenceTransformer m SentenceTransformer(BAAI/bge-small-zh-v1.5) print(m.encode(你好).shape)# 输出: (512,)# 验证推理端点curlhttp://localhost:8080/health# 输出: {status:ok}知识库素材用我们前面 5 篇博客的 Markdown 文件。自己写的内容自己检索——方便验证回答质量。三、核心 Pipeline——入库篇把文档变成向量存进 Chroma分三步。Step 1读取文档fromglobimportglob DATA_DIRdata/blog_postsdocs[]forfinsorted(glob(f{DATA_DIR}/*.md)):withopen(f)asfh:docs.append({title:f,content:fh.read()})Step 2文本分块一篇文章几千字不能整个塞给 embedding 模型——太长了语义不聚焦。需要切成小块fromlangchain_text_splittersimportRecursiveCharacterTextSplitter splitterRecursiveCharacterTextSplitter(chunk_size256,# 每块 ~256 字符chunk_overlap20,# 块间重叠 20 字符避免切断关键句separators[\n\n,\n,。,, ,],)chunks[]fordocindocs:textssplitter.split_text(doc[content])fortintexts:chunks.append({title:doc[title],content:t})这里chunk_size256是按字符算的对于中文大概相当于几十个词。太小了语义碎片化太大了一个块包含多主题——第 7 篇会专门实验这个参数。Step 3Embedding 写入 Chromaimportchromadbfromsentence_transformersimportSentenceTransformer embedderSentenceTransformer(BAAI/bge-small-zh-v1.5)clientchromadb.PersistentClient(path./chroma_db)collectionclient.create_collection(blog_knowledge_base)fori,chunkinenumerate(chunks):# bge-small 官方推荐检索任务加前缀textf为这个句子生成表示以用于检索相关文章{chunk[content]}vecembedder.encode(text).tolist()collection.add(embeddings[vec],documents[chunk[content]],metadatas[{source:chunk[title]}],ids[fchunk_{i}],)print(f入库完成{len(chunks)}个片段)运行输出[1/3] 读取文档... 读取: 01-local-llm-deployment.md (13245 chars) 读取: 02-agent-capabilities.md (7685 chars) 读取: 03-agent-task-decomposition.md (13997 chars) 读取: 04-llama-cpp-exploration.md (8510 chars) 读取: 05-kv-cache.md (9984 chars) [2/3] 分块... 分块完成: 5 篇 → 284 块 (chunk_size256) [3/3] 构建知识库... 写入: 16/284 ... 写入: 284/284 知识库构建完成! 共 284 个片段 ✅ 完成!现在每篇博客都被切成了几十个小块每个小块都被 bge-small 变成了一个512 维的向量存在 Chroma 里。四、核心 Pipeline——检索 推理篇入库是为了查询。当用户提问时Pipeline 的后半段开始工作。Step 4检索defretrieve(query,top_k3):# 用户问题也经过同样的 embeddinginput_textf为这个句子生成表示以用于检索相关文章{query}vecembedder.encode(input_text).tolist()# Chroma 按余弦相似度找出最像的 top-k 条resultscollection.query(query_embeddings[vec],n_resultstop_k,)returnresults[documents][0],results[metadatas][0]关键点用户问题和文档用的是同一个 embedding 模型所以它们在一个向量空间里。检索就是在这个空间里找最近的邻居。Step 5拼 Prompt 推理defrag_pipeline(query,top_k3):chunks,sourcesretrieve(query,top_k)promptf基于以下资料回答问题。如果资料中 没有相关信息请如实说资料中未找到相关信息。 资料{chr(10).join(f[{i1}]{c}fori,cinenumerate(chunks))}问题{query}回答# 调本地 llama-serverOpenAI 兼容接口resprequests.post(http://localhost:8080/v1/chat/completions,json{messages:[{role:user,content:prompt}]},)returnresp.json()[choices][0][message][content]Step 6跑一个完整示例python rag_pipeline.py什么是 KV Cache输出问题: 什么是 KV Cache 来源: 05-kv-cache.md, 01-local-llm-deployment.md 上下文约 146 tokens 回答: KV Cache 是一种用于加速 LLM 推理的缓存技术它通过存储 中间计算结果Key/Value 矩阵避免了每次生成新 token 时重复 计算历史上下文。在 6GB 显存中模型参数占了约 4.688 MiB 计算缓存约 800 MiB剩余约 656 MiB 给 KV Cache...同样的问法不加 RAG 试试python rag_pipeline.py什么是 KV Cache--no-rag输出回答: KV缓存即键值对缓存是一种高效的数据存储和访问机制 广泛应用于Web开发、分布式系统等领域...看出来了吗没有 RAG 时模型把KV Cache理解成了 Web 开发中的键值对缓存——完全是另一个东西。加上 RAG 后它从博客中找到了 LLM 推理场景下的精确定义和具体数字。一字之差天壤之别。五、质量评估——RAG 真的有用吗我设计了 3 个递进式的问题来验证测试问题考察点①“什么是 Flash Attention”模型已知的知识RAG 能否补充细节②“llama.cpp 的 --cache-type-v 参数有什么作用”特定参数知识截止期问题③“DeepSeek-7B 在 ctx8192 时 KV Cache 占多少”定量问题模型最容易幻觉的场景结果如下测试①Flash Attention版本回答质量无 RAG基本正确。“一种优化的注意力机制提高计算效率……”——泛化但没错RAG略有补充。“分解为更小的矩阵操作减少内存依赖……”RAG 价值2/5。模型训练数据里已经包含 Flash Attention 的概念RAG 补充不多。测试②–cache-type-v 参数版本回答质量无 RAG完全错误。“用于指定缓存的类型如文件缓存或内存缓存……”RAG正确。“控制 V 部分的量化类型影响显存使用效率。”RAG 价值5/5。这个参数是 llama.cpp 特有的模型训练数据截止期之后才出现。无 RAG 时模型只能瞎猜RAG 从博客原文中找到了准确定义。测试③DeepSeek-7B 的 KV Cache 显存版本回答质量无 RAG严重幻觉。“大约 10GB 到 20GB 的显存。”RAG精确。“DeepSeek-7B 在 ctx8192 时 KV Cache 占 448 MiB。”RAG 价值5/5。定量问题是模型最薄弱的环节——它不可能记住每个模型在不同配置下的 KV Cache 占用。RAG 直接检索到我们上一篇博客中的精确计算结果。小结无 RAG 有 RAG ──────────────────────────────────── 测试①基本正确 → 略有补充 △差距小 测试②完全错误 → 正确 ✅知识截止期 测试③10-20GB → 448 MiB ✅定量问题3 个测试中 2 次产生质的飞跃。RAG 在特定参数解释和定量问题上表现最强——恰好是通用 LLM 最薄弱的环节。六、云 API 对照如果你用的不是本地模型而是云 APIRAG 的搭建流程完全不变——只需换一行代码# 本地 llama.cppresprequests.post(http://localhost:8080/v1/chat/completions,jsonpayload)# 改成 OpenAIresprequests.post(https://api.openai.com/v1/chat/completions,headers{Authorization:fBearer{os.environ[OPENAI_API_KEY]}},jsonpayload,)Chroma 不变bge-small 不变分块逻辑不变。RAG 架构与推理引擎解耦。无论你用的是本地模型、OpenAI、Claude 还是其他 APIRAG 的核心骨架是一样的。七、总结200 行 Python一个完整的本地 RAG 系统文档 → 切分 → bge-small 编码 → Chroma 向量库 ↑ 用户提问 → bge-small 编码 ────────────┘ ↓ 检索 top-3 → 拼 Prompt → llama.cpp → 回答三个核心收获RAG 不只是提高质量更是省显存。把整篇文档塞进上下文 vs 只检索最相关的几段——KV Cache 相差几十倍。RAG 解决的是模型能答对的问题。前面 5 篇解决了模型能跑和模型能调优RAG 在此基础上让模型能引用真实资料。RAG 适用于任何推理引擎。本地部署和云 API 的 RAG 架构一模一样只差一行 endpoint 配置。预告系统搭好了但参数是调出来的。下一篇我们会做4 组对比实验——分块大小、top-k 数量、KV Cache 影响、以及Python (Chroma) vs C (FAISS) 的检索性能对比——用数据回答RAG 配置怎么调最好。附录完整代码本文所有代码在 GitHub 上RAG 项目代码TODO: 发布后补充链接系列全篇CSDN从零到一用 AI Agent 辅助在 6GB 显卡上本地部署大模型实战 — 部署全流程只有 B 级能力的大模型怎么干出 A 级的活 — 任务拆解方法论Agent 不是更聪明的模型而是长了手脚的模型 — Agent 能力框架从 Ollama 到 llama.cpp一次降一层的本地推理探索 — 推理引擎对比KV Cache 优化实战6GB 显存上的每一 MB 都算数 — 上下文优化[从零搭建本地 RAG 系统200 行 Python 让你的模型带着资料回答问题] — 本文