📚 本文是《AIGC 人话系列》第 5 篇 > 系列目录:01 内容理解|02 RAG|03 三招选型|04 工具清单|05 动手实战(本篇)|06 踩坑|07 行业观察
前四篇讲了原理和选型,这篇直接动手——用最少的代码,跑通一个能用的文档问答。
目标
给它一份文档,它就能回答关于这份文档的问题。
全流程就 6 步
完整代码
from openai import OpenAI
import numpy as np
client = OpenAI()
# 1. 读文件(PDF 可以用 pypdf 先把文字抽出来)
text = open("handbook.txt", encoding="utf-8").read()
# 2. 切分:按段落拼,每块约 500 字,块之间留 50 字重叠
def split(t, size=500, overlap=50):
paras = [p for p in t.split("\n") if p.strip()]
chunks, cur = [], ""
for p in paras:
if len(cur) + len(p) > size:
chunks.append(cur)
cur = cur[-overlap:] + p # 保留尾巴,避免语义被切断
else:
cur += p + "\n"
return chunks + [cur]
chunks = split(text)
# 3. 向量化
def embed(texts):
r = client.embeddings.create(model="text-embedding-3-small", input=texts)
return [d.embedding for d in r.data]
vecs = np.array(embed(chunks))
vecs /= np.linalg.norm(vecs, axis=1, keepdims=True) # 归一化,后面用点积算相似度
# 4. 检索
def search(q, k=3):
qv = np.array(embed([q])[0])
qv /= np.linalg.norm(qv)
scores = vecs @ qv # 余弦相似度
top = scores.argsort()[::-1][:k]
return [chunks[i] for i in top]
# 5. 生成
def ask(q):
hits = search(q)
context = "\n".join(hits)
prompt = (
"严格根据以下资料回答问题,资料里没有的就直说不知道。\n\n"
f"【资料】\n{context}\n\n【问题】{q}"
)
return client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
).choices[0].message.content
# 6. 试试
print(ask("年假有多少天?"))
把 import 和注释去掉,核心逻辑也就 20 来行。
三个最容易踩的坑
| 坑 | 现象 | 解决 |
|---|---|---|
| 忘了归一化 | 检索结果乱七八糟 | 算相似度前先除以向量模长 |
| 切分太粗暴 | 关键信息被切断 | 按段落切,块之间留 overlap |
| 没写「不知道」 | 检索不到也硬编 | Prompt 里明确要求拒答 |
想再进一步
跑通之后,按这个顺序升级:
- 换向量数据库(Chroma / FAISS),免去每次重新计算
- 加重排(Rerank),让检索更准
- 把切分改成按标题结构切——这一步效果提升最明显
- 加多轮对话,把历史一起塞进 prompt
写在最后
这个 20 行的版本当然不能直接上生产,但它能让你真正摸到 RAG 的手感——知道每一行在干嘛,比调一个现成框架有用得多。
对照第 2 篇《RAG 到底是什么?》再看一遍这份代码,你会发现原理和代码是一一对应的。
代码跑不通的话,把报错贴评论区,我帮你看。