从凌晨三点那次实验事故说起吧。当时我在调一个文本分类的流程:上午换了一版清洗后的数据,下午改了两处预处理,晚上重跑训练,然后发现测试集的标签映射表跟前两天不一致,导致评测分数虚高。我花了整整一个通宵排查,最后定位到的原因简单得可笑——一行map函数的参数写反了。类似的经历多了以后,我开始认真思考一件事:数据构建、模型训练、离线测评这条链路,明明每一步都有明确的规则,为什么还需要人坐在中间当“胶水”?
后来我给自己写了一个编排框架,起名叫LegoFlow。名字很直白:像搭乐高积木一样,把数据构建、模型训练、离线测评这些模块拼起来,模块之间的粘合剂换成智能体。它能读目标、拆任务、写代码、跑命令、看输出结果,然后自己决定下一步做什么,把“代码数据构建+训练测评”整条流水线跑完。这篇文章我想把这个项目从头到尾拆开讲清楚,包括为什么这么设计、每个模块怎么实现的、以及跑通之后踩到的那些真实大坑。
1. 从凌晨三点的实验事故到LegoFlow:这套系统到底要解决什么
1.1 手工跑一条“数据构建+训练+测评”链路有多烦
大部分做模型的同学都经历过这个流程:原始数据清洗、构造特征、划分训练集和验证集、写训练脚本、调参、跑测评、导出报告。单看每一步都不复杂,但组合起来就是灾难现场。
- 数据版本没人管。今天改个清洗规则,明天改个标签映射,三天后你根本不知道当前数据集是怎么生成的。
- 预处理逻辑和训练脚本的假设经常悄悄不一致,比如训练时默认输入是去重的,验证时数据里却混进了重复样本。
- 测评口径不统一。准确率、召回率、F1在不同脚本里的实现细节可能有差异,今天高明天低,鬼知道是自己模型进步了还是评估代码改了。
- 所有“根据结果做判断”的动作都落在人身上:loss不降了怎么办,数据分布不对要不要重新来,测试集指标差是调参还是换模型。
我当时的判断是:这些事里,真正需要人类智能的部分其实没那么多。定位问题需要判断力,但“发现问题之后先检查数据再检查代码”这种常规流程,完全可以交给一个带大模型能力的智能体去执行。LegoFlow的初版目的只有一个:让智能体把我从这种重复劳动里解放出来,我只需要告诉它目标,它负责跑完从数据到报告的整条全流程。
1.2 为什么现成的调度框架“差一口气”
提到自动跑流程,很多人第一反应是上调度系统、上工作流引擎,或者用AutoML工具。但实际用了之后会发现它们解决的是不同的问题:
- 传统工作流调度器擅长的是“确定性任务编排”:第一步做什么、第二步做什么都是写死的,任务之间怎么传递参数也是提前定好的。它没法处理“这一步结果异常,下一步该怎么调整”的开放性问题。
- AutoML工具擅长的是超参搜索和模型选择,但它默认你的数据和评测口径是准备好的。数据构建环节它几乎不管。
- 单纯的LLM调用链(比如让大模型写一段代码,你手动去跑)又缺少工程约束,跑挂了不知道原因,跑通了也不知道结果是真是假。
LegoFlow的定位是在这两者之间补一块:保留工作流的骨架,把每个环节的“策略决策”交给智能体,系统负责执行、监控、记录和恢复。你可以把它理解成一个由大模型驾驶、但带全套仪表和安全气囊的流水线。
1.3 LegoFlow到底是什么、不是什么
先澄清一个容易误会的地方:LegoFlow不是一个训练框架,它不管你怎么定义模型结构,也不提供新的训练算法。它做的事情是把“数据构建”“训练”“测评”这些环节用一套协议串起来,然后在每个环节里让智能体去完成具体的、本来需要人来写的代码和判断。
对使用方来说,体验大概是这样的:你提交一个目标,比如“写一个短文本意图分类的数据集,用一个小模型跑通训练和测评,输出一份带指标对比的报告”。LegoFlow会把目标拆成多个子任务,交给对应的Agent去完成。每个Agent可以调用工具:写Python脚本、执行Shell命令、读取上一个环节的产物、把结果写进统一目录。系统的编排层负责协调它们,记忆层负责记录每一步,出了问题能断点恢复。
这套设计如果跑通了,带来的好处是实打实的:我不再需要手动维护五六个脚本之间的隐性约定;每次实验的数据、代码、指标、报告都在一个目录里,可回看、可对比;半夜跑实验再也不用盯着终端等结果。
2. LegoFlow的骨架:编排层、执行层、记忆层怎么分工
2.1 编排层:把“跑完一条全流程”拆成DAG
LegoFlow对用户输入的“目标”不做自由解释,而是先经过一个任务解析模块,把目标拆成固定的一串阶段。内部是按DAG组织的,但大部分情况下是线性链:数据构建 → 数据校验 → 训练准备 → 模型训练 → 测评 → 报告生成。每个阶段对应一个明确的输入输出协议。
为了不让智能体在任务边界上发挥想象力,我给每个阶段定义了严格的输入输出Schema。比如:
- 数据构建阶段的输入是用户任务描述和可用的原始数据路径;输出是一份数据集描述文件,内容包括训练集、验证集、测试集的路径、字段说明、类别列表。
- 训练阶段的输入是数据集描述文件和训练资源配置;输出是模型检查点路径和一份训练日志摘要。
- 测评阶段的输入是模型检查点路径和数据集描述文件;输出是一份标准化的指标JSON。
用JSON Schema约束住每个阶段的输入输出,是我踩了好几次坑之后才坚持下来的设计。一开始我放任Agent自由发挥,让它“自己觉得合适就行”,结果每个阶段的产物格式都不一样。第二步的训练脚本读不到第一步的产物根本是小问题,更麻烦的是报告生成阶段不知道从哪读指标、类别信息和数据统计。所以后来我定了规矩:智能体在阶段内部可以自由,但阶段之间的协议一定锁死。
2.2 执行层:沙箱进程、超时控制、日志回收
每个阶段的实际操作(比如运行Agent生成的Python脚本)并不允许直接在宿主机上裸奔,而是在执行层里跑。我给执行层设计了四个必须项:
- 独立进程执行。每个阶段的任务都在一个新的子进程里运行,环境变量隔离,工作目录独立。
- 资源限制。比如内存上限4GB、最多使用2个CPU核、训练进程如果有GPU需求则限定某一块卡;每个任务还配了超时时间,数据构建脚本超时给30分钟,训练脚本超时给2小时,到点直接杀掉。
- 日志回收与摘要。任务执行过程中,标准输出和标准错误都会追加到产物目录下的日志文件里。Agent判断任务是否成功,不能只看退出码,还得读摘要日志。系统会先自动提取日志里出现的关键行(比如loss、accuracy、error),再交给Agent分析。
- 退出码之外的二次校验。很多脚本退出码是0,但实际产出物根本不对。所以每个任务完成后,系统会先做一个简单的产物存在性检查:该生成的文件在不在、是不是空的、大小是否合理,然后再进入下一步。
这层设计的目的很朴素:让失控的代价足够小。智能体生成的代码再魔幻,也只能在自己的一亩三分地里跑,不影响宿主机,不影响别的任务,超时会被杀掉,产物不完整会被拦截。
2.3 记忆层:所有中间产物落盘,支持断点复跑
跑一个全流程项目最怕什么?跑了一半挂了,之前生成的数据、脚本都没了,得全部从头再来。LegoFlow的记忆层就是为了解决这个问题,原则很简单:每一步的输入和输出都落盘,不要只存在内存里。
项目的目录结构大致长这样:
experiments/exp_001/ ├── task.json # 用户提交的目标,原始描述 ├── dag.json # 拆解后的任务图,记录每个阶段状态 ├── stages/ │ ├── 01_data_build/ │ │ ├── input.json │ │ ├── output.json │ │ ├── scripts/ # Agent 生成的代码和模板代码 │ │ ├── data/ # 生成的数据文件 │ │ └── run.log # 执行日志 │ ├── 02_data_validate/ │ │ ├── output.json │ │ └── report.md │ ├── 03_train/ │ │ ├── script.py │ │ ├── metrics.json │ │ └── checkpoint/ │ └── 04_eval/ │ └── results.json └── final_report.md # 最终报告这个目录结构带来了两个很实际的好处。第一,断点复跑变得简单:编排层启动时读一遍dag.json,哪些阶段状态是completed就直接跳过,从失败阶段继续跑。第二,可回溯性极强:每次实验跑了什么代码、喂了什么数据、出了什么指标,全在同一个实验目录里,对比两个实验的时候只需要diff两个目录。
3. 数据构建Agent:让大模型写生成脚本,而不是让大模型直接吐数据
3.1 最初的教训:直接生成数据会导致什么
做第一版的时候,我的想法最直接:要让智能体“构建数据”,那就让它直接生成数据文件好了——用大模型写一批样本文本,输出成JSON或CSV。但在实际测试中,这个方案迅速败下阵来:
- 大模型直接吐数据,输出数量一多质量就崩。生成几百条还行,到几千条时明显开始重复、模板化,甚至出现前后矛盾。
- 不可审计。这些数据是怎么来的,洗过没有,凭什么说它是干净的?你没法从最终文件里看到生成逻辑。
- 不可增量。如果用户说“我想再加一万条”,直接生成方案基本等于重来。
后来我把思路换了一下:让大模型写一个数据生成脚本,然后由系统来执行这个脚本。大模型的产出物从“数据”变成了“代码”,而代码是可审查、可修改、可复用的。同样一批原始样本,想让数据量翻倍,改个循环的轮数再跑一遍就行;想换清洗规则,改脚本里的一个函数就行。这个转变是整个数据构建Agent成立的关键。
3.2 Schema先行:数据契约怎么定
在让Agent写脚本之前,系统会先要求Agent定义一个数据Schema。这个概念可以类比成数据库建表:先规定好每一列叫什么、是什么类型、取值范围是多少,后面写代码、做校验才不会乱。
举个例子,如果任务是构建一个“短文本意图分类”数据集,Schema大概会长这样:
{ "fields": [ {"name": "text", "type": "string", "description": "用户输入的自然语言文本", "min_length": 2}, {"name": "label", "type": "string", "description": "意图类别", "enum": ["weather_query", "alarm_set", "joke_request", "other"]} ], "dataset_sizes": {"train": 8000, "valid": 1000, "test": 1000}, "distribution": {"weather_query": 0.3, "alarm_set": 0.2, "joke_request": 0.2, "other": 0.3}, "dedup": true, "split": {"strategy": "stratified_by_label", "seed": 42} }这个Schema是Agent和系统之间的契约。Agent后续生成的脚本必须按照这个Schema来,系统后续的校验器也按这个Schema来查。我在项目里把这个Schema叫schema.json,每个阶段都可以引用上一阶段的Schema来做一致性检查。
3.3 生成器脚本模板:Agent只需要填关键逻辑
拿到了Schema之后,Agent下一步是写生成器代码。但这里有一个工程化的关键决策:不是让Agent从零写一个完整的生成脚本,而是用它去填写一个半成品的脚本模板。
为什么这么设计?原因有两个:
- 全量生成代码,Agent很容易写出“看着很合理但根本跑不通”的代码,比如导入一个不存在的库、用错API签名。
- 模板把那些不变量固定住了,比如数据的拆分逻辑、文件输出的目录、日志格式、进度条。Agent只需要贡献它的核心价值:怎么把原始输入变成符合Schema的样本。
我在实际项目里用的模板大致是这样:
import json import random from pathlib import Path SEED = 42 DATA_DIR = Path(__file__).parent / "data" random.seed(SEED) # TODO(agent): 在这里写生成函数 # 要求: # - 输出为 list[dict],每个 dict 包含 schema.json 里定义的所有字段 # - 保证多样性,避免大量重复句式 def generate_samples(total_count: int) -> list[dict]: # agent 填充 raise NotImplementedError def main(): samples = generate_samples(10000) # 以下逻辑由系统固定,Agent 不需要改 ... if __name__ == "__main__": main()Agent要做的就是替掉generate_samples函数的实现,并且command一下告诉执行层“你可以跑了”。这种“挖空-填空”的方式极大减少了生成代码的运行风险。在我后来的测试里,模板化方案的首次运行成功率比从零生成高了非常多。
3.4 脚本执行前的三层检查
让Agent生成完代码,不能直接撒手就跑。执行层在运行脚本之前会强制过三道检查,任何一道不过都会把脚本打回让Agent修改:
- 语法编译检查。用
py_compile做一次编译,语法错误当场暴露。这一步能拦下一大批低级的缩进、括号错误。 - 依赖白名单检查。程序里import的库必须在一个预设的白名单内,比如
json、random、re、pathlib这些标准库,以及少量数据处理库。白名单之外的import直接拒绝,避免Agent脚本在沙箱里尝试装各种不明来路的东西。 - AST约束检查。这一步更细一些,会解析代码的语法树,禁止一些危险或不需要的模式,比如
eval、exec、网络请求、对DATA_DIR之外路径的写操作。
这套检查听起来机械,但实际拦截过的问题让我非常庆幸加了它。有一次Agent生成的脚本因为“感觉用requests拉点公开数据会更准”,真的生成了一个带网络下载的清洗函数。如果没有白名单检查,它就会在沙箱里发请求,不仅慢,而且行为不可控。
3.5 数据质检Agent:把质检也变成自动判断
脚本跑通、数据文件生成之后,还不能直接拿去训练,需要数据质检Agent做一轮自动验收。质检逻辑一部分是规则,一部分是大模型判断:
- 规则类:检查每列是否满足Schema约束(类型、长度、枚举值范围);检查空值比例;检查重复文本比例;检查类别分布和Schema声明的是否接近。
- 大模型类:从训练集随机抽几条样本,让大模型判断“这条样本的标签和文本内容是否匹配”,从抽样层面排查标签错乱。
如果质检不通过,质检Agent会带着“质检报告”回到数据构建Agent那边,让它针对具体问题去修。比如“重复率超过3%,请调整模板库或增加去重逻辑”“类别分布偏离,请调整采样比例”。修完再跑、再查,这个循环会一直持续到质检通过或达到最大重试次数。
关于数据泄露,我还要单独说一句:train、valid、test三个集合在数据构建时就必须保证行级去重的不只是“文本完全相同”的情况,类似文本经过小改动也算疑似泄露。这一步如果偷懒不做,后面训练评测指标都会失真。我在质检规则里专门加了一条按文本的哈希相似度做筛查的逻辑,实际操作中这个规则救了我好几次。
4. 训练与测评Agent:受控执行、指标回传、失败恢复三板斧
4.1 训练Agent怎么读数据集、怎么定任务类型
数据构建阶段跑完,产物目录里有一份dataset.json,里面写清楚了数据路径、字段信息、类别列表。训练Agent的第一步不是写模型,而是先读这个文件,并把任务类型识别出来:是文本分类、序列标注,还是回归任务?然后它会据此决定模型骨架。
为了让动作可控,训练Agent可以选择的模型也走了一个较小的白名单:根据输入数据的模态自动映射到几类基础模型模板。比如文本分类任务默认用某种轻量的预训练语言模型做微调,表格数据默认用梯度提升树。选完模型之后,Agent不是自己去设计模型结构代码,而是去选择并填充一个训练模板。
我在这个环节有个比较深的心得:训练阶段最怕的不是“模型不好”,而是“整体不可复现”。所以LegoFlow对训练期间的每一处核心设置都做了显式记录:选了哪个模型模板、数据词表大小、batch size、学习率、训练轮数、随机种子。这些信息最后会汇总到train_config.json里,训练完以后任何时候都能按这个配置重新跑一遍。
4.2 训练脚本模板:挖空填空题避免自由发挥
训练脚本同样使用“挖空-填空”的方案。模板里已经写好了数据加载、优化器初始化、训练循环、日志记录、检查点保存这些基础设施,Agent需要填的核心是:模型选择、超参、以及数据预处理那一小段。
from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch # ===== Agent 需填写的参数 ===== MODEL_NAME = "pretrained-lm-base" # 模型名称 BATCH_SIZE = 16 LEARNING_RATE = 2.5e-5 EPOCHS = 3 MAX_SEQ_LEN = 64 # ===== 数据预处理(Agent 修改) ===== def preprocess_dataset(dataset): # 这里由 Agent 编写具体的预处理逻辑 ... # ===== 以下训练循环为模板固定内容 ===== ...这个模板的最大价值是降低了失败面。一个训练脚本里最容易出错的部分不是训练循环本身,而是各种边界情况:数据列名对不上、标签映射出错、设备不一致、学习率过大导致NaN。把固定部分锁死之后,Agent犯错的范围大大收窄了。
当然,超参部分虽然是Agent填的,但系统也设置了安全边界。比如BATCH_SIZE不能超过一个上限,学习率不能大于某个阈值,否则执行层直接拒收并让Agent重新填。这既是保护显存,也是防止大模型凭“感觉”乱给一个破坏性超参。
4.3 训练监控:轮询日志、NaN检测、提前止损
训练一旦跑起来,LegoFlow做了两件事:一是轮询日志,二是异常止损。
训练过程中,模板代码会定期向train.log写入当前步数的loss、准确率、当前学习率。监控模块每10秒读一次日志文件,把最新几行提取出来。然后,训练Agent根据这些信息做判断:
- 如果loss出现NaN或剧烈波动,监控模块会直接终止训练,并标记为“训练异常终止”,同时保留终止前的训练日志供排查。
- 如果连续N个评估步数loss基本不降,Agent可以决定终止训练、修改超参再重启,而不是傻等完整几个epoch跑完。
- 如果训练正常结束,监控模块把最后几轮的关键指标整理成摘要,交给后续Agent做判断。
这部分的实现不算难,但收益极大。有一次Agent选了过大的学习率,第一轮loss直接翻到无穷大,如果没有NaN自动检测,整个流程会白白跑上几十分钟才发现废了。现在它在几秒钟内就能杀掉进程,然后把这个失败信息登记在训练任务记录里。
4.4 测评闭环与失败分类恢复
训练完成后进入测评阶段,测评Agent加载训练好的检查点,在测试集上算指标,并把结果写入results.json。这个环节看起来简单,但真正的难点是:测评挂了之后怎么恢复。
不同失败原因对应的恢复策略完全不一样。我把它们做成了一张表,LegoFlow的编排层会查表决定动作:
| 失败类型 | 典型表现 | 自动恢复策略 |
|---|---|---|
| 资源不足 | 进程退出码非零,日志里有OOM或CUDA out of memory | 减小batch size,或换成CPU训练,重试一次 |
| 依赖/导入错误 | 日志里出现ModuleNotFoundError、AttributeError | 把报错信息回传给Agent,让它修改脚本后重跑 |
| 数据问题 | 数据加载时报shape不匹配、标签缺失 | 触发数据质检Agent重新检查并修复数据集 |
| 模型不收敛 | 训练正常结束但评测指标远低于基线 | 调整学习率或训练轮数,重新训练,最多重试两次 |
| 代码逻辑错误 | 日志出现逻辑异常、断言失败 | 让Agent读日志修复代码逻辑,最多重试三次 |
重试不是无脑的。每一次重试都会消耗时间和算力,所以我设定了总重试上限,比如训练和测评整体最多重跑三轮,超过之后直接停到失败状态,并把完整的日志和错误摘要整理成报告,等人来接管。
整个测评闭环还有一步很关键:生成一份可读的报告。在跑测评的同时,报告Agent会汇总整个实验链路的关键信息——数据规模、数据统计、模型配置、训练曲线摘要、测试集指标、与基线的对比。报告直接输出成Markdown,放到实验目录的final_report.md。到这一步,一个完整的“数据构建+训练+测评”流程才算真正闭环。
5. 跑通全流程后的复盘:哪些环节最值得自动化,哪些坑必须躲
5.1 自动化收益排序:数据构建第一,测评第二,训练最后
LegoFlow跑通之后,我把做过的实验按“自动化收益”排了个序,发现和自己最初的直觉并不完全一样:
- 收益最大的是数据构建环节。以前换一个数据形态就要重写一整套清洗生成脚本,现在智能体可以快速生成脚本、执行、自检、修复。更重要的是,数据Schema和质检逻辑一旦定下来,整个团队在数据口径上就对齐了。
- 第二是测评环节。固定测评脚本、统一指标计算方式、自动生成对比报告,省掉的不只是时间,还有“不同实验之间根本没法比”的信任问题。
- 训练环节反而是收益相对小的。因为大部分稳定的实验,训练脚本本来就差不多,超参搜索这种事AutoML工具做得更好。LegoFlow在训练环节最大的价值其实是“受控执行和失败恢复”,而不是“让Agent发明新结构”。
这个排序直接影响了我后来怎么用LegoFlow:数据构建和测评环节我放得很开,让智能体多自由发挥;训练环节我收敛得比较紧,模板和边界条件卡得死死的。
5.2 我踩过的三个坑,每个都值得写三千字
第一个坑是“用户信任崩塌”。初版系统跑通了一个简单任务后,我兴致勃勃拿它做正经实验。结果那天评测报告里写“F1 92.4%”,我觉得高得可疑,一查发现是数据构建阶段Agent生成的代码里,用了一些未来信息做特征。虽然流程每个环节都“成功”了,但结果是错的。从那以后我给自己立了条铁律:数据生成逻辑里的特征构造代码,人工抽查比例不能为零。智能体再聪明,你也要知道它的代码在干什么。
第二个坑是“无限循环”。Agent在判断“数据是否需要修复”时,如果质检规则模棱两可,它会一遍遍修改然后重试,陷入一个无人喊停的循环。一开始我以为加上时间限制就够了,实际发现更有效的是设置一个动作预算:每个Agent在一个阶段内,最多执行多少次“修改—重跑”的循环,超过就降级成“转人工”。系统可以自动化,但不能没有跳闸机制。
第三个坑是“token消耗失控”。让Agent读完整日志、看完整数据集再决策,会让token消耗飞速膨胀。后来我做了个动作:系统在把日志内容喂给Agent之前,会先用规则做压缩,只保留错误行、指标行、关键警告。这一步没有让成功率变差,反而因为上下文更聚焦而变好了,token成本却降到了原来的五分之一左右。
5.3 给想自己搭一套的人几条实在建议
如果你也想搭类似的系统,我给几条不掺水分的建议:
- 先把Schema定好再谈自动化。没有清晰的数据契约,智能体生成再多的代码都是无根之木。建议至少对“数据描述”“阶段产物”“指标格式”三个层面做Schema约束。
- 用模板限制Agent,而不是放任自由发挥。模板不会让系统变蠢,反而能把Agent的创造力集中在真正需要判断的地方。
- 给所有阶段加超时、加资源限制、加产物完整性检查。这些工程细节决定系统在真实场景里可不可用,而不是只能跑演示。
- 从最小的子集跑通全流程,再放开规模。第一次验证LegoFlow我只用了几十条样本跑通了全流程,确认每个环节的协议都没问题了,才开始支撑真实任务。这跟写程序先在单测里验证逻辑一个道理。
最后再说一个个人体会。很多人在尝试用智能体做事时,把注意力全放在“让模型变得更聪明”上,但LegoFlow这个项目让我意识到,成熟的编排、受控的执行环境、可回滚的状态记忆,往往比模型聪明一点更重要。智能体是流水线上那个负责随机应变的工人,但流水线本身的轨道、卡口和安全阀,必须由工程系统来保证。
这套系统现在还在我的工具箱里持续迭代。最近我在给它加“实验对比”能力:把两个实验目录的指标、配置、数据分布自动diff出来,让选择不再靠感觉。希望这篇复盘对你搭自己的智能体流程有启发——哪怕只是帮你少踩一个我踩过的那种凌晨三点的坑,也值了。