1. 从"调包"到"对话":为什么第三篇要聊大模型与机器学习的衔接
前两篇里,我们把数据清洗、特征工程、经典模型训练这一整套流程走了一遍。到了第三篇,我想聊一个很多人卡住的地方:当你已经会用 scikit-learn 跑通一个分类器之后,怎么把大语言模型的能力接进你自己的 Python 机器学习流水线里。这不是让你去训练一个几十亿参数的模型,那不现实,而是把大模型当成一个"会说话的组件",嵌进你原有的数据处理、特征生成、结果解释这些环节中。
我见过太多人在这件事上走两个极端。一个极端是觉得大模型无所不能,什么都想丢给它,结果发现成本高、延迟大、结果还不稳定;另一个极端是觉得大模型跟传统机器学习是两码事,井水不犯河水,于是白白错过了很多提效的机会。实际上,这两者是可以很自然地协作的。传统机器学习擅长的是结构化数据上的数值计算和模式识别,大模型擅长的是非结构化文本的理解、生成和转换。把它们放在一起,你能做的事情比单独用任何一个都多。
这篇文章适合谁看?如果你已经能用 Python 写一些数据处理脚本,知道 pandas 的 DataFrame 怎么用,跑过至少一个 sklearn 的模型,那这篇的内容你基本能跟上。如果你还没到这一步,建议先把前两篇的基础打牢。我会尽量把每个环节的"为什么这么做"讲清楚,而不是只丢一段代码让你抄。代码我会用 Python3 写,涉及大模型调用的部分用通用的接口思路来讲,具体你接哪家的服务,逻辑是相通的。
先说清楚一个前提:我这里讲的"ChatGPT",指的是通过 API 调用的大语言模型服务,不是让你去网页上复制粘贴。只有走 API,你才能把它自动化地嵌进代码里,才能批量处理、才能和你的机器学习流程串起来。这是从"手动用"到"工程化用"的关键一步。
2. 大模型在机器学习流水线里到底能插在哪几个位置
很多人一上来就问"怎么用大模型做机器学习",这个问题太笼统了。你得先想清楚,你要它解决的是流水线里的哪一段。我把它拆成四个最常见的插入点,你可以对照自己的项目看看哪个用得上。
2.1 数据标注与清洗环节:把非结构化文本变成可训练的标签
传统机器学习最头疼的问题之一就是标注数据不够。你有一堆用户评论、客服对话、商品描述,但它们是纯文本,没有标签,你的分类器没法直接学。以前的做法是人工标注,几百条还行,几万条就是灾难。
大模型在这里能帮上大忙。你可以设计一个提示词,让模型对每条文本打上你需要的类别标签,然后把这些标签当作训练数据。比如你要做一个情感分类器,可以先让大模型给一批评论打上"正面/负面/中性",再用这批带标签的数据去训练一个轻量级的本地模型。这样做的价值在于:大模型负责冷启动阶段的标注,本地小模型负责后续的高频推理,成本和速度都能兼顾。
但这里有个坑我必须提醒你:大模型的标注不是百分百准确的,它会有偏差,会有一致性问题。所以你不能拿它标的数据直接当金标准。我的做法是,让大模型标一批,然后人工抽检 10% 到 20%,看看准确率能不能接受。如果某个类别的错误率特别高,就针对性地优化提示词,或者干脆把这个类别交给人工。这个"模型标注 + 人工校验"的混合流程,比纯人工快得多,又比纯模型可靠得多。
2.2 特征工程环节:把文本转成有语义的向量
第二篇里我们讲过 TF-IDF 和词袋模型,这些方法在文本分类上能用,但有个明显短板:它们抓不住语义。"这个手机很好用"和"这部手机体验不错",在词袋模型里几乎没有重叠的词,但意思几乎一样。大模型的嵌入能力可以解决这个问题。
你可以调用嵌入接口,把每条文本转成一个高维向量,这个向量就是文本的语义表示。然后把这个向量当作特征,喂给你原来的分类器、聚类算法或者推荐系统。实测下来,在很多文本任务上,用嵌入向量做特征比用 TF-IDF 的效果要好一截,尤其是在数据量不大、词汇多样性高的情况下。
这里要注意维度问题。嵌入向量通常是几百到几千维,如果你的样本量只有几百条,直接上高维向量容易过拟合。我的经验是,要么先做降维(比如用 PCA 降到 50 到 100 维),要么用正则化强的模型(比如带 L2 的逻辑回归),要么就多攒点数据。别小看这一步,很多人拿了嵌入向量直接丢进模型,结果还不如 TF-IDF,问题往往就出在维度没控制好。
2.3 模型结果解释环节:让黑盒模型的输出说人话
你训练了一个梯度提升树或者神经网络,预测准确率挺高,但老板问你"为什么这条被判定为高风险",你总不能说"因为第 37 个特征的值是 0.82"吧。这时候大模型可以当"翻译官",把模型的输出和关键特征转成人类能读懂的解释。
具体做法是:先用特征重要性或者 SHAP 值找出对这条预测影响最大的几个特征,然后把这些特征和它们的取值拼成一段描述,交给大模型,让它生成一段自然语言的解释。这样既保留了模型的数值严谨性,又让结果变得可读。这个思路在风控、医疗辅助、客服质检这些需要"可解释性"的场景里特别有用。
2.4 流水线编排环节:用自然语言驱动数据处理步骤
这个用法稍微进阶一点。你可以让大模型扮演一个"数据操作助手",你用自然语言描述你想做的数据转换,它帮你生成对应的 pandas 代码。比如你说"把日期列拆成年月日三列,然后按月份分组统计每组的均值",它给你生成代码。你检查没问题就执行。
这个用法能大幅提升探索性数据分析的效率,但必须加人工审核。大模型生成的代码可能有 bug,可能理解错你的意图,直接跑在重要数据上风险很大。我的习惯是让它生成代码后,先在小样本上跑一遍,确认逻辑对了再上全量数据。
3. 动手搭一个最小可用的混合流水线
光说思路不够,我们实际搭一个。场景设定为:你有一批用户反馈文本,想做一个自动分类系统,把反馈分成"功能建议""故障报告""咨询提问"三类。数据是纯文本,没有标签。我们用大模型做标注和特征提取,用 sklearn 做分类器训练。
3.1 环境准备与依赖说明
先把环境理清楚。你需要 Python 3.8 以上,主要依赖这几个:
pip install pandas scikit-learn numpy openai这里openai只是举例,你用哪家的 SDK 都行,核心是你要有一个能调用的模型接口。另外建议装个tqdm来看进度,批量调用的时候心里有数:
pip install tqdm关于密钥管理,我要多说一句。千万不要把 API 密钥硬编码在代码里,更不要提交到代码仓库。用环境变量或者单独的配置文件来管理:
import os api_key = os.environ.get("MODEL_API_KEY")这是最基本的工程习惯,我见过太多人因为把密钥写死在脚本里然后不小心分享出去,导致账单爆炸。
3.2 用大模型给无标签文本打标签
假设你的原始数据是一个 CSV,只有一列feedback_text。我们先读进来,然后设计提示词让模型分类。
import pandas as pd from tqdm import tqdm df = pd.read_csv("feedback.csv") print(f"共 {len(df)} 条反馈") print(df.head())提示词的设计是关键。我一般遵循几个原则:角色明确、任务具体、输出格式固定、给例子。下面是我常用的模板:
SYSTEM_PROMPT = """你是一个文本分类助手。你需要把用户反馈分类到以下三个类别之一: - 功能建议:用户希望增加或改进某个功能 - 故障报告:用户遇到了错误、崩溃、异常 - 咨询提问:用户在询问如何使用或某个功能是否存在 只输出类别名称,不要输出任何其他内容。""" def classify_text(text): # 这里替换成你实际使用的模型调用 response = call_model(SYSTEM_PROMPT, text) return response.strip()批量处理的时候,注意两个问题。第一是速率限制,很多接口对每分钟调用次数有限制,你需要加延时或者做重试。第二是失败处理,网络抖动、接口超时都可能发生,不能让一条失败就中断整个流程。
import time def classify_batch(texts, delay=0.5): results = [] for text in tqdm(texts): try: label = classify_text(text) results.append(label) except Exception as e: print(f"处理失败: {e}") results.append("未知") time.sleep(delay) return results df["label"] = classify_batch(df["feedback_text"].tolist())跑完之后,一定要抽检。随机抽 30 条,人工看看标签对不对:
sample = df.sample(30, random_state=42) for _, row in sample.iterrows(): print(f"文本: {row['feedback_text'][:50]}") print(f"标签: {row['label']}") print("-" * 40)如果发现某一类错误率明显偏高,回去改提示词。常见的改进方向包括:把类别定义写得更细、给每个类别加一两个例子、要求模型先说明理由再给结论(虽然你最终只要结论,但让它"想一下"往往能提升准确率)。
3.3 把文本转成嵌入向量并接入分类器
标签有了,接下来做特征。我们用嵌入接口把文本转成向量:
import numpy as np def get_embedding(text): # 替换成实际的嵌入接口调用 return call_embedding_api(text) embeddings = [] for text in tqdm(df["feedback_text"].tolist()): emb = get_embedding(text) embeddings.append(emb) X = np.array(embeddings) y = df["label"].values print(f"特征矩阵形状: {X.shape}")拿到特征矩阵后,按标准流程划分训练集和测试集,训练一个分类器:
from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) clf = LogisticRegression(max_iter=1000, C=1.0) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred))这里选逻辑回归是有原因的。嵌入向量维度高、样本量往往不大,逻辑回归配合 L2 正则在这种场景下表现稳定,训练快,还能给出每个类别的概率。如果你数据量足够大(上万条以上),可以试试 SVM 或者浅层神经网络,但别一上来就上复杂模型,先把基线跑通。
3.4 用交叉验证确认结果不是运气
单次划分的测试结果可能有偶然性,尤其是数据量小的时候。我习惯用交叉验证再看一眼:
from sklearn.model_selection import cross_val_score scores = cross_val_score(clf, X, y, cv=5, scoring="f1_macro") print(f"5折交叉验证 F1: {scores.mean():.3f} (+/- {scores.std():.3f})")如果交叉验证的均值和单次测试差很多,说明你的数据分布可能不均匀,或者样本量太小。这时候要么多标点数据,要么对结果保持谨慎,别急着上线。
4. 成本、延迟与稳定性:工程化必须算的三笔账
把 demo 跑通只是第一步,真正要把它用起来,你得算账。我见过不少人 demo 跑得挺欢,一上量就发现成本扛不住、延迟太高、接口一挂整个流程就瘫。这一节聊聊怎么把这三笔账算清楚。
4.1 成本账:哪些环节该用大模型,哪些该用本地模型
大模型调用是按量计费的,标注和嵌入都要花钱。如果你的数据是十万条,每条都调一次大模型,账单会很可观。我的策略是分层处理:
| 环节 | 推荐方案 | 理由 |
|---|---|---|
| 冷启动标注 | 大模型标注 + 人工抽检 | 数据量小,准确性要求高 |
| 批量特征提取 | 嵌入接口,可缓存 | 一次调用可复用,避免重复计算 |
| 高频推理 | 本地小模型 | 成本低、延迟低、可控 |
| 结果解释 | 大模型按需调用 | 只在需要解释时触发 |
关键思路是:大模型用在"贵但值得"的地方,本地模型用在"高频且要求快"的地方。嵌入向量算一次就存下来,下次直接用,不要每次都重新算。我一般会把嵌入结果存成 numpy 文件或者放进向量数据库:
np.save("embeddings.npy", X) df[["feedback_text", "label"]].to_csv("labeled_data.csv", index=False)这样下次要用的时候直接加载,省时省钱。
4.2 延迟账:批量调用怎么优化
单条调用延迟可能几百毫秒到几秒,如果你要处理几千条,串行跑下来就是几十分钟。优化方向有几个:
第一,并发调用。用线程池或者异步请求,把多条请求同时发出去。但要注意接口的速率限制,别把并发开太大被封。
from concurrent.futures import ThreadPoolExecutor def process_parallel(texts, max_workers=5): with ThreadPoolExecutor(max_workers=max_workers) as executor: results = list(executor.map(classify_text, texts)) return results第二,批处理接口。有些服务支持一次传多条文本,返回多个结果,这种比逐条调用效率高得多。如果你的服务支持,优先用这个。
第三,缓存。相同的文本不要重复调用,用一个字典或者本地缓存存起来。实际数据里重复内容比你想的多。
4.3 稳定性账:接口挂了怎么办
大模型接口不是百分百可用的,网络会抖、服务会限流、偶尔会超时。你的流水线不能因为一次调用失败就整个崩掉。基本的容错策略包括:
- 重试机制:失败后等几秒重试,最多重试 3 次,每次间隔递增
- 降级方案:如果大模型调用连续失败,切换到本地模型或者返回默认值
- 断点续传:处理到一半挂了,下次能从断的地方继续,不用从头再来
def call_with_retry(func, *args, max_retries=3, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt == max_retries - 1: raise wait = 2 ** attempt print(f"第 {attempt+1} 次失败,{wait} 秒后重试") time.sleep(wait)断点续传的做法是,每处理完一批就存一次结果,记录处理到哪了:
def process_with_checkpoint(texts, checkpoint_file="progress.txt"): start_idx = 0 if os.path.exists(checkpoint_file): with open(checkpoint_file) as f: start_idx = int(f.read().strip()) results = [] for i in range(start_idx, len(texts)): result = call_with_retry(classify_text, texts[i]) results.append(result) if i % 100 == 0: with open(checkpoint_file, "w") as f: f.write(str(i)) return results这些看起来是"额外工作",但真到了生产环境,它们决定了你的流程能不能稳定跑下去。
5. 几个我踩过的坑和对应的解法
这一节讲几个实际做的时候容易翻车的地方,都是我自己或者身边人踩过的。
5.1 提示词不稳定导致标签漂移
同一个提示词,今天跑和明天跑,结果可能不一样。模型更新、温度参数、甚至输入顺序都可能影响输出。我遇到过一次,同一批数据分两天标注,结果两天的标签分布明显不同,导致训练出来的模型效果很差。
解法有两个。一是固定参数,把温度设成 0 或者接近 0,减少随机性。二是加校验,标注完之后统计各类别的比例,如果和预期差太多,就要警惕。更稳妥的做法是,把提示词和模型版本都记录下来,保证可复现。
5.2 嵌入向量维度不匹配
不同模型、不同版本的嵌入接口,输出的向量维度可能不一样。如果你先存了一批 1536 维的向量,后来换了接口变成 1024 维,直接拼在一起就会报错。我的做法是,在存向量的时候把维度和模型标识一起记下来,用的时候先检查一致性。
metadata = { "model": "embedding-model-v1", "dim": X.shape[1], "count": X.shape[0] } import json with open("embeddings_meta.json", "w") as f: json.dump(metadata, f)5.3 类别不平衡被忽略
实际数据里,三类反馈的数量往往不是均匀的。可能"咨询提问"占了 70%,"故障报告"只有 5%。如果你直接训练,模型会倾向于预测多数类,少数类的召回率很低。解法是在训练时设置类别权重:
clf = LogisticRegression( max_iter=1000, class_weight="balanced" )class_weight="balanced"会自动根据类别频率调整权重,让少数类得到更多关注。另外评估的时候不要只看准确率,要看每个类别的精确率和召回率,用classification_report就能看到。
5.4 把大模型输出直接当特征用
有人图省事,让大模型直接输出一个数值当特征,比如"给这段文本的紧急程度打 1 到 10 分"。这个做法风险很大,因为大模型输出的数值不具备稳定的标度,今天给 7 分的明天可能给 5 分,不同文本之间的可比性也差。如果一定要用,建议改成分类输出(比如"高/中/低"),然后自己做编码,这样至少类别边界是清晰的。
6. 从这套流程里能延伸出什么
把上面这套跑通之后,你会发现它是个通用的框架。换一个场景,改改提示词和类别定义,就能用在别的地方。比如电商评论的情感分析、工单的自动分派、文档的自动归档,逻辑都是一样的:大模型负责理解和转换,传统机器学习负责分类和预测,两者各司其职。
我个人的体会是,不要追求"全用大模型"或者"全用传统方法",而是看每个环节的特点。需要语义理解、需要生成、需要处理非结构化文本的地方,大模型有优势;需要快速推理、需要数值计算、需要可解释性的地方,传统方法更合适。把这两者拼起来,才是务实的做法。
还有一个方向值得试试:用大模型生成合成数据来扩充训练集。当你某个类别的样本特别少的时候,可以让大模型根据少量真实样本生成更多类似的文本,缓解数据不平衡。但要注意,合成数据不能完全替代真实数据,它只能作为补充,而且生成的质量要人工把关。
最后分享一个小技巧。在调试提示词的时候,不要一次改很多地方,一次只改一个变量,然后对比结果。这样你才能知道到底是哪个改动起了作用。我见过有人一口气把提示词重写了一遍,结果效果变差了,却不知道是哪里改坏了。把提示词当成代码来管理,版本化、可对比,这是让整个流程可控的关键。