项目背景(约2分钟)

CampusQnA 是一个面向高校学生的智能问答系统。在高校场景中,学校会发布大量公开信息,比如培养方案、校规校纪、办事指南等。这些信息通常分散在不同的 PDF 文档、网页中,学生想要找到特定信息非常困难。

想象一下:一个新生想知道计算机学院有哪些专业,他可能需要:
1. 登录教务处网站
2. 找到培养方案页面
3. 在十几个 PDF 中找到计算机学院的那份
4. 翻页查找专业信息

整个过程可能需要十几分钟甚至更久。CampusQnA 就是要解决这个痛点,让学生通过自然语言提问,几秒钟内就能获得准确答案。

为什么做这个项目?(约1分钟)

  1. 解决实际痛点:学生获取校园信息效率低下
  2. 技术验证:实践 RAG 技术在垂直领域的应用
  3. 学习价值:从数据处理到前后端部署的完整工程实践
  4. 可落地性:轻量级架构,支持 Docker 一键部署

为什么不用关键词检索?(约2分钟)

传统关键词检索(如 Elasticsearch)存在以下问题:

  1. 语义理解不足

    • 用户问”计算机学院有哪些专业”,关键词检索会匹配”计算机”、”学院”、”专业”等词
    • 如果文档中写的是”我院开设软件工程、人工智能等方向”,关键词匹配可能失败
    • 同义词、近义词无法处理
  2. 上下文割裂

    • 关键词检索返回的是文档片段,缺乏上下文理解
    • 无法理解”它”、”此规定”等指代关系
  3. 召回质量不稳定

    • 依赖关键词的精确匹配
    • 对用户提问的表述方式非常敏感

为什么选择 RAG?(约2分钟)

RAG(Retrieval-Augmented Generation,检索增强生成)是当前最成熟的企业级 AI 应用架构:

  1. 准确性:基于向量语义检索,理解用户提问意图
  2. 可溯源:回答基于真实文档,附带来源引用
  3. 时效性:可以随时更新知识库,无需重新训练模型
  4. 成本可控:相比微调大模型,RAG 成本更低
  5. 安全性:避免大模型产生幻觉,所有回答有据可依

核心优势:将大模型的”生成能力”与向量数据库的”检索能力”结合,既保证回答的准确性,又提供生成式回答的友好体验。

技术选型(约3分钟)

后端:FastAPI

选择 FastAPI 的原因:
1. 高性能:基于 Starlette 异步框架,吞吐量高
2. 类型安全:Python 类型提示 + Pydantic,自动生成 API 文档
3. 开发效率:自动生成 Swagger UI,调试方便
4. 现代特性:原生支持异步、依赖注入、中间件
5. 社区活跃:生态完善,部署工具成熟(uvicorn)

前端:Vue 3 + Vite

选择 Vue 的原因:
1. 响应式开发:Composition API 简洁高效
2. 构建速度:Vite 基于 ES Module,冷启动极快
3. 学习曲线平缓:模板语法直观,适合快速开发
4. 单文件组件:HTML/CSS/JS 一体化,代码组织清晰
5. 轻量级:打包体积小,加载快

向量数据库:ChromaDB

选择 ChromaDB 的原因:
1. 嵌入式部署:无需独立数据库服务,适合单机部署
2. 零配置:开箱即用,API 简单友好
3. 原生 Python 支持:与 FastAPI 无缝集成
4. 持久化存储:支持本地文件存储,数据不丢失
5. 轻量级:资源占用低,适合个人服务器

嵌入模型:BAAI/bge-small-zh-v1.5

选择 BGE 的原因:
1. 中文优化:专门针对中文场景训练
2. 轻量级:small 版本约 100MB,推理速度快
3. 高性能:在中文语义检索任务上表现优秀
4. 开源免费:Apache 2.0 许可证,可商用
5. Sentence-Transformers 兼容:与 sentence-transformers 库无缝对接

大模型:DeepSeek

选择 DeepSeek 的原因:
1. 性价比高:API 调用成本低
2. 响应速度快:国内访问延迟低
3. 中文支持好:对中文语境理解到位
4. OpenAI 兼容:API 接口与 OpenAI 一致,迁移成本低

数据流(约2分钟)

用户提问后,系统执行以下流程:

问题 → Embedding → 向量检索 → 召回文档 → 构造 Prompt → LLM 生成

详细步骤

  1. 用户提问:前端接收用户输入,调用 /api/query 接口

  2. Embedding(向量化)

    • 使用 BGE 模型将用户问题转换为向量
    • 向量维度:384 维(bge-small-zh-v1.5)
  3. 向量检索

    • 在 ChromaDB 中进行余弦相似度搜索
    • 返回 Top-K(默认 3)个最相关的文档块
    • 设置相似度阈值(0.8),低于阈值返回”暂无资料”
  4. 召回文档

    • 检索结果包含文档内容、元数据(来源、部门、文件名)
    • 每个文档块包含 chunk_id、chunk_index 等信息
  5. 构造 Prompt

    • 将召回的文档作为参考资料
    • 加入系统指令(只能使用参考资料、附带来源引用)
    • 控制 Prompt 长度,避免超出模型上下文
  6. LLM 生成

    • 调用 DeepSeek API
    • 设置 temperature=0.1(低随机性,保证回答一致性)
    • 设置 max_tokens=1024(限制回答长度)
    • 返回回答和来源信息

遇到的问题(约3分钟)

问题1:PDF 解析失败

现象:部分 PDF 文件无法提取文本,或提取内容乱码

原因
- PDF 格式多样:扫描件(图片)、加密 PDF、复杂布局
- pdfplumber 对某些特殊编码支持不好

解决方案
1. 增加错误处理:捕获解析异常,记录失败文件
2. 预处理:建议用户提供可复制的 PDF
3. 备选方案:考虑使用 PyMuPDF(fitz)作为备选解析器

问题2:中文切块效果差

现象:按字符数切块时,句子被截断,语义不完整

原因
- 中文没有天然的单词边界
- 简单按字符数切分会破坏句子结构

解决方案
1. 基于标点符号分句:使用正则表达式按”。!?;”分割句子
2. 句子级切块:以完整句子为单位,组合成指定长度的块
3. 重叠策略:相邻块之间保留 60 字符的重叠,避免上下文丢失
4. 参数调优:chunk_size=400,chunk_overlap=60(经过测试的最优值)

问题3:Prompt 幻觉

现象:模型会编造文档中不存在的信息

原因
- 大模型本身具有”创作”倾向
- Prompt 指令不够严格
- 检索结果相关性不足

解决方案
1. 严格指令约束:在 Prompt 中明确要求”只能使用参考资料”,”没有信息时返回无法回答”
2. 来源引用强制:要求回答末尾必须列出引用来源
3. 相似度过滤:设置距离阈值(0.8),低相关度结果不送入 LLM
4. 低 temperature:设置 temperature=0.1,减少随机性

问题4:Docker 部署问题

现象:Docker 容器启动后无法访问,或模型加载失败

原因
- 端口映射配置错误
- 环境变量未正确传递
- 向量数据库目录权限问题
- 模型文件体积大,镜像构建慢

解决方案
1. 端口绑定:仅绑定 127.0.0.1,通过 Nginx 反向代理对外提供服务
2. 卷挂载:将 chroma_db、data、.env 挂载到容器外部,数据持久化
3. 资源限制:设置内存限制(1200M),避免 OOM
4. 构建优化:多阶段构建,缓存依赖安装

问题5:模型加载慢

现象:服务启动时 Embedding 模型加载需要数十秒

原因
- BGE 模型约 100MB,首次加载需要从磁盘读取并初始化
- 每次请求都可能触发模型加载

解决方案
1. 单例模式:EmbeddingModel 使用单例,全局只加载一次
2. 启动时预加载:在 FastAPI startup 事件中初始化模型
3. 模型缓存:使用 sentence-transformers 的缓存机制


项目亮点总结(约1分钟)

  1. 完整的工程化实践:从数据处理到部署的全链路
  2. 实用的技术选型:平衡性能、成本和开发效率
  3. 解决真实问题:面向学生的实际需求
  4. 可扩展性强:模块化设计,易于添加新数据源和模型
  5. 生产环境部署:已部署在个人博客站点,真实可用

时间分配建议(总计约15分钟):
- 项目背景:2分钟
- 为什么做/为什么不用关键词:3分钟
- 技术选型:3分钟
- 数据流:2分钟
- 遇到的问题:3分钟
- 总结与提问:2分钟

讲稿结构:以”学生找信息难”的场景切入,引出问题 → 对比传统方案 → 介绍 RAG 方案 → 讲解技术实现 → 分享踩坑经验 → 总结收获。