附录 A:实战项目 — AI Coach 虚拟学习教练#
项目概述
AI Coach 是一个基于 RAG + Agent 的个人知识库与学习督导系统。它展示了如何将本书中讲解的 Prompt Engineering、RAG 架构、Agent 框架、FastAPI 后端和 Vue.js 前端等知识点融合到一个完整的全栈应用中。
📁 源码位置:examples/ai-coach/
A.1 项目背景与设计思路#
为什么做这个项目?#
在学习技术的过程中,我们常常面临三个痛点:
知识碎片化:学习资料散落在各处,需要时找不到
缺乏督促:自学容易三天打鱼两天晒网
反馈不足:不知道自己学得怎么样,哪里需要加强
AI Coach 正是为了解决这三个问题而设计的。它不是一个简单的聊天机器人,而是一个有记忆、有知识、有策略的学习伙伴。
设计原则#
在动手编码之前,我们确立了几个设计原则:
知识驱动:所有回答都基于用户上传的知识库,而非 AI 的通用知识
多模式交互:教练督促、导师讲解、测验考核,满足不同学习场景
数据追踪:记录学习时长、连续天数、难度感受,用数据驱动反馈
流式体验:SSE 流式输出,像真人对话一样逐字显示
A.2 技术架构#
整体架构图#
┌─────────────────────────────────────────────────────┐
│ Vue.js 3 前端 │
│ Vite + Tailwind CSS + Pinia │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 对话页面 │ │ 知识库页面 │ │ 学习计划页面 │ │
│ │ SSE 流式 │ │ 上传/检索 │ │ 目标/进度 │ │
│ └─────┬────┘ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌─────▼──────────────▼──────────────────▼────────┐ │
│ │ Axios + Token 拦截器 + SSE 客户端 │ │
│ └─────────────────────┬──────────────────────────┘ │
├────────────────────────┼─────────────────────────────┤
│ FastAPI 后端 │
│ ┌─────────┐ ┌────────▼────────┐ ┌──────────────┐ │
│ │Auth API │ │ Chat API │ │ Learning API │ │
│ │JWT+RBAC │ │ 普通 + SSE │ │ 目标+记录 │ │
│ └────┬────┘ └────────┬────────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌───────▼────────┐ ┌──────▼───────┐ │
│ │ User │ │ CoachAgent │ │ Progress │ │
│ │ Model │ │ 3 种模式 │ │ Tracker │ │
│ └────┬────┘ └───────┬────────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌───────▼────────┐ ┌──────▼───────┐ │
│ │ SQLite │ │ RAG Engine │ │ SQLite │ │
│ │ (users) │ │ LlamaIndex │ │ (goals) │ │
│ └─────────┘ └───────┬────────┘ └──────────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ ChromaDB │ │
│ │ 向量数据库 │ │
│ └────────────────┘ │
└──────────────────────────────────────────────────────┘
技术选型理由#
技术 |
选择 |
理由 |
|---|---|---|
后端框架 |
FastAPI |
异步原生、自动 API 文档、类型安全 |
ORM |
SQLAlchemy 2.0 async |
异步支持好、生态成熟 |
向量数据库 |
ChromaDB |
轻量级、嵌入式、适合单机部署 |
RAG 框架 |
LlamaIndex |
抽象层次高、开箱即用 |
LLM |
OpenAI GPT-4o |
中文能力强、指令遵循好 |
前端框架 |
Vue.js 3 + Vite |
轻量、响应式、开发体验好 |
CSS |
Tailwind CSS |
原子化、快速原型、一致性好 |
认证 |
JWT + bcrypt |
无状态、前后端分离友好 |
A.3 核心实现详解#
A.3.1 RAG 引擎:让 AI 基于你的知识回答#
RAG(Retrieval-Augmented Generation)是本项目的核心。它让 AI 不再"胡说八道",而是基于你上传的学习资料来回答问题。
工作流程:
用户提问 → 向量检索(ChromaDB)→ 取回相关文档片段 → 注入 Prompt → LLM 生成回答
关键代码解析:
# app/rag/engine.py — 文档添加
async def add_document(self, title, content, tags, doc_id):
doc = LlamaDocument(text=content, metadata={"title": title, "tags": ",".join(tags)})
# 分块:将长文档切成小段,每段 512 个 token,重叠 50 个
parser = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = parser.get_nodes_from_documents([doc])
# 向量化并存入 ChromaDB
self.index.insert_nodes(nodes)
分块策略的选择
分块大小(chunk_size)是 RAG 系统最重要的超参数之一:
太大(>1024):检索精度下降,可能引入无关内容
太小(<256):丢失上下文,回答不完整
推荐:技术文档用 512-1024,对话记录用 256-512
重叠(chunk_overlap)确保跨块的句子不会被截断,通常设为 chunk_size 的 10-20%。
查询流程:
# app/rag/engine.py — 知识检索
async def query(self, question, top_k=5):
retriever = VectorIndexRetriever(index=self.index, similarity_top_k=top_k)
# 过滤低相关度结果(相似度 < 0.5 的丢弃)
postprocessors = [SimilarityPostprocessor(similarity_cutoff=0.5)]
# 用检索到的内容生成回答
synthesizer = get_response_synthesizer(
response_mode="compact",
text_qa_template=QA_PROMPT, # 自定义 Prompt
)
query_engine = RetrieverQueryEngine(
retriever=retriever,
response_synthesizer=synthesizer,
node_postprocessors=postprocessors,
)
response = query_engine.query(question)
A.3.2 CoachAgent:三种模式的智能体#
CoachAgent 是项目的"灵魂"。它不是一个简单的 API 调用封装,而是一个有角色、有策略、有记忆的智能体。
三种模式的 Prompt 设计:
模式 |
角色定位 |
温度 |
核心指令 |
|---|---|---|---|
🎯 教练 |
亲切但有要求的导师 |
0.7 |
督促进度、制定计划、鼓励激励 |
📖 导师 |
耐心的大学教授 |
0.7 |
深入讲解、举例说明、循序渐进 |
✍️ 测验 |
严格的考试官 |
0.7 |
出题考核、评判答案、难度递进 |
Prompt 设计心得
好的系统 Prompt 应该包含四个要素:
角色定义:你是谁?(“你是一位经验丰富的学习教练”)
职责清单:你要做什么?(编号列出 5 个具体职责)
风格约束:怎么做?(“像一位亲切但专业的导师”)
格式要求:输出什么?(“使用中文回答,适当使用 emoji”)
流式输出的实现:
# app/agents/coach.py — 流式生成
async def respond_stream(self, message, history, mode):
# 1. 先做 RAG 检索(非流式,很快)
rag_context = await self.rag_engine.query(message)
# 2. 构建消息列表
messages = [system_prompt + rag_context] + history + [user_message]
# 3. 流式调用 LLM
response_gen = self.llm.stream_chat(messages)
for chunk in response_gen:
yield {"token": chunk.delta} # 逐 token 返回
A.3.3 SSE 流式对话#
传统的 HTTP 请求-响应模式需要等 AI 生成完整回复才能显示,用户体验差。SSE(Server-Sent Events)让我们可以逐字推送,实现"打字机效果"。
后端 SSE 端点:
# app/api/chat.py
@router.post("/message/stream")
async def send_message_stream(chat_req: ChatRequest):
async def event_generator():
yield f"data: {json.dumps({'type': 'start', 'session_id': sid})}\n\n"
async for chunk in coach.respond_stream(message, history, mode):
yield f"data: {json.dumps({'type': 'token', 'token': chunk})}\n\n"
yield f"data: {json.dumps({'type': 'done'})}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
前端 SSE 客户端:
// src/utils/api.js
export function streamChat(message, sessionId, mode, onToken, onDone, onError) {
fetch('/api/chat/message/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ message, session_id: sessionId, mode }),
}).then(async (response) => {
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
// 解析 SSE 数据,逐 token 回调
const data = JSON.parse(line.slice(6))
if (data.type === 'token') onToken(data.token)
}
})
}
为什么选 SSE 而不是 WebSocket?
SSE 是单向的(服务器→客户端),基于 HTTP,更简单
WebSocket 是双向的,适合实时聊天、游戏等场景
对于 AI 对话,用户发一条消息、AI 回一条消息,SSE 完全够用
SSE 天然支持自动重连,且不需要额外的协议升级
A.3.4 JWT 认证与角色授权#
认证流程:
注册/登录 → 服务器返回 access_token + refresh_token
↓
前端存入 localStorage
↓
每次请求自动附加 Authorization: Bearer <token>
↓
后端解码 JWT,验证用户身份
↓
token 过期 → 用 refresh_token 换新 token
角色授权:
# 三级角色体系
role_hierarchy = {"admin": 3, "editor": 2, "user": 1}
# 使用方式:保护需要 admin 权限的端点
@router.delete("/documents/{doc_id}")
async def delete_document(doc_id: str, user: dict = Depends(require_role("admin"))):
...
前端路由守卫:
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (!to.meta.public && !token) {
next('/login') // 未登录,跳转登录页
} else {
next()
}
})
A.4 学习计划追踪#
学习计划模块不仅记录数据,还会基于数据生成 AI 教练反馈:
# 计算统计数据
total_hours = sum(s.duration_minutes for s in sessions) / 60
streak_days = calculate_streak(sessions) # 连续学习天数
completion_pct = total_minutes / expected_minutes * 100
# AI 教练基于数据给反馈
coach_prompt = f"""
- 学习主题:{goal.topic}
- 已学习:{total_hours} 小时
- 连续学习:{streak_days} 天
- 完成度:{completion_pct}%
- 平均难度:{avg_difficulty}/5
请给出:1) 一句鼓励的话 2) 2-3 条具体建议
"""
这种"数据 + AI"的模式,让反馈不再是泛泛而谈,而是基于用户真实学习数据的个性化建议。
A.5 项目运行指南#
环境准备#
# 前置要求
- Python 3.11+
- Node.js 18+
- OpenAI API Key
快速启动#
# 1. 启动后端
cd examples/ai-coach/backend
cp .env.example .env
# 编辑 .env,填入 OPENAI_API_KEY
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python run.py
# → http://localhost:8000 (API 文档: /docs)
# 2. 启动前端(新终端)
cd examples/ai-coach/frontend
npm install
npm run dev
# → http://localhost:5173
Docker 一键部署#
cd examples/ai-coach
cp backend/.env.example backend/.env
# 编辑 .env
docker compose up -d
# → http://localhost:5173
使用流程#
注册账号 → 进入对话页面
上传知识 → 知识库页面,上传你的学习资料(Markdown、TXT)
开始对话 → 选择教练/导师/测验模式,与 AI 交流
设定目标 → 学习计划页面,创建学习目标
记录学习 → 每次学习后记录时长和笔记
查看进度 → 获取 AI 教练的个性化反馈
A.6 涉及的书中知识点#
章节 |
知识点 |
在项目中的体现 |
|---|---|---|
第 6 章 |
Prompt Engineering |
CoachAgent 三种模式的系统提示词设计 |
第 17 章 |
RAG 架构 |
LlamaIndex + ChromaDB 知识库检索增强 |
第 17 章 |
分块策略 |
SentenceSplitter 的 chunk_size/overlap 配置 |
第 18 章 |
Agent 框架 |
CoachAgent 多模式智能体实现 |
第 21 章 |
多 Agent 协作 |
教练、导师、测验三种角色的切换 |
第 22 章 |
Agent 评估 |
学习进度追踪作为间接评估手段 |
A.7 启发与思考#
对读者的启发#
RAG 不只是"检索+生成":好的 RAG 系统需要精心设计分块策略、相似度阈值、Prompt 模板。本项目展示了一个完整的 RAG 工程实践。
Agent 的核心是 Prompt:CoachAgent 的三种模式,本质上是三套不同的系统 Prompt。好的 Prompt 设计能让同一个 LLM 表现出截然不同的"人格"。
数据驱动的 AI 反馈:不要让 AI 凭空给建议。把用户的真实数据(学习时长、难度感受、连续天数)注入 Prompt,AI 的反馈会更有针对性。
SSE 是 AI 应用的标配:流式输出不是锦上添花,而是必需品。用户等待 10 秒看到完整回复 vs 立即看到逐字输出,体验天差地别。
读者可以怎么做#
动手实践建议
初级挑战:
修改 CoachAgent 的 Prompt,添加一个新模式(如"面试官模式")
在学习计划中添加"每周总结"功能
支持上传 PDF 文件(提示:使用
pypdf库)
中级挑战:
将 ChromaDB 替换为 Milvus 或 Qdrant,对比性能
添加多用户支持:每个用户有独立的知识库
实现"学习小组"功能:多个用户共享知识库
高级挑战:
将 CoachAgent 改造为 LangGraph 状态机,实现更复杂的对话流程
添加语音输入/输出(Whisper + TTS)
部署到云端,添加 CI/CD 流水线
实现 Agent 自主学习:根据用户反馈自动优化 Prompt
A.8 项目文件清单#
ai-coach/ # 43 个文件
├── README.md # 项目文档
├── docker-compose.yml # Docker 编排
├── backend/ # Python 后端
│ ├── Dockerfile
│ ├── .env.example
│ ├── requirements.txt
│ ├── run.py
│ └── app/
│ ├── main.py # FastAPI 入口
│ ├── core/
│ │ ├── config.py # 配置(JWT_SECRET, OPENAI_API_KEY...)
│ │ ├── database.py # SQLAlchemy async
│ │ └── auth.py # JWT 认证 + 角色授权
│ ├── models/
│ │ ├── schemas.py # Pydantic 模型
│ │ └── db_models.py # ORM(User, Document, Goal, Session...)
│ ├── api/
│ │ ├── auth.py # 注册/登录/刷新
│ │ ├── chat.py # 对话(普通 + SSE 流式)
│ │ ├── knowledge.py # 知识库 CRUD + RAG 查询
│ │ ├── learning.py # 学习目标 + 进度追踪
│ │ └── health.py # 健康检查
│ ├── agents/
│ │ └── coach.py # CoachAgent(教练/导师/测验)
│ └── rag/
│ └── engine.py # RAG 引擎(LlamaIndex + ChromaDB)
└── frontend/ # Vue.js 前端
├── Dockerfile
├── package.json
├── vite.config.js
├── tailwind.config.js
└── src/
├── main.js
├── App.vue # 侧边栏布局
├── router.js # 路由 + 守卫
├── stores/
│ └── auth.js # Pinia 认证状态
├── views/
│ ├── LoginView.vue # 登录/注册
│ ├── ChatView.vue # 对话(SSE 流式)
│ ├── KnowledgeView.vue # 知识库管理
│ └── LearningView.vue # 学习计划
└── utils/
├── api.js # Axios + Token 拦截器 + SSE
└── markdown.js # Markdown 渲染