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、需求文档、架构文档、接口规范、中间件说明

整体架构可以理解为:

用户浏览器

Vue 3 + Vite 前端

Spring Boot 业务后端

MySQL 业务数据库

FastAPI AI 服务

本地规则兜底或外部模型服务

这里有一个边界我刻意保留得很清楚:浏览器不直接调 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.mdQUICK_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 GrowthServiceGrowthAwardService 答题后的成长反馈
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 工程样例拆开看。先跑起来,再顺着刷题链路读代码,会比只看一堆概念清楚很多。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐