ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于RAG的课程资料问答助手:从文档解析到智能问答的完整实现

基于RAG的课程资料问答助手:从文档解析到智能问答的完整实现 相信不少同学在学习和备课过程中都遇到过这样的场景课程资料分散在多个 PPT、PDF、讲义和作业文档里想快速找到某个知识点的讲解往往要打开好几个文件来回翻找学生提问时老师也要凭记忆或检索大半天才能给出准确答复。这个问题的本质是“资料很全但缺少一个能理解语义、快速定位并组织答案的助手”。本文将围绕“课程资料问答助手”这个案例完整拆解如何基于 AI 编程与智能体开发思路构建一个能对课程资料进行自动检索和问答的小系统。整个过程会从需求分析、架构设计讲到代码实现与运行验证零基础读者可以跟着步骤动手搭建有开发经验的读者也可以直接参考核心链路进行扩展。作为厦门大学林子雨老师《AI编程与智能体开发》课程中的典型综合案例这个项目非常适合用来理解大模型应用开发中的一项关键技术RAGRetrieval-Augmented Generation检索增强生成。它不要求你从零训练模型而是通过“外部知识库 大模型”的组合方式让模型基于指定资料回答问题从而避免模型“凭空编造”。1. 背景与核心概念1.1 什么是课程资料问答助手课程资料问答助手简单来说就是输入一个与课程相关的问题系统从课程资料库中找到最相关的内容片段再交给大语言模型整理成自然语言答案的一种智能应用。例如你有一门《Python 程序设计》课程资料包括课程 PPTPython 基础语法、面向对象、文件操作等章节。课后讲义各章重点整理。作业与实验指导书题目要求与评分标准。传统做法是打开目录逐个文件查找效率低且依赖人的记忆。课程资料问答助手可以做到支持自然语言提问例如“Python 中列表和元组有什么区别”自动定位到 PPT 中“列表”和“元组”对应的章节片段。结合大模型生成一段条理清晰的回答并附上来源提示。1.2 它解决什么问题这个助手解决的核心问题是“知识定位与知识生成”的分离知识定位从海量课程资料中快速找到相关片段。传统的关键词搜索只做字面匹配无法处理同义词、口语化表达和跨章节语义关联。知识生成把找到的片段组织成符合用户问题语境的回答。传统搜索只能返回原文片段不能自动归纳。引入大模型之后这两步被拆成了“检索 生成”的完整链路也就是 RAG 的核心思想。相比直接让大模型回答RAG 的优势在于对比项直接问大模型基于 RAG 的问答助手知识来源模型训练时学到的通用知识指定的课程资料库更新成本需要重新训练或微调新增资料文件即可更新知识库回答可靠性可能“一本正经地胡说八道”可基于资料事实回答并溯源应用场景通用问答课程教学、企业知识库、文档问答1.3 为什么与 AI 编程、智能体开发有关随着 AI 编程工具和智能体开发平台的发展“编程”正在从“手写每一行代码”转向“设计流程、编排能力、调用模型”。课程资料问答助手就是一个典型的智能体应用雏形它具备“感知能力”接收用户问题。它具备“规划与工具使用能力”判断需要检索课程资料库。它具备“记忆与生成能力”基于检索结果生成回答。在学习这个案例时你不仅会掌握 RAG 的代码实现还会逐步理解如何把一个真实业务问题拆解为可执行的智能体流程。这也是当前 AI 编程和智能体开发人才需求大幅增长的原因之一——企业需要的不只是会调接口的人而是能设计完整 AI 应用链路的人。2. 需求分析与整体设计2.1 功能需求拆解在动手写代码前先把课程资料问答助手需要实现的功能列清楚。这里按照用户角度拆分资料入库支持导入 PDF、TXT、Word 等常见格式的课程资料。系统自动读取文本内容。将长文本切分为多个片段并转换为向量Embedding。问答交互用户输入自然语言问题。系统对问题进行向量化。在向量库中检索最相关的若干资料片段。将片段作为上下文调用大模型生成回答。答案溯源在返回答案的同时标注参考资料的来源信息如文件名或章节号。方便用户核对答案是否来自权威资料。管理能力可选扩展查看当前知识库已导入哪些文档。删除或重新导入某个文档。2.2 技术架构系统采用经典的 RAG 架构整体流程如下用户问题 ↓ 问题向量化 ↓ 向量库检索 TopK ↓ 拼接 Prompt 上下文 ↓ 调用大模型生成 ↓ 返回答案与来源从模块角度看系统分为 5 个核心模块模块职责关键组件文档解析模块读取 PDF/TXT 等文件抽取纯文本pdfplumber、python-docx文本切片模块把长文本按章节或固定长度切块自定义 splitter向量化模块将文本片段转为向量Sentence Transformers / Embedding API向量存储与检索模块保存向量并支持相似度检索Chroma / FAISS问答生成模块调用大模型拼接 Prompt 生成答案OpenAI 兼容接口 / 本地大模型2.3 技术选型说明本案例以“可落地、易理解、生产可用”为原则做技术选型说明如下向量化模型可以选择本地开源模型也可以调用在线 Embedding 接口。为了减少环境依赖本文示例以本地 sentence-transformers 模型为主如果你使用 OpenAI 或国内大模型平台的 Embedding 接口替换对应调用代码即可。向量数据库使用 Chroma它是一个轻量级嵌入式向量数据库特别适合学习和小型项目。生产环境可替换为 Milvus、Qdrant 或 Elasticsearch。大模型本文采用“OpenAI 兼容接口”方式调用。很多国内大模型平台也提供兼容接口只需修改 base_url、api_key 和 model 名称。需要注意的是调用外部大模型接口会涉及网络和费用请务必在合法合规、获得授权的前提下使用并注意保护隐私数据。2.4 项目目录结构先规划项目结构后续所有文件都放在这个目录中course-qa-assistant/ ├── data/ │ └── 课程PPT或讲义.pdf ├── src/ │ ├── document_loader.py # 文档解析 │ ├── text_splitter.py # 文本切片 │ ├── vector_store.py # 向量库操作 │ ├── llm_client.py # 大模型调用 │ ├── qa_pipeline.py # 问答主流程 │ └── app.py # 命令行入口 ├── requirements.txt └── README.md如果你的课程资料很多可以在data/下按章节分子目录并在入库时记录文件路径作为来源。3. 环境准备与版本说明3.1 运行环境本文示例以 Python 3.9 为主。你可以在本机创建虚拟环境也可以在云开发环境中运行。版本需要根据你的项目实际情况调整这里重点演示配置思路。python --version # Python 3.9.18示例版本以你本机为准3.2 安装依赖在项目根目录创建虚拟环境并安装依赖cd course-qa-assistant python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate创建requirements.txt内容如下pdfplumber0.10.3 python-docx1.1.0 sentence-transformers2.2.2 chromadb0.4.22 openai1.12.0 fastapi0.109.2 uvicorn0.27.0然后执行安装pip install -r requirements.txt各个依赖库的版本可能会更新如果你安装时遇到版本冲突不要死磕最新版优先选择当前 Python 环境中可用的兼容版本。这里列出的是编写本文时的常见组合。3.3 大模型接口准备本案例需要调用大模型生成回答。你可以选择本地部署的大模型服务例如通过 Ollama、vLLM 等方式启动的 OpenAI 兼容服务。在线大模型平台使用厂商提供的 API Key。为了统一代码我们按照 OpenAI 兼容接口的方式封装一个LLMClient类通过base_url切换不同的服务商。关于 API Key 的安全使用建议通过环境变量配置不要硬编码在代码中。export LLM_API_KEYyour-api-key export LLM_BASE_URLhttps://your-llm-service.example.com export LLM_MODELyour-model-name这里需要提醒涉及外部大模型接口时请务必先确认服务商的调用条款、数据安全政策和费用规则测试阶段不要上传敏感或未授权的资料。4. 核心原理拆解4.1 文档解析把 PDF 变成纯文本课程资料最常见的格式是 PDF 和 PPT 导出的 PDF。pdfplumber是一个非常好用的 PDF 文本抽取库。# 文件路径src/document_loader.py import pdfplumber from docx import Document def load_pdf(file_path: str) - str: 从 PDF 文件中抽取全部文本。 text_list [] with pdfplumber.open(file_path) as pdf: for page in pdf.pages: page_text page.extract_text() if page_text: text_list.append(page_text) return \n.join(text_list) def load_txt(file_path: str) - str: 读取纯文本文件。 with open(file_path, r, encodingutf-8) as f: return f.read() def load_docx(file_path: str) - str: 读取 Word 文档。 doc Document(file_path) return \n.join([para.text for para in doc.paragraphs])需要注意PDF 中如果是以扫描图片形式保存的内容extract_text()会返回空内容这种情况需要先做 OCR光学字符识别。本文不展开 OCR但在实际课程资料处理中扫描版讲义很常见建议后续集成 PaddleOCR 或 Tesseract。4.2 文本切片解决“长文本无法一次塞进模型”的问题大模型输入长度有限且检索时片段太长会降低相关度。因此需要把长文本切分为合适大小的块chunk。常见的切片策略有两种固定长度切片按字符数或 token 数切分简单但可能切断句子。按结构切片根据标题、段落、章节分界符切片保留语义完整性效果好但实现稍复杂。本案例采用“先按段落粗分再按固定大小合并”的混合策略。设计思路是用空行把文本分成多个段落块。设置每个块的目标字符数例如 500 字符和重叠字符数例如 50 字符。如果段落太短就合并下一个段落如果段落太长则按句子切割。# 文件路径src/text_splitter.py import re def split_text(text: str, chunk_size: int 500, overlap: int 50) - list[str]: 将长文本切分为带重叠的块。 # 按空行分割为段落 paragraphs [p.strip() for p in re.split(r\n\s*\n, text) if p.strip()] chunks [] current_chunk for para in paragraphs: if len(current_chunk) len(para) chunk_size: current_chunk para \n else: if current_chunk: chunks.append(current_chunk.strip()) # 如果段落本身超过 chunk_size按句子切分 if len(para) chunk_size: sentences re.split(r(?[。.!?])\s*, para) temp for sent in sentences: if len(temp) len(sent) chunk_size: temp sent else: if temp: chunks.append(temp.strip()) temp sent if temp: current_chunk temp else: current_chunk para if current_chunk: chunks.append(current_chunk.strip()) return chunks这里把按空行切分的段落作为基本单位是为了保证每个 chunk 内部语义相对完整。字符数不是唯一标准实际使用时可根据模型的最大输入长度和中文 token 占比调整。4.3 向量化让计算机理解语义计算机无法直接比较两段文字的语义是否相关因此需要把文本映射到向量空间。向量化模型Embedding Model会把一句话转换为一个高维向量语义相近的句子在向量空间中距离更近。本案例使用sentence-transformers加载本地开源模型。为了便于演示以下代码以常见的BAAI/bge-small-zh-v1.5为例。模型首次加载时会从网上下载权重如果网络不稳定建议提前手动下载并设置本地路径。如果你使用的是在线 Embedding 接口可以替换为对应的 API 调用。# 文件路径src/embedding.py from sentence_transformers import SentenceTransformer _model None def get_embedding_model(model_name: str BAAI/bge-small-zh-v1.5): 加载本地向量化模型单例模式。 global _model if _model is None: _model SentenceTransformer(model_name) return _model def embed_texts(texts: list[str]) - list[list[float]]: 将文本列表转换为向量列表。 model get_embedding_model() vectors model.encode(texts, normalize_embeddingsTrue) return vectors.tolist() def embed_query(query: str) - list[float]: 将查询问题转换为向量。 return embed_texts([query])[0]对于中文资料normalize_embeddingsTrue会做归一化处理后续计算余弦相似度时更方便。注意不同模型的向量维度不同如果中途更换模型需要重建向量库。4.4 向量存储与检索从“大海捞针”到“精确召回”向量库的核心作用是存储向量并支持高效的相似度检索。Chroma 的使用非常直观# 文件路径src/vector_store.py import chromadb from chromadb.config import Settings class VectorStore: def __init__(self, persist_dir: str ./chroma_db): # 使用持久化目录重启后数据不丢失 self.client chromadb.PersistentClient(pathpersist_dir) self.collection self.client.get_or_create_collection( namecourse_materials, metadata{hnsw:space: cosine} ) def add_documents(self, ids: list[str], texts: list[str], metadatas: list[dict], vectors: list[list[float]]): 向向量库写入文档。 self.collection.add( idsids, documentstexts, metadatasmetadatas, embeddingsvectors ) def search(self, query_vector: list[float], top_k: int 5): 根据查询向量检索最相关的文档。 results self.collection.query( query_embeddings[query_vector], n_resultstop_k ) return results写入时ids是每个块的唯一标识推荐用文档名_序号生成metadatas保存来源信息例如文件名、章节标题、页码等documents保存原始文本内容便于生成答案时拼接上下文。4.5 查询增强与 Prompt 设计很多 RAG 初学者只做一次“问题 → 向量检索 → 拼接 → 生成”效果往往不理想原因出在检索到的内容不够准或者 Prompt 指令不够清晰。本案例在检索前加入一个可选步骤对查询问题进行关键词扩展。例如用户问“Python 里怎么合并两个列表”可以扩展为“Python 合并列表 extend 列表求和”。这样可以在不使用额外模型的情况下提高召回率。Prompt 设计是问答质量的关键建议遵循以下原则明确角色告诉模型你现在是“课程答疑助手”必须基于给定资料回答。限定范围如果资料中没有相关内容直接回答“资料中没有找到请参考教材或询问老师”不要编造。要求结构化请分点回答并标注来源。控制输入长度拼接的上下文不要超过模型最大输入的一半。你是一名课程答疑助手。请根据以下课程资料用中文回答问题。 要求 1. 回答应忠于资料内容不要编造。 2. 如果资料中没有答案请回复“当前课程资料中没有找到相关内容”。 3. 使用清晰的分点结构。 课程资料 {context} 学生问题{question}4.6 大模型生成完成最后一步调用大模型时采用 OpenAI 兼容接口方式。通过环境变量配置可以灵活切换不同服务商。# 文件路径src/llm_client.py import os from openai import OpenAI class LLMClient: def __init__(self): self.client OpenAI( api_keyos.getenv(LLM_API_KEY, your-api-key), base_urlos.getenv(LLM_BASE_URL, https://your-llm-service.example.com) ) self.model os.getenv(LLM_MODEL, your-model-name) def chat(self, messages: list[dict], temperature: float 0.2): response self.client.chat.completions.create( modelself.model, messagesmessages, temperaturetemperature ) return response.choices[0].message.contenttemperature设置为较低值如 0.2是为了让答案更稳定、更忠实于资料。如果是头脑风暴类场景可以适当调高。5. 完整实战案例5.1 创建项目结构在项目根目录下执行mkdir -p course-qa-assistant/src mkdir -p course-qa-assistant/data cd course-qa-assistant将你的课程资料如Python讲义.pdf放到data/目录下。5.2 编写文档入库脚本文档入库是构建知识库的第一步。新建src/build_kb.py将前面几个模块串起来实现完整入库流程# 文件路径src/build_kb.py import os import uuid from document_loader import load_pdf, load_txt, load_docx from text_splitter import split_text from embedding import embed_texts from vector_store import VectorStore DATA_DIR ./data PERSIST_DIR ./chroma_db def load_document(file_path: str) - str: 根据文件后缀选择对应的解析函数。 if file_path.lower().endswith(.pdf): return load_pdf(file_path) elif file_path.lower().endswith(.txt): return load_txt(file_path) elif file_path.lower().endswith(.docx): return load_docx(file_path) else: raise ValueError(f不支持的文件格式: {file_path}) def build_knowledge_base(): store VectorStore(PERSIST_DIR) for file_name in os.listdir(DATA_DIR): file_path os.path.join(DATA_DIR, file_name) if not os.path.isfile(file_path): continue print(f正在处理文件: {file_name}) text load_document(file_path) chunks split_text(text) # 生成 ID 和元数据 ids [f{file_name}_{i} for i in range(len(chunks))] metadatas [{source: file_name} for _ in range(len(chunks))] vectors embed_texts(chunks) store.add_documents(ids, chunks, metadatas, vectors) print(f完成: {file_name}共生成 {len(chunks)} 个文本块) print(知识库构建完成) if __name__ __main__: build_knowledge_base()这个脚本会在项目根目录生成chroma_db文件夹用于持久化向量数据。第一次运行需要下载向量化模型耗时取决于网络情况。5.3 编写问答主流程创建一个qa_pipeline.py把检索和大模型生成串起来# 文件路径src/qa_pipeline.py from vector_store import VectorStore from embedding import embed_query from llm_client import LLMClient class QAPipeline: def __init__(self): self.store VectorStore() self.llm LLMClient() def search_context(self, query: str, top_k: int 5) - str: 根据问题检索相关文本块拼接为上下文。 query_vector embed_query(query) results self.store.search(query_vector, top_ktop_k) documents results[documents][0] metadatas results[metadatas][0] context_parts [] for doc, meta in zip(documents, metadatas): source meta.get(source, 未知来源) context_parts.append(f[来源: {source}]\n{doc}) return \n\n.join(context_parts) def answer(self, question: str) - str: 完整问答流程。 context self.search_context(question) prompt f你是一名课程答疑助手。请根据以下课程资料用中文回答问题。 要求 1. 回答应忠于资料内容不要编造。 2. 如果资料中没有答案请回复“当前课程资料中没有找到相关内容”。 3. 使用清晰的分点结构。 课程资料 {context} 学生问题{question} messages [ {role: system, content: 你是一名严谨的课程答疑助手。}, {role: user, content: prompt} ] answer_text self.llm.chat(messages) return answer_text if __name__ __main__: qa QAPipeline() while True: question input(请输入问题输入 exit 退出) if question.strip().lower() exit: break print(正在检索和生成请稍候...) ans qa.answer(question) print(\n ans \n)5.4 编写命令行入口为了方便交互也可以用app.py封装一个更友好的命令行工具# 文件路径src/app.py import argparse from qa_pipeline import QAPipeline def main(): parser argparse.ArgumentParser(description课程资料问答助手) parser.add_argument(--question, -q, help要提问的问题) args parser.parse_args() qa QAPipeline() if args.question: answer qa.answer(args.question) print(answer) else: print(进入交互模式输入 exit 退出) while True: question input( ) if question.strip().lower() exit: break if question.strip(): print(qa.answer(question)) if __name__ __main__: main()5.5 运行与验证第一步构建知识库。cd course-qa-assistant python src/build_kb.py预期输出类似正在处理文件: Python讲义.pdf 完成: Python讲义.pdf共生成 56 个文本块 知识库构建完成第二步启动问答助手。python src/app.py --question Python 中列表和元组有什么区别如果资料中有相关内容系统会返回一段结构化的中文回答并可能引用来源信息。如果资料中确实没有相关内容系统会回复“当前课程资料中没有找到相关内容”这是符合预期的表现。6. 进阶用 FastAPI 封装为 Web 服务课程资料问答助手在实际使用时通常是给老师或学生通过网页访问的。这里把核心链路封装成一个简单的 Web API。# 文件路径src/web_api.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from qa_pipeline import QAPipeline app FastAPI(title课程资料问答助手) qa QAPipeline() class QuestionRequest(BaseModel): question: str class AnswerResponse(BaseModel): answer: str app.post(/api/ask, response_modelAnswerResponse) def ask_question(req: QuestionRequest): if not req.question.strip(): raise HTTPException(status_code400, detail问题不能为空) answer qa.answer(req.question) return AnswerResponse(answeranswer) app.get(/health) def health_check(): return {status: ok}启动服务uvicorn web_api:app --host 0.0.0.0 --port 8000测试接口curl -X POST http://localhost:8000/api/ask \ -H Content-Type: application/json \ -d {question: Python 中函数定义的语法是什么}注意0.0.0.0表示监听所有网卡接口部署到公网时必须加认证和访问控制否则任何人都可以调用你的大模型接口造成不必要的费用和安全风险。7. 常见问题与排查思路在实际运行过程中比较常见的问题集中在文档解析、向量维度不匹配和模型调用三个方面。整理成下面的排查表问题现象常见原因解决思路PDF 解析后内容为空PDF 是扫描图片无文字层先做 OCR 识别再解析文本中英文混杂的文本切片效果差按字符切分导致句子被截断调整切片策略按句子或段落边界切分向量库检索结果相关性差向量模型不适合中文改用中文开源模型如 bge-small-zh-v1.5检索不到刚导入的文档向量库未持久化或知识库未重建检查 chroma_db 目录确认入库成功调用大模型报 401API Key 错误或未设置环境变量检查 LLM_API_KEY、LLM_BASE_URL 配置大模型回答超时网络不稳定或上下文过长缩短 top_k 和 context 长度或更换模型服务返回答案和资料无关Prompt 中上下文被截断或检索结果太靠后调试检索 TopK检查片段顺序多次重复导入同一文档没有去重逻辑入库前按文件名检查是否已存在针对“导入文档重复”的问题建议在业务流程中先查询文档列表。Chroma 的collection.get(where{source: file_name})可以按元数据查询如果已经存在可以选择跳过或先删除再写入。8. 最佳实践与工程建议8.1 数据安全与版权意识这是课程资料问答助手最容易忽略的一点。在构建知识库时请务必注意只使用你拥有版权或已获授权的课程资料。不要上传涉及个人隐私、考试答案或未公开的敏感信息。调用外部大模型接口时确认服务商是否会保存对话数据。生产环境建议使用私有化部署的大模型避免数据出境风险。8.2 日志与可观测性在开发阶段直接打印答案就能调试。但一旦部署为服务必须记录详细日志包括用户问题。检索到的 TopK 片段及其来源。模型返回的原始结果。每次调用耗时。这些日志能帮你定位“答非所问”到底是检索问题还是生成问题。建议在QAPipeline中加入结构化日志并记录检索结果的来源文件。8.3 检索质量优化如果发现助手经常答非所问优先按以下步骤排查单独打印检索到的 Top5 片段人工判断是否相关。如果片段本身就不相关说明向量模型或切片策略需要调整。如果片段相关但答案不对说明 Prompt 指令不够清晰。尝试增加一个“重排Rerank”步骤先用向量检索召回 Top20再用重排模型或规则对候选片段进行精细排序。8.4 系统扩展方向课程资料问答助手只是起点后续可以扩展为更完整的智能体应用多轮对话记忆保存历史对话支持追问。意图识别判断用户是在问概念、问作业还是查成绩。追问澄清当问题不明确时反向询问用户。接入课程表、作业系统、考试题库等外部工具实现真正的智能体编排。9. 总结与学习路线通过这个案例你已经亲手搭建了一个“课程资料问答助手”整个过程涉及了 AI 编程与智能体开发中的几个关键环节文档解析、文本切片、向量化、向量检索、Prompt 设计和模型调用。这几部分组合起来就是目前大模型应用中最主流的 RAG 架构。如果这个案例你成功跑通了接下来可以按以下路线继续深入替换不同的向量模型和重排模型比较检索效果。把命令行工具升级为 Web 应用加一个简单的聊天界面。引入多轮对话记忆让助手能够支持追问。学习智能体开发框架把问答助手作为其中一个工具能力嵌入更大的应用。在实际课程项目验收中重点关注三个维度一是答案的准确率是否忠于课程资料二是检索速度资料量增加到几百份后响应是否依然流畅三是用户体验是否支持常见问题的自然表达。最后分享一个调试小技巧当你觉得助手回答得不好时不要急着改提示词先打印出检索到的资料片段看一眼。RAG 项目里80% 的“答非所问”问题都出在检索环节而不是生成环节。把检索引擎调准了大模型生成才会“言之有据”。希望这个案例能帮你打通大模型应用开发的第一条完整链路顺手还能解决课程答疑的日常痛点。
返回列表