项目实训:收尾阶段的回归验证与部署配置修复
目录
四、.gitignore幽灵规则:一条*.txt如何杀死Docker构建
1. knowledge.js:finally把成功的流杀了
4. FaceCropService:一个 JNI 对象的线程安全边界
6. WebConfig.java:删掉一段什么都没做的代码
1. LoginResponse:Jackson的is前缀陷阱
2. RateLimitInterceptor的两个匹配问题
3. DetectionService的@Value无默认值
5. LLMService中temperature/maxTokens的null字段
8. CoTService temperature硬编码为0.3
一、背景:做完所有功能之后
前七篇博客从Spring Boot骨架搭建写到了RAG/CoT/ReAct/Agent编排。到第七篇结束时,后端28个API端点就绪,103个Java源文件,mvn test 24/24全部通过。代码推到了Gitee。然后发生了三件事。
第一件事。把仓库clone到另一个目录,mvn compile—编译过。然后试着启动音频检测服务—找不到model.safetensors。试着docker compose up—视频容器报pretrained/best_model.pth not found。试着docker build — Dockerfile里COPY requirements.txt .失败,因为文件根本不存在。第二件事。回到原目录想继续改— git status 报 fatal: not a git repository。.git 目录由于磁盘操作或 VS Code崩溃丢失了。重新git init → git remote add → git fetch → git checkout -f master。远程代码覆盖了所有本地修改。第三件事。第六篇博客记录的那10项Bug修复,有5项因为覆盖而回滚。前端同学的J 修改不在我的推送范围里,后端的FaceCropService线程安全修复忘了 commit。总结一下当时的状况:代码能编译,服务跑不起来;Bug修过一遍,又回来了;模型文件在本地能加载,进了容器就找不到;requirements.txt从建项目那天起就没被Git跟踪过。
这一篇博客记录的就是把这些坑一个一个填平的过程。这一轮没有新增任何业务功能,是质量保障和工程可靠性工作。
二、回归验证:前几篇修过的Bug还在吗
拉取代码后第一件事是编译验证:
$ .\mvnw.cmd compile test-compile
BUILD SUCCESS
102 source files compiled
0 errors, 0 warnings
然后对着第六篇博客的记录逐项核对:
|
问题 |
类型 |
状态 |
|
|---|---|---|---|
|
1 |
LLMService 手动 JSON 拼接 |
Java |
✅ 保留 |
|
2 |
AgentOrchestrator 手动 JSON 拼接 |
Java |
✅ 保留 |
|
3 |
LoginResponse boolean→Boolean |
Java |
✅ 保留 |
|
4 |
taskWatcher.js require 路径 |
JS |
✅ 保留 |
|
5 |
knowledge.js 文章分类 |
JS |
✅ 保留 |
|
6 |
knowledge.js finally 块 abort |
JS |
❌ 丢失 |
|
7 |
result.js 嵌套字段 |
JS |
❌ 丢失 |
|
8 |
app.json tabbar 图标 |
JS |
❌ 丢失 |
|
9 |
FaceCropService 线程安全 |
Java |
❌ 丢失 |
|
10 |
Records.vue default-sort |
Vue |
❌ 丢失 |
5项Java修改在Gitee上保留了(backend/目录在master分支内)。5 项前端修改全部丢失 — wechat-app/和web-admin/的变更不在我这次推送的master分支范围内。FaceCropService是唯一一个"后端修改却丢了"的例外 — 在本地修完后忘了commit,而checkout -f不会为未提交的代码停一秒钟。
结论:需要重新修复这 6 项回归Bug,然后继续往下排查新问题。
三、模型文件:968MB
1. HuggingFace模型文件的构成
Wav2Vec2模型在pretrained/目录下是一组文件,不是单个文件:
asvspoof-finetuned/
├── config.json # 模型架构参数(768维, 12层, 12头)
├── model.safetensors # 权重(360.78MB)← 唯一的"大"文件
├── preprocessor_config.json # 预处理器配置(采样率16000)
├── processor_config.json # 处理器配置
├── tokenizer_config.json # 分词器配置
├── vocab.json # 词汇表
└── special_tokens_map.json # 特殊token映射
HuggingFace的from_pretrained(dir)在目录下同时找config.json和model.safetensors。有config没权重会报错,有权重没config也会报错。 这两个文件必须共存。
2. from_pretrained()如何解析路径
代码中:
# ai-services/audio/src/app.py
MODEL_DIR = Path(os.getenv("MODEL_DIR", "pretrained/asvspoof-finetuned"))
# ai-services/audio/src/utils.py
model = Wav2Vec2ForSequenceClassification.from_pretrained(str(model_dir))
from_pretrained()的第一优先级是检查HuggingFace Hub的模型ID,如果不是则视为本地路径。本地路径的解析基准是CWD(当前工作目录)。Flask从ai-services/audio/启动(Dockerfile的WORKDIR/app),所以pretrained/asvspoof-finetuned解析为/app/pretrained/asvspoof-finetuned/。
但模型文件实际在根目录pretrained/下 — 路径差了两级。
3. 两个pretrained目录的路径对齐
项目中存在两个pretrained/ 目录树:
|
位置 |
内容 |
状态 |
|---|---|---|
|
根目录 |
配置文件 + 3个权重文件(968MB) |
本地存在,Git未跟踪 |
|
|
12个配置文件(无权重) |
Git 已跟踪 |
根目录的是早期手动下载的,ai-services/下的是HuggingFace clone的配置文件(不含权重)。两个目录凑一块才完整。但.gitignore规则是:
ai-services/**/pretrained/*.pth
ai-services/**/pretrained/**/*.safetensors
这个规则覆盖了AI服务目录下的模型文件,但不覆盖根目录的。如果权重在根目录,git add -A会尝试跟踪它们,然后被Gitee 100MB限制挡回来。
解决方案:
|
文件 |
从 |
到 |
|---|---|---|
|
model.safetensors (360.78 MB) |
|
|
|
model.safetensors (362.59 MB) |
|
|
|
best_model.pth (244.57 MB) |
|
|
搬迁后:代码的相对路径归正、.gitignore自动忽略大文件、根目录pretrained/只剩空壳可安全删除。
四、.gitignore幽灵规则:一条*.txt如何杀死Docker构建
两个 AI 服务的Dockerfile都有:
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
但git ls-files确认这两个文件从未进入仓库。.gitignore中有一条延续很久的规则:
*.txt
这是项目初期为了防止实习报告文档草稿误提交而加的。副作用:所有.txt 文件—包括requirements.txt—全部不被Git跟踪。Docker构建时COPY requirements.txt .找不到源文件,直接失败。
重建依赖清单。不是凭记忆写的,是逐文件检查了Python import语句:
音频服务(6个Python文件)实际依赖:
flask → Flask,request
numpy → np 数组运算
soundfile → sf.read 音频解码
torch → 模型推理
transformers → Wav2Vec2Processor,Wav2Vec2ForSequenceClassification
scipy → signal.resample (FFT 重采样,可选,代码有降级路径)
视频服务(6个Python文件)实际依赖:
flask + flask-cors → Flask, CORS
torch + torchvision → 推理 + transforms
Pillow → 图片处理
numpy → 数组运算
opencv-python → cv2 视频抽帧
face-recognition → 人脸检测
pandas → 数据集加载
PyYAML → 读取 config.yaml
版本号用>=放宽以兼容不同环境:
# ai-services/audio/requirements.txt
flask>=3.0,<4.0
numpy>=1.24,<3.0
soundfile>=0.12,<1.0
torch>=2.0,<3.0
transformers>=4.40,<5.0
scipy>=1.10,<2.0
# ai-services/video/requirements.txt
flask>=3.0,<4.0
flask-cors>=4.0,<5.0
torch>=2.0,<3.0
torchvision>=0.15,<1.0
Pillow>=10.0,<12.0
numpy>=1.24,<3.0
opencv-python>=4.8,<5.0
face-recognition>=1.4,<2.0
pandas>=2.0,<3.0
PyYAML>=6.0,<7.0
.gitignore添加例外:
*.txt
!requirements.txt
! 前缀反转换规则,且递归生效。
五、Docker部署:容器隔离带来的三个陷阱
1. COPY和volume的本质区别
在展开问题之前,需要区分两种文件传递方式的根本差异:
|
方式 |
时机 |
可见范围 |
|---|---|---|
|
|
|
写入镜像层,所有基于该镜像的容器可见 |
|
|
|
宿主机实时映射到容器,镜像本身无这些文件 |
本项目的模型文件全部通过volumes挂载(因为太大,不在Git中)。理解这层差异后,三个问题全部暴露了根因。
2. 陷阱一:视频容器缺模型挂载
音频服务有模型挂载:
audio-detection:
volumes:
- ./ai-services/audio/pretrained:/app/pretrained
视频服务只有数据挂载:
video-detection:
volumes:
- video_data:/app/data # 没有模型
容器中/app/pretrained/为空。api/app.py的MODEL_PATH = 'pretrained/best_model.pth'找不到文件,代码虽然有防御检查,但模型以随机初始化权重运行 — XceptionNet随机权重做DeepFake检测就不行。
修复:
video-detection:
volumes:
- ./ai-services/video/pretrained:/app/pretrained
- video_data:/app/data
3. 陷阱二:音频容器环境变量指向不存在路径
ai-services/audio/Dockerfile:
ENV MODEL_DIR=/app/outputs/wav2vec2-mid/best
/app/outputs/是训练脚本的默认输出目录,Docker构建时不执行训练,此路径永远为空。app.py的fallback能找到wav2vec2-base基座模型(volume已挂载),但明明有在ASVspoof上微调过的模型却用不上。
微调模型和预训练模型的准确率不在一个量级 — 预训练模型从未见过语音合成、语音转换、重放攻击等特定欺骗模式。强制降级等于白费训练产出。
ENV MODEL_DIR=/app/pretrained/asvspoof-finetuned
4. 陷阱三:独立构建的镜像不完整
视频Dockerfile缺少COPY pretrained/。音频有这行(因为配置文件轻量、Git已跟踪),视频的pretrained/目录只有本地文件。如果不用docker-compose的volume覆盖,直接docker build出来的镜像无法加载模型。
COPY pretrained/ ./pretrained/
六、第一轮代码审查:6个Bug的技术分析
以下六个Bug大多在之前的博客中出现过(现在回归了),加上一个新发现(WebConfig)。这里就不展开详细说了
1. knowledge.js:finally把成功的流杀了
2. result.js:外层包装吃掉了内层数据
3. app.json:选中的tab和没选中一模一样
4. FaceCropService:一个 JNI 对象的线程安全边界
5. Records.vue:一个冒号的距离
6. WebConfig.java:删掉一段什么都没做的代码
七、第二轮代码审查:全量扫描后的9项精修
第一轮修复完成后,又对全项目103个Java文件 + 20+ Python文件 + 全部前端代码做了第二轮全量审查。新发现 13 个问题,在跳过API Key相关后修复了 9 项。
1. LoginResponse:Jackson的is前缀陷阱
private boolean isNewUser; // Jackson 序列化后 → "newUser",不是 "isNewUser"
Jackson 的默认命名策略对boolean类型的is前缀有特殊处理:剥离is,将isNewUser序列化为newUser。前端如果期望isNewUser字段,解析会失败。影响范围:AuthController.login()中日志输出和返回值。
修复:
private Boolean isNewUser; // 包装类型,Jackson 不剥离 is 前缀
AuthController中调用处从.isNewUser()改为.getIsNewUser()(Lombok 对Boolean类型生成getIsNewUser而非isNewUser)。
2. RateLimitInterceptor的两个匹配问题
路径匹配。限流规则path.contains("/api/llm/")使用contains而非startsWith。
/api/llm/analyze/stream 会被匹配(有意),但路径中包含这些子串的其他URI(如果有)也会被误伤。
公开路径旁路风险。isPublicPath中path.startsWith("/api/auth") 没有尾部 /,会匹配/api/auth/login(有意),也会匹配/api/authanything(非有意,虽然当前无此路径)。
修复:
if (path.startsWith(entry.getKey())) // contains → startsWith
path.startsWith("/api/auth/") // 补尾部 /,精确前缀
3. DetectionService的@Value无默认值
@Value("${audio.service.url}") // 配置缺失 → 启动失败
@Value("${video.service.url}") // 同理
@Value注解不提供默认值时,如果配置文件中缺少该属性,Spring容器启动会直接抛出异常。
修复:
@Value("${audio.service.url:http://localhost:5000/audio/detect}")
@Value("${video.service.url:http://localhost:5002/api/detect/video}")
4. AgentController的不安全类型转换
String sessionId = (String) request.getOrDefault("sessionId", UUID.randomUUID().toString());
Map<String, Object>.getOrDefault()返回Object。如果调用方传入sessionId: 123(数字),(String)强制转换会发生ClassCastException。
修复:
String sessionId = String.valueOf(request.getOrDefault("sessionId", UUID.randomUUID().toString()));
5. LLMService中temperature/maxTokens的null字段
requestBody.put("temperature", llmConfig.getTemperature()); // null 时 JSON 含 null 值
requestBody.put("max_tokens", llmConfig.getMaxTokens());
LLMConfig的temperature(Double)和maxTokens(Integer)默认值为null。Jackson序列化时null值会包含在JSON 中("temperature": null),部分LLM API拒绝包含null的请求体。
修复:
if (llmConfig.getTemperature() != null) {
requestBody.put("temperature", llmConfig.getTemperature());
}
if (llmConfig.getMaxTokens() != null) {
requestBody.put("max_tokens", llmConfig.getMaxTokens());
}
6. KnowledgeController冗余死代码
String kw = /* ... */;
if (kw == null || kw.trim().isEmpty()) {
return Result.error("搜索关键词不能为空");
}
String validationError = InputValidator.validateKeyword(kw); // validateKeyword内部含 null/empty 判断
if (validationError != null) { // 此if永远不可达
return Result.error(validationError);
}
直接删除InputValidator.validateKeyword(kw)的调用和后续判空代码块。
7. test_api.py跳过测试被误判为失败
report.add("视频检测", None, "跳过...") # None → if passed → False → 计入失败
跳过测试在报告中显示为失败项,误导测试结果判断。
修复:None → True。
8. CoTService temperature硬编码为0.3
requestBody.put("temperature", 0.3); // 硬编码,忽略配置
CoT推理目前唯一硬编码temperature的LLM调用点。其他调用均从llmConfig.getTemperature()取配置值。如需统一管理,可在LLMConfig中新增cotTemperature配置项。
9. EmbeddingService 响应解析缺少防御
List<Map<String, Object>> data = (List<Map<String, Object>>) response.get("data");
List<Double> embedding = (List<Double>) data.get(0).get("embedding");
如果SiliconFlow API返回结构变更(如 {"data": {"embedding": [...]}}),直接抛ClassCastException,无任何提示。
八、小结
这一轮的收尾工作前后跨度约一个下午加两次代码审查。做的事可以分三个阶段来理解:
第一阶段——回归验证。拉下代码逐项核对之前的修复,5项还在、5项丢了、1项新增(WebConfig)。重新修一遍不仅是恢复功能,更是对每个Bug根因的再次确认。
第二阶段——部署就绪。三个Docker配置漏洞 — 缺挂载、指错路径、缺COPY—在本地开发时全部隐藏得完美。本地文件系统是一个扁平空间,所有文件都能互相访问;容器是独立沙箱,只看得见显式声明要带进去的东西。理解这层差异,就理解了所有Docker部署问题的根因。
第三阶段——全量精修。第二轮审查发现的9个问题,单个体量都很小 — 大多数是一行代码的修改。但它们的共同特征是:不修不会炸,修了对得起代码质量。mvn compile零error零warning,是这类精修的最终验收标准。
回过头来看,这个项目从三个月前一个空的mvn archetype:generate,到现在拥有103个Java文件、28个API、RAG检索、CoT思维链、ReAct推理、Agent编排、SSE流式、JWT认证、MySQL持久化、Docker一键部署—外加两轮全量代码审查、19个文件的精修、零编译警告的编译输出。
完成了
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)