项目开发笔记
# AI 智能面试辅助平台
# 项目目录结构
项目采用前后端分离架构,后端基于 Spring Boot 4.1 + Java 25 + Spring AI 2.0,前端基于 React 18 + TypeScript + Vite。后端按"通用能力(common)+ 基础设施(infrastructure)+ 业务模块(modules)"三层组织,业务模块按领域划分。
interview-guide/ # 项目根目录
├── app/ # 后端应用(Spring Boot 4.1 + Java 25)
│ ├── build.gradle # 构建配置(依赖、bootRun .env 注入、UTF-8 编码)
│ ├── Dockerfile # 后端镜像构建
│ └── src/
│ ├── main/
│ │ ├── java/interview/guide/
│ │ │ ├── App.java # 主启动类
│ │ │ │
│ │ │ ├── common/ # 通用基础能力(跨模块复用)
│ │ │ │ ├── ai/ # AI 能力封装
│ │ │ │ │ ├── LlmProviderRegistry.java # LLM Provider 注册中心
│ │ │ │ │ ├── StructuredOutputInvoker.java # 结构化输出调用器
│ │ │ │ │ ├── PromptSanitizer.java # Prompt 安全清洗
│ │ │ │ │ ├── AgentUtilsConfiguration.java # spring-ai-agent-utils 配置
│ │ │ │ │ └── ...
│ │ │ │ ├── annotation/ # 自定义注解
│ │ │ │ │ └── RateLimit.java # @RateLimit 可重复限流注解
│ │ │ │ ├── aspect/ # 切面
│ │ │ │ │ └── RateLimitAspect.java # 限流切面(Redis Lua 实现)
│ │ │ │ ├── async/ # 异步任务(Redis Stream)
│ │ │ │ │ ├── AbstractStreamConsumer.java # Stream 消费者基类
│ │ │ │ │ ├── AbstractStreamProducer.java # Stream 生产者基类
│ │ │ │ │ └── recovery/ # 任务恢复配置
│ │ │ │ ├── config/ # 全局配置
│ │ │ │ │ ├── S3Config.java / StorageConfigProperties.java # S3 对象存储
│ │ │ │ │ ├── CorsConfig.java / CorsProperties.java # 跨域
│ │ │ │ │ ├── OpenApiConfig.java # Swagger 文档
│ │ │ │ │ ├── LlmEmbeddingConfig.java / LlmProviderProperties.java # LLM
│ │ │ │ │ └── ...
│ │ │ │ ├── evaluation/ # 统一评估引擎(文字/语音面试共用)
│ │ │ │ │ ├── UnifiedEvaluationService.java # 统一评估服务
│ │ │ │ │ ├── EvaluationReport.java # 评估报告
│ │ │ │ │ └── QaRecord.java # 问答记录
│ │ │ │ ├── exception/ # 异常处理
│ │ │ │ │ ├── GlobalExceptionHandler.java # 全局异常处理
│ │ │ │ │ ├── BusinessException.java / ErrorCode.java
│ │ │ │ │ └── RateLimitExceededException.java
│ │ │ │ ├── result/ # 统一响应
│ │ │ │ │ └── Result.java # Result<T> 统一返回
│ │ │ │ ├── constant/ # 常量(异步任务流、公共常量)
│ │ │ │ ├── model/ # 通用模型(AsyncTaskStatus)
│ │ │ │ ├── log/ # 日志脱敏(ErrorLogSanitizer)
│ │ │ │ └── transaction/ # 事务执行器
│ │ │ │
│ │ │ ├── infrastructure/ # 基础设施层
│ │ │ │ ├── file/ # 文件处理
│ │ │ │ │ ├── DocumentParseService.java # 文档解析(Apache Tika)
│ │ │ │ │ ├── FileStorageService.java # S3 文件存储
│ │ │ │ │ ├── FileValidationService.java # 文件校验
│ │ │ │ │ ├── FileHashService.java # 文件哈希(重复检测)
│ │ │ │ │ ├── TextCleaningService.java # 文本清洗
│ │ │ │ │ └── ...
│ │ │ │ ├── export/ # 导出
│ │ │ │ │ └── PdfExportService.java # PDF 导出(iText 8)
│ │ │ │ ├── mapper/ # MapStruct 对象映射
│ │ │ │ │ ├── ResumeMapper.java / InterviewMapper.java
│ │ │ │ │ ├── KnowledgeBaseMapper.java / RagChatMapper.java
│ │ │ │ └── redis/ # Redis 服务
│ │ │ │ ├── RedisService.java # Redis 操作封装
│ │ │ │ └── InterviewSessionCache.java # 面试会话缓存
│ │ │ │
│ │ │ └── modules/ # 业务模块(按领域划分)
│ │ │ ├── resume/ # 简历模块
│ │ │ │ ├── ResumeController.java
│ │ │ │ ├── service/ # 上传/解析/评分/持久化/历史/删除
│ │ │ │ ├── listener/ # Redis Stream 异步分析
│ │ │ │ │ ├── AnalyzeStreamProducer.java # 简历分析生产者
│ │ │ │ │ ├── AnalyzeStreamConsumer.java # 简历分析消费者
│ │ │ │ │ └── ResumeAnalysisRecoveryScheduler.java
│ │ │ │ ├── model/ # 实体与 DTO
│ │ │ │ └── repository/ # JPA 仓库
│ │ │ │
│ │ │ ├── interview/ # 模拟面试模块
│ │ │ │ ├── InterviewController.java
│ │ │ │ ├── service/ # 出题/评估/会话/持久化/历史
│ │ │ │ ├── skill/ # Skill 驱动出题
│ │ │ │ │ ├── InterviewSkillService.java
│ │ │ │ │ └── InterviewSkillController.java
│ │ │ │ ├── listener/ # 异步评估任务
│ │ │ │ ├── model/
│ │ │ │ └── repository/
│ │ │ │
│ │ │ ├── knowledgebase/ # 知识库模块
│ │ │ │ ├── KnowledgeBaseController.java
│ │ │ │ ├── KnowledgeBaseInterviewController.java # 知识库面试
│ │ │ │ ├── RagChatController.java # RAG 问答(SSE 流式)
│ │ │ │ ├── service/ # 知识库服务(17 个)
│ │ │ │ │ ├── KnowledgeBaseVectorService.java # 向量化
│ │ │ │ │ ├── KnowledgeBaseQueryService.java # RAG 查询
│ │ │ │ │ ├── RagChatSessionService.java # RAG 会话
│ │ │ │ │ ├── KnowledgeBaseQuestionGenerationService.java # 题目生成
│ │ │ │ │ └── ...
│ │ │ │ ├── listener/ # 异步向量化
│ │ │ │ ├── metrics/ # 指标
│ │ │ │ ├── model/
│ │ │ │ └── repository/
│ │ │ │
│ │ │ ├── voiceinterview/ # 语音面试模块
│ │ │ │ ├── controller/ # REST 接口
│ │ │ │ ├── handler/ # WebSocket 处理
│ │ │ │ │ └── VoiceInterviewWebSocketHandler.java
│ │ │ │ ├── service/ # 语音服务
│ │ │ │ │ ├── QwenAsrService.java # 语音识别(DashScope)
│ │ │ │ │ ├── QwenTtsService.java # 语音合成
│ │ │ │ │ ├── DashscopeLlmService.java # LLM 流式输出
│ │ │ │ │ └── ...
│ │ │ │ ├── context/ # 上下文管理
│ │ │ │ ├── config/ / dto/ / model/ / repository/ / listener/
│ │ │ │
│ │ │ ├── interviewschedule/ # 面试安排模块
│ │ │ │ ├── InterviewScheduleController.java
│ │ │ │ ├── service/ # 邀请解析/日历管理/状态流转
│ │ │ │ ├── model/
│ │ │ │ └── repository/
│ │ │ │
│ │ │ └── llmprovider/ # 多模型 Provider 模块
│ │ │ ├── controller/ # Provider CRUD / 语音配置 / 测试连接
│ │ │ ├── service/ # 配置落盘 / 加密 / 重载
│ │ │ ├── dto/ / model/ / repository/
│ │ │
│ │ └── resources/
│ │ ├── application.yml # 应用配置
│ │ ├── logback-spring.xml # 日志配置(UTF-8)
│ │ ├── voice-interview-opening.yml # 语音面试开场白
│ │ ├── db/migration/ # Flyway 数据库迁移脚本
│ │ │ ├── V1__init_schema.sql # 初始化 schema
│ │ │ ├── V20260723__ensure_pgvector_store.sql # pgvector 向量表
│ │ │ └── ...(共 9 个增量迁移)
│ │ ├── fonts/ # 中文字体(PDF 导出用)
│ │ ├── prompts/ # AI 提示词模板(StringTemplate)
│ │ │ ├── resume-analysis-*.st # 简历分析
│ │ │ ├── interview-question-skill-*.st # Skill 出题
│ │ │ ├── interview-evaluation-*.st # 面试评估
│ │ │ ├── interview-evaluation-summary-*.st # 评估汇总
│ │ │ ├── knowledgebase-query-*.st # RAG 查询改写
│ │ │ ├── knowledgebase-question-generation-*.st # 题目生成
│ │ │ ├── jd-parse-system.st # JD 解析
│ │ │ └── voice-interview-context-summary.st # 语音上下文压缩
│ │ ├── scripts/ # Redis Lua 脚本
│ │ │ └── rate_limit_single.lua # 限流脚本
│ │ └── skills/ # 面试 Skill 定义(10+ 方向)
│ │ ├── _shared/references/ # 共享参考知识库
│ │ ├── java-backend/ # Java 后端
│ │ ├── java-backend-tencent/ # Java 后端(腾讯专项)
│ │ ├── ali-backend/ # 阿里专项
│ │ ├── bytedance-backend/ # 字节专项
│ │ ├── frontend/ # 前端
│ │ ├── python-backend/ # Python 后端
│ │ ├── algorithm/ # 算法
│ │ ├── system-design/ # 系统设计
│ │ ├── test-development/ # 测试开发
│ │ └── ai-agent-dev/ # AI Agent 开发
│ │
│ └── test/java/interview/guide/ # 测试
│ ├── common/ # 通用能力测试
│ ├── infrastructure/ # 基础设施测试
│ ├── modules/ # 业务模块测试
│ └── rag/ # RAG 测评(rag-eval 标签)
│
├── frontend/ # 前端应用(React 18 + TypeScript + Vite)
│ ├── package.json / pnpm-lock.yaml # 依赖(pnpm 包管理)
│ ├── vite.config.ts # Vite 配置(含 /api、/ws 代理)
│ ├── tsconfig*.json / eslint.config.js # TS 与 ESLint 配置
│ ├── index.html # HTML 入口
│ ├── nginx.conf # Nginx 配置(Docker 部署)
│ ├── Dockerfile # 前端镜像构建
│ ├── playwright.config.ts / e2e/ # E2E 测试(Playwright)
│ ├── public/ # 静态资源
│ └── src/
│ ├── main.tsx # 应用入口
│ ├── App.tsx # 根组件 + 路由
│ ├── api/ # API 接口层
│ │ ├── request.ts # Axios 封装
│ │ ├── stream.ts # SSE 流式请求
│ │ ├── resume.ts / interview.ts / voiceInterview.ts
│ │ ├── knowledgebase.ts / ragChat.ts
│ │ ├── interviewSchedule.ts / llmProvider.ts / skill.ts
│ │ └── history.ts / batchUpload.ts
│ ├── components/ # 公共组件
│ │ ├── Layout.tsx # 布局
│ │ ├── InterviewPanel.tsx / InterviewChatPanel.tsx # 面试面板
│ │ ├── AudioRecorder.tsx / AudioPlayer.tsx # 录音/播放
│ │ ├── RealtimeSubtitle.tsx # 实时字幕
│ │ ├── RadarChart.tsx / ScoreProgressBar.tsx # 图表/评分
│ │ ├── batch-upload/ # 批量上传组件
│ │ ├── interviewschedule/ # 面试安排组件(日历/表单/列表)
│ │ └── knowledgebaseInterview/ # 知识库面试组件
│ ├── pages/ # 页面组件
│ │ ├── UploadPage.tsx # 简历上传
│ │ ├── ResumeDetailPage.tsx # 简历分析详情
│ │ ├── HistoryPage.tsx # 简历历史
│ │ ├── InterviewHubPage.tsx # 面试中心(入口)
│ │ ├── InterviewPage.tsx # 模拟面试
│ │ ├── InterviewHistoryPage.tsx # 面试记录
│ │ ├── InterviewSchedulePage.tsx # 面试安排
│ │ ├── VoiceInterviewPage.tsx # 语音面试
│ │ ├── VoiceInterviewEvaluationPage.tsx # 语音评估
│ │ ├── KnowledgeBaseUploadPage.tsx # 知识库上传
│ │ ├── KnowledgeBaseManagePage.tsx # 知识库管理
│ │ ├── KnowledgeBaseQueryPage.tsx # 知识库问答
│ │ ├── KnowledgeBaseInterview*.tsx # 知识库面试(入口/题库/答题)
│ │ └── SettingsPage.tsx # 系统设置
│ ├── hooks/ # 业务 Hooks
│ │ ├── useBatchUpload.ts / useBatchStatusPolling.ts
│ │ ├── useInterviewConfig.ts / useInterviewSchedule.ts
│ │ └── useTheme.ts
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数(日期/评分/语音 WS 等)
│ ├── constants/ # 常量(路由/知识库面试)
│ └── assets/ # 静态资源
│
├── docker-compose.yml # 完整部署(前端+后端+PG+Redis+MinIO)
├── docker-compose.dev.yml # 本地开发依赖(PG+Redis+RustFS)
├── docker/postgres/ # PostgreSQL 初始化脚本
├── docs/ # 架构文档
│ ├── voice-interview-architecture.md # 语音面试架构
│ └── testing/ # 测试文档
├── doc/ # 开发文档
│ └── 简历上传与分析全链路分析.md
├── scripts/ # 脚本
├── gradle/
│ ├── libs.versions.toml # 依赖版本目录(统一版本管理)
│ └── wrapper/ # Gradle Wrapper
├── settings.gradle # Gradle 设置(rootProject: interview-guide)
├── gradle.properties # Gradle 属性(JDK 版本、编码)
├── .env.example # 环境变量示例
├── .githooks/ # Git Hooks
└── README.md
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# 分层说明
| 层级 | 包路径 | 职责 |
|---|---|---|
| common | interview.guide.common | 跨模块通用能力:AI 调用、限流注解+切面、Redis Stream 异步模板、全局配置、统一评估引擎、异常处理、统一响应 |
| infrastructure | interview.guide.infrastructure | 基础设施:文件解析与 S3 存储、PDF 导出、MapStruct 映射、Redis 操作 |
| modules | interview.guide.modules | 6 个业务模块:resume(简历)、interview(模拟面试)、knowledgebase(知识库+RAG)、voiceinterview(语音面试)、interviewschedule(面试安排)、llmprovider(多模型配置) |
# 关键资源目录
| 目录 | 说明 |
|---|---|
resources/prompts/ | 17 个 StringTemplate 提示词模板,覆盖简历分析、出题、评估、RAG 查询、题目生成、JD 解析等场景 |
resources/skills/ | 10+ 面试方向 Skill 定义(SKILL.md + skill.meta.yml),每个方向含考察范围、难度分布和参考知识库 |
resources/db/migration/ | Flyway 迁移脚本,V1__init_schema.sql 初始化 + 8 个增量迁移 |
resources/scripts/ | Redis Lua 限流脚本 |
# 核心功能与技术亮点
以下功能点是本项目最值得学习、可写入简历的核心技术实现,每个都涉及一定的设计深度。
# 1、基于 Redis Stream 的异步任务处理框架
核心类:
common/async/AbstractStreamProducer、AbstractStreamConsumer
简历分析、知识库向量化、面试评估等耗时操作统一走 Redis Stream 异步处理,而非同步阻塞调用 LLM。项目用模板方法模式抽象出生产者/消费者基类,各业务只需实现 parsePayload、processBusiness、markCompleted 等钩子方法。
技术亮点:
- 失败自动重试:消费失败后按
retryCount重新入队,超过MAX_RETRY_COUNT才标记失败,避免因 LLM 偶发抖动导致任务直接报废 - Pending 消息认领:消费者崩溃后遗留的 Pending 消息会被其他消费者按
PENDING_IDLE_TIMEOUT认领,防止消息丢失 - 任务恢复调度:
ResumeAnalysisRecoveryScheduler定时扫描中断状态的任务重新投递,应对应用重启场景 - Fencing Token 防并发:任务带
attempt版本号,防止恢复调度器和正常消费链路同时处理同一任务 - Stream 最大长度限制:
STREAM_MAX_LEN防止 Stream 无限膨胀
简历话术:基于 Redis Stream + 模板方法模式实现异步任务框架,支持失败重试、Pending 消息认领、宕机恢复和 Fencing Token 并发防护,解耦耗时 LLM 调用与接口响应。
# 2、RAG 检索增强问答(多路召回 + 流式输出)
核心类:
modules/knowledgebase/service/KnowledgeBaseQueryService
知识库问答不是简单的"向量检索 → 拼 Prompt → 调 LLM",而是做了完整的检索优化链路。
技术亮点:
- Query 改写:用 LLM 将用户口语化问题 + 历史对话改写为更适合向量检索的查询,改写失败自动降级为原问题
- 双路召回融合:改写 Query 和原始 Query 各检索一次,按 Document ID 去重、按分数降序取 TopK,兼顾语义改写收益和原始查询保底
- 动态检索参数:根据问题长度分档选择 TopK 和相似度阈值(短查询用小 TopK + 高阈值,长查询用大 TopK + 默认阈值),避免一刀切
- SSE 流式 + 探测窗口归一化:流式输出时先缓冲前 120 字符探测是否为"无结果"模板,命中则立即返回固定文案,未命中则尽快透传,兼顾首包延迟和拒答识别
- 全链路 Micrometer 埋点:rewrite / retrieve / generate / total 四个阶段分别记录耗时和成功/拒绝/错误状态,可观测性强
- Prompt 注入防护:系统提示词拼接
ANTI_INJECTION_INSTRUCTION,防止用户构造恶意问题绕过知识库范围
简历话术:实现 RAG 检索增强问答,包含 Query 改写、双路召回融合、动态 TopK/阈值、SSE 流式探测窗口归一化和全链路 Micrometer 指标埋点。
# 3、统一面试评估引擎(分批 + 二分恢复 + 降级兜底)
核心类:
common/evaluation/UnifiedEvaluationService
文字面试和语音面试共用同一套评估逻辑,核心难点是:题目多时单次 LLM 调用容易超时或返回格式错误。
技术亮点:
- 问答组不可拆分:主问题和其追问构成一个
QaGroup,分批时以组为最小单元,保证上下文完整 - 分批评估:按
evaluationBatchSize将问答组分批,每批独立调用 LLM 评估,降低单次 Token 消耗和超时风险 - 二分恢复:某批评估失败时,若组数足够则二分为左右半批各自重试,对失败侧递归下钻;不可再拆时整段加试一次,仍失败才降级。用
extraBudget限制额外调用次数,防止无限重试 - 二次汇总:所有批次评估完成后,再调一次 LLM 做全局汇总(综合评语 + 优势 + 改进建议),汇总失败降级为批次结果拼接
- 索引映射容错:LLM 返回的
questionIndex可能错位(1-based 偏移、缺失),通过isConsistentOneBasedShift检测整批一致偏移并按位置对齐,非法索引只降级该题为 0 分,不影响其他题 - 显式降级标记:降级分数明确标注"模型评估失败后的系统降级",不伪装成真实评分
简历话术:设计统一面试评估引擎,支持分批评估、问答组不可拆分、失败二分恢复(带预算控制)、二次汇总和索引映射容错,保证评估结果完整性和可靠性。
# 4、实时语音面试(WebSocket + 虚拟线程 + 句子级并发 TTS)
核心类:
modules/voiceinterview/handler/VoiceInterviewWebSocketHandler
语音面试是全项目技术复杂度最高的模块,处理管线:用户音频 → ASR → LLM → TTS → AI 音频。
技术亮点:
- 虚拟线程执行管线:LLM/TTS/JDBC 等阻塞操作跑在
newVirtualThreadPerTaskExecutor()上,不阻塞调度线程,充分利用 Java 25 虚拟线程高并发优势 - 句子级并发 TTS:LLM 流式输出期间,每检测到一个完整句子就并发启动 TTS 合成,用
Semaphore控制并发数,OrderedTtsChunkEmitter保证音频按序下发,首包延迟约 200ms - 服务端 VAD + 手动提交:ASR 自动断句产生 final segment,但提交给 LLM 由前端手动
submit控制,避免抢话 - 回声防护:AI 说话期间及结束后 800ms 冷却期内丢弃麦克风输入,防止扬声器声音被重新录入触发 LLM
- 上下文压缩:
VoiceContextCompressor对超长对话历史做摘要压缩,控制 LLM Token 消耗 - 开场白音频预热:
@PostConstruct时预合成开场白音频缓存,首次连接直接播放,避免冷启动延迟 - ASR 断线重连:ASR 连接异常时自动重连并重试音频块发送,最多重试 2 次
- 暂停超时检测:
@Scheduled每 30s 检查无活动会话,4:30 发警告,5:00 自动暂停 - 全链路 Micrometer 埋点:first_token_latency / llm.duration / tts.duration / turn.duration / asr.merge_wait 等指标
简历话术:基于 WebSocket + Java 25 虚拟线程实现实时语音面试,支持句子级并发 TTS(首包 200ms)、服务端 VAD、回声防护、上下文压缩、ASR 断线重连和全链路指标埋点。
# 5、可重复注解的多维度限流(Redis Lua 原子检查)
核心类:
common/annotation/RateLimit、common/aspect/RateLimitAspect
技术亮点:
- 可重复注解:
@Repeatable支持同一方法标注多条限流规则(如 GLOBAL + IP + USER),所有规则在一次 Lua 调用中原子检查和扣减,避免多次 Redis 往返 - Lua 脚本 + EVALSHA:脚本预加载为 SHA1 缓存,每次用
evalSha执行;Redis 重启导致NOSCRIPT时自动重新加载并重试 - 多维度 Key 设计:用
{ClassName:MethodName}HashTag 保证同一方法的多维度 Key 落在同一 Redis 槽位,Lua 脚本可跨 Key 原子操作 - 失败规则定位:Lua 返回负数表示第几条规则触发,切面据此定位并执行对应降级
- 降级方法支持:限流触发时可调用指定的
fallback方法(无参或参数一致),而非直接抛异常 - IP 穿透:依次解析
X-Forwarded-For/X-Real-IP/Proxy-Client-IP等头,适配反向代理场景
简历话术:基于可重复注解 + Redis Lua 实现多维度(Global/IP/User)原子限流,支持 EVALSHA 缓存、NOSCRIPT 自愈、HashTag 槽位对齐和方法级降级。
# 6、多 LLM Provider 动态管理
核心类:
common/ai/LlmProviderRegistry
技术亮点:
- 运行时切换:Provider 配置存数据库,设置页可动态新增/切换默认聊天模型和向量模型,无需改代码重启
- ChatClient 缓存:按
providerId缓存 ChatClient / ChatModel / EmbeddingModel,reload()清空缓存重建 - 多类型 Client:
getDefaultChatClient(带工具+记忆)、getPlainChatClient(无工具,用于结构化输出)、getVoiceChatClient(带工具+流式 ToolCall,无记忆),按场景选择 - API Key 加密落盘:
ApiKeyEncryptionService对 API Key 加密存储,运行时解密,配置写到~/.interview-guide/ - Advisor 编排:可配置 SafeGuardAdvisor(敏感词拦截)、MessageChatMemoryAdvisor(对话记忆)、ToolCallingAdvisor(工具调用)、SimpleLoggerAdvisor
- OpenAI 兼容:通过
ApiPathResolver适配 DashScope / Kimi / DeepSeek / GLM / LM Studio 等不同 BaseUrl 路径
简历话术:实现多 LLM Provider 动态管理,支持运行时切换、ChatClient 缓存、API Key 加密、按场景编排 Advisor(SafeGuard/Memory/ToolCall),兼容多家 OpenAI 协议模型。
# 7、Skill 驱动智能出题
核心类:
modules/interview/skill/InterviewSkillService、resources/skills/
技术亮点:
- SKILL.md 定义考察范围:每个面试方向一个
SKILL.md+skill.meta.yml,定义知识点、难度分布、参考题库,新增方向只需加目录 - Tool Calling 集成:
interviewSkillsToolCallback注册为 LLM 工具,LLM 出题时可调用 Skill 资源 - 历史题目去重:出题时排除该用户已有会话中问过的题目,避免重复考察
- 10+ 方向覆盖:Java 后端、阿里/字节/腾讯专项、前端、Python、算法、系统设计、测开、AI Agent 等
简历话术:基于 SKILL.md + Spring AI Tool Calling 实现 Skill 驱动智能出题,支持 10+ 面试方向、历史题目去重和参考知识库加载。
# 8、内容哈希去重
核心类:
infrastructure/file/FileHashService
上传简历时计算文件内容哈希,命中已分析过的相同简历直接返回历史结果,避免重复消耗 LLM Token。简单但实用的成本优化。
# 环境搭建
# 1、PostgreSQL + PGvector 向量数据库
# 安装
- 这条命令会在后台启动一个带有向量扩展(pgvector)的 PostgreSQL 17 数据库容器。
docker run -d \
--name my_pgvector \
-p 5432:5432 \
-e POSTGRES_PASSWORD=123456 \
-v /Users/guide/docker/postgresql/data:/var/lib/postgresql/data \
pgvector/pgvector:pg17
2
3
4
5
6
# PGvector 向量功能
虽然镜像已经内置了向量功能,但 PostgreSQL 的设计是“插件按需加载”。按理说你得手动执行 CREATE EXTENSION vector;。
但是! 如果你用的是 Spring AI,你会发现哪怕不手动输入命令,项目也能跑。这是因为 Spring AI 的 PgVectorStore 已经在底层帮你把脏活累活干了。在 Spring 容器启动时,自动检测并初始化 PostgreSQL 的向量数据库环境。
不过,需要注意的是:当 initialize-schema: false 时,Spring AI 不会自动创建 vector_store 表(上面源码中我也注释了相应的代码)。
spring:
ai:
vectorstore:
pgvector:
initialize-schema: true
2
3
4
5
建议开发环境设置为 true,方便快速启动。生产环境设置为 false,手动管理数据库 schema,避免意外变更。
# 2、RustFS 高性能 S3 兼容的对象存储服务搭建
# 安装
# 创建目录
mkdir /tmp/rustfs/data
# 拉取镜像
docker pull rustfs/rustfs
# 启动容器
docker run -d \
--name rustfs_local \
--user root \
-p 9000:9000 \
-p 9001:9001 \
-v /tmp/rustfs/data:/data \
-v /tmp/rustfs/logs:/logs \
-e RUSTFS_ACCESS_KEY=rustfsadmin \
-e RUSTFS_SECRET_KEY=rustfsadmin \
-e RUSTFS_CONSOLE_ENABLE=true \
-e RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS=true \
rustfs/rustfs:latest \
/data
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# RustFS 管理后台
运行成功后,访问 http://localhost:9001/ (opens new window) 进入管理后台。
用户名:rustfsadmin
密码:rustfsadmin
在管理后台可以:
创建和管理存储桶(Bucket)
查看文件列表
配置访问策略
生成 Access Key / Secret Key
你需要自己本地创建存储桶并且添加访问密钥,然后修改 application.yml 中对应的配置。
# RustFS vs Minio
- 为什么没有首选 MinIO ,可以参考这篇文章:对标MinIO!全新一代分布式文件系统诞生! (opens new window)
| 比较维度 | Ceph | MinIO | RustFS |
|---|---|---|---|
| 开发语言 | C++ | Go | Rust |
| 开源许可证 | LGPL-2.1 / LGPL-3.0 | AGPL-3.0 | Apache-2.0 |
| 元数据中心 | 有(集中式/分布式元数据) | 无(去中心化) | 无(去中心化) |
| 块存储支持 | ✅ 支持 (RBD) | ❌ 不支持 | ❌ 不支持 |
| 文件存储支持 | ✅ 支持 (CephFS) | ❌ 不支持 | ❌ 不支持 |
| 架构设计 | 重型架构,设计复杂 | 轻量级架构,简洁高效 | 轻量级架构,极致性能 |
| 社区活跃度 | 高 | 高 | 中(处于上升期) |
| 许可证友好度 | 中(LGPL 限制较多) | 差(AGPL 对商用不友好) | 优(Apache 协议极其友好) |
| 性能表现 | 强依赖硬件配置与调优 | 高性能、低延迟,适合大对象 | 极高性能,利用 Rust 零成本抽象 |
| 文件协议 | S3, RBD, CephFS 等多种 | S3 | S3 |
| 使用/运维难度 | 高(配置复杂,门槛高) | 低(开箱即用,运维简单) | 低(部署简单,配置精简) |
| 扩容能力 | EB 级 | EB 级 | EB 级 |
| 硬件资源需求 | 高(对 CPU/内存消耗大) | 中(资源占用适中) | 低(内存安全且占用极低) |
| 内存稳定性 | 稳定 | 高并发下可能存在抖动/GC 压力 | 极稳定(无 GC,内存管理精细) |
| 扩容难度 | 难度高(数据重平衡压力大) | 难度低 | 难度低 |
| 数据重平衡 | 资源占用高,影响业务 | 资源占用低 | 资源占用低 |
| 商业支持 | ✅ 完善 | ✅ 完善 | ✅ 提供 |
| 成熟度/稳定性 | 极高(多年生产环境验证) | 高(对象存储事实标准) | 较低(新兴项目,稳定性待验证) |
# Spring Boot 集成
添加依赖
// build.gradle
dependencies {
// AWS S3 SDK for RustFS storage
implementation 'software.amazon.awssdk:s3:2.25.27'
}
2
3
4
5
注意:使用 AWS SDK v2 而非 v1,v2 是完全重写的异步优先版本,性能更好,API 设计更现代。
配置属性
# application.yml
app:
storage:
endpoint: http://localhost:9000 # RustFS 服务地址
access-key: ${RUSTFS_ACCESS_KEY} # 访问密钥(建议使用环境变量)
secret-key: ${RUSTFS_SECRET_KEY} # 私密密钥
bucket: interview-guide # 存储桶名称
region: us-east-1 # 区域(S3协议需要,可任意设置)
2
3
4
5
6
7
8
安全提示:生产环境中,access-key 和 secret-key 应通过环境变量或密钥管理服务(如 Vault、AWS Secrets Manager)注入,严禁硬编码。
配置属性类
使用 @ConfigurationProperties 实现类型安全的配置绑定:
@Data
@Component
@ConfigurationProperties(prefix = "app.storage")
public class StorageConfigProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucket;
private String region = "us-east-1";
}
2
3
4
5
6
7
8
9
10
11
S3 客户端配置
@Configuration
@RequiredArgsConstructor
public class S3Config {
private final StorageConfigProperties storageConfig;
@Bean
public S3Client s3Client() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
storageConfig.getAccessKey(),
storageConfig.getSecretKey()
);
return S3Client.builder()
.endpointOverride(URI.create(storageConfig.getEndpoint()))
.region(Region.of(storageConfig.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.forcePathStyle(true) // 关键配置:使用路径风格访问
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
关键配置说明:
| 配置项 | 说明 |
|---|---|
endpointOverride | 覆盖默认的 AWS 端点,指向本地 RustFS |
forcePathStyle(true) | 必须开启,否则 SDK 会使用虚拟主机风格(bucket.endpoint)导致 DNS 解析失败 |
region | S3 协议要求,本地部署可设为任意值 |
# 3、模型基座
大模型 API 申请和 Ollama 部署本地模型
# 云端 API vs 本地部署
在开始之前,先了解两种方案的特点,选择适合你的方式:
| 对比项 | 阿里云百炼 API (云端) | Ollama 本地部署 (本地) | 点评 |
|---|---|---|---|
| 部署难度 | 零部署,开箱即用 | 需要安装、配置环境 | 想快速上线选云端,想折腾底层选本地。 |
| 硬件要求 | 无要求 | 极高(内存/显存是刚需) | 本地跑 7B 模型至少得 16G 内存。 |
| 推理性能 | 极快(专业 GPU 集群) | 取决于你的显卡 | 生产环境的吞吐量云端完胜。 |
| 费用成本 | 按 Token 计费(有免费额度) | 完全免费 | 适合“白嫖”党做本地实验。 |
| 数据隐私 | 数据上传云端 | 数据 100% 本地化 | 金融、内网环境建议选本地。 |
| 适用场景 | 生产环境、高质量业务 | 开发测试、隐私敏感场景 | 没有银弹,看你的业务痛点在哪。 |
如果是为了更好的分析效果,且不想因为硬件性能导致系统“假死”,个人强烈建议首选阿里云百炼 API。本地模型虽然香,但对普通开发机来说,处理长文本分析简直是“折磨”。
# 方案选择
云端:阿里云百炼 API
新用户通常有免费试用额度,可在模型用量 (opens new window)界面查看
使用千问的模型,需要业务管理空间授权,不然访问不了
本地:Ollama 本地部署
- 安装教程:todo
# 4、Redis 搭建
# 安装
- redis-server --appendonly yes 代表:启动命令,容器启动后执行的命令。这里显式启动 Redis 服务并开启 AOF 持久化(Append Only File),以提供更高的数据安全性。
docker run -d \
--name redis \
-p 6379:6379 \
--restart always \
-v /Users/guide/docker/redis/data:/data \
redis:8.0 redis-server --appendonly yes
2
3
4
5
6
# 服务启动
# 1、后端启动
# 环境搭建脚本
项目仓库里的 docker-compose.dev.yml,也可以一条命令启动 PostgreSQL、Redis、RustFS:
# 本地开发依赖服务
# 启动依赖服务
docker compose -f docker-compose.dev.yml up -d
# 停止依赖服务
docker compose -f docker-compose.dev.yml down
# 停止并清除数据
docker compose -f docker-compose.dev.yml down -v
2
3
4
5
6
7
8
启动后建议确认 PostgreSQL 和 Redis 的端口确实映射到了宿主机:
docker ps --format '{{.Names}} {{.Ports}}'
正常情况下应看到类似下面的结果:
interview-postgres 0.0.0.0:5432->5432/tcp
interview-redis 0.0.0.0:6379->6379/tcp
2
如果只看到 interview-postgres 5432/tcp 或 interview-redis 6379/tcp,说明容器内部服务虽然启动了,但宿主机访问不到它。此时后端启动时通常会报:
Connection to localhost:5432 refused
这种情况可以重建容器配置,数据卷不会被删除:
docker compose -f docker-compose.dev.yml up -d --force-recreate postgres redis
# .env 文件
复制 .env.example 为 .env 文件
需要修改的地方
# (修改点)阿里云百炼 AI (DashScope) - LLM + ASR + TTS 服务
AI_BAILIAN_API_KEY=sk-af3b1c65036949e78bb769b1aa64d9ce
# (修改点)Provider API Key 加密密钥(必填;生产环境请替换为随机长字符串,部署后保持不变)
APP_AI_CONFIG_ENCRYPTION_KEY=F3k9xP2qL8mN4vB7cR5tY1uI6oW0aZ8s
# (修改点)PostgreSQL(compose 默认:库 interview_guide / 用户 postgres / 密码 password / 映射端口 5432)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=interview_guide
POSTGRES_USER=postgres
POSTGRES_PASSWORD=123456
# (修改点)ARustFS / S3 兼容存储(docker-compose.dev.yml 默认凭证为 rustfsadmin,API 映射 9000)
APP_STORAGE_ENDPOINT=http://localhost:9000
APP_STORAGE_ACCESS_KEY=Hu5EbAKQGGu7wr2OPjWE
APP_STORAGE_SECRET_KEY=9n7wvDQGDQCe8UaZ0dmq0jVv4pZ09OMqWCc9Z4ra
APP_STORAGE_BUCKET=interview-guide
# ==========================================
# RustFS 安全配置
# ==========================================
# 默认账号密码
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
# (修改点)RPC 通信密钥(这个不填,会导致 docker-compose.dev.yml 文件启动失败)
RUSTFS_RPC_SECRET=8f2a4c9e1b7d3f6a0c5e8b2d4f7a9c1e6b3d5f8a2c4e7b9d1f3a6c8e0b2d4f7a
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# .env 文件如何生效
很多人以为 Spring Boot 会自动读取 .env 文件,其实不会。application.yml 里的 ${AI_BAILIAN_API_KEY} 占位符是从环境变量取值,而 Spring Boot 本身不读 .env 文件。
那为什么 ./gradlew :app:bootRun 启动时 .env 能生效?因为 app/build.gradle 的 bootRun task 里手动解析了 .env 并注入为子进程环境变量:
// app/build.gradle
tasks.named('bootRun') {
def envFile = rootProject.file('.env')
if (envFile.exists()) {
envFile.eachLine { line ->
// 解析 KEY=VALUE,通过 environment key, val 注入
environment key, val
}
}
}
2
3
4
5
6
7
8
9
10
补充:
docker-compose.dev.yml里的${POSTGRES_USER:-postgres}是 docker-compose 自己读.env(其默认行为),只给依赖容器用,与 Spring Boot 应用无关。
# IDEA 中如何启动
既然 .env 靠 bootRun task 注入,那在 IDEA 里直接点主类的绿色按钮运行,会读不到 .env(因为没走 bootRun)。有三种方式解决:
方案 A(推荐):用 Gradle 面板跑 bootRun
IDEA 右侧 Gradle 面板 → app → Tasks → application → 双击 bootRun。这走的是 build.gradle 里那段 .env 注入逻辑,和命令行完全等价,绿色按钮也能点。
方案 B:装 EnvFile 插件
IDEA Settings → Plugins → 搜 EnvFile 安装 → Run Configuration 里勾选 Enable EnvFile → Add 选 .env 文件。之后直接运行主类即可,插件自动注入。
方案 C:手动配置环境变量
Run Configuration → Environment variables 旁的图标 → 把 .env 里的 KEY=VALUE 逐条粘贴。变量少时可用,多了麻烦。
注意:方案 B/C 是直接运行主类(不走 bootRun),
build.gradle里那段.env注入逻辑不会生效,必须靠插件或手动补。方案 A 最省事。
# swagger 地址
http://localhost:8080/swagger-ui/index.html (opens new window)
# 2、前端启动
指令
cd frontend
corepack enable
pnpm install
pnpm dev
corepack enable 执行可能会失败,可执行 corepack disable 关闭,手动指定下载 pnpm 版本
2
3
4
5
6
7
# 踩坑记录
# 文件权限
创建 RustFS 容器时可能遇到文件权限问题:
Io error: Permission denied (os error 13)
简单粗暴的解决办法——直接给本地映射路径提权:
chmod 777 -R /tmp/rustfs/
# 前端项目运行异常
报错信息:
10:46:36 [vite] error while updating dependencies:
Error: EPERM: operation not permitted, rename 'D:\codes\project\javaguide\interview-guide\frontend\node_modules\.vite\deps_temp_45b681d5' -> 'D:\codes\project\javaguide\interview-guide\frontend\node_modules\.vite\deps'
2
解决方式:
- 删除 node_modules 目录, 重新安装依赖并启动
pnpm install
pnpm dev
2
# 命令行启动报 jvm 异常或中文乱码
报错信息:
Starting a Gradle Daemon (subsequent builds will be faster)
FAILURE: Build failed with an exception.
* What went wrong:
Gradle requires JVM 17 or later to run. Your build is currently configured to use JVM 8.
2
3
4
5
6
原因:
- 本地系统默认 jdk8(path 路径),导致 Gradle 启动失败
解决方式:
有三种方式
- 点击 IDEA 右上角的绿色启动按钮
- 修改 path 路径
- 在项目一级目录下新建
gradle.properties文件
# 指定 jdk 版本
org.gradle.java.home=D:\\software\\java\\jdk\\jdk25
# 指定编码格式,避免控制台中文乱码
org.gradle.jvmargs=-Dfile.encoding=UTF-8
2
3
4