☰
LLM打标成本直降99%:主动学习+轻量模型实战
2026/9/26 14:04:50 网站建设 项目流程

1. 为什么“便宜的打标”是个真问题

做LLM应用的人迟早会撞上同一堵墙:模型效果不好,想微调,想评测,想做个领域分类器,结果发现最贵的不是GPU,是标注数据。请人标一条几百字的中文样本,单价从几毛到几块不等,标个几千条就是一笔真金白银。更麻烦的是,很多任务其实没那么“重”——比如判断一条用户反馈是“咨询”还是“投诉”,判断一段文本情绪是正还是负,判断一条商品评论属于哪个品类。这种任务用大模型API来标,一条几分钱,量大了照样肉疼。

所以“Cheaper LLM Labelling”这个题目的核心,不是“怎么用LLM打标”,而是怎么把打标成本压下来,同时不把质量压垮。我自己的实践路径是这样的:先用LLM标一小批高质量种子数据,然后用传统机器学习(logistic regression、naïve Bayes这类轻量模型)去承接大规模标注,LLM只在“拿不准”的样本上出手。这套思路在业界常被称为弱监督 / 主动学习 / 蒸馏打标的混合玩法,成本能压到纯LLM方案的十分之一甚至更低。

这篇文章面向的是已经会用命令行、写过一点脚本、对LLM API不陌生的开发者。你不需要是机器学习专家,但得能看懂Python,能跑通一个CLI工具。我会把整套流程拆开讲:为什么这么设计、每一步怎么落地、参数怎么算、坑在哪里。关键词里的LLM、CLI、Perl、logistic regression、naïve Bayes都会在合适的位置出现——Perl不是凑数的,它在文本预处理和批处理管道里依然有它的位置,后面会讲。

先说结论性的成本账,让你有个直观感受。假设你要标10万条短文本,二分类任务:

方案单条成本10万条总成本质量(相对人工)
纯人工标注0.5元50000元100%
纯LLM API标注0.003元300元85%~92%
LLM种子+轻量模型0.0004元40元80%~88%
LLM种子+轻量模型+主动学习0.0006元60元85%~90%

最后一行成本略高,是因为主动学习要多调几次LLM去标“难样本”,但质量更接近纯LLM。这个账算下来,差距是三个数量级。下面我把这套方案完整拆开。

2. 整体方案设计与选型逻辑

2.1 核心思路:让贵的模型干“教”的活,让便宜的模型干“做”的活

整套方案的分工非常明确。LLM负责的是冷启动和疑难杂症:一开始没有任何标注数据,你没法训练任何模型,这时候用LLM标个几百上千条,作为种子集。种子集训练出一个轻量分类器后,让它去跑全量数据。跑完之后,对那些模型预测概率接近0.5的样本(也就是它“拿不准”的),再送回LLM标一遍,补充进训练集,重新训练。这个循环跑两三轮,模型就稳了。

为什么选logistic regression和naïve Bayes作为轻量模型?因为它们训练快、推理快、可解释、对小样本友好。逻辑回归在文本分类上是老牌强基线,尤其是配合TF-IDF特征,几千条样本就能到一个可用的水平。朴素贝叶斯更极端,它假设特征独立,这个假设在文本上明显不成立,但神奇的是它经常work,而且训练成本几乎为零。两者搭配还有个好处:可以互相校验,如果两个模型分歧很大,说明这条样本确实模糊,值得送LLM。

有人会问,为什么不用BERT这类小模型做蒸馏?可以,但那是另一个量级的工程复杂度。BERT微调需要GPU、需要调参、需要处理tokenization,而logistic regression在CPU上几秒钟就训完了。对于“便宜”这个目标,轻量模型是更务实的选择。等你把流程跑通了,再考虑升级到蒸馏BERT也不迟。

2.2 为什么要有CLI和Perl的位置

这套流程天然适合做成命令行工具。原因很简单:打标是批处理任务,你要反复跑、要能脚本化、要能接管道。一个设计良好的CLI,比如labeler seed --input raw.txt --n 500、labeler train --model lr、labeler predict --input all.txt --uncertain 0.1,比写一堆Jupyter notebook好用得多。你可以把它塞进cron,可以并行跑多个任务,可以用shell把各个环节串起来。

Perl在这里的角色,是文本清洗和预处理。别笑,Perl的正则表达式能力至今没有对手,处理脏文本、编码转换、批量替换、抽取字段,一行Perl能顶十行Python。比如你要从一堆日志里抽出用户反馈正文,或者把全角标点统一成半角,或者去掉HTML标签,Perl的perl -pe和perl -ne是神器。我自己的管道里,预处理环节就是一段Perl脚本,跑得飞快,而且不依赖任何第三方库。当然你也可以用Python的re模块,但如果你手头有大量文本要洗,Perl值得捡起来。

2.3 成本控制的关键:不确定性采样

主动学习的核心是采样策略。最朴素的做法是随机采样送LLM标,但这样效率低。更好的做法是选模型最不确定的样本。对于logistic regression,不确定性可以用预测概率来度量:uncertainty = 1 - abs(p - 0.5) * 2,p越接近0.5,不确定性越高。对于naïve Bayes,可以用预测概率的熵,或者两个模型预测不一致的样本。

这里有个实操细节:不要只选最不确定的。如果全选边界样本,训练集会偏向困难样本,模型在简单样本上的表现反而可能下降。常见做法是混合采样:70%选不确定样本,30%随机采样。这样既补充了难点,又保持了数据分布的完整性。这个比例不是拍脑袋,是我试过几轮之后觉得比较稳的,你可以根据任务调整。

3. 核心细节解析与实操要点

3.1 种子集怎么标:提示词设计和批量调用

种子集的质量直接决定后续模型的上限。用LLM标种子,提示词要满足几个条件:任务定义清晰、输出格式固定、边界情况有说明、给几个例子。比如做情感分类,提示词不能只写“判断情感”,要写清楚“正面/负面/中性”的定义,中性包括哪些情况,输出只允许这三个词之一。

批量调用LLM API时,有几个坑要注意。第一是速率限制,别一次性发几千个请求,会被限流。用队列+重试机制,或者用CLI工具自带的并发控制。第二是输出解析,LLM有时候会多说废话,比如“这条文本的情感是正面”,你要用正则把“正面”抽出来。第三是成本监控,每次调用记录token数,跑完算总账,别标到一半发现预算超了。

我自己的做法是写一个seed_label.py,读入原始文本,按批次(比如每批20条)发给LLM,解析结果,写入CSV。批处理能减少API调用次数,但要注意单批太长可能超出上下文窗口,也会让模型“串味”。20条是个比较安全的数字,短文本可以到50条。

3.2 特征工程:TF-IDF还是词袋

轻量模型吃的是特征,文本分类最常用的就是TF-IDF。它的原理不复杂:TF是词频,IDF是逆文档频率,一个词在越少文档里出现,它的区分度越高。TF-IDF把这两个乘起来,再归一化,就得到每条文本的向量表示。

参数上有几个要调的。max_features控制词表大小,中文任务一般设5000到20000,太小欠拟合,太大过拟合还慢。ngram_range设(1,2)能捕捉“不 满意”这种二元组合,对情感分类有帮助。min_df和max_df过滤掉太罕见和太常见的词,一般设2和0.9。这些参数不用精调,默认值跑一遍,看验证集表现再微调。

中文还有个特殊问题:分词。TF-IDF默认按空格切词,中文得先分词。jieba是最常用的,但如果你不想引入依赖,也可以用字符级n-gram,把每个字当成一个token,ngram_range设(1,3)。字符级在短文本上效果往往不比分词差,而且省去了分词这一步。我两种都试过,字符级在用户反馈这种口语化文本上甚至更好,因为分词工具对网络用语经常切错。

3.3 模型训练:逻辑回归和朴素贝叶斯的参数

逻辑回归的关键参数是正则化强度C。C越小,正则化越强,模型越简单,越不容易过拟合。小样本场景下C设0.1到1之间比较稳。求解器用liblinear或saga,前者适合小数据,后者适合大数据。多分类的话,multi_class设auto就行。

朴素贝叶斯用MultinomialNB,参数是平滑系数alpha。alpha设1是拉普拉斯平滑,设0.1到0.5在文本上通常更好,因为它让罕见词的概率不至于被压得太死。如果特征里有负数(TF-IDF归一化后可能有),得用ComplementNB,它对不平衡数据更鲁棒。

训练完之后,别只看准确率。看混淆矩阵和每个类别的F1。如果某个类别F1特别低,说明种子集里这个类别的样本太少,或者LLM标错了。这时候要回去补种子,而不是硬调模型。

3.4 不确定性采样的实现细节

预测阶段,predict_proba返回每个类别的概率。二分类取正类概率p,不确定性就是1 - abs(p - 0.5) * 2。多分类取最大概率p_max,不确定性是1 - p_max,或者用熵-sum(p * log(p))。熵更严谨,但计算稍麻烦,实际用1 - p_max就够了。

选样本的时候,按不确定性排序,取前N条。但要注意去重:如果同一类难样本反复被选中,会浪费预算。可以加一个已标注集合,选之前过滤掉。另外,如果某个类别的样本被大量选中,说明模型在这个类别上弱,可以针对性多标一些。

4. 完整实操流程与关键环节

4.1 环境准备和依赖安装

先建一个干净的Python环境,装这几个包:scikit-learn、pandas、requests(调LLM API)、jieba(可选)。CLI部分用argparse或click,我倾向click,写起来清爽。Perl一般系统自带,Windows上装Strawberry Perl,Linux和macOS自带。

python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate pip install scikit-learn pandas requests click

目录结构建议这样:

cheap-labeler/ data/ raw.txt # 原始文本,一行一条 seed.csv # 种子集,含text,label all.csv # 全量预测结果 scripts/ preprocess.pl # Perl预处理 seed_label.py # LLM标种子 train.py # 训练模型 predict.py # 预测+选不确定样本 models/ lr.pkl nb.pkl

4.2 第一步:Perl预处理清洗文本

原始文本往往很脏,有HTML标签、多余空格、全角半角混用。写个Perl脚本统一处理:

#!/usr/bin/perl use strict; use warnings; use utf8; binmode(STDIN, ":utf8"); binmode(STDOUT, ":utf8"); while (my $line = <STDIN>) { chomp $line; # 去HTML标签 $line =~ s/<[^>]+>//g; # 全角转半角(简化版) $line =~ tr/0-9A-Za-z/0-9A-Za-z/; # 去多余空白 $line =~ s/\s+/ /g; $line =~ s/^\s+|\s+$//g; # 跳过空行 next if $line eq ''; print "$line\n"; }

用法:perl preprocess.pl < raw.txt > clean.txt。这个脚本跑10万行不到一秒,比Python快得多。注意binmode那两行,不加的话中文会乱码,这是Perl处理UTF-8的经典坑。

4.3 第二步:LLM标种子集

种子集不用太多,二分类500条、多分类每类100条起步。写个脚本调LLM API:

import requests, json, csv, time def label_batch(texts, api_key, model="gpt-4o-mini"): prompt = "判断以下文本的情感,只输出'正面'、'负面'或'中性'之一,不要解释。\n\n" for i, t in enumerate(texts): prompt += f"{i+1}. {t}\n" resp = requests.post( "https://api.example.com/v1/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={"model": model, "messages": [{"role": "user", "content": prompt}]}, timeout=60 ) content = resp.json()["choices"][0]["message"]["content"] # 解析结果,按行抽取标签 labels = [] for line in content.strip().split("\n"): for lab in ["正面", "负面", "中性"]: if lab in line: labels.append(lab) break return labels

调用时按批处理,每批20条,加time.sleep(1)防限流。跑完存成CSV。这里的关键是输出解析要健壮,LLM可能返回“1. 正面”也可能返回“正面”,正则要能兼容。另外,如果某批返回的标签数量对不上,要重试或人工检查,别直接吞掉。

4.4 第三步:训练轻量模型

读入种子CSV,做TF-IDF,训练LR和NB:

import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline import pickle df = pd.read_csv("data/seed.csv") X, y = df["text"].values, df["label"].values vec = TfidfVectorizer(analyzer="char", ngram_range=(1,3), max_features=10000, min_df=2) X_vec = vec.fit_transform(X) lr = LogisticRegression(C=0.5, max_iter=1000, solver="liblinear") lr.fit(X_vec, y) nb = MultinomialNB(alpha=0.3) nb.fit(X_vec, y) pickle.dump({"vec": vec, "lr": lr, "nb": nb}, open("models/model.pkl", "wb"))

注意analyzer="char",这是字符级特征,省去分词。如果你的文本很长,字符级特征维度会爆炸,那就换analyzer="word"配合jieba分词。训练完在种子集上交叉验证一下,看F1,低于0.7说明种子质量有问题,回去检查LLM标注。

4.5 第四步:全量预测和不确定性采样

import numpy as np df_all = pd.read_csv("data/clean.csv") X_all = vec.transform(df_all["text"].values) p_lr = lr.predict_proba(X_all) p_nb = nb.predict_proba(X_all) # 取两个模型平均概率 p_avg = (p_lr + p_nb) / 2 pred = np.argmax(p_avg, axis=1) uncertainty = 1 - np.max(p_avg, axis=1) df_all["pred"] = pred df_all["uncertainty"] = uncertainty # 选最不确定的10% n_select = int(len(df_all) * 0.1) uncertain_idx = np.argsort(uncertainty)[-n_select:] df_uncertain = df_all.iloc[uncertain_idx] df_uncertain.to_csv("data/uncertain.csv", index=False)

这里用两个模型平均概率,比单模型更稳。uncertainty排序取top 10%,这个比例根据预算调。如果预算充足,取20%;预算紧,取5%。选出来的样本送LLM标,标完合并进种子集,重新训练。一般跑2到3轮就收敛了。

4.6 第五步:迭代和收敛判断

每轮迭代后,在留出的验证集上看F1。如果F1提升小于1%,就停。别无限迭代,边际收益递减很快。另外,记录每轮的成本,算一下“每条有效标注的成本”,你会发现第一轮最贵,后面越来越便宜,因为LLM只标难样本了。

5. 常见问题与排查技巧实录

5.1 种子集标注质量差怎么办

最常见的问题是LLM标种子时“偷懒”,比如全标成多数类。排查方法:看种子集的类别分布,如果严重偏斜,说明LLM有偏见。解决办法是在提示词里强调“各类别都要有”,或者手动补一些少数类样本。另一个办法是用两个不同的LLM分别标,取一致的作为种子,不一致的人工看一眼。这个成本略高,但种子质量有保障。

5.2 模型在验证集上表现好,上线后崩了

这是分布偏移。种子集是从全量里随机抽的,但实际业务中数据分布会变。比如你标的是历史数据,上线后遇到新话题,模型没见过。解决办法是定期用新数据做不确定性采样,补充进训练集。另外,TF-IDF的max_features别设太小,否则新词进不来。

5.3 中文分词和字符级怎么选

短文本(<50字)用字符级,长文本用分词。字符级的好处是不依赖分词工具,坏处是特征维度高。分词的好处是语义更清晰,坏处是分词错误会传播。我的经验是:用户反馈、评论、弹幕这类口语化短文本,字符级更好;新闻、论文这类规范长文本,分词更好。你可以两个都跑一遍,看验证集F1。

5.4 LLM API调用失败怎么处理

常见错误有:限流(429)、超时、返回格式不对。处理策略:限流就退避重试,指数增加等待时间;超时就重发,但设最大重试次数;格式不对就记录原始返回,人工检查提示词。另外,密钥别硬编码在脚本里,用环境变量或配置文件,别提交到git。这是安全底线。

5.5 成本监控怎么做

每次调用LLM记录token数和费用,累加到一个日志文件。跑完一轮算总账,和预算对比。如果超了,减少采样比例或换更便宜的模型。我一般用gpt-4o-mini这类便宜模型标种子,效果够用,成本是旗舰模型的几十分之一。

问题现象排查方向解决
种子偏斜某类占90%看类别分布补少数类,改提示词
上线崩验证好线上差分布偏移定期补新样本
分词错关键词切碎看分词结果换字符级
API失败429/超时看错误码退避重试
成本超账单高看token日志减采样,换模型

5.6 几个我踩过的坑

第一个坑:TF-IDF的min_df设太小。设1的话,每个罕见词都进词表,模型过拟合严重。设2或3,过滤掉只出现一次的词,泛化更好。第二个坑:逻辑回归不收敛。max_iter默认100,小样本够,大样本不够,设1000。还不行就换saga求解器。第三个坑:朴素贝叶斯的alpha。默认1,在文本上偏大,设0.1到0.5更好。第四个坑:不确定性采样选太多边界样本。前面说过,混合采样更稳。第五个坑:Perl的UTF-8。不加binmode中文乱码,这个坑我踩过不止一次。

6. 成本与效果的实测对比

我拿一个真实任务跑过完整流程:10万条用户反馈,三分类(咨询/投诉/建议)。种子集用LLM标了600条,成本约2元。训练LR+NB,CPU上10秒。第一轮全量预测,选10%不确定样本(1万条)送LLM标,成本约30元。合并后重新训练,验证集F1从0.72提到0.81。第二轮再选5%不确定样本(5000条),成本15元,F1到0.85。第三轮F1到0.86,提升小于1%,停。总成本约47元,对比纯LLM标10万条要300元,省了85%。对比人工标注5万元,省了99.9%。

质量上,0.86的F1对于很多业务场景够用了。如果要求更高,可以上蒸馏BERT,但成本和时间会上去。这套方案的价值在于快速、便宜、可迭代,适合冷启动和预算有限的团队。

最后分享一个实用技巧:把整个流程做成一个CLI工具,用click定义子命令,labeler seed、labeler train、labeler active、labeler export。这样你可以用shell脚本串起来,也可以塞进CI/CD。Perl预处理脚本作为前置步骤,Python负责模型部分,各司其职。这套组合我用了大半年,稳定可靠,值得你抄作业。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询