30天从零开始学AI应用开发(Day 19):项目二完结:RAG 知识库问答系统,把自己攒的资料变成私人顾问

这是系列的第 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']}")

运行起来,问几个问题试试。重点验证三件事:

  1. 资料里有答案的问题,答得准不准
  2. 资料里没有答案的问题,它有没有老实说不知道
  3. 回答末尾有没有标出来源

第三点特别重要。能标出来源的 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应用 开发

相关推荐
深度之眼1 小时前
深度学习研0研1如何正确 “阅读文献” ?分享 “3步高效阅读法” !
人工智能·深度学习
SWAGGY..1 小时前
【C++进阶】:(7)红黑树的原理与 C++ 实现:结构设计、插入调整及性质验证
android·java·开发语言·c++·算法
袖清暮雨1 小时前
机器学习之K近邻算法(KNN)
人工智能·机器学习·近邻算法
xn71331 小时前
Personal AI Agent 架构实战:Memory、权限、跨 App 与本地/云端设计
人工智能·后端·agent
(Charon)1 小时前
【C++面试】堆内存与栈内存:string、vector和对象到底存在哪里
开发语言·c++·面试
野生码农AI实战1 小时前
AI 复盘技能 49 分钟跑不完,被我当场改革:现在 43 秒干完
人工智能
橘和柠1 小时前
模型下载全攻略:HuggingFace、hf-mirror、ModelScope 国内实操
人工智能
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(92):EMPO²——让 Memory 从经验复用走向主动探索
论文阅读·人工智能·学习·开源·github
谢亮_vipxieliang1 小时前
Go defer、panic与recover核心知识点
开发语言·后端·golang