这是系列的第 19 篇。整个系列写给零基础、想入行 AI 的朋友,每天一篇,30 天后你会做出 3 个能写进简历的项目。
这篇解决什么问题
Day 1 的时候我埋了一个包袱,说 Day 19 会做 RAG 知识库问答系统,让大家在评论区说说想用 AI 解决的具体问题,说不定就是我的素材。今天来结账。
四天的铺垫到这里收口:Day 15 讲清了为什么需要 RAG,Day 16 学会把文字变向量,Day 17 用 ChromaDB 建库,Day 18 解决了切分和检索。今天把它们组装成一个完整系统。
和昨天的区别在哪?昨天是个验证性的小脚本,今天是一个有人愿意用的东西:能导入一整批文档、能追问、能标出答案来自哪份文件的哪一页、资料里没有就老实说不知道。这就是你简历上的第二个项目,也是当前招聘市场上最吃香的技能方向。
一、先看系统长什么样
先把结构画清楚,写代码时就不会乱。我们分三层:
- 数据层:文档进来,切分,向量化,存进 ChromaDB
- 检索层:用户提问,找出相关的片段
- 回答层:把片段和问题交给大模型,生成带来源的回答
对应的文件结构建议这样分,别全堆在一个文件里:
text
rag-demo/
├── config.py # 密钥和参数配置
├── ingest.py # 文档导入(读文件、切分、入库)
├── rag.py # 检索和回答的核心逻辑
└── app.py # 跑起来的入口
分文件是专业习惯,也是简历上的加分点,面试官看到目录结构就知道你不是随手写的。
二、第一步:导入文档
先写 config.py,把配置集中管理:
python
# config.py
API_KEY = "你的密钥" # 正式项目放 .env,用 os.getenv 读
BASE_URL = "https://api.deepseek.com"
CHAT_MODEL = "deepseek-chat"
EMBED_MODEL = "embedding-model-name" # 换成你平台的 embedding 模型名
DB_PATH = "./chroma_db"
CHUNK_SIZE = 500
CHUNK_OVERLAP = 50
再写 ingest.py,负责把文档变成库里的片段:
python
# ingest.py
import os
import chromadb
from openai import OpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter
from config import API_KEY, BASE_URL, EMBED_MODEL, DB_PATH
oai = OpenAI(api_key=API_KEY, base_url=BASE_URL)
def read_doc(path):
"""读文档,先支持 txt 和 md,够用了"""
with open(path, "r", encoding="utf-8") as f:
return f.read()
def ingest_file(path, collection):
"""把一份文档切分入库,带来源信息"""
text = read_doc(path)
filename = os.path.basename(path) # 记下文件名,后面要标来源
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)
chunks = splitter.split_text(text)
print(f"{filename}:切成 {len(chunks)} 段")
for i, chunk in enumerate(chunks):
vec = oai.embeddings.create(
model=EMBED_MODEL, input=chunk
).data[0].embedding
collection.add(
documents=[chunk],
embeddings=[vec],
ids=[f"{filename}_{i}"], # 唯一编号
metadatas=[{"source": filename, "chunk": i}] # 元数据:来源
)
关键在最后那行 metadatas。它把"这段内容来自哪个文件"一起存进了库,后面回答时才能标来源。Day 18 讲切分时提过的"给片段贴身份证",就是用在这里。
批量导入一个文件夹:
python
if __name__ == "__main__":
client = chromadb.PersistentClient(path=DB_PATH)
collection = client.get_or_create_collection(name="knowledge")
folder = "./docs" # 把你的资料放这个文件夹
for name in os.listdir(folder):
if name.endswith((".txt", ".md")):
ingest_file(os.path.join(folder, name), collection)
print("全部导入完成,共", collection.count(), "个片段")
三、第二步:检索加回答
核心逻辑写在 rag.py:
python
# rag.py
import chromadb
from openai import OpenAI
from config import API_KEY, BASE_URL, CHAT_MODEL, EMBED_MODEL, DB_PATH
oai = OpenAI(api_key=API_KEY, base_url=BASE_URL)
client = chromadb.PersistentClient(path=DB_PATH)
collection = client.get_or_create_collection(name="knowledge")
def retrieve(question, n=3):
"""检索最相关的 n 个片段,返回内容加来源"""
vec = oai.embeddings.create(model=EMBED_MODEL, input=question).data[0].embedding
result = collection.query(query_embeddings=[vec], n_results=n)
hits = []
for doc, meta in zip(result["documents"][0], result["metadatas"][0]):
hits.append({"text": doc, "source": meta["source"]})
return hits
def answer(question):
"""检索 + 生成回答"""
hits = retrieve(question)
# 把资料拼起来,标上编号,方便让 AI 引用
context = "\n\n".join(
f"[{i + 1}] (来自 {h['source']})\n{h['text']}"
for i, h in enumerate(hits)
)
prompt = f"""你是一个严谨的知识库助手。请只根据下面的资料回答问题。
要求:
1. 资料里没有提到的内容,直接回答"资料中没有相关内容",不要编造。
2. 回答时在句末标注依据来源,格式如 [1]。
3. 回答用中文,条理清晰,不超过 300 字。
资料:
{context}
问题:{question}"""
response = oai.chat.completions.create(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content, hits
四、第三步:跑起来
app.py 做个简单的命令行交互:
python
# app.py
from rag import answer
print("知识库助手已就绪,输入问题开始(输入 退出 结束)")
while True:
question = input("\n你:").strip()
if question == "退出":
break
if not question:
continue
result, hits = answer(question)
print("\nAI:", result)
# 把用到的资料来源列出来,方便用户核对
print("\n--- 依据来源 ---")
for i, h in enumerate(hits, 1):
print(f"[{i}] {h['source']}")
运行起来,问几个问题试试。重点验证三件事:
- 资料里有答案的问题,答得准不准
- 资料里没有答案的问题,它有没有老实说不知道
- 回答末尾有没有标出来源
第三点特别重要。能标出来源的 AI,用户才敢信。 这是 RAG 相比普通聊天机器人的核心优势,面试时也值得强调。
五、这个项目强在哪
对比一下你的项目一,你会发现项目二的价值跨度:
项目一是"用 AI 处理文件",项目二是"让 AI 掌握你的私有知识"。后者能解决的问题更值钱:企业知识库、客服机器人、产品文档助手、法律条文问答,技术底座全是它。
写在简历上大概是这个形态:
RAG 知识库问答系统(个人项目)
- 背景:个人积累的文档资料分散,检索靠关键词经常搜不到,需要语义级问答
- 实现:基于 LangChain 文本切分 + ChromaDB 向量库 + 大模型构建 RAG 流程;设计 chunk_size 与 overlap 双参数切分策略,并通过问题改写提升召回质量
- 增强:回答强制标注来源文件,提示词层面约束"资料外不编造",有效抑制大模型幻觉
- 技术栈:Python、LangChain、ChromaDB、向量检索、大模型 API
明天还有接口化的内容,把项目二的完成度再提一档,到时候用更完整的话术写进简历。
常见报错排查
报错一:检索出来的片段和问题完全不相关。
先检查 embedding 是否用了同一个模型(入库和查询必须一致,Day 16 讲过的坑)。再检查切分是否合理,用 Day 18 的自测方法看片段是否完整。
报错二:AI 明明资料里有答案,却说"资料中没有相关内容"。
两种情况:一是召回没命中,加大 n_results 试试;二是提示词约束过严,AI 太保守了。可以改成"优先根据资料回答,资料中确实没有的信息再说明没有"。
报错三:老是返回同样几条片段。
检查 ids 是不是重复了,导致重复内容覆盖。ids 里带上文件名和序号就不会出这个问题。
报错四:导入大文件时很慢。
每条片段单独请求 embedding 确实慢。改成批量请求(embedding 接口一般支持一次传多条),速度能快好几倍。这个优化留给你自己动手,做出来可以直接写进简历。
报错五:中文文件名入库后乱码。
确认读取文件时用了 encoding="utf-8",写入时也一样。
今天的作业
用你自己的真实资料(笔记、手册、行业文档都可以)跑通整个系统,然后做三个测试:一个资料里有答案的问题、一个需要综合两段内容的问题、一个资料里完全没有的问题。把三个回答和来源截图贴到评论区。
我最想知道的是:资料里没有的那个问题,它有没有老实交代。
明天预告
Day 20:《用 FastAPI 给你的 RAG 穿上接口,别人也能访问了》。现在你的系统只能自己在本机跑,明天把它变成一个能被别人调用的服务。做完之后,面试官可以打开链接直接问你的知识库,这个体验完全不一样。而且 FastAPI 是后端开发的标配,学一次受益很久。
*系列目录:30天从零开始学AI应用 开发