AI智能体协作开发:从原型到生产的咖啡一爆检测器实战
1. 项目概述:从原型到生产的咖啡一爆检测器
去年我捣鼓出了一个咖啡一爆检测器的原型,还写了个三篇的系列文章来记录。那个原型确实能用——从去年十一月开始,我就一直用它来监控我自己的咖啡烘焙。但说到底,它还是个“概念验证”阶段的产物,代码结构一团乱麻,模型也没法复用,更别提部署到实际的硬件上了,每次烘焙都得开着我的笔记本电脑。这个系列,就是关于它的“生产级”重建。最终的结果是一个基于音频频谱图变换器的模型,在一爆检测上达到了97.4%的准确率和100%的精确度,并且能在树莓派5上运行,每处理一个10秒的音频窗口只需要2.09秒。从数据准备、训练、评估,到ONNX INT8量化导出、边缘设备验证,再到一个完整的Gradio交互界面,整个流程我只用了两个晚上就搞定了。
但我并不是靠自己在代码库里埋头苦干实现的。我严格地扮演了“工程负责人”的角色,而具体的实现工作,则交给了Warp终端及其内置的AI智能体Oz来完成。我的职责完全集中在架构层面:设计整个工作流,设定智能体与代码库交互的严格规则;定义科学方法,包括模型规格、测试策略、评估指标和数据集标注方案;最后是指导执行,引导智能体完成实现并审查输出。用这种方式工作了一个周末,Warp/Oz完成了一个包含18个“故事”的开发史诗,提交了10个拉取请求,产生了跨越75个文件的11,087行Python代码,其中52次提交明确由智能体共同完成。GitHub Copilot审查了每一个PR,在28轮审查中标记了111个问题。
模型已经发布在Hugging Face上,数据集也已开源,源代码在GitHub上。这篇文章的重点不是模型本身,而是让这一切成为可能的系统。机器学习相关的科学细节会在第二和第三篇文章中详述。在这里,我想展示我用来指导一个AI智能体完成一个复杂、多阶段机器学习项目的具体架构,以及如何在这个过程中不丢失对关键工程决策的控制权。
2. 核心架构:导演、编码员与审查员的三方协作
这个项目的核心模式,是在三个角色之间建立并严格执行一种职责分离的机制。这不是一个君子协定,而是通过一个名为AGENTS.md的文件硬编码到项目中的规则手册。每当Oz开始一项任务,它都被强制要求先阅读这份规则。
2.1 角色定义与职责边界
我(人类)负责:
- 架构设计:定义代码仓库的结构、模块边界,并强制推行Hugging Face的
save_pretrained/from_pretrained作为标准的模型打包契约。这意味着所有模型、处理器都必须能通过这两个方法无缝保存和加载,确保可复现性和部署一致性。 - 机器学习科学:这包括模型选型(为什么选择音频频谱图变换器而不是卷积神经网络)、数据划分策略(采用“录制级别”划分以防止数据泄露)、类别权重计算以及超参数设定的数学依据。每一个决策背后都需要有支撑的理由,而不是随意尝试。
- 工作流约束:定义项目的“游戏规则”,编写参数化的技能脚本,并管理整个开发史诗的状态。我需要预先想好智能体每一步应该做什么、不能做什么,以及如何验证结果。
- 质量门禁:审查每一个拉取请求,解读评估指标(如准确率、精确率、召回率),并决定何时需要重新训练,何时可以发布。
Oz(Warp的终端原生智能体)负责:
- 终端执行:直接运行训练循环、评估脚本、ONNX导出命令,甚至通过SSH在树莓派5上执行验证任务。它充当了我的“虚拟双手”。
- 代码生成:编写那些必要但繁琐的样板代码,例如自定义的
WeightedLossTrainer子类、命令行参数解析器、pytest测试脚手架以及音频数据加载器。 - 技能调用:执行参数化的技能文件。这些文件(如
.claude/skills/train-model/SKILL.md)编码了精确的命令序列和验证检查步骤,确保操作的一致性。 - 状态管理:读取史诗文档,更新上下文,并在完成一个阶段后勾选对应的“故事”。这相当于一个自动化的项目进度跟踪器。
GitHub Copilot负责:
- 异步代码审查:在所有的10个PR中,Copilot扮演了严格的代码审查员角色,标记类型安全问题、API误用、缺失的错误处理以及依赖项管理问题。
这里有一个关键的认知需要明确:Copilot从未发现过一个机器学习逻辑错误。每一个数据泄露的修复、超参数的修正、精确率与召回率之间的权衡决策,都来自于我。Copilot本质上是一个针对代码的“激进”的linter(代码检查工具),而不是机器学习科学的审查员。它确保代码是整洁、类型安全的,但无法判断模型设计或数据处理流程在科学上是否正确。如果你依赖AI代码审查来验证你的机器学习管道逻辑,你很可能会发布一个代码整洁但功能错误的模型。
2.2 状态管理:让智能体“记住”上下文
管理一个长期运行的AI智能体项目,最大的挑战之一是上下文漂移。智能体在完成两三个任务后,很容易忘记之前的约定、项目结构或进行中的修改,开始基于过时的假设生成代码。为了解决这个问题,我建立了一个强制性的状态读取循环,核心是三个文件:
AGENTS.md- 规则手册这个文件位于仓库根目录,是智能体进入项目时必须阅读的“宪法”。它不仅包含编程规范(如使用Python 3.11+、完整的类型提示、通过ruff和pyright检查),更重要的是定义了工作流。最关键的一条规则是:“在开始任何任务前:阅读
docs/state/registry.md→ 打开史诗文件 → 检查GitHub Issue。” 这条规则强制智能体在动笔写代码之前,必须先了解项目的全局状态和当前任务的具体要求,从而将上下文漂移的风险降到最低。史诗状态管理 - 清单
docs/state/registry.md文件指向当前活跃的史诗。史诗文件本身(例如docs/state/epics/coffee-first-crack-detection.md)则包含了18个“故事”,这些故事被分组到6个不同的阶段,每个都链接到一个GitHub Issue。 智能体在任务前后必须遵循一个严格的协议:- 任务开始前:1) 读取注册表找到活跃史诗;2) 打开史诗文件,检查故事状态;3) 打开对应的GitHub Issue,阅读最新要求;4) 在
feature/{issue-number}-{slug}分支上工作。 - 故事完成后:1) 在史诗文档中勾选该故事;2) 更新“活跃上下文”部分;3) 在GitHub Issue中评论并关闭它;4) 在GitHub史诗总Issue中打勾;5) 发起一个引用该故事Issue的PR。 通过这套流程,18个故事得以有条不紊地交付,不会遗漏任何步骤,也不会搞不清下一步该做什么。
- 任务开始前:1) 读取注册表找到活跃史诗;2) 打开史诗文件,检查故事状态;3) 打开对应的GitHub Issue,阅读最新要求;4) 在
参数化技能 - 操作手册技能是位于
.claude/skills/目录下的Markdown文件,它们为常见操作编码了精确的命令序列。每个技能都定义了前置条件、要执行的命令以及验证步骤。 我编写了四个核心技能:train-model/SKILL.md: 包含数据验证和检查点保存的端到端训练流程。evaluate-model/SKILL.md: 测试集评估及指标报告生成。export-onnx/SKILL.md: ONNX模型导出(FP32 + INT8)及模型大小、延迟基准测试。push-to-hub/SKILL.md: 将模型和数据集发布到Hugging Face Hub。 当我对Oz说“训练模型”时,它不会即兴发挥。它会读取技能文件,并严格遵循我定义的序列执行。这消除了因智能体猜测命令行标志、跳过验证步骤或忘记将特征提取器配置与模型权重一起保存而导致的整类错误。
3. 实战演练:从搭建到故障排查
有了清晰的架构和规则,智能体就可以开始执行了。第一个实质性提交是feat(S5/S6/S8): implement train.py, evaluate.py, inference.py。在一次操作中,Oz生成了训练管道、评估工具和滑动窗口推理模块。它遵守了AGENTS.md的规则,使用了正确的基础模型,并按照我的要求,用类别加权的交叉熵损失函数配置好了WeightedLossTrainer子类。
然而,训练很快就失败了。
3.1 遭遇“输入特征”与“输入值”之坑
Oz编写的数据集适配器返回的字典键是input_features——如果你看过其他Hugging Face音频管道(比如Whisper)的示例,这个猜测是合理的,因为Whisper确实使用input_features。但问题在于,本项目使用的ASTFeatureExtractor返回的键是input_values。模型默默地接收不到任何有效输入,导致损失函数值爆炸。
修复这个问题的提交(75bbb4b)的差异只有一行代码:
# src/coffee_first_crack/train.py - _HFDatasetAdapter.__getitem__ - “input_features”: inputs[“input_features”].squeeze(0), + “input_values”: inputs[“input_values”].squeeze(0),这是一个典型的“单行Bug”,但如果你不知道问题出在哪,可能需要在训练日志前枯坐一个小时来排查。这个Bug暴露了Hugging Face音频API中一个已知但未解决的不一致性问题。Oz从最常见的教程示例中进行模式匹配,却匹配到了错误的模式。
同一个提交还修复了另一个问题:将accelerate>=0.26.0添加到了pyproject.toml中。Hugging Face Trainer在运行时需要这个依赖,但并没有在顶层显式导入。Oz在代码生成时没有发现它,因为直到实际开始训练,才会触发ImportError。
3.2 验证循环的实际操作
在开发过程中,验证循环是实时进行的。例如,Oz可能会遇到一个pyright类型检查失败,它会诊断类型问题,修复它们,然后运行完整的ruff check→ruff format→pyright→pytest链条,直到所有检查都通过。这个过程完全在终端内由智能体自主完成,我只需要在关键节点(如查看评估指标)进行干预。
3.3 Copilot作为第三方审查员的贡献
在整个项目的10个PR中,Copilot提交了28轮审查,包含111条独立评论。分布如下:
- PR #23 (树莓派5 ONNX验证): 36条评论,6轮审查——审查最严格的PR。
- PR #17 (导出、脚本、测试): 26条评论,5轮审查。
- PR #27 (数据准备): 16条评论,3轮审查。
- PR #16 (训练、评估、推理): 10条评论。
- PR #28 (Gradio界面): 10条评论。
Copilot主要捕捉了以下几类问题:
- 类型安全:缺失的类型提示、错误的返回类型、未标注类型的函数签名。
- 未使用的导入:重构后遗留的无效代码。
- API误用:已弃用的参数、缺失的同步调用、不正确的异常处理。
- 依赖卫生:缺失的显式依赖、版本锁定问题。
- 文档与文案:具有误导性的文档字符串、Gradio界面中不准确的文本。
4. 数据工程:从零构建与防泄漏设计
在智能体能够训练任何模型之前,我必须从零开始构建训练数据。市面上没有公开的咖啡烘焙一爆音频数据集——Hugging Face上没有,Kaggle上没有,学术文献里也没有。这意味着我需要自己录制烘焙过程,使用Label Studio进行标注,并设计一个“录制级别”的数据管道,以防止“块级别”的数据泄露。这种泄露在时间序列音频机器学习中会悄无声息地虚高测试指标。
4.1 数据收集与标注实践
我录制了多个完整的咖啡烘焙过程,每个过程可能持续12到15分钟。一爆(First Crack)是咖啡豆在烘焙中释放气体和水分时发出的清脆爆裂声,通常发生在烘焙的中后期。使用Label Studio这样的工具,我可以精确地在音频时间轴上标注出一爆开始和结束的时间段。
关键注意事项:标注的一致性至关重要。不同烘焙批次、不同咖啡豆、甚至环境噪音都会影响一爆声音的特征。我制定了明确的标注规则,例如,将连续、密集的爆裂声区间视为一个一爆事件,而将孤立的、间隔很远的单一声响视为噪音或异常值。这些规则被记录在项目的README或专门的标注指南中,确保后续如果扩展数据集,标注标准能够统一。
4.2 防数据泄露的拆分策略
这是本项目数据工程中最关键的一环。一个天真的做法是:将整个长音频文件切割成许多短片段(例如10秒一个),然后随机地将这些片段分配到训练集、验证集和测试集。这种做法会导致严重的数据泄露。因为来自同一次烘焙录制的相邻片段,在声学特征上具有极高的相似性(相同的环境噪音、相同的设备、相同的咖啡豆批次)。如果随机的拆分使得来自同一次录制的片段既出现在训练集又出现在测试集,那么模型在测试时表现“良好”,可能只是因为它记住了这次录制特有的背景噪音模式,而不是真正学会了一爆的声学特征。
我的解决方案是“录制级别”拆分:以每次完整的烘焙录音为单位进行拆分。例如,我有15次烘焙的录音,我可能将10次录音的所有片段用于训练,2次用于验证,3次用于测试。这样,模型在测试时遇到的,是完全未曾“听”过的、独立的烘焙过程,评估结果才具有真正的泛化意义。这个策略是在项目架构阶段就由我(人类)定义好,并作为硬性规则写入数据准备脚本的约束条件,智能体Oz只是严格执行。
4.3 数据增强与预处理
为了提升模型的鲁棒性,在训练前对音频数据进行增强是必要的。常见的音频增强手段包括:
- 添加背景噪音:混入轻微的、不相关的环境音(如风扇声、远处交通声),模拟真实烘焙环境。
- 时间拉伸与音高变换:轻微改变音频的速度或音高,增加数据的多样性。
- 增益调整:随机微调音频的音量,模拟麦克风距离或增益设置的差异。
在实现时,我通过Hugging Face的datasets库结合torchaudio或audiomentations库来集成这些增强操作。重要的是,增强只应用于训练集,验证集和测试集必须保持原始状态,否则无法客观评估模型性能。
5. 模型选择、训练与边缘部署
5.1 为什么选择音频频谱图变换器
在原型阶段,我可能尝试过更简单的卷积神经网络模型。但对于生产级重建,我选择了音频频谱图变换器。原因如下:
- 强大的长程依赖建模能力:Transformer的自注意力机制天生擅长捕捉序列中远距离元素之间的关系。一爆声音虽然是一个短时事件,但其出现的前后语境(烘焙进程中的其他声音变化)可能包含重要信息,AST能更好地利用这些信息。
- 在音频分类任务上的SOTA表现:AST在多个权威音频分类基准(如AudioSet)上取得了领先成果,其架构经过充分验证。
- 与Hugging Face生态的无缝集成:Hugging Face
Transformers库提供了预训练的AST模型和配套的特征提取器,可以极大简化开发流程,并方便后续的模型共享。
我选择了MIT/ast-finetuned-audioset-10-10-0.4593作为基础模型进行微调。这是一个在大型通用音频数据集上预训练好的模型,通过在我们的专用咖啡一爆数据集上进行微调,可以快速获得优良的性能,即所谓的“迁移学习”。
5.2 训练配置与技巧
- 损失函数:由于“一爆”和“非一爆”的片段数量可能不均衡(非一爆片段占大多数),我使用了类别加权的交叉熵损失。这迫使模型更加关注少数类(一爆)的识别,有助于提高召回率。权重的计算基于训练集中各类别的频率倒数。
- 学习率与优化器:通常使用较小的学习率(如5e-5)进行微调,以避免破坏预训练模型已经学到的有价值特征。优化器选择AdamW,并配合线性学习率预热和衰减策略。
- 评估指标:除了整体准确率,我尤其关注精确率和召回率。对于一爆检测,高精确率(100%)至关重要,因为这意味着模型几乎不会误报(将非一爆声音识别为一爆)。误报会导致烘焙师错误判断烘焙阶段。在保证高精确率的前提下,再尽可能提升召回率(检测出一爆事件的能力)。
5.3 ONNX量化与树莓派部署
为了在资源受限的树莓派5上实时运行模型,必须对模型进行优化。
- ONNX导出:首先将训练好的PyTorch模型导出为标准ONNX格式。这确保了模型可以在多种推理引擎上运行。
- INT8量化:这是关键步骤。通过量化,将模型权重和激活值从32位浮点数转换为8位整数。这能显著减少模型大小(约减少75%)和提高推理速度,同时通常只带来极小的精度损失。Hugging Face的
optimum库提供了与transformers无缝集成的量化工具。 - 边缘验证:通过SSH连接到树莓派5,使用ONNX Runtime运行量化后的模型,并测量其处理一个10秒音频片段的延迟。最终达到了2.09秒的推理时间,这对于咖啡烘焙(一个持续数十分钟的过程)的实时监控来说是完全可以接受的。
部署注意事项:树莓派上需要安装合适版本的ONNX Runtime(通常选择ARM64版本)。同时,要确保音频输入模块(如从USB麦克风读取数据)与模型推理模块能够高效协同工作,避免因数据I/O成为瓶颈。
6. 构建Gradio交互界面
为了让其他人能够轻松体验模型,我构建了一个Gradio Space并部署在Hugging Face上。这个界面允许用户上传一段10秒左右的咖啡烘焙音频,模型会返回是否检测到一爆,并可视化的显示模型对音频片段的预测结果。
实现要点:
- 简化用户输入:支持文件上传,同时提供几个示例音频供快速测试。
- 集成预处理:界面后端需要集成与训练时完全相同的音频预处理(重采样、特征提取)逻辑。
- 结果可视化:除了“是/否”的检测结果,还可以展示模型输出的置信度分数,或者绘制音频波形图并在检测到一爆的时间点进行高亮标记,使结果更加直观。
7. 通用AGENTS.md模板与经验总结
基于这个项目的成功经验,我可以提炼出一个简化的AGENTS.md模板,适用于任何希望引入AI智能体协作的软件项目:
# AGENTS.md - [你的项目名] 本项目AI编码代理的规则与上下文。 ## 规则 * 使用 [语言] [版本]+,所有公开函数/方法需有完整的类型提示。 * 在标记代码完成前,必须通过 [代码格式化工具] 和 [代码检查工具]。 * 所有依赖必须在 [依赖管理文件] 中声明,禁止临时安装。 * 大文件(如数据集、模型检查点)应上传至 [远程存储,如Hugging Face Hub],禁止提交到Git。 * `data/`、`experiments/`、`exports/` 等目录已在 `.gitignore` 中,请保持。 * 所有随机数生成器必须使用 `configs/default.yaml` 中定义的种子进行初始化。 * 每个“故事”对应一个PR,分支命名:`feature/{issue编号}-{简短描述}`。 * **在开始任何任务前**:阅读 `docs/state/registry.md` → 打开当前史诗文件 → 查看对应的GitHub Issue以获取最新要求。 ## 快速命令 ### 环境搭建 `poetry install` 或 `pip install -e .` ### 构建/测试/部署 `python -m pytest tests/` - 运行测试 `python scripts/train.py --config configs/train.yaml` - 训练模型 `python scripts/export_onnx.py --model-path ./model` - 导出ONNX模型 ## 代码库架构src/ # 源代码 ├── module_a/ # 模块A:负责核心功能X ├── module_b/ # 模块B:负责数据处理Y └── utils.py # 通用工具函数 tests/ # 单元测试 scripts/ # 可执行脚本 configs/ # 配置文件 docs/state/ # 项目状态与史诗文档
## 史诗状态管理流程 **任务开始前**: 1. 阅读 `docs/state/registry.md` 找到活跃史诗。 2. 打开史诗文件,检查当前故事状态。 3. 打开对应的GitHub Issue,阅读评论了解最新需求。 4. 在 `feature/{issue-number}-{slug}` 分支上开展工作。 **故事完成后**: 1. 在史诗文档中勾选该故事。 2. 更新史诗中的“活跃上下文”部分。 3. 在GitHub Issue中评论并关闭它。 4. 在GitHub史诗总Issue中打勾。 5. 发起一个引用该故事Issue的Pull Request。这个文件不是写给人的文档,它是你代码库的系统提示词。你省略的每一条规则,都意味着智能体将自行做出决定——而且每次的决定可能都不一样。
8. 关键经验与避坑指南
回顾整个项目,以下是一些值得分享的经验和教训:
上下文管理是生命线:
AGENTS.md中强制性的状态读取循环是整个工作流中我最不愿放弃的部分。没有它,智能体的上下文会在两三个任务后发生漂移,开始基于过时的假设生成代码。如果你也在运行长周期的智能体项目,并且用不同的方式解决了上下文问题,我很想知道具体细节。明确区分“代码正确”与“逻辑正确”:GitHub Copilot等工具是优秀的代码审查员,能捕捉语法、类型和API使用的错误。但它们无法理解领域特定的逻辑。机器学习中的数据泄露、超参数选择、评估指标权衡等核心问题,必须由具备领域知识的人类工程师来把控。AI辅助的是“编码”,而不是“设计”和“决策”。
技能文件是防止“幻觉”的利器:为常见操作(训练、评估、部署)编写参数化的技能文件,可以确保智能体每次都执行完全相同、经过验证的步骤。这消除了因智能体“即兴发挥”而引入的随机错误,将智能体的行为约束在可预测的范围内。
从第一个Bug开始学习:像
input_featuresvsinput_values这样的Bug很有教育意义。它提醒我们,即使使用成熟的框架,也存在细微的不一致性和“坑”。在AI辅助开发中,人类需要扮演“领域知识注入者”和“边界情况检查者”的角色。当智能体基于常见模式做出合理但错误的假设时,你需要有能力并快速介入纠正。为迭代而设计:机器学习项目本质上是迭代的。你的架构应该能轻松支持重新训练、重新评估和重新部署。通过将配置(超参数、路径)外部化,使用标准的模型保存/加载模式,以及自动化测试和评估管道,你可以让智能体高效地执行多轮迭代,而你只需关注结果指标并做出科学决策。
这个项目最终产出了一个准确率97.4%、精确率100%的咖啡一爆检测模型,一个公开的数据集,一套完整的训练和部署代码,以及一个可交互的演示界面。更重要的是,它验证了一种高效的人机协作模式:人类负责架构、科学和决策,AI负责执行、生成和检查。这种模式不仅适用于机器学习项目,也可以扩展到许多其他类型的软件开发中,将开发者从繁琐的重复劳动中解放出来,更专注于创造性的设计和问题解决。
