🐸 青蛙编程网
首页 / AI / 检索增强 RAG / 2. 环境搭建与第一个 RAG

2. 环境搭建与第一个 RAG

这是什么:最小 RAG = 不引任何框架,用十几行代码就能跑通「切分 → 检索 → 带资料生成」的小闭环。

为什么用:上一章讲清了 RAG 的原理,可原理图再漂亮也不等于会写。先手搓一个最笨但完整的版本,才能看清每一步到底在干什么。

如何用:本章用 NumPy 拼出切分、向量检索、带资料生成这三步,跑通后再让它敢说「资料中没有」。

2.1 环境准备

按 虚拟环境 venv 教程建环境(沿用 OCR 教程那套):

mkdir rag-lab && cd rag-lab
python -m venv .venv
.venv\Scripts\Activate.ps1        # (macOS/Linux: source .venv/bin/activate)

pip install openai numpy

Key 和模型名沿用 LLM 教程 的约定,从环境变量读,不写死在代码里:

$env:LLM_API_KEY = "你的key"
$env:LLM_BASE_URL = "https://api.openai.com/v1"   # 可换成任意兼容服务
$env:EMBED_MODEL = "text-embedding-3-small"       # 向量模型
$env:CHAT_MODEL = "gpt-4o-mini"                   # 对话模型

版本说明:NumPy 2.5.4 要求 Python ≥3.12,openai 3.35.1 要求 ≥3.10——本教程用 Python 3.12 及以上,站点基线 3.15 直接可用。 前置检查:兼容服务必须支持 /v1/embeddings 接口(下一章会讲不支持时怎么办)。

2.2 最小 RAG:三步闭环

新建 first_rag.py,整段照抄可跑:

import os
import numpy as np
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["LLM_API_KEY"],
    base_url=os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
)
EMBED_MODEL = os.environ.get("EMBED_MODEL", "text-embedding-3-small")
CHAT_MODEL = os.environ.get("CHAT_MODEL", "gpt-4o-mini")

# 「知识库」:先用几条手写资料代替真实文档
DOCS = [
    "RAG 是检索增强生成:先检索资料,再让大模型基于资料回答。",
    "向量嵌入把文字映射成高维数字,语义相近的文字距离更近。",
    "Chroma 是一个开源的向量数据库,支持本地持久化。",
]

def embed(texts):
    """文字 → 向量。openai SDK 会把输入按批次发给模型"""
    resp = client.embeddings.create(model=EMBED_MODEL, input=texts)
    # 按 index 排序,保证返回顺序和输入顺序一致
    return [d.embedding for d in sorted(resp.data, key=lambda d: d.index)]

def cosine(a, b):
    """余弦相似度:1 = 完全同向,0 = 无关,-1 = 相反"""
    a, b = np.asarray(a), np.asarray(b)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

# ① 建索引:把资料向量化一次
VECTORS = embed(DOCS)

# ② 检索:拿问题向量跟每条资料算相似度
def search(question, top_k=2):
    qv = embed([question])[0]
    scored = sorted(
        ((cosine(qv, v), text) for v, text in zip(VECTORS, DOCS)),
        key=lambda x: x[0], reverse=True,
    )
    return scored[:top_k]

# ③ 生成:把检索到的资料和问题一起交给模型
def ask(question):
    hits = search(question)
    context = "\n".join(f"[{i + 1}] {text}" for i, (_, text) in enumerate(hits))
    prompt = (
        "只根据下面资料回答问题。资料里没有的信息,就回答「资料中未提及」。\n\n"
        f"资料:\n{context}\n\n"
        f"问题:{question}"
    )
    resp = client.chat.completions.create(
        model=CHAT_MODEL,
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content, hits

answer, hits = ask("RAG 是什么?")
print(answer)
for score, text in hits:
    print(f"  {score:.3f}  {text}")

运行:

python first_rag.py

你会看到(示例):

RAG 是检索增强生成,即先检索资料,再让大模型基于这些资料来回答问题。
  0.812  RAG 是检索增强生成:先检索资料,再让大模型基于资料回答。
  0.655  向量嵌入把文字映射成高维数字,语义相近的文字距离更近。

2.3 读懂这三步

代码 作用
client.embeddings.create(...) 把文字换成向量,可一次传一批
sorted(resp.data, key=...) 保证向量顺序和输入顺序对应
np.dot / np.linalg.norm 点积与长度,余弦相似度的手写实现
zip(VECTORS, DOCS) 索引与原文一一配对
context 里的 [1] [2] 编号,后面能让模型标出处

2.4 让它敢说「资料中没有」

提示词里那句「资料里没有的信息,就回答『资料中未提及』」是 RAG 里最值钱的一句话——它把「不知道」变成了合法答案,模型就不容易瞎编。

再稳一点,可以加个兜底:

if not hits or hits[0][0] < 0.3:      # 最高相似度太低,说明没找到相关资料
    return "抱歉,知识库里没有找到相关资料。", hits

2.5 常见翻车现场

报错 / 现象 原因 修法
KeyError: 'LLM_API_KEY' 没设环境变量 $env:LLM_API_KEY = "你的key"
404 / NotFoundError 服务不支持 /v1/embeddings 换支持嵌入的服务,或走 第 4 章 4.6 的本地嵌入
401 Unauthorized Key 错误或没余额 检查 Key
答案全靠编 没给资料或提示词太松 确认 context 非空,提示词写死「只根据资料回答」
检索结果跑题 嵌入模型不合适 换嵌入模型,并重建索引(第 4 章 4.5)

2.6 动手改

  1. 把 DOCS 换成 10 条你自己的资料(比如你博客的简介),问三个问题,看检索排序合不合理
  2. 故意问一个资料里没有的问题,看看模型是老实说「未提及」还是开始编
  3. 把 top_k 改成 1 和 3,对比答案质量的变化

2.7 小结

  • 最小 RAG = 建索引 → 检索 → 生成,三步就够
  • 嵌入用 client.embeddings.create,相似度用 NumPy 手写余弦
  • 提示词里必须写死「只根据资料回答,没有就说未提及」
  • top_k 是第一个要调的旋钮

下一章:文档切分,决定 RAG 效果上限的那一步 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。