用 200 行 Python 搭一个全本地 RAG:一次笔记本工程师的踩坑实录

用 200 行 Python 搭一个全本地 RAG:一次笔记本工程师的踩坑实录

没有 API Key,没有云服务,一台 RTX 4050 的笔记本从零跑起来的 RAG demo。 代码地址:github.com/spark-prair...

起因

最近在学大模型相关的工程实践,RAG(Retrieval-Augmented Generation,检索增强生成)是绕不开的一块。

我不想从"调用 OpenAI API + LangChain"开始------那会让我学到的是"怎么用某个框架",而不是"RAG 到底是什么"。我想要的是:

  • 完全本地运行,不依赖任何云服务(省钱,也是为了数据隐私)
  • 核心代码集中在一个文件里,方便反复读、反复改
  • 能跑得动,但不要追求 SOTA(笔记本上跑 70B 是不现实的)

于是花了一个周末,做了一个总共只有四个依赖包、核心代码 200 行的 RAG demo。这篇文章记录从选型到踩坑的完整过程。

一句话 RAG

在开始之前先把概念扯清楚。RAG 的本质可以用一句话概括:

让大模型在回答之前,先从你自己的文档里"查资料"。

流程只有六步:

复制代码
加载文档 → 切分成 chunk → 向量化 → 入向量库 → 相似度检索 → 喂给 LLM 生成答案

第一步到第四步是"建索引",做一次就好;第五、六步是每次提问都要做的。

为什么要这么绕?因为大模型本身有三个硬伤:

  • 知识截止:训完之后的事它不知道
  • 幻觉:不确定的时候会编造合理的答案
  • 私有数据:你公司的文档、你的个人笔记都不在公开语料里

RAG 通过"先查再答"把这三个问题都缓解了------知识在外挂的向量库里,想更新就重新入库,不用重新训模型。

技术选型

我的硬件是 RTX 4050 Mobile(6GB 显存)+ 32GB 内存。选型原则是**"小而够用"**:

组件 选型 理由
LLM Ollama + qwen2.5:3b Ollama 把"本地跑大模型"做到一条命令;3B 模型量化后约 2GB 显存,RTX 4050 能跑
Embedding BAAI/bge-small-zh-v1.5 约 100MB,CPU 都能跑;中文效果在同量级里是第一梯队
向量库 ChromaDB 文件型数据库,pip install 完就能用
Web UI Gradio 几行代码出一个像模像样的聊天页面

特意没用 LangChain/LlamaIndex。它们适合快速搭产品,但会把 RAG 的每一步藏在抽象层后面,学习阶段反而看不清到底发生了什么。等我把每一步都手搓一遍、理解透了,再用框架才有意义。

核心代码长什么样

整个 RAG pipeline 就是一个 RAGPipeline 类,六个方法对应六个步骤。挑关键点讲讲。

切分(split_text):起手最简单的做法是"每 500 字切一段,相邻段重叠 50 字"。重叠是为了避免一句话正好被切在两段中间,两边都捞不到关键信息。

为什么是 500 字?这是个经验折中------chunk 太大语义稀释(检索时匹配不到真正相关的东西),太小又缺上下文(模型看不到完整意思)。

向量化(index):每个 chunk 过一遍 BGE 模型,变成一个固定长度的向量,塞进 Chroma。这里有两个容易踩的细节:

python 复制代码
embeddings = self.embedder.encode(
    all_chunks,
    normalize_embeddings=True,  # 归一化到单位长度
).tolist()

self.collection = self.client.get_or_create_collection(
    name=collection_name,
    metadata={"hnsw:space": "cosine"},  # 必须指定用余弦距离
)

Chroma 默认用的是欧氏距离(L2),对文本相似度不合适,要手动指定成 cosine。我第一次跑的时候没注意这个,检索出来的结果莫名其妙,排查了半小时才发现。

检索(retrieve) :用户问题过同一个 embedding 模型转成向量,然后在 Chroma 里找 top-k 最相似的。同一个模型这点很关键------不然向量空间对不上,结果全是乱的。

生成(generate):RAG 的灵魂在 prompt 里这两句:

erlang 复制代码
你是一个严谨的问答助手. 请只根据下面提供的资料片段回答用户问题.
如果资料里没有相关信息, 直接回答"资料中未提及", 不要编造.

这就是在压制幻觉。再加上要求它末尾标注引用的片段编号,就做到了"可追溯"------用户看到答案能知道是从哪几段推出来的。

真正花时间的不是代码,是环境

代码其实写得挺顺,难的是让它在我这台机器上跑起来。下面这几个坑依次踩过,每一个都让我对相关技术栈多了一层理解。

坑 1:PyTorch 装完认不出 GPU

用 conda 装完 PyTorch 后,跑验证脚本:

bash 复制代码
python -c "import torch; print(torch.cuda.is_available())"

报错:

javascript 复制代码
ImportError: /home/.../libtorch_cpu.so: undefined symbol: iJIT_NotifyEvent

这个 iJIT_NotifyEvent 是 Intel VTune 的 JIT 分析接口,属于 Intel MKL。查了一下才明白:conda-forge 和 pytorch 官方 channel 各自维护的 MKL 版本经常错位,新版 MKL 挪走了一些符号,PyTorch 链接不上就报这个错。

解法:干脆别用 conda 装 PyTorch,改成 pip:

bash 复制代码
pip install torch --index-url https://download.pytorch.org/whl/cu121

pip 装的 wheel 是静态链接好的,不依赖 conda 环境里的 MKL,干净利落。

启发:PyTorch 官方从 2.5 开始已经不再维护 conda channel,现在统一推荐 pip。conda 擅长管 Python 解释器本身,但对带 C/CUDA 扩展的包,pip 反而更稳。

坑 2:Gradio 6.0 悄悄改了 API

按教程写的代码:

python 复制代码
gr.ChatInterface(fn=chat, type="messages", ...)

在新版 Gradio 上直接报错:

css 复制代码
TypeError: ChatInterface.__init__() got an unexpected keyword argument 'type'

翻 release notes 才知道:Gradio 5.x 里有两种消息格式------老的 tuples 格式 [(user, bot), ...] 和新的 messages 格式 [{"role": "user", ...}](和 OpenAI 对齐)。6.0 把后者变成了唯一默认值 ,参数就去掉了。同期被移动的还有 theme,从 Blocks() 挪到了 launch()

启发 :开源项目升级大版本经常破坏 API。所以 requirements.txt 不能只写下界:

shell 复制代码
gradio>=5.0,<7.0   # 锁上界, 避免意外升级到 7.0 又炸

另外建议 pip freeze > requirements-lock.txt 把你跑通时的精确版本存下来。这是开源项目的基本礼貌------别人 clone 你的仓库不应该再踩一遍同样的坑。

坑 3:Chroma 数据库向前不兼容

升级了 Chroma 版本后,重跑索引:

vbnet 复制代码
KeyError: '_type'

原因是旧版本 Chroma 写进 SQLite 的 collection 配置 JSON 格式变了,新版本读不懂旧格式。

解法 :删掉 data/chroma/ 重建。反正向量数据本来就能从源文档重算出来,不算什么损失。

启发 :这是一个所有存数据的程序都要面对的问题------库升级了,之前存下来的数据怎么办?业界有几种做法:

  1. 版本号 + migration 脚本:PostgreSQL、SQLAlchemy 都这么干。稳,但工程量大
  2. 向前兼容读:新版代码能读旧格式,只是不按旧格式写
  3. 直接报错让你手动迁移:Chroma 目前就是这样

早期库走第三种是合理的------demo 项目里数据都能重建,代价不大。真的要做存重要数据的系统,就得严肃考虑前两种了。

一些让仓库"看起来专业"的小事

除了代码,我在仓库上做了这些事,都花不了几分钟但效果明显:

  • 在 README 里放 demo 截图和徽章(shields.io 生成)
  • 写清楚 requirements.txtrequirements-lock.txt 两份文件的分工
  • .gitignore 里忽略 data/__pycache__/、HF 缓存等运行时产物
  • 在 README 末尾加了一节"常见问题",把踩过的坑都用折叠块写出来
  • GitHub 仓库设置里加了 topics(ragollamachromadb 等)
  • 补了 LICENSE 文件------README 里写 MIT 不算数,要有文件才合法

这些事做完之后,仓库看起来像是真的被用过、被维护着,而不是一个 fork 下来就扔的 demo。这种"专业感"在技术圈里其实是一种隐性的信任。

我的一些真实感受

做完这个项目,对 RAG 最大的感受是:它并不神奇,难点在工程细节而非模型本身

向量检索的原理(余弦相似度、HNSW 索引)很朴素。真正影响最终效果的是那些被教程一笔带过的决策:chunk 切多大、overlap 留多少、top-k 取几个、prompt 怎么写压得住幻觉、检索漏掉关键片段时怎么办......这些没有标准答案,只能靠具体场景反复试。

第二个感受是**"小工具" 比 "大框架" 更适合学习**。如果我一上来就用 LangChain,RetrievalQA.from_chain_type() 一行就跑通了,但我永远不会知道 metadata={"hnsw:space": "cosine"} 这个参数的存在。踩过了才是自己的。

下一步

这个 demo 还很简陋,有几个明显可以优化的点:

  • 加 PDF 支持 :现在只吃 .md.txt,真实场景 PDF 占大头
  • 加 Reranker:向量检索召回 top-20,再用 cross-encoder 精排出 top-3,业界公认性价比最高的优化
  • 加评估:没评估的 RAG 就是玄学,改个参数只能靠感觉说"好像好一点"
  • 加对话历史:让它能回答"那刚才第二点展开讲讲"这种上下文依赖的问题

这些做完我会写第二篇,带上真实的对比数据。


仓库地址:github.com/spark-prair...

如果这篇文章对你有帮助,点个 star 就是对我最大的鼓励 :)

相关推荐
前端7411 小时前
Cursor 被 SpaceX 盯上了:600 亿美元买的不是编辑器,是你的键盘
人工智能
俊哥V1 小时前
每日 AI 研究简报 · 2026-04-23
人工智能·ai
czkm1 小时前
AI有情绪吗?从AI夸我是写作领域大神说起
人工智能·程序员·ai编程
smileNicky1 小时前
Spring AI系列之基于MCP协议实现天气预报工具插件
人工智能·spring boot·spring
deephub1 小时前
LLM 幻觉的架构级修复:推理参数、RAG、受约束解码与生成后验证
人工智能·python·大语言模型·ai幻觉
uzong1 小时前
最新:DeepSeek V4 国产大模型之光,万亿参数重构 AI 格局,让国产大模型迈入普惠新纪元
人工智能·后端
墨染天姬2 小时前
【AI】DeepSeek开源cuda算子库TileKernels
人工智能·开源
Agent手记2 小时前
多系统集成破局:企业级智能体打通异构系统的完整解决方案 | 2026全链路落地实操
人工智能·ai
sunneo2 小时前
从“生成视频”到“生成表演”:米哈游LPM 1.0如何重新定义数字角色的“灵魂”
人工智能·ai作画·aigc·ai编程·游戏美术