Skip to content

第四阶段:第一个 RAG(Chroma + LLM) ​

上一阶段我们学会了用向量找相关文档,但每次都要重算向量。本阶段把向量存进向量数据库 Chroma,再把检索结果交给 LLM 生成答案——这就是完整的 RAG(检索增强生成)。

一、流程 ​

  1. 文档 → 向量:用本地 bge-m3 把文档转成向量;
  2. 存入 Chroma:向量和原文一起持久化保存到磁盘;
  3. 检索 top 3:把问题转成向量,找出最相近的 3 条文档;
  4. 生成回答:把"资料 + 问题"一起交给 LLM,让它只根据资料回答。
bash
pip install chromadb # 安装 Chroma 向量数据库

二、04_rag.py 完整代码 ​

python
import os
import chromadb # 向量数据库
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv() # 读取.env 里的 LLM 配置

# 对话模型:中转平台上的 LLM,负责"生成回答"
llm = OpenAI(api_key=<redacted>("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"))

# Embedding 模型:本地 LM Studio 上的 bge-m3,负责"文字转向量"
emb = OpenAI(base_url="http://localhost:1234/v1", api_key="<redacted>")

def embed(texts):
"""把一组文字转成向量,返回普通 Python 列表(Chroma 需要列表格式)"""
resp = emb.embeddings.create(model="bge-m3", input=texts)
return [item.embedding for item in resp.data]

# 创建持久化客户端:数据保存在当前目录的 chroma_db 文件夹,程序重启后仍在
client = chromadb.PersistentClient(path="./chroma_db")

# 获取或创建一个"集合"(类似数据库里的表)
# hnsw:space 设为 cosine,表示用余弦距离衡量相似度
collection = client.get_or_create_collection(
"company_docs", metadata={"hnsw:space": "cosine"})

# 公司知识库:5 条规章制度
docs = [
"公司年假为每年 15 天,入职满一年后生效",
"报销需在费用发生后 30 天内提交发票",
"办公室 Wi-Fi 密码每月更新一次,可找行政获取",
"员工生日当月可领取 200 元购物卡",
"加班需提前在 OA 系统中申请,否则不计加班费",
]

# 只有集合为空时才写入,避免每次运行都重复添加
if collection.count() == 0:
collection.add(
ids=[f"doc{i}" for i in range(len(docs))], # 每条文档需要唯一 id
documents=docs, # 原文
embeddings=embed(docs), # 对应的向量
)

def ask(question):
"""RAG 主流程:检索 → 拼 Prompt → 调用 LLM"""
# 1. 检索:把问题转成向量,找最相近的 3 条
results = collection.query(query_embeddings=embed([question]), n_results=3)
retrieved = results["documents"][0] # 第一个(也是唯一一个)问题的检索结果
print("")
for d in retrieved:
print(" -", d)

# 2. 拼 Prompt:把资料和问题放在一起,并加上防编造约束
context = "\n".join(retrieved)
prompt = f"""请只根据下面的资料回答问题。
如果资料中没有答案,就回答"资料中没有相关信息",不要编造。
资料:
{context}
问题:{question}"""

# 3. 调用 LLM 生成回答;temperature=0 让回答稳定、少发挥
resp = llm.chat.completions.create(
model=os.getenv("LLM_MODEL"),
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
print("", resp.choices[0].message.content)

# 命令行循环提问,输入 exit 退出
while True:
q = input("\n问题:")
if q == "exit":
break
ask(q)

三、实测结果(真实运行数据) ​

问题检索到(top 3)回答
我入职半年能休年假吗年假 / 生日 / 加班不能。资料显示:年假入职满一年后生效。你入职半年,尚未满一年。
不提前申请加班会怎样?加班 / 年假 / 报销不提前在 OA 系统中申请加班,则不计加班费。
公司几点下班?年假 / 加班 / 报销资料中没有相关信息。

第一个问题里,LLM 结合资料做了推理(半年 < 一年 → 不能),这是单纯检索做不到的。

四、要点 ​

  1. 向量库总会返回"最接近"的几条,哪怕完全无关。问"几点下班"时,它照样返回了年假、加班、报销。所以 Prompt 里的防编造约束("没有就说资料中没有相关信息")非常关键,否则 LLM 可能根据无关资料瞎编。
  2. temperature=0:问答类任务要稳定、忠实于资料,不需要创意。

五、提交代码 ​

chroma_db/ 是本地生成的数据库文件,不要传到 GitHub:

bash
echo chroma_db/ >>.gitignore # 把数据库目录加入忽略列表
git add.
git commit -m "04 第一个 RAG"
git push

下一节预告 ​

用 LlamaIndex 读取 PDF / Word 文档、自动切分,构建一个回答时注明出处的企业知识库。

👉 下一阶段:LlamaIndex 企业知识库