课程资料问答助手:基于RAG构建智能知识库的完整实践 📅 发布时间:2026/9/1 9:41:18 👁 浏览次数: 在高校课程教学场景中有一个经常被低估的问题老师手里积累了十几份PDF讲义、PPT、往届试卷和代码示例学生课前想预习、课后想复习却只能靠“CtrlF”在文件里逐份搜索。如果某个概念散落在三份不同资料里学生很难拼出完整答案老师想快速答疑也经常要重新打开几十个文件才能确认自己之前是怎么表述的。“课程资料问答助手”这个案例就是专门解决这个问题的——把静态的课程资料变成一套可以对话的知识库系统。这篇案例来自厦门大学林子雨老师的《AI编程与智能体开发》课程第8.10节。它表面上是一个“课程资料QA助手”教学演示实际上覆盖了当前大模型应用开发最核心的一条技术主线文档加载、文本切片、向量化、相似度检索、大模型生成。这条链路有一个专门的名字叫RAG即检索增强生成。它几乎可以迁移到企业知识库问答、产品说明书智能客服、内部制度查询等所有知识密集场景。我给这篇文章的判断是不要把课程资料问答助手当成一个简单的“PDF问答Demo”来看它是从“传统编程”走向“AI编程与智能体开发”的最佳切入点。你不需要训练模型不需要理解复杂的深度学习原理只需要掌握数据和提示词的组织方式就能做出一个能真正辅助教学、辅助业务的知识问答系统。下面我们把这个案例完整拆解从原理讲到代码实现再讲到生产环境的坑和优化方向。1. 课程资料问答助手到底在解决什么问题在动手写代码之前先想清楚这个案例要解决的核心痛点。很多初学者看到“问答助手”就以为是做聊天机器人其实这里的重点不是对话而是“基于特定资料的精准回答”。常规的通用大模型例如直接打开网页端提问它能回答“什么是二叉树”但它不知道怎么回答“我们这门课第3章里的二叉树定义和教材版本有什么不同”因为它没有上过这门课也没有读过老师的讲义。大模型的训练数据存在截止时间也不可能覆盖每一位老师自己编写的个性化讲义。课程资料问答助手要做的就是把这些个性化资料“注入”到大模型的回答链路中让每一次回答都紧扣指定教材、课件和实验手册。这个案例真正解决的问题有三层。第一层检索效率问题。学生不用再逐一打开PDF搜索问答助手可以直接返回答案并指出答案来自哪份资料。第二层回答一致性问题。老师讲课时对某个概念的表述是固定的问答助手通过检索讲义原文可以让回答口径与课程保持一致避免模型自由发挥。第三层教学资源复用问题。课程资料从“静态文件”变成了“动态知识服务”后续还能扩展成测验出题、知识点图谱、学习路径推荐等更复杂的教学智能体。所以这个案例的价值不在“聊天”而在“知识管理”。它适合教师、教育产品开发者、刚入门AI编程的开发者也适合任何需要在私域资料上做问答的研发人员。2. 核心概念RAG与智能体开发课程资料问答助手的技术底座是RAG检索增强生成。在开始写代码前有必要把几个容易混淆的概念一次说清楚。2.1 大模型的边界在哪无论调用ChatGPT还是国产大模型API模型本身都只是一个“文本生成引擎”。它知道很多通用知识但它没有你的课程资料也没有你数据库里的业务数据。直接问一个没有上下文的模型它只能根据训练时的记忆来回答这在专业领域很容易出现两个问题编造知识也就是所谓的“幻觉”回答口径与你的业务资料不一致。RAG的思路不是重新训练模型而是在用户提问时先去外部知识库中检索与问题最相关的内容片段把这些片段作为上下文一起交给大模型让大模型基于这些片段来组织答案。2.2 RAG的完整链路一条标准的RAG链路包含五个步骤离线准备把课程资料从PDF、Word、Markdown等格式中抽取为纯文本。文本切片把长文档切成固定长度的小片段片段太多太长都会影响检索效果。向量化把每个文本片段通过Embedding模型转换成一个向量向量可以理解为“文本的数学语义表示”。建立索引把所有向量放入向量数据库比如FAISS、Chroma、Milvus以便快速找到相似片段。在线问答用户提问时把问题也转成向量在索引中搜索最相似的TopK个片段把片段拼接进提示词让大模型生成回答。2.3 智能体开发在这里意味着什么这个案例标题里出现了“智能体开发”很多初学者会误以为一定要用到复杂的AutoGPT或者LangChain Agent。实际上在课程教学场景中智能体更多指的是“具备自主完成特定任务能力的程序系统”。课程资料问答助手的“智能体”体现在哪里体现在它不仅能回答单次问题还能做到多轮对话中记住用户刚才问的话题、能判断什么情况下资料库无法回答问题、能返回引用来源。这些能力让系统从“搜索工具”升级为“助教助手”这也是智能体开发和普通工具调用的关键差异。换句话说RAG是当前智能体开发中最常见、最容易被低估的基础能力。把课程资料问答助手的RAG链路做扎实再往上叠加对话记忆、意图识别、工具调用就是一个完整的智能体项目。3. 系统架构设计与技术选型本节给出课程资料问答助手的整体架构以及技术选型理由。整体流程可以用一条数据链路概括课程资料(PDF/Markdown) ↓ 文本抽取 原始文本 ↓ 切片 文本片段列表 ↓ Embedding模型 向量矩阵 ↓ FAISS索引 向量知识库 ↓ 相似度检索 相关片段TopK ↓ 大模型生成 最终答案 来源引用在技术选型上我倾向于使用轻量、易安装、对教学友好的组件。课程案例不需要一上来就上重型的分布式向量数据库关键是把链路跑通。模块方案选型理由开发语言Python 3.10AI生态最成熟教学代码可读性好Embedding模型BAAI/bge-small-zh-v1.5中文文本表示效果好模型小本机可运行向量索引FAISS安装简单适合百万级以内知识库大模型APIOpenAI兼容接口可切换通义千问、DeepSeek、GPT等多种模型交互界面Streamlit几十行代码即可生成Web界面文档解析pypdf处理标准PDF文本提取足够这里有一个容易踩坑的地方很多人一上来就选择LangChain或LlamaIndex框架虽然框架能减少代码量但会隐藏大量关键细节。如果对RAG每一步的原理没有体感后面遇到检索质量差的问题会非常被动。因此这个案例用原生Python实现离线建库和在线问答把每一步都暴露出来更符合“AI编程与智能体开发”课程的教学目标。4. 环境准备与依赖安装开始写代码之前先准备好开发环境。本文示例面向Python 3.10及以上版本建议使用虚拟环境隔离依赖。创建项目目录mkdir course-qa-assistant cd course-qa-assistant python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate安装依赖。项目所需依赖统一写在requirements.txt中# 文件路径requirements.txt openai1.30.0 faiss-cpu1.7.4 sentence-transformers2.6.0 pypdf4.0.0 numpy1.24.0 streamlit1.36.0 python-dotenv1.0.0执行安装pip install -r requirements.txt安装过程中最值得关注的是sentence-transformers它会自动下载PyTorch依赖包体积较大。如果你的网络环境普通安装时间可能较长建议使用国内镜像源pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple此外项目在运行时还需要一个OpenAI兼容的大模型API接口。国内开发者常见的做法是使用通义千问的DashScope兼容模式也可以使用DeepSeek、智谱等厂商提供的OpenAI兼容端点。没有固定唯一选择只要把base_url和api_key换成你自己的即可。尤其要注意的是API密钥的管理。不要把api_key硬编码在Python文件中更不要提交到Git仓库。推荐的做法是写入项目根目录的.env文件并在.gitignore中忽略该文件。# 文件路径.env OPENAI_API_KEY你的API密钥 OPENAI_BASE_URLhttps://dashscope.aliyuncs.com/compatible-mode/v1代码中使用python-dotenv加载环境变量。5. 数据处理把课程资料变成可检索的向量知识库这部分是整个问答助手的离线基础。先把课程资料从原始文档转换为向量知识库后续所有问答都依赖这个知识库的检索质量。5.1 文档加载课程资料最常见的形式是PDF讲义。为了让示例容易运行本文封装一个基于pypdf的PDF加载函数它会把PDF每一页的文本抽取出来并拼接成完整字符串。# 文件路径kb_builder.py from pypdf import PdfReader def load_pdf(file_path: str) - str: 从PDF文件中抽取文本返回拼接后的完整文本 reader PdfReader(file_path) pages_text [] for page in reader.pages: text page.extract_text() if text: pages_text.append(text) return \n.join(pages_text)这段代码看起来简单但有一个非常实际的坑pypdf只能处理“文本型PDF”。如果课程PPT是扫描图片再导出为PDFextract_text()会得到大量乱码或空文本。这种情况需要先做OCR识别课程入门阶段可以先手动确认自己的资料是否是文本型PDF。实际教学中我建议把实验手册或讲义另存为Markdown或纯文本格式读取速度更快也便于后续文本清洗。5.2 文本清理从PDF抽取出的文本通常包含页眉、页脚、页码、目录、奇奇怪怪的换行符。这些噪声如果不处理会直接影响向量化效果。比如页眉里的课程名重复出现几十次检索时可能会把不相关的内容都拉到前排。一个简单的清理函数可以这样写import re def clean_text(text: str) - str: 清理PDF抽取文本中的常见噪声 # 去除页码单独一行出现的纯数字 text re.sub(r\n\d{1,4}\n, \n, text) # 去掉首尾空白 text text.strip() # 压缩连续空白 text re.sub(r[ \t], , text) # 压缩连续换行 text re.sub(r\n{3,}, \n\n, text) return text清洗时不要过于激进例如把换行全部删掉会影响中文分句结构。保留段落级别的换行即可。5.3 文本切片策略切片是RAG中最容易影响效果、最需要调参的环节。如果切片太长一个片段里包含多个知识点检索时会被无关信息干扰如果切片太短片段语义不完整大模型生成的答案又会缺乏上下文。常用的做法是按固定字符长度切片并设置重叠区域。重叠的意义在于避免一个完整知识点被切在两段的边界上。def split_text(text: str, chunk_size: int 400, overlap: int 80) - list[str]: 将长文本按固定长度切分为片段并保留重叠区域 if chunk_size overlap: raise ValueError(chunk_size 必须大于 overlap) chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] if chunk.strip(): chunks.append(chunk) if end len(text): break start end - overlap return chunks对于400字符的chunk_size大约覆盖中文一两百字的知识点描述对课程讲义来说是一个比较稳妥的起点。后续可以通过检索效果来调整。5.4 向量化并写入FAISS索引把切片后的文本片段逐条转换成向量然后写入FAISS索引。这里选择BAAI/bge-small-zh-v1.5作为Embedding模型。选择它的理由是中文效果比早期的multilingual模型更好模型体积小在没有GPU的普通笔记本上也能运行。from sentence_transformers import SentenceTransformer import faiss import numpy as np def build_index(chunks: list[str]): 将文本片段向量化构建FAISS索引并返回 model SentenceTransformer(BAAI/bge-small-zh-v1.5) vectors model.encode(chunks, normalize_embeddingsTrue) dimension vectors.shape[1] index faiss.IndexFlatIP(dimension) index.add(vectors) return index, model, vectors这里使用IndexFlatIP也就是内积相似度计算的是余弦相似度。因为encode时设置了normalize_embeddingsTrue向量已经做了归一化内积等价于余弦相似度。如果课程资料量更大比如达到几十万片段可以从IndexFlatIP升级到IndexIVFFlat通过聚类实现更快的检索。但在课程案例阶段Flat索引足够。5.5 主流程串联把上面的函数串联起来得到建库脚本。脚本接收一个PDF文件路径最终把切片、索引内容保存到本地knowledge_base.npy中。为了后续调试方便我同时保存了文本片段列表和索引。# 文件路径build_kb.py import numpy as np import faiss from kb_builder import load_pdf, clean_text, split_text, build_index PDF_PATH course_material.pdf if __name__ __main__: print(Step1: 加载PDF...) raw_text load_pdf(PDF_PATH) print(Step2: 清洗文本...) clean clean_text(raw_text) print(Step3: 切片...) chunks split_text(clean, chunk_size400, overlap80) print(f共生成 {len(chunks)} 个文本片段) print(Step4: 构建向量索引...) index, model, vectors build_index(chunks) # 保存文本片段 np.save(chunks.npy, np.array(chunks, dtypeobject), allow_pickleTrue) # 保存向量索引 faiss.write_index(index, knowledge_base.index) print(知识库构建完成)执行后项目目录下会生成两个核心文件chunks.npy保存文本片段knowledge_base.index保存FAISS向量索引。这两个文件就是“课程知识库”的物理载体。后续问答阶段不再需要重新解析PDF直接加载这两个文件即可。6. 核心代码实现检索与问答链路知识库构建完成之后进入在线问答阶段。这一阶段的核心逻辑是接收用户问题 → 向量化 → 检索TopK片段 → 拼接提示词 → 调用大模型生成回答。6.1 检索模块检索模块负责从知识库中找到与问题最相关的文本片段。# 文件路径retriever.py import numpy as np import faiss from sentence_transformers import SentenceTransformer class Retriever: def __init__(self, index_path: str, chunks_path: str): self.index faiss.read_index(index_path) self.chunks np.load(chunks_path, allow_pickleTrue).tolist() self.model SentenceTransformer(BAAI/bge-small-zh-v1.5) def search(self, query: str, top_k: int 4) - list[str]: 返回与问题最相关的文本片段列表 q_vector self.model.encode([query], normalize_embeddingsTrue) scores, indices self.index.search(q_vector, top_k) results [] for score, idx in zip(scores[0], indices[0]): if idx 0: continue results.append(self.chunks[idx]) print(f检索得分: {score:.4f}) return results值得注意的一点是这里没有对检索结果做任何后置过滤。实际使用中分数过低的片段往往与问题无关可以在后续加入阈值逻辑。比如得分低于0.3的片段可以丢弃。6.2 问答生成模块问答生成模块把检索结果交给大模型。这里的关键点在提示词设计要明确告诉模型“只能根据提供的资料回答资料里没有的内容要直接说明”。这样可以最大限度减少大模型“编造”的可能性。# 文件路径generator.py from openai import OpenAI import os from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL), ) def generate_answer(question: str, context_chunks: list[str]) - str: 基于检索到的资料片段生成回答 context \n\n---\n\n.join(context_chunks) prompt f你是课程资料问答助手。你只能依据下面提供的课程资料片段回答用户问题。 要求 1. 如果资料片段中有明确答案请直接回答并尽量引用原文的相关表述。 2. 如果资料片段中没有相关信息请回答“课程资料中未找到相关内容”。 3. 不要根据你自己的理解编造答案。 4. 如果问题与课程无关请提示用户只询问课程相关内容。 课程资料片段 {context} 用户问题 {question} response client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: 你是一个严谨的课程资料问答助手。}, {role: user, content: prompt}, ], temperature0.2, ) return response.choices[0].message.content这段代码有几个要点temperature设置为0.2目的是让回答更倾向于稳定、克制而不是天马行空。system消息和prompt里同时做了强调是对“不要越界回答”的双重保险。context_chunks通过分隔符拼接让大模型清楚知道哪些内容来自不同资料片段。6.3 命令行主程序把检索和生成串起来先用命令行跑通问答流程便于快速验证。# 文件路径qa_cli.py from retriever import Retriever from generator import generate_answer def main(): retriever Retriever(knowledge_base.index, chunks.npy) print(课程资料问答助手已就绪输入问题开始提问输入exit退出) while True: question input(\n你的问题: ).strip() if question.lower() in (exit, quit): break if not question: continue chunks retriever.search(question, top_k4) answer generate_answer(question, chunks) print(\n回答:, answer) if __name__ __main__: main()命令行运行方式python qa_cli.py这是最快能看到效果的验证方式排除了前端干扰。6.4 多轮对话与来源引用如果只做到单轮问答它只是一个搜索引擎的翻版还算不上“智能体”。真正的智能体还需要两个能力多轮对话记忆和来源引用。多轮对话可以通过把历史消息传给大模型实现。基本做法是维护一个messages列表把用户和助手的对话历史都保存下来在调用API时一起传入。同时为了让回答可追溯可以在生成回答后附上“来源片段截取”方便用户回到原始文档核对。conversation_history [] def chat(question: str): conversation_history.append({role: user, content: question}) response client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: 你是课程资料问答助手...}, *conversation_history, ], temperature0.2, ) answer response.choices[0].message.content conversation_history.append({role: assistant, content: answer}) return answer需要小心的是对话历史不能无限增长。大模型的上下文窗口有限历史消息过多会挤压检索到的资料片段空间。一般可以只保留最近两轮对话或者在历史超过一定长度时做截断。7. 运行效果与验证方法课程资料问答助手写完以后怎么判断它是否合格建议从三个维度验证检索是否准确、回答是否忠于资料、错误场景是否兜底。7.1 手动构造测试集不要只凭一两个问题就得出结论。更稳妥的做法是从课程资料中找出10个有明确答案的问题例如“什么是RAG”“数据库事务的四个特性是什么”再找出5个资料中不存在的问题例如“今天北京天气怎么样”分别测试系统的正面回答能力和拒答能力。预期结果分两类资料内问题回答能命中要点并且引用的文字和课程讲义口径一致。资料外问题系统明确回复“课程资料中未找到相关内容”而不是自由发挥编出一条答案。7.2 使用Streamlit搭建可视化验证界面命令行运行虽然效率高但不够直观。课程案例中用Streamlit几十行代码就能做一个Web界面方便现场演示和学生使用。# 文件路径app.py import streamlit as st from retriever import Retriever from generator import generate_answer st.set_page_config(page_title课程资料问答助手, layoutcentered) st.title(课程资料问答助手) st.cache_resource def load_retriever(): return Retriever(knowledge_base.index, chunks.npy) retriever load_retriever() if history not in st.session_state: st.session_state.history [] question st.text_input(输入你的问题) if st.button(提问): if question.strip(): related_chunks retriever.search(question, top_k4) answer generate_answer(question, related_chunks) st.session_state.history.append((question, answer)) st.success(回答生成完毕) for q, a in reversed(st.session_state.history): st.markdown(f**问题** {q}) st.markdown(f**回答** {a}) st.divider()运行方式streamlit run app.py浏览器访问http://localhost:8501即可打开Web页面。注意st.cache_resource的作用是让Retriever只加载一次避免每次点击都重复加载Embedding模型否则页面响应会非常慢。7.3 如何判断运行成功运行成功不是“程序不报错”这么简单而是同时满足以下条件建库阶段打印出切片数量大于10个检索阶段的相似度得分稳定在一个合理区间通常0.4以上才是有相关性的问答阶段的回答确实基于课程资料内容而不是一套空洞的通用解释。如果发现回答经常出现与资料无关的内容优先检查检索返回的片段是否准确而不是急着换大模型。RAG系统里检索质量决定回答质量的上限。8. 常见问题与排查思路课程资料问答助手虽然原理不复杂但在实际跑通过程中新手会遇到不少问题。下面整理出最常见的六类现象和排查方法。问题现象可能原因排查方式解决方案建库时PDF提取出乱码PDF是扫描版或加密文档用PDF阅读器检查是否支持选中文字使用OCR工具或改用Markdown/文本格式课程资料中文检索效果差Embedding模型选错用几个语义近义词测试相似度换成BAAI/bge-small-zh-v1.5等中文专用模型回答总是“未找到相关内容”切片太小或检索TopK太少打印检索到的片段看是否相关调大chunk_size调大top_k到5或8回答重复或自相矛盾上下文拥挤prompt约束不足观察提示词拼接后的完整内容缩短历史对话强化prompt规则调用API超时网络问题或模型响应慢单独用脚本测试API调用加入重试机制或更换响应更快的模型再次运行向量维度不一致换过Embedding模型版本对比两次向量维度固定模型版本重新建库这里重点提醒一个容易忽略的问题很多同学在课程学习阶段为了省事用了英文Embedding模型处理中文文本。这样不是不能运行而是检索质量会有明显下降。中文语义和英文语义的向量空间不完全一致用中文专用模型是提高检索质量最直接的手段。另一个高频问题在线程安全方面。课程资料如果比较大第一次建库可能需要几分钟此时不要重复启动多个建库进程否则会同时写入索引文件造成数据损坏。9. 从课程案例到生产级智能体最佳实践把课程案例跑通只是第一步真正要在生产环境落地还有大量工程问题需要考虑。下面这些实践建议来自常见的知识库项目经验值得在后续学习中反复对照。9.1 切片策略要结合文档结构固定长度切片是RAG的起步做法但不是最优做法。课程讲义通常有章节结构更好的切片策略是优先按标题、段落、列表等结构切分让每个片段尽量对应一个完整知识点。如果课程资料是Markdown可以基于Markdown标题层级切分如果是PDF可以先检测“第X章”“第X节”等页码模式作为切片的边界。9.2 检索结果要有质量分层不要盲目相信向量相似度。在实际应用中应该对检索结果设置最低分数阈值分数太低的片段直接丢弃。同时可以引入混合检索把向量检索和关键词检索比如BM25结合起来再用Rerank模型对候选片段重排序。这样做能明显提升长尾问题的回答质量。9.3 回退机制与用户反馈一个好的问答助手必须知道自己的能力边界。当检索到的所有片段得分都很低时不要强行让大模型基于弱相关片段回答而应该直接返回“课程资料中未找到相关内容”。在用户交互界面中还应该提供“这个答案有帮助吗”的反馈入口收集用户的点赞和点踩数据用于后续优化检索和切片。9.4 从单机FAISS到生产级向量数据库本文的FAISS方案适合课程案例和实验环境。如果课程资料数量达到几十万片段、多用户并发访问建议迁移到支持分布式部署的向量数据库例如Milvus或Elasticsearch。迁移过程并不是简单的替换还涉及索引类型选择、分片策略、监控告警、数据备份等问题。这些内容对智能体开发来说属于生产环境必备的工程能力。9.5 API密钥与数据安全课程资料可能包含未公开的教学成果接入大模型API时要注意数据安全边界。不同大模型服务商对API输入数据的存储和使用政策不同如果要处理敏感资料上线前必须确认数据不会用于模型训练。API密钥应保存在环境变量或密钥管理服务中并设置调用频率限制和预算上限避免密钥泄露后被恶意调用产生高额费用。9.6 从问答助手到课程智能体跑通这个案例后你会发现它已经具备一个教学智能体的雏形。你可以继续在它上面叠加对话记忆管理让多轮追问更自然出题器工具让大模型根据课程章节自动生成选择题学习路径规划根据学生的薄弱点推荐复习资料自动摘要把整章讲义压缩成重点提纲。每个能力本质上都是在“课程资料问答助手”的RAG链路上增加新的工具和新的提示词流程。这也是智能体开发的魅力所在核心不是某个单一模型有多强而是你能把模型、数据和工具组合成一个能解决实际问题的工作流。如果你正在学习AI编程与智能体开发建议先把这个案例完整复现然后选择一个真实的课程章节重新建库尝试调整切片长度、top_k、temperature等参数记录不同参数对回答质量的影响。参数调优的经验比任何教程都更能帮你建立对RAG系统的直觉。