1. 第七周的分水岭:从“复习笔记”到“可上线的应用”
第七周之前,我的状态非常典型:一个写了好几年 Java 后端的开发者,决定转向 AI 工程化,于是在前六周里拼命补知识。特征工程看了一些,损失函数推导过一遍,Transformer 的注意力机制也能讲个大概,Prompt 调优更是收藏了十几篇文档。可当我把这些散落知识点放到一起,试着做一个“能上线的应用”时,才发现自己根本交不出东西。Week 7 没有学新算法,也没有继续刷模型榜单,我只做了一件事:把之前记在笔记里、散在代码片段里的知识,串成一个真正能被别人调用的服务。
这一周值得单独写一篇,是因为它暴露了转型路上最常见的问题:“学过”和“能用”之间差着一整条工程链路。模型准确率再高,装不进 Java 服务、跑不稳、没法监控、出了问题排查不了,就只是 notebook 里的漂亮数字。我给自己定的 Week 7 目标是做一个可上线的文本意图识别小应用:用户发一段文本,服务返回分类结果和置信度。验收标准不是某个指标多好看,而是能不能在干净环境里一键启动、连续请求不崩、日志能追踪、模型可回滚。
我把这一周的完整过程、踩坑链路和最终沉淀下来的工作流放在下面,给同样从 Java 后端转 AI 工程化的朋友一条可以复用的路线。
1.1 前六周学的是零件,不是系统
前六周我学了很多孤立单元:线性回归、逻辑回归、Embedding、RNN/CNN、Transformer、Precision/Recall、A/B 测试、模型量化。这些知识点单独拎出来,每一个都能写一篇笔记,但一旦要组成应用,它们各自属于哪个层级、由谁负责、接口怎么定义,我之前完全没有概念。
最直接的感受是:我可以在 Jupyter 里跑通模型训练,却不知道模型文件应该放哪里、Java 服务如何加载、请求进来之后特征应该由谁算、预测出错时日志应该记哪些字段。这些内容不会出现在大多数教程里,恰恰是工程化考验人的地方。第七周让我意识到,散落知识点不是问题,问题是没有一个“总装图”把它们固定到系统里。
1.2 我给第七周定义的验收标准
第七周开始时,我写下了四条必须满足的验收标准,而不是“我要做个模型”这种模糊目标:
- 标准一:项目可以通过 Maven 打包,在空机器上只装 JDK 和 Docker 就能启动;
- 标准二:服务提供 HTTP 接口,支持单条和批量预测,返回结果包含分类、置信度和请求 ID;
- 标准三:模型文件不放在代码仓库里,而是放在独立模型存储路径,版本号显式传递;
- 标准四:关键指标有日志和内存态统计,服务重启后能自动加载最新模型并完成预热。
这条标准直接把我的学习方式从“继续吸收”切换到“组装交付”。后面所有的决策,都围绕这四条展开。
2. 先把系统边界画清楚:模型、服务、数据各就各位
写 Java 后端的时候,我习惯先画模块边界再写代码。到了 AI 工程化,这个习惯救了我。Week 7 第一天我没有写任何训练脚本,而是花了大半天画了一张系统边界图:模型从哪来、服务怎么拿到模型、请求如何变成特征、预测结果如何返回、日志如何回流。
2.1 模型是资源,不是业务逻辑
很多 Java 开发者第一次接触 AI 项目,会试图把模型推理代码直接写进业务代码里,比如在 Controller 里加载模型文件、在 Service 里写预处理逻辑。这么做在 Demo 阶段没问题,但只要模型更新版本,或者需要灰度上线,就会变成灾难。
我更建议把模型看成独立的版本化资源,与业务代码完全分离。模型文件由离线训练流程产出,通过固定 Schema 导出;Java 服务只负责加载指定版本的模型,处理请求并返回结果。业务代码和模型之间通过一个薄薄的 Predictor 接口隔离,这样演化空间会大很多。
2.2 训练环境、预热脚本、在线服务各自独立
训练脚本使用 Python 生态确实高效,但我不打算把 Python 进程嵌进 Java 服务里。Week 7 我采用的方式是:Python 负责训练和导出模型,Java 负责加载和推理。两者通过模型文件和特征 Schema 通信。这样做的直接好处是,线上服务不需要装 Python 环境、不需要管理 Python 依赖,部署复杂度低很多。
预热脚本也独立存在。服务启动后,先加载一个固定数量的测试请求,调用若干次推理,确认模型输出符合预期后再接收线上流量。这个预热动作在本地很难发现价值,真正部署时能拦住大量低级错误。
2.3 数据闭环从第一天就要设计
“模型上线不是终点,而是数据循环的起点。”这句话我在前几周听过很多遍,但 Week 7 才真正理解。线上日志不能只记录业务结果,还要记录输入文本、模型版本、预测结果、置信度、是否人工复核,以及最终的正确标签。
我把日志字段设计成一张固定的表,而不是临时拼几个字段。这样后续做离线评估、Bad Case 分析、模型迭代时,都能直接回放线上数据,不需要重新造数据。
| 边界项 | 训练期 | 线上期 |
|---|---|---|
| 模型存放 | 本地文件 | 独立模型目录 |
| 模型版本 | 训练时间戳 | 显式版本号 |
| 数据来源 | 标注样本 | 线上日志 + 人工复核 |
| 特征计算 | Python 脚本 | Java 内部逻辑 |
| 评估方式 | 离线验证集 | 灰度对比 + 人工抽检 |
2.4 资源预算先算再写
Java 后端转 AI 工程化,很多人会忽略一个问题:模型推理不仅吃 CPU,还吃内存,有时候还吃 GPU。Week 7 我的原型是个文本模型,参数量不大,单次推理大概几十毫秒,但模型加载后占的内存是固定的。我先估算了一下:一个模型文件约 200MB,JVM 堆配置 1GB,再加缓存和线程池,一个容器至少要 1.5GB 内存才稳。
如果不提前算资源预算,部署很容易出现 OOM。更麻烦的是,服务在流量高峰才崩溃,排查起来会特别痛苦。所以我把容器内存、JVM 堆内存、模型加载内存写进了部署文档,避免后续接手的人靠猜。
3. 端到端落地:一个文本意图识别服务是怎么从零变可用的
边界画完后,我开始做端到端原型。为了不引入过多业务干扰,我选了一个非常经典的场景:客服工单自动分类。输入一段用户描述,输出“退换货、物流咨询、价格问题、产品故障”等八个类别之一。这个场景足够小,又包含完整的训练、推理、评估、迭代闭环。
3.1 数据准备与标签设计
数据是训练的起点。我没有直接拿公开数据集,而是用模拟项目 X 自己的业务形态造了一套小样本:先写规则生成一批模板文本,再人工修正明显不合理的地方。数据量不大,大约 800 条训练样本、200 条验证样本、200 条测试样本。
标签设计比想象中重要。一开始我设计了十五个细分类别,结果训练后混淆矩阵里一堆相邻类别互相打架。后来我把相近类别合并成八个,边界清晰了许多。这个动作让我切身体会到:类别粒度是标注成本、模型难度和业务可用性的三方博弈。
3.2 模型选型:先从小模型开始
很多教程会推荐直接上大规模预训练模型,但工程化要综合考虑。我的场景是中短文本、类别固定、延迟敏感。经过对比,我选择了一个轻量级的预训练编码器作为底座,后面接一个简单的分类头。这个组合参数量不大,单次 CPU 推理在 30 毫秒到 50 毫秒之间,满足我的验收目标。
选型逻辑是:先跑通最小可行性链路,再在链路稳定的基础上换更强模型。如果一上来就用最大的模型,不仅训练慢,Java 侧加载和推理也有更多不确定性,排错时会非常痛苦。
3.3 训练、评估、导出:把笔记里的知识点串成流水线
前六周学的知识点终于在这里汇合:特征处理、损失函数、评估指标、模型保存。我的训练脚本非常简单,但它是完整的:
# 示意脚本,重点看流程,不是具体参数 train_df = load_labeled_data("data/intents.json") valid_df = load_labeled_data("data/valid.json") tokenizer = load_tokenizer("model/encoder") model = build_intent_model(num_labels=8) train(model, train_df, valid_df, epochs=6, batch_size=16) eval_out = evaluate(model, valid_df) print(eval_out.confusion_matrix()) save_model(model, tokenizer, "export/model_v1")导出模型时,我额外保存了一份特征 Schema 文件,里面定义清楚:输入字段名、截断长度、标签列表、向量维度、预处理顺序。这份 Schema 是后面 Java 侧不会写歪的保险。
3.4 Java 侧服务封装:模型只加载一次
Java 侧我使用最常用的 Web 框架搭建 REST 接口。第一版 Controller 特别简单:
@RestController @RequestMapping("/v1/intents") public class IntentController { private final IntentPredictor predictor; public IntentController(IntentPredictor predictor) { this.predictor = predictor; } @PostMapping public PredictResponse predict(@RequestBody PredictRequest request) { return predictor.predict(request.getText()); } }关键在于IntentPredictor的实现。模型初始化必须在应用启动阶段完成,而不是在第一个请求进来时才开始加载。初始化完成后,所有请求共用同一个模型副本,避免每次创建加载。
特征预处理的顺序也必须和训练时完全一致。我在 Java 侧复刻了分词、截断、ID 映射和 Padding 的逻辑,并用训练阶段的几十条样本做了回归测试。这一步是散落知识点变成工程细节的地方。
3.5 同步还是异步:先同步,再加异步
“在线推理要不要做异步”这个问题,我在 Week 7 里反复权衡过。对于交单式分类接口,同步响应最简单,调用方拿到结果就能继续流程。Batch 场景可以单独做异步任务。
我最终选择:默认同步,接口超时控制在 200 毫秒内,同时预留了一个批量接口,用于离线数据处理。异步虽然能提升吞吐,但会引入消息队列、回调、结果存储等复杂度,在原型阶段没必要。
4. 一次错误率飙升的线上排查:从现象到根因的完整链路
Week 7 后半段,我自认为部署已经稳定,结果一次灰度直接把我的自信打碎了。这个问题不常见,却非常值得完整复盘。
4.1 现象:灰度流量进来后,准确率只剩一半
我在生产环境复制了一小部分流量,只放了一个模型版本。刚开始日志显示响应正常,延迟也稳定。但人工抽检发现,明显属于“物流咨询”的文本被分到了“产品故障”,之前离线测试时准确率超过 90%,线上看起来只有 50% 左右。
第一反应是模型过拟合,或者是线上文本分布和训练分布不一致。但仔细看 Bad Case,发现错误并不是随机的,而是有规律:几乎所有文本都被分到相邻或靠后类别的概率变高。这个规律提示我,问题可能出在特征侧,而不是模型本身。
4.2 缩小范围:请求参数、模型版本、特征拼接
我用请求 ID 找出了具体请求,开始在本地重现。我用相同的模型文件、相同的输入文本,在本地测试脚本里跑了一次,结果分类完全正确。问题不在模型。然后我检查了 Java 服务的打印日志,发现请求文本被正确接收,但经过预处理之后,Token ID 序列和本地 Python 脚本里的输出完全对不上。
这时我意识到,问题很可能出在特征构造顺序。训练时我的预处理流程是“先分词、再截断、后 Padding”,而在 Java 侧我写成了“先截断、再分词、后 Padding”。大部分文本长度没有超过截断阈值,所以差异不明显;但一遇到特定长度的文本,截断位置不同,Token 序列就不同,模型输出自然就偏了。
4.3 定位根因:离线与在线特征处理不一致
这个根因让我非常惭愧,因为它属于最基础的特征工程问题。离线和在线的特征处理流程没有对齐,模型再强也白搭。
我复盘后发现,这不只是代码顺序问题,更是协作流程问题。Python 训练脚本和 Java 推理服务是不同时间写的,在没有统一 Schema 的情况下,两边很容易各按各的理解实现。改了几次之后,只有不经意触发到长度边界时才会暴露。
4.4 修复方案:固化特征 Schema,建立回归用例
修复动作有三层。第一层,把特征 Schema 提升为唯一事实来源,里面明确写下预处理每一个步骤的顺序、参数和输出维度;第二层,在 Python 侧和 Java 侧分别写一个输出对齐测试,用同样的输入跑若干条,逐字段对比;第三层,把这类对比测试加进 Maven 的集成测试流程,每次构建自动执行。
这之后,我再也没有遇到过由特征顺序不一致导致的线上事故。这个坑让我明白,AI 工程化里最隐蔽的风险往往不是模型,而是训练与推理之间的流程漂移。
5. 效果评估的工程化:准确率之外,还要看稳定性和可迭代性
模型能用之后,我开始认真思考效果评估。前六周学的指标在这个阶段显得特别有用,但也特别容易被误用。
5.1 准确率会骗人:必须看混淆矩阵和 Bad Case
准确率 90% 看起来不错,但客服工单分类场景里,类别分布天生不均衡。如果“产品故障”类样本特别多,模型只要偏向这一类,准确率就会虚高。
我每次评估都看混淆矩阵,重点关注容易混淆的类别对。比如“退换货”和“价格问题”为什么会互相串。接着我会抽样看 Bad Case,仔细读那些被分错的文本,判断是标注问题、特征问题还是模型能力问题。这个方法比盯着一个整体指标有用得多。
5.2 在线灰度:先把流量切到新模型上观察
模型有新旧版本时,不能直接全量切。我用了一个最简单的灰度方式:在 Java 配置里加一个分流参数,新模型先承接 5% 的请求,观察延迟、异常率、置信度分布,再逐步扩大到 20%、50%、100%。
灰度期间我会同时记录新模型和旧模型的预测结果,形成对比日志。即使新模型离线指标更好,线上也要先跑一段时间再决定是否转正,因为离线分布永远不等于线上分布。
5.3 人工复核闭环:把线上误判送回训练集
线上日志里,置信度低或者人工标记为“需要复核”的请求,我会定期捞出来。随机抽一部分让业务方人工标注,然后把标注好的数据合并进下一轮训练集。这个闭环虽然简单,但能让模型持续吃到真实分布的数据。
这一步也是 Java 后端能力最容易发挥的地方:写定时任务、导出数据、生成标注队列,都是后端开发的日常能力。AI 工程化并不是只写 Python,更多时候是在构建数据和模型之间的自动化管道。
5.4 迭代节奏:小步、频繁、可回滚
确定迭代节奏时,我给自己定的原则是:小步、频繁、可回滚。每轮迭代尽量只改一个变量,比如换了更多训练数据、调了正则化参数,或者升级了编码器。不要同时改三个东西,否则出了问题根本不知道是哪一步引起的。
回滚方案要在上线前就准备好。我的模型目录里保留最近三个版本,Java 侧配置支持随时切换版本号并重启加载。这个动作成本很低,但能显著降低试错心理压力。
6. 第七周之后:沉淀成一套可以复用的 Java + AI 工作流
Week 7 结束时,我的收获不是某一个模型,而是一套可复制的工作流。它把散落知识点变成了清晰的分工:Python 负责训练和实验,Java 负责服务和工程化,两边通过 Schema、版本号和日志对齐。
6.1 一页纸交付清单
每次做新的 AI 功能,我现在都会先过一遍清单:
- 数据是否有稳定来源,标签口径是否清晰;
- 训练流程是否可重复,是否依赖人工手动步骤;
- 模型导出格式是否固定,是否带版本号和 Schema;
- Java 服务是否在启动时加载模型,是否做过预热;
- 特征处理是否有离线和在线对齐测试;
- 日志是否记录了请求、版本、置信度和最终标签;
- 是否有灰度开关、回滚方案;
- 是否有 Bad Case 回流和人工复核入口。
这份清单让项目从“好像能做出来”变成“确定能交付”。
6.2 Java 背景在 AI 工程化里的优势
转型容易陷入“一定要写很多 Python”的焦虑,但 Week 7 让我看到本职经验的巨大价值。Java 后端对并发、稳定性、可观测性、依赖管理的理解,都是 AI 工程化必需的。模型再强,最终也要变成稳定运行在生产环境里的服务。
我更建议 Java 开发者把切入点放在模型服务化、数据管道、离线在线一致性、评估闭环这些位置,而不是只盯着模型训练。这样反而能更快拿到结果。
6.3 下一个阶段的方向
第七周之后,我给自己排了三个练习方向:把当前原型接入真实业务数据源,做成一个完整的自动标注工具;尝试换一个更大的预训练模型,对比延迟和效果;再研究一下请求级限流和排队策略,避免流量尖峰打垮推理服务。
我不再追求一次学完全部内容,而是每做一个项目,就补上这个项目暴露出的盲区。这个节奏比当初漫无目的地刷知识点踏实得多。
如果你也正在从 Java 后端往 AI 工程化转型,Week 7 最值得做的事,就是停止“继续学”,找一个哪怕是玩具级的场景,把它完整接进 Java 服务里,部署、监控、灰度、回滚都走一遍。走完这一遍,那些散落知识点自然会长成一个系统。