目录

一、背景:做完所有功能之后

二、回归验证:前几篇修过的Bug还在吗

三、模型文件:968MB

1. HuggingFace模型文件的构成

2. from_pretrained()如何解析路径

3. 两个pretrained目录的路径对齐

四、.gitignore幽灵规则:一条*.txt如何杀死Docker构建

五、Docker部署:容器隔离带来的三个陷阱

1. COPY和volume的本质区别

2. 陷阱一:视频容器缺模型挂载

3. 陷阱二:音频容器环境变量指向不存在路径

4. 陷阱三:独立构建的镜像不完整

六、第一轮代码审查:6个Bug的技术分析

1. knowledge.js:finally把成功的流杀了

2. result.js:外层包装吃掉了内层数据

3. app.json:选中的tab和没选中一模一样

4. FaceCropService:一个 JNI 对象的线程安全边界

5. Records.vue:一个冒号的距离

6. WebConfig.java:删掉一段什么都没做的代码

七、第二轮代码审查:全量扫描后的9项精修

1. LoginResponse:Jackson的is前缀陷阱

2. RateLimitInterceptor的两个匹配问题

3. DetectionService的@Value无默认值

4. AgentController的不安全类型转换

5. LLMService中temperature/maxTokens的null字段

6. KnowledgeController冗余死代码

7. test_api.py跳过测试被误判为失败

8. CoTService temperature硬编码为0.3

9. EmbeddingService 响应解析缺少防御

八、小结


一、背景:做完所有功能之后

        前七篇博客从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 statusfatal: not a git repository.git 目录由于磁盘操作或 VS Code崩溃丢失了。重新git initgit remote addgit fetchgit 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.jsonmodel.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/ 目录树:

位置

内容

状态

根目录pretrained/

配置文件 + 3个权重文件(968MB)

本地存在,Git未跟踪

ai-services/audio/pretrained/

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)

pretrained/asvspoof-finetuned/

ai-services/audio/pretrained/asvspoof-finetuned/

model.safetensors (362.59 MB)

pretrained/wav2vec2-base/

ai-services/audio/pretrained/wav2vec2-base/

best_model.pth (244.57 MB)

pretrained/

ai-services/video/pretrained/

搬迁后:代码的相对路径归正、.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的本质区别

在展开问题之前,需要区分两种文件传递方式的根本差异:

方式

时机

可见范围

COPY(Dockerfile)

docker build

写入镜像层,所有基于该镜像的容器可见

volumes(docker-compose)

docker compose up

宿主机实时映射到容器,镜像本身无这些文件

本项目的模型文件全部通过volumes挂载(因为太大,不在Git中)。理解这层差异后,三个问题全部暴露了根因。

2. 陷阱一:视频容器缺模型挂载

音频服务有模型挂载:

audio-detection:
    volumes:
      - ./ai-services/audio/pretrained:/app/pretrained

视频服务只有数据挂载:

video-detection:
    volumes:
      - video_data:/app/data    # 没有模型

容器中/app/pretrained/为空。api/app.pyMODEL_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(如果有)也会被误伤。

公开路径旁路风险isPublicPathpath.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());

LLMConfigtemperature(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 → 计入失败

跳过测试在报告中显示为失败项,误导测试结果判断。

修复:NoneTrue

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个文件的精修、零编译警告的编译输出。


完成了

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐