RAG 实战第一课:不靠框架,手搓一条文档问答链路
生命不息,折腾不止。这篇不碰任何框架,用不到 80 行 Python 把「一堆文档 → 一个能答问的 AI」这条最短链路亲手跑通,向量化、Embedding、相似度检索这三块地基一次讲透。
一、先搞懂一个问题:为什么大模型读不懂你的文档
你肯定遇到过这种尴尬:花几天写好的团队文档、产品手册、笔记,丢给大模型让它「基于这些内容回答」,结果它要么答非所问,要么一本正经地胡说八道。
根源有两层:
- 模型脑子里没你的文档。大模型的「知识」在训练那天就定格了,你后来写的私有文档、内部规范,它一个字都没见过,自然答不上来;
- 上下文窗口装不下。你想「把整本书贴给模型」?几万字直接塞进去,一是贵,二是超了上下文长度直接报错,三是塞得越多它反而越抓不住重点。
于是就有了 RAG(Retrieval-Augmented Generation,检索增强生成)。思路特别朴素,就两步:
- 检索(Retrieval):先在你的文档库里,找出和问题最相关的几段文字;
- 生成(Generation):把这几段文字当成「参考资料」,连同问题一起喂给大模型,让它照着资料作答。
这样模型不用「背」你的私有数据,而是用的时候现查,还能给出处、能溯源。这套方案是现在企业知识库、智能客服、私有问答机器人的绝对主流。
对比一下「微调」:微调是花钱花时间改模型权重,让模型「记住」;RAG 是外挂一个可随时更新的检索层。对绝大多数「私有文档问答」场景,先上 RAG,别急着微调。
二、Embedding 是啥:把一句话变成一串数字
RAG 里的「检索」不是 Ctrl+F 那种关键词匹配,而是语义检索——你说「怎么备份数据库」,它能找到写着「数据持久化方案」的段落,虽然一个字都没对上。
这靠的就是 Embedding(向量嵌入):用一个专门的模型,把一段文字转换成一串固定长度的数字,比如:
1 | "猫" → [0.23, -0.41, 0.87, 0.05, ...] (1536 个数字) |
关键在模型被训练成了一件事:语义越相近的文字,转出来的向量在空间里离得越近;语义无关的,离得越远。所以「猫」和「小猫咪」的向量特别像,「猫」和「青藏铁路」的向量八竿子打不着。
怎么量「像不像」?最常用的是余弦相似度(Cosine Similarity)——算两个向量夹角的余弦值:
1 | cos(θ) = (A·B) / (|A| × |B|) |
结果落在 -1 到 1 之间,越接近 1 表示越相似。它只关心方向、不受向量长度干扰,所以成了 RAG 检索的默认度量。
模型怎么选? 我上一课之前踩过坑,直接给你结论:
| 模型 | 维度 | 价格 | 适合场景 |
|---|---|---|---|
text-embedding-3-small |
1536 | $0.02 / 1M tokens | 绝大多数场景,性价比之王 |
text-embedding-3-large |
3072 | $0.13 / 1M tokens | 复杂语义、要极致效果 |
bge-large-zh-v1.5 |
1024 | 开源免费 | 纯中文场景,本地部署 |
起步就用 text-embedding-3-small,便宜、够用、效果扎实。这些 embedding 模型都是 OpenAI 兼容接口,走中转站 ai.aklibk.com 就能国内直连 + 免绑卡 + 人民币按量付费,几万篇文档向量化下来就几毛钱的事,别去官网一张卡一张卡地开。
三、切分(Chunk):别把整篇文档塞进去
有了 Embedding 还不够,你还得先把文档切成小块(Chunk),再逐块向量化。为什么?两个原因:
- 一篇文档动辄几千字,塞进 embedding 模型会被「平均」成一个糊掉的向量——讲了十件事,每件都沾一点,反而一件都检索不准;
- 检索时要的是「最相关的那一小段」,不是整篇,喂给大模型也要精不要多。
切多大合适?业界公认的甜区是 256~512 个 token,重叠(overlap)占块长的 10%~20%,避免把一段完整的意思拦腰砍断。中文的话,一个汉字大概对应 0.6 个 token,起步可以先按「300 字符 + 50 字符重叠」来切,跑通了再优化。
第一课先不整花活,用最朴素的定长切分 + 重叠,把链路跑通。更聪明的「按语义边界切、保留标题层级」留到下一篇讲。
四、手搓最短链路:一步步写出能跑的代码
先装依赖(就俩):
1 | pip install openai numpy |
然后建一个 rag.py,整条链路 —— 切分 → 向量化 → 索引 → 检索 → 生成 —— 全在这 80 行里:
1 | import os |
把 RELAY_API_KEY 换成你自己的 key,python rag.py 跑起来,几秒钟就能看到它基于你的「私有文档」回答问题。
五、跑起来看效果,体会「语义检索」的魔法
上面这段代码最值得你品的是这段:
1 | scores = [cosine(qvec, v) for v in vecs] |
问「RAG 是什么?」时,问题向量会和四个 chunk 向量挨个算余弦相似度——那个写着「RAG 是检索增强生成……」的 chunk 一定最高分,从而被捞出来当参考资料。它不靠关键词命中,靠的是「意思像」。就算你的问题写成「检索增强生成是个啥?」,照样能命中。这就是向量检索和 Ctrl+F 的本质区别。
跑起来你会观察到几件事:
- 切分后
chunk 数量和你的文档长度直接相关; - 换不同的
top_k,捞出来的上下文不同,回答详略也跟着变; - 问一个知识库里完全没有的问题,那句「资料里没有就明说不知道」的 system 提示就起作用了——这是挡幻觉的底线。
六、第一课小结 + 现在就跑得通的几个坑
到这里,你已经手动跑通了「文档 → 向量 → 检索 → 喂给大模型」的最短链路,三块地基都踩实了:
- 向量化:用
text-embedding-3-small把文字变成 1536 维向量; - 切分:300 字符 + 50 重叠,把长文档切成可检索的小块;
- 相似度检索:余弦相似度 +
argsort取 top-k。
顺手把几个新手必踩的坑也点了:
- 向量是中间产物,原文必须一起存。只存向量不存文字,检索出来你都不知道命中了啥;
- 先归一化再点积。不归一化,长文本的向量模长大,会「作弊」式地拿高分;
- embedding 模型前后必须一致。索引时用 small、查询时换 large,维度都对不上,直接算不了;
embeddings返回顺序要按index排。有的服务商返回是乱序的,不排序会把文档和向量对错号,检索结果全废。
数据量一大(上千篇文档),内存里堆 numpy 数组就撑不住了,这时候才轮到 FAISS、Chroma、pgvector 这些向量库出场——那是下一篇的活。
生命不息,折腾不止。第一课我们把「朴素 RAG」的玩具版跑通了,但离真正好用还差一口气。下一篇 RAG 实战第二课:给这条链路装上三样缺一不可的零件——更聪明的切分策略(按语义边界切、保留标题层级)、向量数据库落盘(FAISS,告别全塞内存)、以及 Hybrid Search(BM25 关键词 + 向量检索两条腿走路),亲手把你的私有文档库从「玩具」升级成「能吃下上千篇文档」的小型知识库。折腾起来。