
先说一个我印象很深的场景。某次内部评审一位同事投屏展示了一套AI 直接生成的建模方案——特征清单、模型选型、参数配置、预期指标一页页 PPT 看着挺唬人逻辑也顺。结果有人问了一句:这套东西在哪跑过数据换一版会怎样现场安静了十几秒。因为那套方案从生成到展示中间没有任何一步是能被执行、被复现、被证伪的。它本质上是一段写得很好看的文字而不是一个能用的东西。这件事之后我基本把AI 建模这件事的用法固定下来了:让 AI 写代码别让它直接吐模型。这里的吐模型包含三种形态——直接告诉你该用什么模型和参数、直接给你一份预测结果、直接描述一个模型结构。这三种做法看起来效率很高但它们有一个共同缺陷产出物无法被机器验证。而代码不一样代码跑一遍就知道对不对。这篇内容适合两类人看:一类是已经在用 AI 辅助建模、但总觉得结果玄学、不敢往生产上推的人另一类是还没开始用、想知道怎么把 AI 真正嵌进建模流程而不是当搜索引擎用的人。下面我会把为什么换思路契约怎么定提示词怎么写代码怎么验收坑在哪这几件事拆开讲中间穿插可以直接抄的目录结构、脚本骨架和提示词模板。1. 为什么AI 直接给模型这条路越走越窄1.1 三个典型翻车现场我见过太多类似的情况归纳下来大概三类。第一类是参数幻觉。你问 AI 某个梯度提升模型在你这批数据上学习率该设多少它会给你一个非常自信的数字还附带一段解释说这个值在类似场景下表现良好。问题是它并不知道你数据的量级、噪声水平、特征维度、正负样本比例。它给出的是一个在统计上看起来合理的数而不是一个在你数据上验证过的数。你照着填进去跑出来效果一般你甚至会怀疑是自己数据不行而不是这个数字本来就是拍脑袋的。第二类是指标幻觉。AI 直接告诉你这个方案准确率能达到 0.92。这个数字是哪来的没有来源。它可能是从训练语料里某篇博客抄来的可能是根据任务难度推测的也可能纯粹是语言模型在补全一个看起来合理的句子。最麻烦的是这种数字极具误导性——它会让你对方案产生不该有的期待从而跳过本该做的基线对照。第三类是结构幻觉。让 AI 直接描述一个模型结构它经常给你一个层级清晰、术语正确的架构图式描述但你真去实现的时候会发现:某一层的输入输出维度对不上、某个模块在主流框架里根本没有现成实现、某个注意力机制的拼接方式在数学上根本不成立。这些错误在纯文本层面是看不出来的只有在写代码、跑张量、看 shape 的时候才会暴露。这三类问题的共性在于:产出物是不可执行的所以错误没有暴露的时机。文字可以自洽地错下去代码不行。1.2 不可验证的产出等于没有产出建模这件事的本质不是想出一个方案而是在一个具体的数据分布上找到一个泛化误差可接受的函数。这句话里有两个关键词:具体的数据分布、可接受的误差。这两者都只能用实验来回答。所以一个建模产出物要真正有价值它必须满足三个条件:可执行:能在你的环境里跑起来而不是只能在脑子里跑。可重复:同样的输入跑两次结果应该一致(在允许的随机范围内)。可拆解:出问题时能定位到具体是哪一步、哪个特征、哪个超参导致的。一份模型方案描述三条全不满足。一段建模脚本三条全部满足——只要你写对了。我经常用一个类比:让 AI 直接给模型就像让一个没去过你家的司机凭记忆画一张路线图让 AI 写代码就像让他写一份导航指令你可以立刻照着开一遍开错了当场就知道错在第几个路口。后者的纠错成本低一个数量级。1.3 代码是唯一能把意图翻译成事实的中间产物还有一个更底层的原因。AI 擅长的是生成符合语言习惯的文本而建模需要的是符合数据规律的逻辑。这两者之间有一道鸿沟。代码恰好是跨越这道鸿沟的桥——它一半是语言(可以生成)一半是数学(必须验证)。当你把需求表达成写一个脚本读这个 CSV做这些处理训这个模型输出这些指标AI 生成的每一行代码都对应一个可检验的动作。你可以逐行看:这个fit是不是在切分之后调的这个transform是不是只在训练集上拟合的这个指标计算是不是漏了某类样本每一个问号都有确定的答案。而当你把需求表达成给我一个建模方案你得到的是十几个模糊的断言每个断言都需要你再去追问、再去验证验证成本反而更高。这就是我坚持让 AI 写代码的核心逻辑:不是为了让它干活而是为了让它的错误可见。2. 把写代码这件事拆成可交付的契约2.1 数据契约:先把输入长什么样钉死AI 写的建模代码之所以经常跑不通八成原因不在算法而在它根本不知道你的数据长什么样。你不说它就按最常见的假设来:数值列连续、类别列是字符串、没有缺失、时间列是标准格式。你的真实数据但凡有一项不符合脚本就崩或者悄悄算错。所以第一步不是写提示词是写数据契约。我通常会给 AI 一份这样的东西:# data_contract.yaml dataset: 模拟项目X 用户行为表 row_count_approx: 1200000 primary_key: user_id time_column: name: event_date type: date granularity: day range: 2023-01-01 ~ 2024-06-30 columns: - name: user_id dtype: string null_rate: 0.0 note: 唯一标识不要参与特征 - name: age dtype: int null_rate: 0.18 range: 14 ~ 92 note: 存在明显异常值100 视为脏数据 - name: city_tier dtype: category null_rate: 0.03 values: [一线, 二线, 三线, 其他, 未知] - name: gmv_30d dtype: float null_rate: 0.0 distribution: 长尾中位数约 120P99 约 3800 - name: label dtype: int values: [0, 1] positive_rate: 0.061这份契约看着啰嗦但它解决的是 AI 建模里最耗时间的一类返工:生成的代码跑不起来你还得逐行改数据类型和缺失处理。有了它AI 生成的read_csv后面会自然带上dtype映射、缺失填充策略、异常值截断逻辑而且是按你数据的真实情况来的。我踩过的坑是:早期我只给列名不给分布AI 给gmv_30d做了标准化结果因为长尾太重标准化之后 99% 的值挤在 -0.3 附近模型几乎学不到东西。后来加上长尾这个描述它会主动建议先做对数变换。你对数据的描述越具体AI 的默认假设就越接近现实。2.2 接口契约:所有建模脚本必须长同一个骨架这是我认为最有价值的一条经验。如果你每次让 AI 写脚本都从零开始你会得到十个风格迥异、互不兼容的文件维护成本高到爆炸。解决办法是先定死一个骨架之后所有生成都往骨架里填。我的骨架长这样:project/ ├── config/ │ └── base.yaml # 所有可调参数代码里不出现魔法数字 ├── data/ │ ├── raw/ # 只读永不修改 │ └── processed/ ├── src/ │ ├── io_utils.py # 读写、路径解析 │ ├── features.py # 特征工程纯函数无副作用 │ ├── split.py # 数据切分独立成文件 │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估入口 │ └── predict.py # 推理入口 ├── tests/ │ └── test_smoke.py # 冒烟测试 └── runs/ └── {timestamp}_{tag}/ # 每次实验一个目录存配置、日志、指标、模型这个骨架的价值在于两点。一是切分和训练物理隔离。split.py单独一个文件意味着你可以在代码审查时一眼看出切分逻辑对不对而不是在一千行训练脚本里找train_test_split在哪一行。数据泄漏这个坑八成都出在切分和特征工程的顺序上把这个顺序用文件结构固定下来能挡掉一大半问题。二是配置外置。所有超参、路径、阈值全部走config/base.yaml代码里不写死。这样做的好处是 AI 生成代码时不会把n_estimators100这种数字硬编码进去你调参时只需要改配置不需要重新生成代码。给 AI 的提示词里我会直接把这个结构贴上去然后加一句:严格按此结构生成不要新增顶层目录不要在函数内部硬编码路径和超参。2.3 评估契约:指标口径必须你来定这是最容易被忽略、后果最严重的一环。如果你不指定AI 会默认用准确率而准确率在类别不平衡场景下几乎毫无意义。我见过一个正样本占比 6% 的任务模型把所有样本都预测成负类准确率 94%看起来漂亮得不行。所以我一定会在提示词里写死评估部分:切分方式:时间序列任务用时间切分(按event_date取最后 20% 做验证)不允许随机切分。主指标:AUC 和 KS因为业务方要看排序能力。辅助指标:在指定阈值下的精确率、召回率、F1。基线:必须同时训练一个全预测负类的 dummy 和一个逻辑回归作为下限和参照。分组评估:按city_tier分组输出指标看模型在不同群体上是否稳定。这五条写进去之后AI 生成的evaluate.py会自带这些逻辑。更重要的是它没法再编造一个漂亮的数字了因为指标是脚本算出来的你跑一遍就知道真实水平。提示:评估契约里一定要写输出到文件而不是打印到控制台。前者可以留档对比后者看完就没了。3. 提示词怎么写AI 才会交付能跑的脚本3.1 别问帮我建个模型要给完整上下文包我总结了一个四段式提示词结构实测下来生成质量比随口问高出一大截:背景与目标:这是什么任务预测什么用来干什么。数据契约:上一节那份 YAML直接贴进去。接口契约:目录结构 必须遵守的约束。验收标准:什么样的代码算合格。举个具体的例子我实际用的一段提示词大概是这样:背景:二分类任务预测用户在 30 天内是否会复购。样本量约 120 万 正样本率 6.1%。要求模型可解释性优先于极限精度。 数据:见下方 data_contract.yaml略。 约束: - 严格按 project/ 目录结构生成只生成 src/ 下四个文件 - 所有超参从 config/base.yaml 读禁止硬编码 - 特征工程只允许用训练集拟合的统计量处理后必须能 transform 测试集 - 随机种子统一从 config 读默认 42 - 不要引入除 pandas、numpy、scikit-learn 之外的依赖 验收: - 每个文件顶部有函数级 docstring - features.py 中每个特征函数是纯函数输入输出都是 DataFrame - train.py 执行完后runs/ 下必须产生 config 快照、metrics.json、模型文件这套说法看起来限制很多但它把 AI 的自由度压缩到了写正确的代码这一个维度上反而更容易出好东西。约束越具体生成越稳定这是我用下来最反直觉但最确定的一条。3.2 分阶段生成:先骨架后血肉一次性让 AI 生成整套代码出错概率很高而且出错之后你不知道该改哪。我的做法是分三到四轮:第一轮:只生成骨架和函数签名。每个函数体先写raise NotImplementedError。这一轮的目的是确认接口设计对不对函数职责划分合不合理。这一轮几乎不会有 bug因为你审的是设计。第二轮:填特征工程。单独针对features.py生成这时候可以给更细的数据描述。特征工程是建模里最需要懂业务的部分值得单独一轮。第三轮:填训练和评估。这部分逻辑相对标准化AI 出错的概率较低但仍然要检查切分顺序和指标计算。第四轮:补测试和日志。让 AI 给tests/test_smoke.py写一个用 100 行假数据跑通全流程的测试。分阶段的好处是每一轮的产出都是可验证的。骨架能 import 成功特征函数能对小 DataFrame 跑出预期列训练能在一百行数据上跑完。任何一轮出问题你只需要回退这一轮不用推翻全部。我早期的做法是一次性生成结果经常是跑不通但看不出哪不通然后在两千行代码里大海捞针。分阶段之后调试时间至少省了三分之二。3.3 让 AI 自己写自检断言这一招我觉得特别值。在提示词里加一句:在每个关键步骤后加入 assert 语句验证数据形状、类型和取值范围符合预期。生成出来的代码大概是这样的:import pandas as pd def build_features(df: pd.DataFrame, stats: dict) - pd.DataFrame: 基于训练集统计量构造特征不修改入参。 out df.copy() out[gmv_log] np.log1p(out[gmv_30d].clip(lower0)) out[age_filled] out[age].fillna(stats[age_median]) out[age_clipped] out[age_filled].clip(14, 90) # 自检:特征不能引入 NaN取值范围要在训练分布覆盖区间内 assert out[gmv_log].isna().sum() 0, gmv_log 出现 NaN assert out[age_clipped].between(14, 90).all(), age 截断失效 assert len(out) len(df), 特征构造改变了行数 return out这些 assert 平时是多余代码但在 AI 生成场景下它们是廉价的正确性保险。因为 AI 最容易犯的错不是逻辑错而是忘了处理边界情况assert 能在第一时间把它顶出来而不是等到模型指标异常时你再去回溯。更狠一点可以让它写数据契约校验函数:读入数据后立刻检查列是否存在、类型是否匹配、缺失率是否超过阈值、标签是否只有 0 和 1。这道关卡设在流程最前面能挡掉后面 90% 的莫名其妙的报错。4. 生成代码的验收链路:从能跑到可信4.1 第一层:静态检查与依赖对齐代码生成出来先别急着跑。第一件事是静态检查。# 语法与未定义变量 python -m pyflakes src/ # 风格与潜在问题 python -m ruff check src/ # 依赖是否都能装上 pip install -r requirements.txt --dry-run别小看这一步。AI 生成的代码经常出现:引用了没 import 的库、用了已经废弃的 API、变量名拼错但语法合法、在 f-string 里用了同名变量导致输出永远是字面量。这些问题在运行时可能不报错但结果是错的属于典型的静默失败。依赖对齐也值得单独说。AI 的训练语料有滞后性它可能给你写from sklearn.experimental import enable_hist_gradient_boosting这种老写法或者用某个库已经改名的参数。我在项目里会固定一个requirements.txt把版本号写死然后在提示词里贴上去让它只使用这些版本支持的 API。注意:如果你的核心库是 1.x 版本一定要在提示词里明确写出。我吃过一次亏AI 按新版本的参数名生成代码结果在我本地老版本上直接 TypeError改了六处才跑通。4.2 第二层:小样本冒烟测试静态检查过了第二步是拿一百行假数据跑全流程。这一步的目只有一个:确认管道是通的。# tests/test_smoke.py import numpy as np import pandas as pd from src.features import build_features from src.split import time_split from src.train import train_model def test_full_pipeline_runs(): rng np.random.default_rng(42) n 200 df pd.DataFrame({ user_id: [fu{i} for i in range(n)], event_date: pd.date_range(2024-01-01, periodsn, freqD), age: rng.integers(18, 70, n).astype(float), city_tier: rng.choice([一线, 二线, 三线], n), gmv_30d: rng.lognormal(4.5, 1.0, n), label: rng.integers(0, 2, n), }) df.loc[df.index[:10], age] np.nan train_df, valid_df time_split(df, ratio0.2) assert len(train_df) len(valid_df) n assert train_df[event_date].max() valid_df[event_date].min() stats {age_median: float(train_df[age].median())} tr build_features(train_df, stats) va build_features(valid_df, stats) assert list(tr.columns) list(va.columns) model, metrics train_model(tr, va, config{seed: 42}) assert auc in metrics assert 0.0 metrics[auc] 1.0这个测试最关键的一句是assert train_df[event_date].max() valid_df[event_date].min()。它验证的是时间切分没有重叠——这是数据泄漏最常见的入口。如果 AI 把切分写成了df.sample(frac0.8)这一行会直接失败你在跑真实数据之前就知道出问题了。另一句重要的断言是list(tr.columns) list(va.columns)它验证训练集和验证集经过特征工程后列顺序一致。这个坑很隐蔽:如果某个类别特征在验证集里没出现某个取值One-Hot 之后列数会不一样模型预测时直接崩。4.3 第三层:基线对照与消融代码跑通了不代表模型有用。第三步是拿基线比对。我固定跑三个东西:模型用途预期表现Dummy(全预测负类/正类)下限参照AUC 约 0.5逻辑回归(单特征 gmv_30d)单变量参照AUC 约 0.62逻辑回归(全特征)线性参照AUC 约 0.71AI 生成的梯度提升方案主方案需显著优于 0.71这张表的意义在于:如果主方案 AUC 只有 0.70那说明这一堆特征工程和调参基本白做不如用最简单的逻辑回归。这种情况我遇到过不止一次而且往往是 AI 生成的复杂方案还不如基线——因为它在特征工程阶段引入了噪声或者调参方向错了。消融实验也很重要。让 AI 顺手生成一个按特征组逐个移除、观察指标变化的脚本。跑完之后你会得到类似移除gmv_log后 AUC 掉 0.04移除city_tier掉 0.005的结论。前者说明这个特征很关键后者说明这个特征基本没用可以从管线里删掉降低维护成本和过拟合风险。4.4 第四层:可复现性复跑最后一层也是最容易被跳过的一层:把同样的配置再跑一遍看指标是否一致。如果两次结果差了几个千分点说明有随机性没被固定。常见来源包括:随机种子没传给模型、train_test_split没设random_state、并行训练的线程数不同导致浮点累加顺序不同、数据读取时read_csv的顺序不稳定。# config/base.yaml 里的固定项 seed: 42 n_jobs: 4 deterministic: truePython 生态里要完全确定得做几件事:random.seed(42)、np.random.seed(42)、模型构造函数里传random_state42、环境变量PYTHONHASHSEED0。少一样都可能出问题。复跑一致之后还有一个动作:换一份数据(比如把时间范围往前推三个月)再跑一遍。这一步检验的是代码对数据分布的鲁棒性。我见过 AI 生成的代码里写死了某个类别的取值列表换一批数据就直接 KeyError。这种问题只有换数据才会暴露。5. AI 写代码时最容易埋的六个坑5.1 数据泄漏:最隐蔽也最致命这个坑排第一没有争议。典型表现是:先对整个数据集做标准化/填充/编码然后再切分训练集和验证集。这样验证集的统计信息泄漏进了训练过程指标虚高上线后一落千丈。AI 之所以经常这么写是因为教程里的示例代码为了简洁通常就是先处理再切分。它学到了这个模式。你要在提示词里明确纠正:所有依赖数据统计量的变换必须在训练集上 fit然后 transform 验证集。禁止对全量数据做 fit。更隐蔽的一种泄漏是时间泄漏。比如用全时间段的用户平均消费额作为特征去预测某个时间点的行为。这个特征在训练时看着很强实际预测时根本拿不到。检查方法很简单:问自己这个特征在预测时刻真的已知吗不知道的一律砍掉。5.2 随机性失控AI 生成的代码里种子经常只设了一处其他地方漏掉。比如设了np.random.seed但模型用的是random_stateNone。或者交叉验证的cv参数用了KFold(shuffleTrue)没设种子。我的做法是把种子做成一个贯穿全局的配置项在train.py入口处统一设置并且在提示词里要求所有涉及随机的函数都必须显式传入 seed。5.3 库版本与 API 漂移前面提过一次这里补充一个具体例子。某些库的参数名在不同大版本之间改过AI 可能混用新旧写法。还有一种情况是它用了某个实验性模块你的环境里根本没启用。应对办法是在提示词里贴 requirements并要求它不要使用标注为 experimental 的模块。另外跑之前先pip check一遍看依赖冲突。5.4 指标口径与业务目标错位AI 会默认优化它认为标准的指标。你说是分类任务它就优化准确率你说是回归它就优化 MSE。但业务上你可能关心的是在召回率不低于 40% 的前提下最大化精确率这种带约束的目标它不会主动想到。所以评估契约里必须写清楚决策场景:阈值怎么定、误判的两种代价是否对称、最终要交付的是分数还是二值标签。这几点定了指标才有意义。5.5 过度工程化AI 有个倾向:你让它写个特征工程它给你搭一套带注册器、插件机制、抽象基类的框架两百行代码里有一百五十行是基础设施。看起来很专业实际上你只是想算五个特征。我的应对话术是:优先选择最短实现除非有必要不引入抽象层。单个文件不超过 200 行。这条约束加上之后代码可读性明显提升。5.6 编码与路径的隐性依赖中文列名、Windows 路径分隔符、相对路径依赖当前工作目录——这三样凑一起代码在你机器上跑得好好的换个人就崩。AI 生成时通常不会主动处理这些。统一处理方式:所有路径用pathlib.Path拼接读文件时显式encodingutf-8入口脚本开头把工作目录固定到项目根。这三行代码能省掉大量在我这能跑的扯皮。6. 把单次生成升级成常态化建模流水线6.1 目录与实验命名:让每次生成都可追溯单次让 AI 写代码价值有限。真正的价值在于把它变成一条可重复的流水线:每次有新想法让 AI 在既有骨架上生成变体跑完自动落盘然后横向对比。runs/目录的命名我固定成{YYYYMMDD_HHMM}_{tag}每个目录里至少包含四样东西:runs/20240612_1030_lgbm_v3/ ├── config.yaml # 本次运行的完整配置快照 ├── metrics.json # 所有指标含分组指标 ├── feature_list.txt # 本次使用的特征清单 └── model.pkl # 模型文件配置快照这一项千万别省。我吃过亏:两次实验指标差了 0.03查了半天以为是特征改动导致的最后发现是其中一次的配置文件被顺手改了没记录。有了快照任何两次实验的差异都可以精确 diff 出来。6.2 用版本管理锁住每次生成的差异AI 生成的代码一定要进版本管理而且每次生成单独一个提交。这样你能清楚地看到这次相比上次改了什么。如果指标变差直接git diff就能定位到是哪个改动引入的。我的习惯是提交信息写清楚三件事:改了什么、为什么改、预期影响。比如将 gmv 特征改为对数变换预期改善长尾分布下的模型稳定性。这条记录在三个月后回看时价值极高。6.3 让 AI 做第二双眼睛生成代码只是第一层用法。第二层用法是让它审查你自己写的代码。提示词可以这样组织:以下是我写的特征工程代码请从四个角度审查: 1. 是否存在数据泄漏全量 fit、时间穿越 2. 是否存在训练/验证不一致的处理 3. 边界情况空值、除零、类别缺失是否覆盖 4. 是否有更简洁的等价写法 只指出问题不要重写。这个用法的价值在于AI 在发现模式化错误这件事上确实比人敏感。人写代码时会陷入自己的思路看不出自己漏了什么AI 没有这个包袱它会老老实实把每一处可疑的地方列出来。你不需要它全对只要它列出的十条里有两三条是真问题就已经赚了。我实际用下来它抓得最准的是全量 fit和除零这两类抓得最不准的是业务语义层面的问题——比如某个特征在业务上其实不可用。所以审查结果要你自己再过一遍不能照单全收。6.4 一个我常用的组合拳最后分享一个我最近用得比较顺的循环大概长这样:有新想法写一段自然语言描述附上数据契约和骨架约束让 AI 生成features_v2.py。跑冒烟测试通过后跑真实数据结果落到runs/。如果指标提升让它生成消融脚本确认是哪几个特征在起作用。如果指标下降把metrics.json和feature_list.txt贴给 AI问它根据这两份结果你认为问题可能出在哪通常能给出几个值得一试的方向。挑一个方向回到第 1 步。这个循环里 AI 全程只做两件事:写代码、分析结果。它从来不直接告诉你模型该长什么样。所有的判断都建立在真实跑出来的数字上。我个人在实际操作中的体会是这套流程最反直觉的一点是:它前期比直接问方案慢但后期快得多。第一次搭骨架、写契约、调断言可能要花小半天但从第二个实验开始每个新想法的验证周期能压缩到一两个小时而且结论是可信的。而直接问方案那套每次都能在五分钟内拿到一份看起来很完整的文档然后花两天时间发现它跑不通。如果你现在手上正好有个建模任务我的建议是:先别问 AI该用什么模型先把你的数据写成一份契约把目录结构定下来然后让它写第一个features.py。跑通那一百行假数据的时候你会对AI 建模这件事有完全不同的理解。