FEATURED · 精选文章

基于RAG的课程资料问答助手:从零搭建智能体应用

发布时间 / 2026/9/1 10:11:30
来源 / 创域科博编辑部
栏目 / 资讯中心
基于RAG的课程资料问答助手:从零搭建智能体应用 课程资料问答助手是AI编程与智能体开发课程中非常典型的一个综合案例。学生在学习过程中经常需要快速查找教材、课件和实验指导书中的知识点但课程资料以PDF、Word、Markdown、PPT等多种格式分散存放人工检索效率低直接问通用大模型又往往得不到课程专属的准确回答。这个案例的核心目标是让一个智能体基于课程资料回答学生提问系统先检索相关文档片段再结合大语言模型生成回答同时展示答案对应的资料出处。本案例适合刚学完Python基础、对RAG检索增强生成有一定了解但还没有完整做过智能体项目的读者。通过本案例你可以掌握用AI编程工具如Cursor快速生成项目骨架把多种格式课程资料转成可检索的向量库用Streamlit搭建一个能实际使用的问答界面以及面对报错时如何按数据流排查问题。文章会先讲清楚RAG为什么是这类助手的合理方案然后给出完整可运行的实现最后补充排错清单和生产化改造思路。1. 课程资料问答助手到底要解决什么问题1.1 课程资料的整理和检索成本被低估了一门课程的资料往往散落在多个地方教材PDF、课件PPT、实验指导书Word、讲义Markdown、补充阅读材料TXT。学生想查一个概念时第一反应是打开搜索引擎或者直接问大模型但这样做有几个明显问题。搜索引擎返回的是通用网页不一定是老师课件里的表述方式。通用大模型虽然知识面广但并不知道你这门课的教学重点、实验要求、考核范围更可能在不熟悉的细节上给出看似合理、实际错误的回答。对课程场景来说答案是否有据可查比答案是否流畅更重要。课程资料问答助手要解决的就是这件事把课程资料变成可检索的知识库让大模型只能依据资料内容回答而不是凭记忆编造。学生在输入框里问“什么是生成器”“列表和元组有什么区别”系统先找到资料里相关段落再组织成回答并标注来源。这样整个回答链路是可追溯的。1.2 为什么不直接让大模型记忆课程内容一种简单思路是把所有课程资料塞进大模型的上下文窗口让模型基于这些内容回答。这个思路在小规模演示时可行但会很快碰到上限。大模型的上下文窗口再大也装不下一门课程的全部PDF和PPT。一次提问塞入几十万字响应速度和成本都不可控。而且课程资料是不断更新的每次更新都重新构造Prompt会非常浪费。RAG的做法是把“知识的存储”和“知识的生成”分离向量库负责存储和检索大模型负责根据检索结果生成回答。提问时只把最相关的几个片段取出来拼进Prompt模型看到的是聚焦后的内容回答质量更高来源也更清楚。RAG的核心流程可以用一句话概括先把待检索的课程资料切成小块并向量化再在提问时用向量相似度找到最相关的几个资料块最后将资料块和问题一起交给大模型生成答案。1.3 这个案例在课程中的定位在一个完整的AI编程与智能体开发课程里8.10案例属于“把大模型能力落地成具体应用”的阶段。前面通常已经讲过提示词工程、大模型API调用、LangChain基础组件等内容到了这个案例才把文档加载、文本切片、向量检索、大模型调用、Web界面串成一条完整链路。这个案例也适合用AI编程的方式来做。使用Cursor、Continue等AI辅助编程工具时开发者可以先描述需求让AI生成项目骨架再人工审查和调整关键部分。这里和低代码智能体开发平台的区别在于低代码平台把RAG流程封装成黑盒操作简单但出了问题难以排查代码实现则能清楚看到每一步数据流更适合教学和二次开发。2. 环境准备与项目骨架生成2.1 技术选型与版本约束本案例的课程教学版本建议采用Python 3.10或3.11框架组合为Streamlit LangChain Chroma OpenAI兼容接口。组件作用注意事项Python 3.10运行环境建议使用虚拟环境避免污染系统PythonStreamlit快速搭建问答Web界面对RAG场景足够不需要额外写前端LangChain封装文档加载、切片、Prompt组装版本差异较大导入路径要以安装版本为准Chroma本地向量数据库适合课程演示单机即可运行OpenAI兼容接口提供向量化和对话能力可以接OpenAI也可以接DeepSeek、通义、智谱等国内模型选择Chroma而不是Elasticsearch或Milvus原因很简单课程资料量一般在几十到几百个文档量级Chroma本地运行、配置简单足够支撑教学演示。生产环境中需要处理大量并发和水平扩展时再迁移到pgvector、Milvus等更完整的向量数据库。2.2 用Cursor等AI编程工具生成项目骨架这个案例如果用AI编程工具来做第一步不是手写目录而是把需求描述清楚后让工具生成骨架。推荐在Cursor中新建文件夹并打开然后输入类似下面的提示词请帮我生成一个课程资料问答助手的Python项目使用Streamlit作为前端LangChain作为RAG框架Chroma作为向量数据库支持读取pdf、txt、md、docx格式的课程资料。 项目结构如下 - requirements.txt - build_knowledge_base.py 用于加载资料、切片、向量化并写入Chroma - app.py 用于启动Streamlit问答界面 - data/ 存放课程资料 - knowledge_base/ 存放向量库 要求 1. build_knowledge_base.py 支持通过命令行参数指定资料目录和向量库目录。 2. app.py 使用st.chat_input实现问答回答后展示检索到的来源片段。 3. 向量化模型和大模型都使用环境变量配置。 4. 代码要有清晰注释方便教学演示。AI生成代码后不要直接运行先做两件事检查依赖版本检查导入路径。LangChain从0.1到0.3的导入路径变化较大例如OpenAIEmbeddings在langchain_openai里Chroma在langchain_community.vectorstores里。生成代码后最好逐个import验证一遍否则容易在启动阶段报模块不存在。2.3 安装依赖与目录准备创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate在Windows下激活命令为venv\Scripts\activate。然后在项目根目录创建requirements.txt内容如下streamlit1.30 langchain0.2 langchain-community0.2 langchain-openai0.1 chromadb0.4 pypdf4.0 python-docx1.1安装pip install -r requirements.txt这里要提醒一个坑不要把版本号写成latest。LangChain和Chroma的升级速度很快不同小版本之间可能存在兼容性问题。教学演示时建议锁定一个已经验证过的版本组合例如把langchain0.2.16写死。如果安装后出现ModuleNotFoundError优先检查是不是某个包没有随依赖一起装上比如langchain-community经常被遗漏。3. 核心实现让问答助手真正基于课程资料回答问题3.1 课程资料加载与文本抽取build_knowledge_base.py的第一步是遍历资料目录根据文件后缀选择不同的加载器。from pathlib import Path from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader def load_documents(data_dir: str): data_path Path(data_dir) docs [] for file_path in data_path.iterdir(): if not file_path.is_file(): continue suffix file_path.suffix.lower() try: if suffix .pdf: loader PyPDFLoader(str(file_path)) docs.extend(loader.load()) elif suffix in (.txt, .md): loader TextLoader(str(file_path), encodingutf-8) docs.extend(loader.load()) elif suffix .docx: loader Docx2txtLoader(str(file_path)) docs.extend(loader.load()) else: print(f跳过不支持的文件格式: {file_path.name}) except Exception as e: print(f加载失败: {file_path.name}, 错误: {e}) return docs为什么要单独处理每种格式因为PDF、Word、纯文本的解析机制完全不同。PyPDFLoader处理PDF中的文本层Docx2txtLoader处理Word文档TextLoader处理纯文本。这里最常见的坑是PDF扫描版也就是里面的文字其实是一张图片用PyPDFLoader读出来会是空白。遇到这种情况需要先用OCR工具把PDF转成可复制文字的版本否则后续检索必然失败。3.2 文本切片直接决定检索质量的一个环节加载出来的文档可能很长如果整篇作为一条记录写入向量库检索精度会非常差。所以要先把文档切成小块每一块包含一个相对完整的语义单元。from langchain.text_splitter import RecursiveCharacterTextSplitter def split_documents(docs): splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , ., !, ?, , ] ) return splitter.split_documents(docs)chunk_size500表示每个切块尽量控制在500个字符左右chunk_overlap50表示相邻切块之间保留50个字符的重叠避免一个知识点的句子被切断后信息丢失。切块大小没有绝对标准需要根据资料特征调整。如果课程资料以概念定义为主500字符左右是一个合适的起点。如果资料包含大量代码建议把chunk_size调大到800并且把换行符\n作为主要分隔符避免把一段完整代码切碎。3.3 向量化与向量库构建文本变成向量后才能通过余弦相似度或欧氏距离寻找语义相近的片段。这一步需要调用外部Embedding模型。from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma def build_vectorstore(docs, persist_dir: str): embeddings OpenAIEmbeddings() vectorstore Chroma.from_documents( documentsdocs, embeddingembeddings, persist_directorypersist_dir ) return vectorstoreOpenAIEmbeddings()默认会读取环境变量OPENAI_API_KEY。如果使用国内支持OpenAI协议的服务还需要配置OPENAI_BASE_URL。比如在启动脚本前执行export OPENAI_API_KEY你的API Key export OPENAI_BASE_URL模型服务商提供的Base URL第一次运行会请求Embedding模型把切好的文本块全部向量化并写入knowledge_base目录。向量库构建完成后后续启动问答程序不需要重新构建只需要读取已有的向量库。3.4 检索问答主流程问答程序的核心逻辑可以拆成四步读取向量库、根据问题检索相关片段、组装Prompt、调用大模型生成回答。from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_community.vectorstores import Chroma from langchain.prompts import ChatPromptTemplate def create_qa_chain(persist_dir): embeddings OpenAIEmbeddings() vectorstore Chroma( persist_directorypersist_dir, embedding_functionembeddings ) retriever vectorstore.as_retriever(search_kwargs{k: 4}) llm ChatOpenAI(modelgpt-4o-mini, temperature0) prompt ChatPromptTemplate.from_messages([ (system, 你是一个课程资料问答助手。请只根据提供的课程资料片段回答问题不要编造资料中没有的内容。如果资料中没有相关信息请明确说明。), (human, 课程资料片段如下\n\n{context}\n\n学生的问题是{question}) ]) return retriever, llm, prompt检索参数k表示每次取回几个片段。k4在多数课程场景下够用既能保证信息充分又不会让Prompt过长。如果问题涉及的知识点跨多份资料可以适当调大。模型参数temperature0是为了让回答尽量稳定、保守减少自由发挥。回答时手动组装数据流的版本更直观def answer_question(retriever, llm, prompt, question): docs retriever.invoke(question) context \n\n.join([doc.page_content for doc in docs]) messages prompt.invoke({context: context, question: question}) response llm.invoke(messages) return response.content, docs这一步展示了RAG和普通大模型调用的本质区别大模型不是直接接收问题而是先通过检索拿到“和这个问题相关的资料”再基于这些资料回答。如果检索不到任何相关内容应该让模型明确说不知道而不是生成一个猜测性的答案。3.5 Streamlit问答界面最后编写app.py把上面的逻辑接入Web界面。import streamlit as st from build_knowledge_base import load_documents, split_documents from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma st.set_page_config(page_title课程资料问答助手, layoutwide) st.title(课程资料问答助手) st.cache_resource def init_qa(): embeddings OpenAIEmbeddings() vectorstore Chroma( persist_directoryknowledge_base, embedding_functionembeddings ) retriever vectorstore.as_retriever(search_kwargs{k: 4}) return retriever retriever init_qa() if messages not in st.session_state: st.session_state.messages [] for msg in st.session_state.messages: with st.chat_message(msg[role]): st.markdown(msg[content]) question st.chat_input(请输入你的课程问题例如什么是生成器) if question: st.chat_message(user).markdown(question) st.session_state.messages.append({role: user, content: question}) docs retriever.invoke(question) context \n\n.join([doc.page_content for doc in docs]) from langchain_openai import ChatOpenAI from langchain.prompts import ChatPromptTemplate llm ChatOpenAI(modelgpt-4o-mini, temperature0) prompt ChatPromptTemplate.from_messages([ (system, 你是一个课程资料问答助手。请只根据提供的课程资料片段回答问题不要编造。资料中没有的信息请明确说明。), (human, 课程资料片段如下\n\n{context}\n\n学生的问题是{question}) ]) messages_input prompt.invoke({context: context, question: question}) response llm.invoke(messages_input) answer response.content with st.chat_message(assistant): st.markdown(answer) st.session_state.messages.append({role: assistant, content: answer}) with st.expander(查看检索到的资料片段): for i, doc in enumerate(docs, start1): st.markdown(f片段 {i}:) st.text(doc.page_content[:300])st.cache_resource保证向量库只加载一次避免每次点击都重新读取。st.session_state负责保存多轮对话记录。st.expander把检索到的原文折叠起来既提供了答案溯源能力又不会让界面显得杂乱。4. 运行验证与问题排查4.1 从构建向量库到启动页面先确认资料已经放进data目录然后执行向量库构建命令python build_knowledge_base.py --data_dir ./data --persist_dir ./knowledge_base正常输出会显示每个文件的加载情况最后提示切片数量和向量库写入位置。如果这一步出现网络报错优先检查OPENAI_API_KEY和OPENAI_BASE_URL是否配置正确。启动Web界面streamlit run app.py启动后浏览器会自动打开本地页面。如果使用远程服务器可以在命令行参数中加--server.address 0.0.0.0。4.2 提问验证与输出样例以Python课程资料为例在输入框提问“Python中的生成器是什么”。期望的结果不是模型凭知识生成定义而是基于课程讲义中的原文片段回答并且展开“查看检索到的资料片段”后能看到与生成器相关的教材段落。验证检索质量有一个简单方法在回答前打印检索到的文档片段。如果问题和片段完全不相关说明向量检索或文本切片出了问题。如果片段相关但答案不准确重点检查Prompt是否约束了模型“只依据资料回答”。4.3 常见报错与应对问题现象常见原因检查方式处理建议提问后报401或403API Key未设置或失效查看终端是否有AuthenticationError检查环境变量重启服务回答为空或超时模型接口响应慢或上下文过长查看Streamlit日志调小k值或换用更快的模型检索结果与问题无关切片过大或资料质量差打印检索片段看内容调小chunk_size精简资料文件PDF资料检索不到PDF是扫描版没有文本层用PDF阅读器尝试复制文字先OCR再导入新建向量库后查不到新资料启动时读取的是旧持久化目录查看knowledge_base目录修改时间重新构建并重启服务LangChain导入报错版本差异导致模块路径变化检查报错中的模块名调整import路径或锁定依赖版本4.4 七步排查链路当问答助手出现异常时按照数据流顺序排查比在Streamlit界面里反复点按钮更有效确认问题是否到达后端在Streamlit回调中打印question。确认文档加载是否正常检查构建向量库时的日志是否有“加载失败”提示。确认向量库是否构建成功查看knowledge_base目录是否生成文件大小是否正常。确认检索结果是否命中打印retriever.invoke(question)返回的片段数量和内容。确认Prompt组装是否正确打印最终发给模型的messages_input。确认大模型API调用是否成功查看返回类型和异常。确认页面展示逻辑是否有误检查st.session_state中的消息列表。这条链路本质上和RAG数据流一致。任何一环断开都会表现为“最终回答不对”但真正的根因可能在前面的某一层。5. 从课程演示到生产环境还差哪些工作5.1 本机跑通的教学版存在哪些边界课程案例在本地跑通后停留在“演示可用”的水平。它使用本地Chroma存储向量API Key直接配置在环境变量中没有并发控制没有日志系统也没有用户权限管理。同一个时间只有少数几个学生访问时没问题但如果部署成全院公开服务这些短板都会暴露。另一个容易被忽略的问题是向量库更新策略。教学版每次都是全量重建如果课程资料只有几十份全量重建完全可行。但如果资料增长到几千份全量重建的时间和API调用成本都会明显上升需要设计增量更新方案。5.2 从学习版到正式服务的能力对比能力项课程演示版生产环境版本向量库Chroma单机存储pgvector、Milvus或ES支持动态扩容文本切片固定500字符按文档结构动态切分结合标题层级检索策略向量相似度Top-K混合检索向量关键词重排序模型调用直接调用大模型API通过网关统一管理多模型和限流用户权限无校园统一认证、按课程授权日志追踪Streamlit默认日志结构化日志、请求链路追踪资料更新手动全量重建文件监听、定时增量更新效果评估人工抽查自动化评测相关性和引用命中率5.3 回答质量和数据安全不能只靠模型自觉生产环境还需要考虑两个容易被忽视的问题。一是回答质量评估RAG系统并不是把资料接入大模型就万事大吉资料更新、切片参数调整、模型更换都可能影响回答质量需要建立一套测试问题集每次变更后跑一遍回归。二是课程资料的版权和隐私课件和讲义属于课程团队的内容资产对外提供服务前要确认使用范围同时要记录用户的提问日志防止恶意爬取或提示注入。6. 扩展方向与可复用开发清单6.1 从单轮问答扩展到真正的智能体当前实现是无状态的单轮问答学生每次提问都不参考之前的对话。可以扩展的方向包括多轮对话记忆把历史问题保存到st.session_state构造Prompt时追加最近几轮对话让追问“那它和迭代器有什么区别”这样的问题能被理解。支持多知识库按课程、学期、老师隔离知识库学生进入页面时选择对应的课程而不是所有课程混在一起检索。增加工具调用当检索不到答案时允许智能体调用在线搜索、课程实验环境或考试系统等外部工具拓展能力边界。反馈收集在回答末尾加上“答案是否有帮助”的反馈按钮把用户反馈写入日志用于后续评估和调优。6.2 RAG效果调优的优先级如果回答效果不理想不要一开始就换大模型先按以下优先级排查原始文档是否干净扫描版PDF是否已OCR。切片大小和重叠是否匹配资料结构。检索返回的Top-K片段是否真的相关。Prompt是否明确要求只依据资料回答。模型能力是否足够是否换更强的模型。是否需要对检索结果做重排序。前两项直接决定输入质量输入质量差后面再怎么优化Prompt也有限。6.3 从课程案例迁移到真实项目前的检查清单下面的清单可以直接用于发布前自查[ ] 所有课程资料是否已转换成可提取文本的格式PDF扫描版是否处理完成。[ ] 是否验证过切片参数在当前资料集上的效果。[ ] 是否检查了向量库构建日志确认没有文件加载失败。[ ] 是否配置了环境变量并且测试过Embedding和对话两条API链路。[ ] 是否处理了“检索不到相关内容”的情况模型是否会主动承认不知道。[ ] 是否展示答案来源让学生可以回到原文核对。[ ] 是否记录用户提问日志便于发现高频问题和回答质量短板。[ ] 是否明确了课程资料的版权和使用范围。[ ] 是否设置了并发限制防止单机服务被打满。[ ] 是否建立了一组测试问题集在每次变更后运行回归验证。6.4 这个案例最有价值的练习点对学习AI编程与智能体开发的人来说这个案例最重要的收获不是会写一个Streamlit页面而是理解了一条完整、可验证的智能体数据流从原始资料出发经过加载、切片、向量化、检索、Prompt组装、模型生成最后回到用户界面。每一步都可观察、可测试、可调优。这个能力可以迁移到文档问答、客服机器人、知识库搜索引擎等更多场景。如果刚开始接触AI编程建议先把这个最小闭环完整跑通再逐步加入多轮对话、多知识库和外部工具避免一上来就搭建过大的架构。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻