01. 校园课程智能助手项目介绍与成品演示
1. 项目简介
做一个能查真实数据、能按文档作答、能帮你存计划的 AI 课程助手。
学生登录后,可以让助手查询自己的课表、根据已上传的课程资料回答问题,并生成学习计划保存下来。
本节不写代码,先理清技术栈、看成品效果、搞懂 MCP / Agent / RAG 各自的职责,后续各节再逐块实现。
2. 学完你能得到什么
- 一个可运行、可演示的 AI Agent 全栈项目(Vue 3 + FastAPI + MCP + LangChain Agent + RAG)
- 搞懂 MCP、ReAct Agent、RAG 在真实业务系统中如何分工、如何串起来
- 掌握面试常被问到的几个工程问题:
- 多用户场景下,Agent 如何做到不越权查询他人数据
- 如何约束模型不编造知识库中没有的内容
- 如何用 SSE 把工具调用过程实时推送到前端
3. 适合谁
| 适合 | 不太适合 |
|---|---|
| 刚学完 Python 基础,想做第一个 AI 实战项目 | 完全没学过 Python |
| 了解大模型、RAG、Agent 等概念,但还没动手做过 | 想系统学习深度学习、模型训练等理论 |
| 想要一个能跑起来、能演示、能写进简历的完整项目 | 已有 Agent 项目实战经验,想看进阶内容 |
没有 FastAPI、Vue 基础也可以跟学,用到的地方会逐步讲解。
教学方式:
- 手把手带敲,关键代码逐行讲解
- 后端代码全部手写,会重点讲解核心部分
- 前端代码不手敲,提供前端工程文件并简单讲解
- 先讲业务和效果,再讲概念,最后写代码,避免一上来被框架名词淹没
- 专有名词都会配通俗说明
4. 技术栈
4.1 技术选型
下表用来建立「名称 → 职责」的对应关系。第一次看到这么多名词不用慌,不用一次记牢,后面每节只用到其中一两个,用到时都会详细讲。
| 层级 | 技术 | 职责 |
|---|---|---|
| 前端 | Vue 3 + Element Plus + Vite | 页面、表格、对话窗口;marked + DOMPurify 安全渲染 Markdown 回答 |
| 后端业务 | FastAPI + SQLAlchemy + MySQL | 登录、课表、文档、计划等接口 |
| 鉴权 | JWT | 标识当前用户,助手不能越权查询他人数据 |
| 模型调用 | LangChain | 统一封装大模型调用与消息格式;走 OpenAI 兼容接口,默认对接 DeepSeek,也可换成其他兼容模型 |
| 工具协议 | FastMCP(独立进程) | 把「查课表 / 检索资料 / 存计划」暴露为标准工具 |
| 智能编排 | LangChain create_agent(底层基于 LangGraph) |
按 ReAct 模式决定是否调用工具、调用哪个、调用几次 |
| MCP 客户端 | langchain-mcp-adapters | 让 Agent 连接 MCP 服务,把 MCP 工具加载成可调用的工具 |
| 流式推送 | SSE | 把处理状态、工具调用过程和回答文字推送到前端 |
| 知识库 | Chroma + Embedding(RAG) | 文档向量化存储,为回答提供可检索的原文片段;Embedding 默认使用硅基流动的 bge-m3 |
关于 Chroma:本课选用 Chroma 是因为它轻量、零运维,适合教学和原型验证。生产环境中常用 pgvector、Milvus 等方案,RAG 的核心流程是一样的。
4.2 运行时服务
运行时需要同时启动三个服务:
flowchart LR
STU["学生<br/>提问 · 查看"]
ADM["管理员<br/>维护 · 入库"]
subgraph SYS[系统边界 · 校园课程智能助手]
FE["前端 Vue 3<br/>对话页 · SSE"]
API["FastAPI 服务<br/>业务 API · Agent"]
MCP["MCP 服务<br/>课表 · 检索 · 计划"]
DB[("MySQL<br/>课表 · 计划")]
VDB[("Chroma<br/>向量知识库")]
end
LLM["大模型 API<br/>第三方"]
EMB["Embedding API<br/>第三方"]
STU --> FE
ADM --> FE
FE --> API --> MCP
MCP --> DB
MCP --> VDB
API --> LLM
MCP --> EMB
classDef base fill:#F1EFE8,stroke:#5F5E5A,color:#444441
classDef biz fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ext fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class STU,ADM base
class FE,API,MCP,DB,VDB biz
class LLM,EMB ext
style SYS fill:none,stroke:#888780,stroke-dasharray:6 4虚线框内是本项目自己开发和部署的服务,框外右侧是需要申请 Key 的第三方 API。
| 进程 | 默认地址 | 职责 |
|---|---|---|
| 前端 | http://127.0.0.1:5173 |
页面交互与对话 UI |
| FastAPI | http://127.0.0.1:8000 |
业务 API,课程助手的对话入口 |
| FastMCP | http://127.0.0.1:8001/mcp |
执行查课表、检索资料、存计划 |
MCP 服务没启动时,助手无法调用任何工具。演示前请确认三个进程都在运行。
5. 成品演示
演示前,管理员先上传课程文档并完成入库。然后学生 aaa 登录,在助手里依次操作:
flowchart LR
A["管理员 admin<br/>上传文档并入库"] --> B["学生 aaa<br/>登录"]
B --> C["问:明天有什么课?<br/>调用 查询我的课表"]
C --> D["问:实验要预习吗?<br/>调用 检索课程资料"]
D --> E["说:生成计划并保存<br/>调用 保存学习计划"]
E --> F["切换学生 bbb<br/>验证数据隔离"]
classDef base fill:#F1EFE8,stroke:#5F5E5A,color:#444441
classDef ai fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class A,B,F base
class C,D,E ai| 操作 | 你会看到什么 |
|---|---|
| 问「我明天有什么课?」 | 过程区出现「查询我的课表」,回答与课表页完全一致 |
| 问「实验需要预习吗?」 | 过程区出现「检索课程资料」,回答依据已上传的文档;文档里没有的规定,助手不会编造 |
| 说「帮我生成一份学习计划并保存」 | 过程区出现「保存学习计划」,学习计划页新增一条记录 |
切换到学生 bbb 再问课表 |
看不到 aaa 的课表和计划,数据按用户严格隔离 |
从过程区可以直接看出:这不是一个只会聊天的机器人,而是在调用真实的业务接口。
6. 业务说明
6.1 角色与权限
| 角色 | 演示账号 | 主要能力 |
|---|---|---|
| 学生 | aaa / 123(有课表) bbb / 123(无课表,用于验证数据隔离) |
课表、学习计划、课程助手 |
| 管理员 | admin / admin |
课程管理、文档管理(上传知识库)、用户管理;也可以用助手验证检索效果 |
数据隔离原则:
- 课表、学习计划:只属于当前登录用户
- 知识库文档:全站共享,由管理员维护,学生通过助手检索(不是每人一份私有库)
6.2 学生侧能力
flowchart LR
STU["学生"] --> S1["我的课表<br/>只读列表"]
STU --> S2["问资料<br/>助手检索知识库"]
STU --> S3["学习计划<br/>手动填写或助手生成"]
S1 --> DB[("MySQL<br/>课表 · 学习计划")]
S3 --> DB
S2 --> VDB[("Chroma<br/>向量知识库")]
classDef base fill:#F1EFE8,stroke:#5F5E5A,color:#444441
classDef biz fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ai fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class STU base
class S1,S3,DB biz
class S2,VDB ai- 我的课表:只读列表。演示数据由脚本按「今天 / 明天 / 后天」写入,通过
course_id与课程表关联。 - 问资料:管理员先在「文档管理」中上传并入库,学生再在助手中提问,由检索工具查询知识库。
- 学习计划:可以手动填写保存,也可以由助手生成;只有用户明确要求保存时才写入数据库。
6.3 管理员侧能力
- 课程管理:维护课程名称、代码等元数据(与向量库解耦)。
- 文档管理:上传
.txt / .md / .pdf / .docx→ 保存文件 → 立即向量化入库;支持预览抽取出的纯文本、重新入库,删除文档时同步清理向量。
- 支持批量上传;其中任意一个文件失败,整批回滚,不会出现「一半入库、一半没入库」。
- 扫描版 PDF 没有文字层,抽不出文本,无法入库(本课不涉及 OCR)。演示顺序:先入库文档,再提问。 没有入库时,资料类问题会得到「检索为空」。
7. 核心概念:MCP、Agent、RAG
7.1 MCP:把业务能力封装成标准工具
业务层本来就有「查课表、检索、存计划」这些函数。MCP 把它们包装成工具,运行在独立进程中,Agent(或调试用的 MCP Inspector)通过统一协议调用。
| 工具名 | 过程区显示 | 作用 |
|---|---|---|
get_my_schedule |
查询我的课表 | 查询当前登录用户的课表 |
search_course_docs |
检索课程资料 | 只读检索知识库 |
save_study_plan |
保存学习计划 | 写入当前用户的学习计划 |
flowchart LR
AG["ReAct Agent"] --> CLI["FastAPI<br/>MCP 客户端 · 携带 JWT"]
CLI -->|MCP 协议| SRV["FastMCP 服务<br/>解析 JWT 得到 user_id"]
SRV --> T1["get_my_schedule"]
SRV --> T2["search_course_docs"]
SRV --> T3["save_study_plan"]
T1 --> DB[("MySQL")]
T3 --> DB
T2 --> VDB[("Chroma")]
classDef biz fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ai fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class CLI,DB biz
class AG,SRV,T1,T2,T3,VDB ai本课不在进程内手写 Tool Calling 清单,而是把工具部署在独立的 MCP 进程中(Streamable HTTP 协议),FastAPI 通过 langchain-mcp-adapters 作为客户端连接,拉取工具后交给 Agent 使用。
身份传递(重点): 前端登录拿到的 JWT 随对话请求传给 FastAPI,FastAPI 再原样转发给 MCP;工具侧自己解析 JWT 得到 user_id,并再查一次数据库确认该用户仍处于启用状态。用户 id 绝不由模型填写,这样即使模型被诱导,也查不到别人的数据。
7.2 Agent:决定是否以及如何使用工具
在系统提示词的约束下,大模型除了生成文字,还要决定:
- 是否需要调用工具
- 调用哪一个工具
- 工具返回后,如何组织成面向用户的回答
本课采用 ReAct(Reason + Act)模式:边推理边行动,工具执行完后把结果交回模型,直到可以给出最终回答。为防止模型陷入死循环,循环轮数有上限(默认 6 轮,可在配置中调整)。
flowchart TB
Q["用户提问"] --> R["模型推理<br/>结合系统提示词"]
R --> J{"需要调用工具?"}
J -->|是| T["通过 MCP 调用工具"]
T --> O["工具结果返回模型"]
O --> R
J -->|否| A["生成最终回答<br/>SSE 逐字推送"]
classDef base fill:#F1EFE8,stroke:#5F5E5A,color:#444441
classDef ai fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class Q,A base
class R,J,T,O ai前端通过 SSE 实时接收处理过程,例如:
- 正在思考…
- 正在查询我的课表…
- 查询我的课表完成
- 随后逐字显示回答正文
后端推送的事件分为六种:status(状态)、tool_start(工具开始)、tool_end(工具结束)、token(回答文字)、done(完成)、error(出错)。前端根据事件类型更新过程区和对话气泡。
多轮对话: 服务端不保存会话,每次提问时由前端把之前的对话记录(只含问答文字,不含过程区内容)一起发给后端,模型据此理解上下文。
关于「明确要求时才保存」: 分两层保证。
- 提示词约束:系统提示词规定,只有用户明确说「保存」「记录」「存下来」时才能调用保存工具;只说「帮我规划一下」时只返回计划文本。这一层依赖模型遵守规则,偶尔仍可能误判。
- 代码兜底:同一轮对话里,保存工具最多成功执行一次,防止模型重复保存。
7.3 RAG:文档可检索,回答有据可依
RAG(Retrieval-Augmented Generation,检索增强生成)分为写入和读取两段:
- 写入(入库):上传文档 → 抽取纯文本 → 分块 → Embedding 向量化 → 存入 Chroma
- 读取(检索):用户提问 → 问题向量化 → 检索相似的原文片段 → 交给模型组织回答
本课的具体参数:每块约 500 字、相邻块重叠 50 字;每次检索取最相似的 3 个片段,并带上来源文件名。
flowchart LR
subgraph W[写入:上传文档时执行]
W1["上传文档"] --> W2["抽取纯文本"] --> W3["分块"] --> W4["Embedding<br/>向量化"]
end
subgraph R[读取:用户提问时执行]
R1["用户提问"] --> R2["问题向量化"] --> R3["检索相似片段"] --> R4["模型组织回答"]
end
W4 --> VDB[("Chroma<br/>向量库")]
VDB --> R3
classDef biz fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ai fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class W1,W2,W3,W4 biz
class R1,R2,R3,R4,VDB ai
style W fill:none,stroke:#B4B2A9,stroke-dasharray:4 4
style R fill:none,stroke:#B4B2A9,stroke-dasharray:4 4两条关键约定:
- 向量化发生在上传(或重新入库)时,而不是每次对话时。
- 区分「检索失败」和「没检索到」:
- 未配置 Embedding Key 或检索服务异常,属于检索失败,助手要如实说明「当前无法查询资料」,不能说成「资料里没有」;
- 检索正常但没有相关片段,才说「知识库中没有检索到相关内容」。
- 两种情况都禁止模型假装已查阅资料、编造内容。
7.4 三者如何串起来
flowchart LR
FE["Vue 3<br/>SSE 流式"] --> API["FastAPI<br/>转发 JWT"] --> AG["ReAct Agent<br/>create_agent"]
AG <--> LLM["大模型<br/>推理 · 决策"]
AG --> MCP["MCP 服务<br/>3 个工具"]
MCP --> DB[("MySQL<br/>课表 · 计划")]
MCP --> VDB[("Chroma<br/>向量库")]
ADM["管理员<br/>上传文档"] --> ING["文档入库<br/>分块 · 向量化"] --> VDB
classDef base fill:#F1EFE8,stroke:#5F5E5A,color:#444441
classDef biz fill:#E1F5EE,stroke:#0F6E56,color:#085041
classDef ai fill:#EEEDFE,stroke:#534AB7,color:#3C3489
class FE,ADM base
class API,DB,ING biz
class AG,LLM,MCP,VDB ai上面一行是一次对话请求的链路,下面一行是管理员的文档入库链路。
| 能力 | 在链路中的位置 | 如果没有它 |
|---|---|---|
| Agent | 决策层:决定调用哪些工具 | 系统只是普通的 CRUD 页面 |
| MCP | 执行层:真正完成业务操作 | 模型只能空口回答 |
| RAG | 资料层:提供可检索的文档依据 | 规定类问题只能靠模型记忆,容易编造 |
8. 小结
- 本课 = 校园业务 + MCP 工具 + ReAct Agent + RAG 知识库。
- 学生侧三项能力:查课表、问资料、存计划;管理员负责课程元数据与文档入库。
- MCP提供工具,Agent决定如何使用工具,RAG 提供可检索的文档依据。
- 两条贯穿全课的工程原则:用户身份由系统传递,不由模型填写;检索不到就如实说,不许编造。