【一站式Agent学习平台】学Agent开发,用这个就够了
GitHub:
https://github.com/Earth-OL-Player/ai_learn_project
文章目录
一、先说结论
这是一套可以本地跑起来的 AI Agent 学习平台。
它不是只放资料链接的导航站,也不是单独的面试题库。项目把学习路线、热门面试题、AI 智能刷题、AI 评分、本题追问、成长体系和管理后台放在一个完整工程里。对学习者来说,它是一个练 Agent、RAG、大模型应用开发的产品;对开发者来说,它也是一个可以拆开看的全栈 Agent 项目。
我做这个项目时有一个很朴素的想法:学 Agent 不能只看概念。最好能看到一个真实项目里,前端怎么组织交互,后端怎么管理状态,AI 服务怎么接模型,模型挂了以后怎么兜底。
这个仓库目前分成三个工程:
ai_learn_project
├── ai-learn-web # Vue 3 前端,负责学习平台、刷题工作台、个人中心和管理端
├── ai-learn-backend # Spring Boot 后端,负责认证、题库、互动、成长和 AI 服务调用
├── ai-service # FastAPI AI 服务,负责评分 Agent、讨论 Agent 和模型适配
├── doc # 需求、架构、接口、中间件、迭代和截图资料
├── xuanchuan # 宣传文章和平台文案
└── QUICK_START.md # 本地启动说明
项目首页大概是这样:

二、为什么我想做一个 Agent 学习平台
学 AI 应用开发时,很容易遇到一个问题:资料太多,但练习太少。
今天收藏 LangChain,明天收藏 LangGraph,后天又看到 RAG、向量数据库、结构化输出、Function Calling、工具调用、模型评测。每个方向都有价值,但如果没有一条能落地的路线,最后经常变成收藏夹越来越满,真正能讲清楚的东西没增加多少。
面试时更明显。很多问题并不是背一个定义就能过。
比如:
| 面试题 | 真正考察的东西 |
|---|---|
| RAG 效果不好怎么排查? | 文档切分、召回、重排序、Prompt、评测和日志 |
| Agent 工具调用失败怎么办? | 超时、重试、降级、状态恢复和错误提示 |
| LangGraph 适合解决什么问题? | 多步骤状态机、条件分支、可观测性和流程编排 |
| 大模型输出 JSON 不稳定怎么处理? | 结构化输出、Schema 校验、兜底和重试 |
| AI 服务接入业务系统后怎么控成本? | 限流、模型分级、调用记录和用户权益 |
这些题只看标准答案,很难知道自己是不是真的会。
所以我把平台设计成一条学习链路:
学习路线
-> 热门面试题
-> AI 智能刷题
-> AI 评分和本题追问
-> 成长体系和刷题记录
-> 回到薄弱点继续补
用户不是只读资料,而是先答题,再拿到反馈,然后围绕当前题继续追问。这个过程更接近真实面试和真实项目复盘。
三、当前已经做了哪些功能
项目不是空壳页面,当前已经形成了前端、Java 后端、Python AI 服务、MySQL 数据库的完整链路。
| 模块 | 当前能力 |
|---|---|
| 首页 | 展示平台定位、学习入口和功能导航 |
| 学习路线 | 用 Markdown 管理 AI 应用开发路线和资料 |
| 热门面试题 | 按 AI Agent、RAG、向量检索等方向整理题目 |
| AI 智能刷题 | 支持分类抽题、答题、评分、重答、下一题 |
| AI 本题讨论 | 围绕当前题继续追问,支持 SSE 流式输出 |
| 成长体系 | 经验、等级、段位、徽章墙和学习天数 |
| 刷题记录 | 记录最高分、最近分、练习统计和薄弱题 |
| 建议评论区 | 支持建议、评论、点赞、排序和登录引导 |
| 管理后台 | 支持用户、题库、兑换码、模型配置和日志级别管理 |
| AI 服务 | FastAPI 提供结构化评分和讨论能力,支持本地规则兜底 |
AI 智能刷题页面是目前最核心的页面:
学习路线页面用 Markdown 管理内容,适合持续补充资料:

热门面试题页面用来沉淀高频题:

成长体系页面负责给练习一个长期反馈:

刷题记录页面可以看到练习结果和薄弱题:

建议评论区用于收集功能建议和学习反馈:

四、技术栈
项目技术栈没有刻意堆新东西,优先选了容易本地启动、社区资料多、后续可维护的组合。
| 层级 | 技术选型 |
|---|---|
| 前端 | Vue 3、Vite、TypeScript、Pinia、Vue Router、Element Plus、Markdown-It、DOMPurify |
| Java 后端 | Java 17、Spring Boot、Spring Security、JWT、MyBatis、Flyway |
| AI 服务 | Python 3.11+、FastAPI、Uvicorn、LangChain、LangGraph、OpenAI 兼容模型 |
| 数据库 | MySQL 8.4 LTS |
| 配置 | 环境变量、占位符配置、本地 .env |
| 文档 | Markdown、需求文档、架构文档、接口规范、中间件说明 |
整体架构可以理解为:
这里有一个边界我刻意保留得很清楚:浏览器不直接调 AI 服务,AI 服务也不直接写业务表。
前端只调 Java 后端。后端负责鉴权、题库、会话状态、成长结算和数据落库,再通过内部 Token 调用 Python AI 服务。Python 只做模型相关能力,比如评分、讨论、流式输出和本地兜底。
这样拆开以后,本地开发比较好排查问题:
| 问题 | 优先看哪里 |
|---|---|
| 页面不显示 | ai-learn-web |
| 登录、权限、题库、成长不对 | ai-learn-backend |
| AI 评分、讨论、流式输出异常 | ai-service |
| 数据没有保存 | MySQL 和 Flyway migration |
| 模型不返回 | AI 服务环境变量、模型地址、API Key、Token |
五、AI 智能刷题是怎么跑起来的
AI 智能刷题不是简单地把用户答案丢给大模型。它有一个明确的状态机。
QUESTIONING 等待出题
ANSWERING 用户正在回答当前题
DISCUSSING 已评分,可以围绕当前题继续追问
一次完整流程是这样的:
用户进入刷题页
-> 后端读取当前刷题状态
-> 用户点击开始或下一题
-> 后端按分类和历史得分抽题
-> 用户提交答案
-> 后端优先调用 Python AI 服务评分
-> AI 服务不可用时切到 Java 本地规则评分
-> 后端保存题目统计,更新经验和徽章
-> 用户围绕当前题继续追问
-> Python AI 服务流式返回讨论内容
对应的接口大致如下:
| 接口 | 用途 |
|---|---|
GET /api/v1/practice/categories |
查询题目分类 |
GET /api/v1/practice/state |
恢复当前刷题状态 |
POST /api/v1/practice/next-question |
抽取下一题 |
POST /api/v1/practice/retry |
重新回答当前题 |
POST /api/v1/practice/messages/stream |
流式处理出题、答题或讨论 |
POST /internal/v1/practice/answer/grade |
Python 内部评分接口 |
POST /internal/v1/practice/discuss/stream |
Python 内部流式讨论接口 |
这里最重要的是,AI 只是链路的一部分,不是整个系统。题库、用户当前状态、答题统计、成长经验和徽章都在 Java 后端里统一管理。
六、抽题策略:不是随机抽一个题就完事
刷题如果完全随机,体验会很差。用户可能连续刷到已经高分通过的题,也可能一直刷不到薄弱题。
当前抽题策略保留了几个简单但有效的维度:
题目权重 = 基础权重
+ 题目重要性加权
+ 答题次数加权
+ 历史最高分加权
+ 小幅随机扰动
对应代码在 QuestionSelectionService,核心意思是:
private double calculateWeight(PracticeQuestionRecord question) {
int answeredCount = NumberUtils.toIntOrZero(question.getAnsweredCount());
int bestScore = NumberUtils.toIntOrZero(question.getBestScore());
double importanceScore = safeDouble(question.getImportanceScore());
// 答题次数越少、历史最高分越低、题目重要性越高,权重越高。
double weight = BASE_WEIGHT;
weight += normalizePercent(importanceScore) * IMPORTANCE_WEIGHT_FACTOR;
weight += ANSWER_COUNT_WEIGHT_FACTOR / (1 + Math.max(0, answeredCount));
weight += (1 - normalizePercent(bestScore)) * BEST_SCORE_WEIGHT_FACTOR;
return Math.max(MIN_WEIGHT, weight);
}
我比较喜欢这种可解释的实现。它没有一上来就搞复杂推荐系统,但用户能明显感受到:没刷过、分数低、重要性高的题会更容易出现。
以后如果要做得更细,可以继续加入知识点、题目难度、最近练习时间、连续低分次数,甚至接入 RAG 做相似题推荐。
七、评分 Agent:先结构化,再让后端校验
评分结果不能只返回一段自然语言。前端要展示得分、命中点、遗漏点、问题、参考答案和改进建议,后端也要保存统计数据,所以评分必须结构化。
一个评分结果大概长这样:
{
"score": 86,
"isCorrect": true,
"hitPoints": ["理解了 RAG 的检索增强思想"],
"missingPoints": ["没有说明向量检索和重排序"],
"problems": ["答案对数据入库流程描述较弱"],
"referenceAnswer": "参考答案占位符",
"improvementAdvice": "建议补充 Embedding、向量库、召回、重排序、生成之间的关系"
}
Java 后端调用 AI 服务时,不直接信任模型文本,而是通过 PracticeAiClient 做一次内部调用:
public Optional<PracticeAiGradingResult> grade(
Long userId,
PracticeQuestionRecord question,
String userAnswer,
AiModelRequestConfig modelConfig) {
if (!isEnabled()) {
return Optional.empty();
}
try {
ObjectNode payload = objectMapper.createObjectNode();
payload.put("userId", String.valueOf(userId));
payload.put("questionCode", question.getCode());
payload.put("question", question.getQuestion());
payload.put("questionType", question.getQuestionType());
payload.put("standardAnswer", question.getStandardAnswer());
payload.put("userAnswer", userAnswer);
appendModelConfig(payload, modelConfig);
JsonNode data = postJson(AiServiceConstants.PRACTICE_GRADE_PATH, payload).orElse(null);
if (data == null) {
return Optional.empty();
}
return Optional.of(toPracticeAiGradingResult(data));
} catch (RuntimeException exception) {
LOGGER.warn("AI 服务评分结果转换失败,已切换后端本地评分:questionCode={}", question.getCode(), exception);
return Optional.empty();
}
}
这段代码有一个重点:失败时返回 Optional.empty(),上层再切到本地评分规则。
也就是说,模型服务挂了,平台仍然能给出基础评分,只是会告诉用户当前使用的是兜底能力。学习产品最怕的是“用户答完了,系统直接崩掉”。兜底不一定完美,但至少要把流程跑完。
八、讨论 Agent:围绕当前题继续追问
很多时候,评分只是第一步。
用户真正需要的是继续问:
我这个答案哪里不严谨?
如果面试官追问向量检索,我该怎么接?
这道题能不能用项目案例回答?
RAG 和 Agent 在这个问题里有什么区别?
所以平台在评分后会进入 DISCUSSING 阶段。此时用户可以围绕当前题继续追问,后端会把题目、用户最近答案、评分摘要、短期讨论历史一起发给 AI 服务。
Java 后端对 Python 流式讨论的调用大概是这样:
public Optional<String> discussStream(
PracticeQuestionRecord question,
String lastUserAnswer,
String gradingSummary,
String discussionHistoryJson,
String message,
AiModelRequestConfig modelConfig,
Consumer<String> chunkConsumer) {
if (!isEnabled()) {
return Optional.empty();
}
try {
ObjectNode payload = buildDiscussPayload(question, lastUserAnswer, gradingSummary, discussionHistoryJson, message);
appendModelConfig(payload, modelConfig);
return postEventStream(AiServiceConstants.PRACTICE_DISCUSS_STREAM_PATH, payload, chunkConsumer);
} catch (ClientStreamClosedException exception) {
throw exception;
} catch (RuntimeException exception) {
LOGGER.warn("AI 服务流式讨论失败,已切换后端本地讨论:questionCode={}", question.getCode(), exception);
return Optional.empty();
}
}
这里没有把讨论做成通用聊天,而是限定在当前题里。原因很简单:刷题页应该服务刷题,不应该变成一个没有边界的聊天窗口。
九、SSE 流式输出:用户不用等完整回答
AI 回答如果要等完整结果出来再展示,等待感会比较明显。项目里使用 SSE 做流式输出。
前端请求的是:
POST /api/v1/practice/messages/stream
后端入口在 PracticeController:
@PostMapping(value = "/messages/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> handleMessageStream(@RequestBody PracticeMessageRequest request) {
AuthenticatedUser authenticatedUser = AuthContext.getUser();
RateLimitLease aiLease = acquireAiConcurrency(authenticatedUser);
SseEmitter emitter = new SseEmitter((long) SSE_TIMEOUT_MILLIS);
String traceId = TraceContext.getTraceId();
AtomicBoolean streamClosed = new AtomicBoolean(false);
AtomicReference<Future<?>> streamTaskReference = new AtomicReference<>();
registerEmitterLifecycle(emitter, streamClosed, streamTaskReference, aiLease);
Future<?> streamTask = aiStreamTaskExecutor.submit(
() -> emitMessageStream(request, emitter, authenticatedUser, traceId, streamClosed, aiLease)
);
streamTaskReference.set(streamTask);
return ResponseEntity.ok()
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(emitter);
}
这个地方有几个工程细节值得看:
| 细节 | 作用 |
|---|---|
SseEmitter |
把 AI 回复分片推给前端 |
aiStreamTaskExecutor |
用受控线程池承接流式任务 |
RateLimitLease |
限制 AI 并发,避免单个用户无限开流 |
TraceContext |
异步任务里保留链路追踪 |
| 生命周期回调 | 客户端断开时取消任务并释放并发名额 |
这些东西看起来不如模型调用“酷”,但真实业务里非常关键。AI 功能上线后,最先暴露的问题通常不是 Prompt 不够好,而是超时、并发、断连、日志和成本控制。
十、Python AI 服务:默认本地规则,配置后再接真实模型
ai-service 是单独的 FastAPI 服务,当前对 Java 后端开放内部接口:
GET /health
POST /internal/v1/practice/answer/grade
POST /internal/v1/practice/discuss/stream
Python 侧会根据环境变量判断是否启用真实模型:
return (
bool(settings.ai_grading_base_url.strip())
and bool(api_key)
and model.upper() != LOCAL_RULE_MODEL
and api_key != AI_GRADING_API_KEY_PLACEHOLDER
)
默认模型名是 LOCAL_RULE,也就是不会主动调用外部模型。这样做有两个好处。
第一,本地新同学可以不配置模型 Key,先把前端、后端、数据库和 AI 服务链路跑通。
第二,仓库里不会出现真实密钥。所有 Token、API Key、数据库密码都通过环境变量或本地私有配置注入。
如果想接真实模型,可以在 ai-service/.env 里配置:
AI_SERVICE_TOKEN=AI_SERVICE_TOKEN本地占位符
AI_GRADING_BASE_URL=模型服务地址占位符/v1/chat/completions
AI_GRADING_API_KEY=AI_GRADING_API_KEY占位符
AI_GRADING_MODEL=模型名占位符
AI_GRADING_MODEL_PROVIDER=模型供应商占位符
AI_GRADING_TIMEOUT_SECONDS=20
AI_GRADING_MAX_OUTPUT_TOKENS=800
项目里也有 DeepSeek 供应商识别逻辑,后续可以按自己的模型供应商继续扩展。
十一、成长体系:让刷题有反馈
学习产品如果没有反馈,很容易半途停下。这个项目里把答题结果和成长体系打通了。
当前成长信息包括:
| 项目 | 说明 |
|---|---|
| 经验值 | 根据刷题最高分等数据计算 |
| 等级 | 例如 AI 入门者、AI 实践者、AI Agent 玩家 |
| 段位 | 用更直观的方式展示学习阶段 |
| 学习天数 | 根据练习记录统计 |
| 平均最高分 | 反映当前题库掌握情况 |
| 徽章墙 | 完成指定条件后获得徽章 |
| 新获得徽章 | 当次答题或追问后即时展示 |
成长服务代码也保持得比较直接:
private GrowthResponse buildGrowthResponse(User user, List<BadgeResponse> newBadges) {
int experience = NumberUtils.toNonNegativeInt(user.getExperience());
GrowthLevel level = growthRuleService.resolveLevel(experience);
GrowthRank rank = growthRuleService.resolveRank(experience);
int nextLevelExperience = level.nextLevelExperience();
int currentLevelExperience = level.minExperience();
return new GrowthResponse(
0,
experience,
level.displayCode(),
level.displayName(),
rank.displayName(),
level.levelValue(),
currentLevelExperience,
nextLevelExperience,
level.progressText(experience),
growthMapper.countCompletedAnswers(user.getId()),
growthMapper.averageBestScore(user.getId()),
Math.max(0, nextLevelExperience - experience),
growthAwardService.calculateLearningDays(user.getId()),
growthAwardService.findBadgeWall(user.getId()),
newBadges
);
}
这里没有做复杂的游戏系统。我的目标只是让用户每次答题后能看到一点变化:分数变了、经验变了、徽章可能出现了,个人中心里的记录也能查到。
十二、数据库设计:核心表不复杂,但边界要清楚
当前核心数据都在 MySQL 里,Flyway 管理表结构版本。
| 表 | 作用 |
|---|---|
users |
用户账号、昵称、头像、经验、等级、段位 |
questions |
系统题库,包含题目编码、题目内容、分类、参考答案 |
user_practice_sessions |
用户当前刷题会话,记录阶段和当前题 |
user_question_stats |
用户每道题的答题汇总,记录次数、最高分、最近分 |
badges |
徽章定义 |
user_badges |
用户已获得徽章 |
suggestions |
用户建议 |
comments |
评论和回复 |
system_settings |
系统配置,例如模型权益、日志级别等 |
我没有给这些表设计外键。这个项目是私人项目,当前更看重迭代灵活性和迁移稳定性。资源归属、删除状态、用户权限这些规则由后端业务代码控制。
数据库变更统一新增 Flyway migration,不改历史 migration。这样本地和服务器环境不容易出现 Flyway 校验不一致。
十三、本地启动方式
建议本地准备:
| 环境 | 推荐版本 |
|---|---|
| JDK | 17 |
| Maven | 3.9.x 或兼容版本 |
| Node.js | 20 LTS 或 22 LTS |
| Python | 3.11+ |
| MySQL | 8.4 LTS |
1. 启动 MySQL
按仓库里的 doc/中间件/MySQL.md 创建数据库和业务账号。后端启动时会通过 Flyway 自动初始化表结构。
后端环境变量示例:
DATABASE_URL="jdbc:mysql://127.0.0.1:3306/ai_learn?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false"
DATABASE_USERNAME="本地MySQL用户名占位符"
DATABASE_PASSWORD="本地MySQL密码占位符"
SPRING_FLYWAY_ENABLED="true"
JWT_SECRET="至少32字节本地JWT随机密钥占位符"
JWT_EXPIRES_IN_SECONDS="7200"
AI_SERVICE_ENABLED="true"
AI_SERVICE_BASE_URL="本地AI服务地址占位符,例如本机8000端口"
AI_SERVICE_TOKEN="AI_SERVICE_TOKEN本地占位符"
AI_SERVICE_TIMEOUT_SECONDS="15"
2. 启动 AI 服务
cd ai-service
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
$env:AI_SERVICE_TOKEN="AI_SERVICE_TOKEN本地占位符"
$env:AI_SERVICE_LOG_LEVEL="INFO"
uvicorn app.main:app --host 0.0.0.0 --port 8000
健康检查:
localhost:8000/health
3. 启动 Java 后端
cd ai-learn-backend
mvn spring-boot:run
健康检查:
Invoke-RestMethod -Uri "本地后端服务地址占位符/api/v1/health" -Method Get
4. 启动前端
cd ai-learn-web
npm install
npm run dev
前端默认地址:
localhost:5173
注意:文章开头我只放 GitHub 仓库。线上入口不是本篇重点,想体验或二次开发的话,建议直接从源码跑一遍。
十四、读这个项目,可以重点看哪些代码
如果你是为了学 Agent 工程化,我建议按下面顺序看。
| 顺序 | 文件或目录 | 看什么 |
|---|---|---|
| 1 | README.md、QUICK_START.md |
项目定位和启动方式 |
| 2 | doc/2.架构设计/2.1架构设计文档.md |
前端、后端、AI 服务边界 |
| 3 | ai-learn-web/src/pages/practice-agent |
刷题工作台交互 |
| 4 | PracticeController |
外部刷题接口和 SSE 入口 |
| 5 | PracticeService |
状态机编排:出题、答题、讨论 |
| 6 | QuestionSelectionService |
抽题权重策略 |
| 7 | PracticeAiClient |
Java 调 Python AI 服务 |
| 8 | AnswerGradingDomainService |
Java 本地兜底评分 |
| 9 | ai-service/app/api/practice.py |
Python 内部接口 |
| 10 | PracticeAgentService |
Python 评分 Agent 和讨论 Agent |
| 11 | GrowthService、GrowthAwardService |
答题后的成长反馈 |
| 12 | db/migration |
业务表结构演进 |
如果只想快速理解 AI 智能刷题,就看这条链路:
PracticeController
-> PracticeService
-> QuestionSelectionService
-> PracticeGradingService
-> PracticeAiClient
-> ai-service/app/api/practice.py
-> PracticeAgentService
这条链路看完,基本就能理解一个 AI 功能从页面按钮到模型响应,再到业务落库的完整过程。
十五、这个项目适合谁
我觉得它比较适合这几类人:
| 人群 | 可以怎么用 |
|---|---|
| 正在学 AI Agent 的开发者 | 先看学习路线,再刷题练表达 |
| 准备 AI 应用开发面试的人 | 用热门面试题和 AI 评分做复盘 |
| 想做全栈 AI 项目的人 | 参考 Vue + Spring Boot + FastAPI 的拆分方式 |
| 想接入大模型到业务系统的人 | 看 AI 服务调用、Token、超时、兜底和 SSE |
| 想练习 AI 编程助手协作的人 | 看仓库里的文档、规范和迭代资料 |
如果你已经会调模型 API,但不太清楚怎么把它接到业务系统里,这个项目会更有参考价值。因为它不止有 Prompt,还有用户状态、权限、数据库、成长体系、限流和兜底。
十六、后续我准备继续做什么
目前项目已经能跑通核心流程,但还有不少地方值得继续补。
| 方向 | 计划 |
|---|---|
| 题库 | 补充更多 Agent、RAG、向量检索、模型评测场景题 |
| 推荐 | 引入知识点维度,让薄弱点复习更准确 |
| AI 服务 | 增加熔断、重试退避和模型成本记录 |
| RAG | 在当前题库基础上尝试资料检索和相似题推荐 |
| 后台 | 优化题库导入、质量检查和内容维护 |
| 前端 | 继续打磨移动端体验和刷题记录分析 |
| 文档 | 补充更多接口示例和部署检查清单 |
我不会急着把它做成一个很重的平台。现在更重要的是把 Agent 学习这条线打磨顺:资料能看,题能刷,答案能评,问题能追问,结果也能留下来。
十七、最后
如果你正在学 Agent 开发,我建议不要只停留在“看工具文档”和“跑一个 Demo”。
真正有价值的部分,往往在模型调用之外:状态怎么管、失败怎么兜底、结果怎么校验、成本怎么控、用户怎么感知进度、数据怎么沉淀。
这个项目就是围绕这些问题做的。它可以当学习平台用,也可以当 Agent 工程样例拆开看。先跑起来,再顺着刷题链路读代码,会比只看一堆概念清楚很多。
更多推荐

所有评论(0)