
现在做分类任务动不动就上大模型仿佛不调个API、不写几百行Prompt就不算做过AI。但真到了生产环境尤其是那种QPS几千、延迟要求卡在50毫秒以内、还要跑在边缘设备上的场景LLM那套东西根本扛不住。我最近刚把一个文本分类的线上服务从LLM方案切回了传统机器学习单次推理从300多毫秒直接压到2毫秒以内服务器成本降了将近两个数量级。这篇就聊聊非LLM的快速分类模型到底怎么选、怎么训、怎么部署以及为什么在很多场景下它比LLM更值得你花时间。1. 什么场景下该果断放弃LLM1.1 延迟与吞吐的硬约束先说一个我踩过的真实坑。去年有个项目做用户评论的实时风控最初图省事直接调LLM接口做二分类判断评论是否违规。离线测试准确率确实漂亮能到94%左右。但上线之后问题全暴露了高峰期接口平均响应时间飙到800毫秒P99直接突破3秒而且按调用量算下来每个月API费用够养一个后端团队。后来我算了一笔账。假设日均请求量500万次LLM方案单次调用按最便宜的模型算也要0.001元左右一天就是5000块一个月15万。而换成XGBoost这类模型一台4核8G的机器就能扛住每秒几千次推理一个月服务器成本不到2000块。差了将近两个数量级这不是优化能弥补的差距这是架构选型的问题。延迟方面更直观。LLM推理哪怕是最小的蒸馏模型单次前向传播也要几十毫秒起步加上网络往返、排队实际端到端延迟很难压到100毫秒以内。而传统分类模型特征工程做完之后XGBoost或LightGBM的单条推理通常在1到5毫秒逻辑回归甚至能到微秒级。这个差距在实时系统里是致命的。1.2 数据隐私与离线部署需求另一个必须考虑非LLM方案的情况是数据不能出本地。金融、医疗、政务这些领域原始数据根本不允许发到外部接口。你可能会说可以私有化部署开源LLM但一个7B参数的模型推理至少需要十几G显存而一个XGBoost模型文件可能就几兆跑在CPU上绰绰有余。我做过一个医疗文本分类的项目要把病历摘要分成十几个科室类别。医院内网环境GPU资源紧张根本不可能给分类任务单独分配显卡。最后用TF-IDF加线性SVM的方案模型文件不到2MB推理在CPU上跑单线程每秒能处理上万条完全满足需求。准确率比微调后的小模型只低了不到3个百分点但资源消耗差了三个数量级。1.3 任务本身是否真的需要语义理解这是最容易被忽略的一点。很多分类任务其实根本不需要深层语义理解关键词和统计特征就足够了。比如垃圾邮件识别、情感极性判断、简单意图分类这些任务的决策边界往往在词袋层面就很清晰了。我见过太多团队上来就用LLM做意图分类结果发现用户query里只要出现退款退货这些词基本就是售后意图根本不需要模型理解整句话的语义。这种场景下一个精心设计的特征工程加梯度提升树效果不比LLM差速度却快几百倍。当然这不是说LLM没用。当分类依赖复杂的上下文推理、需要处理长文档、或者类别边界模糊需要世界知识时LLM确实有优势。关键是要判断你的任务到底属于哪一类。我的经验是如果人工标注员看一眼关键词就能判断个八九不离十那大概率不需要LLM。2. 非LLM分类模型的核心技术选型2.1 从逻辑回归到梯度提升树的谱系非LLM的分类模型选择其实很丰富从最简单的逻辑回归到复杂的集成模型各有适用场景。我一般按数据规模和特征维度来选。数据量小、特征维度高、需要强解释性的场景逻辑回归和线性SVM是首选。比如文本分类里TF-IDF特征动辄几万维线性模型训练快、推理快、还能直接看特征权重判断哪些词对分类贡献大。我之前做新闻分类用线性SVM在20万条数据上训练几分钟就跑完了F1能到0.92。数据量中等、特征有非线性交互的场景随机森林和梯度提升树更合适。XGBoost和LightGBM是这方面的主力。它们能自动捕捉特征间的交互对缺失值和异常值也鲁棒。缺点是训练比线性模型慢但推理速度依然很快因为树的数量和深度都是可控的。数据量很大、追求极致速度的场景可以考虑朴素贝叶斯或者简单的决策树。朴素贝叶斯在文本分类上经常有惊喜训练和推理都极快虽然特征独立假设很强但在高维稀疏文本数据上表现往往不差。下面这张表是我在实际项目中总结的选型参考模型训练速度推理速度可解释性适用场景逻辑回归极快极快强高维稀疏特征、基线模型线性SVM快极快中文本分类、小样本朴素贝叶斯极快极快中文本分类、实时过滤随机森林中快中通用分类、特征交互XGBoost中慢快中结构化数据、竞赛LightGBM快快中大规模数据、高维特征2.2 特征工程才是真正的胜负手用非LLM模型特征工程的重要性怎么强调都不过分。LLM可以端到端学习表示传统模型不行你得把领域知识喂进去。文本分类最基础的是TF-IDF和n-gram。TF-IDF捕捉词的重要性n-gram捕捉局部词序。我一般会同时用word-level和char-level的n-gram中文场景下char-level的2-gram和3-gram特别有用能捕捉到分词错误带来的噪声。但光有TF-IDF不够。我通常会加几类手工特征文本长度、标点符号比例、是否包含特定关键词、词性分布、情感词计数。这些特征在特定任务上往往比词袋特征更有区分度。比如做垃圾评论识别感叹号数量和重复字符比例就是很强的信号。数值特征的处理也有讲究。分桶、交叉、归一化这些操作对树模型和线性模型的影响完全不同。树模型对单调变换不敏感但线性模型必须做标准化。我一般会先做EDA看特征分布再决定怎么处理。还有一个容易被忽略的点是特征选择。高维稀疏特征里大量噪声不做选择直接喂给模型效果和速度都会受影响。我常用基于卡方检验或互信息的特征选择保留Top-K个特征。K的取值需要实验一般从几千到几万不等。2.3 训练流程中的关键参数以XGBoost为例几个核心参数直接决定模型表现和推理速度。max_depth控制树的最大深度默认6。深度越大模型越复杂容易过拟合推理也越慢。我一般从4开始试根据验证集表现调整。文本分类任务通常不需要太深的树因为特征已经很高维了。n_estimators是树的数量默认100。这个参数和learning_rate要配合调。学习率小就需要更多树训练慢但效果可能更好。我一般用早停法在验证集上监控避免过拟合。subsample和colsample_bytree控制行采样和列采样比例能提升泛化能力。我一般设0.8左右数据量小的时候可以更低。推理速度方面n_estimators和max_depth是主要影响因素。如果推理延迟要求严格可以适当降低这两个参数用少量精度换速度。我做过测试把n_estimators从200降到50推理速度提升3倍多F1只掉了不到1个百分点。LightGBM在速度上更有优势它用直方图算法和leaf-wise生长策略训练比XGBoost快很多推理也略快。但leaf-wise容易过拟合需要控制num_leaves参数。我一般设31到127之间根据数据量调整。3. 从零搭建一个快速分类服务的完整流程3.1 数据准备与标注质量把控数据是模型的上限这句话在非LLM场景下尤其正确。LLM有预训练知识兜底传统模型完全靠标注数据学。我一般会先做数据清洗去重、去噪、处理缺失值。文本数据还要做标准化比如全半角转换、大小写统一、去除特殊字符。但要注意有些特殊字符本身可能就是特征比如表情符号在情感分类里很重要不能一刀切去掉。标注质量是另一个关键。我见过太多项目因为标注标准不统一导致模型学偏。建议先做一轮标注一致性检查让多个人标同一批数据算一下Kappa系数。如果低于0.8说明标注标准需要细化。类别不平衡是分类任务的常见问题。非LLM模型对不平衡数据比LLM敏感得多。我的处理策略是先看业务需求如果少数类更重要就用过采样或调整类别权重如果只是评估指标问题可以用F1或AUC代替准确率。SMOTE这类合成采样方法我一般慎用容易引入噪声尤其是在高维文本数据上。数据划分也有讲究。时间序列数据不能随机划分要按时间切分否则会数据泄露。我一般按7:1:2分训练、验证、测试集验证集用于调参和早停测试集只在最后评估一次。3.2 特征管道与模型训练的代码骨架下面是一个完整的文本分类管道示例用scikit-learn和XGBoost实现import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.pipeline import Pipeline, FeatureUnion from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, f1_score import xgboost as xgb import jieba import re # 自定义中文分词器 def chinese_tokenizer(text): text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return jieba.lcut(text) # 手工特征提取 def extract_manual_features(texts): features [] for text in texts: features.append([ len(text), # 文本长度 text.count(!) text.count(), # 感叹号数量 text.count(?) text.count(), # 问号数量 len(re.findall(r\d, text)), # 数字串数量 len(set(text)) / (len(text) 1), # 字符多样性 ]) return np.array(features) # 构建特征管道 word_tfidf TfidfVectorizer( tokenizerchinese_tokenizer, max_features50000, ngram_range(1, 2), min_df3, max_df0.95, sublinear_tfTrue ) char_tfidf TfidfVectorizer( analyzerchar, max_features30000, ngram_range(2, 3), min_df3, sublinear_tfTrue ) # 训练流程 def train_classifier(texts, labels): X_train, X_test, y_train, y_test train_test_split( texts, labels, test_size0.2, stratifylabels, random_state42 ) # 特征提取 X_train_word word_tfidf.fit_transform(X_train) X_test_word word_tfidf.transform(X_test) X_train_char char_tfidf.fit_transform(X_train) X_test_char char_tfidf.transform(X_test) X_train_manual extract_manual_features(X_train) X_test_manual extract_manual_features(X_test) # 拼接特征 from scipy.sparse import hstack, csr_matrix X_train_all hstack([X_train_word, X_train_char, csr_matrix(X_train_manual)]) X_test_all hstack([X_test_word, X_test_char, csr_matrix(X_test_manual)]) # 训练XGBoost model xgb.XGBClassifier( max_depth6, n_estimators200, learning_rate0.1, subsample0.8, colsample_bytree0.8, objectivebinary:logistic if len(set(labels)) 2 else multi:softmax, eval_metriclogloss, tree_methodhist, n_jobs-1, random_state42 ) model.fit( X_train_all, y_train, eval_set[(X_test_all, y_test)], verbose10 ) y_pred model.predict(X_test_all) print(classification_report(y_test, y_pred)) return model这段代码有几个关键点值得说明。tree_methodhist用直方图算法加速训练大数据量下比精确贪心算法快很多。sublinear_tfTrue对TF做对数缩放避免高频词主导。手工特征用csr_matrix转稀疏后拼接保持整体稀疏性。3.3 模型评估与阈值调优分类模型的评估不能只看准确率。我一般会看这几个指标精确率、召回率、F1、AUC。具体侧重哪个取决于业务。风控场景通常更看重召回率宁可错杀不可放过。推荐场景更看重精确率避免打扰用户。F1是综合指标适合类别平衡的场景。AUC衡量排序能力和阈值无关适合评估模型本身。阈值调优是容易被忽略的一步。模型输出的是概率默认0.5作为阈值不一定最优。我一般会在验证集上画PR曲线根据业务需求选阈值。比如要求召回率不低于0.95就在满足这个条件的点选精确率最高的阈值。多分类场景下每个类别的阈值可以单独调。有些类别容易混淆可以适当提高阈值减少误报。我做过一个意图分类把咨询和投诉的边界调了很久最后给投诉设了更高的阈值因为误报成本更高。交叉验证也是必要的。我一般用5折 stratified K-fold确保每折类别分布一致。如果数据有时间属性就用时间序列交叉验证。交叉验证不仅能评估模型稳定性还能帮你判断是否过拟合。4. 部署与性能优化的实战细节4.1 模型序列化与推理服务封装训练完的模型要部署成服务第一步是序列化。XGBoost支持多种格式我一般用model.save_model(model.json)存成JSON跨平台兼容性好。也可以用joblib存pickle但版本兼容性要注意。特征管道也要一起序列化。TF-IDF的词汇表和IDF权重必须和训练时完全一致否则推理结果会偏。我一般把vectorizer和model打包成一个对象用joblib一起存。推理服务用FastAPI或Flask封装都行。FastAPI性能更好支持异步适合高并发。下面是一个简单的服务示例from fastapi import FastAPI from pydantic import BaseModel import joblib import numpy as np from scipy.sparse import hstack, csr_matrix app FastAPI() # 启动时加载模型 artifact joblib.load(classifier_artifact.joblib) word_tfidf artifact[word_tfidf] char_tfidf artifact[char_tfidf] model artifact[model] class PredictRequest(BaseModel): text: str app.post(/predict) def predict(req: PredictRequest): text req.text X_word word_tfidf.transform([text]) X_char char_tfidf.transform([text]) X_manual csr_matrix(extract_manual_features([text])) X hstack([X_word, X_char, X_manual]) proba model.predict_proba(X)[0] pred int(np.argmax(proba)) return {label: pred, confidence: float(proba[pred])}部署时用gunicorn加uvicorn worker根据CPU核数设置worker数量。我一般设workers 2 * cpu_cores 1但要注意每个worker都会加载一份模型内存要算够。4.2 批处理与缓存策略单条推理虽然快但高并发下批处理能进一步提升吞吐。XGBoost支持批量预测一次传多条数据比循环单条快很多。我一般会在服务层做一个微批处理攒够一定数量或等一小段时间再一起推理。缓存是另一个优化点。如果请求文本重复率高可以用LRU缓存存最近的结果。我用functools.lru_cache做过测试在重复率30%的场景下QPS能提升40%以上。模型层面也有优化空间。XGBoost可以转成ONNX格式用ONNX Runtime推理速度通常比原生快20%到50%。LightGBM也有类似的转换工具。如果延迟要求极致还可以考虑用Treelite把树模型编译成C代码进一步减少开销。特征计算也可以优化。TF-IDF的transform是主要耗时点尤其是char-level的n-gram。我试过预计算词汇表索引、用稀疏矩阵运算加速效果有限。后来发现瓶颈在Python的GIL改用多进程或者把特征计算用Cython重写提升明显。4.3 监控与模型退化处理上线不是终点。非LLM模型对数据分布变化比LLM敏感因为没有预训练知识兜底。我一般会监控几个指标请求量、延迟分布、预测类别分布、置信度分布。预测类别分布突变往往意味着数据分布变了。比如某个类别的占比突然从10%涨到40%大概率是输入数据出了问题。置信度分布整体下降也是信号说明模型对当前数据不确定。模型退化到一定程度就需要重新训练。我一般设一个阈值比如F1下降超过5个百分点就触发重训。重训用最新标注数据可以全量重训也可以增量训练。XGBoost支持xgb_model参数做增量但要注意增量训练容易遗忘旧数据我一般还是全量重训。A/B测试是验证新模型的好方法。把流量分一小部分给新模型对比关键指标。我一般从5%流量开始观察一周再决定是否全量。5. 几个容易踩的坑和我的应对经验5.1 特征穿越与数据泄露这是最隐蔽也最致命的坑。特征里如果包含了未来信息离线评估会虚高上线就崩。我踩过一次做用户流失预测特征里有个最近30天登录次数。但标注时用的是未来30天是否流失而特征计算窗口和标注窗口有重叠导致模型学到了穿越信息。离线AUC 0.95上线后掉到0.6。排查方法是仔细检查每个特征的计算时间窗口确保只用到预测时间点之前的信息。时间序列数据尤其要注意特征工程必须在时间切分之后做不能先做特征再切分。5.2 类别不平衡下的评估陷阱类别不平衡时准确率会骗人。99%的负样本全预测负也能有99%准确率但模型毫无价值。我一般会看混淆矩阵和每个类别的F1。如果少数类F1很低说明模型没学到少数类的模式。处理方法有几种调整类别权重、过采样少数类、或者用focal loss这类专门处理不平衡的损失函数。XGBoost的scale_pos_weight参数可以调整正负样本权重二分类场景下设为负样本数除以正样本数。多分类可以用sample_weight参数给每个样本加权。但要注意过采样和加权都会改变模型的概率校准。如果业务需要准确的概率输出还要做概率校准用Platt scaling或isotonic regression。5.3 推理速度与精度的权衡生产环境经常要在速度和精度之间做取舍。我的经验是先确定延迟上限再在这个约束下最大化精度。具体做法是固定其他参数单独调n_estimators和max_depth画一条精度-延迟曲线。找到拐点即精度提升开始放缓的位置。我做过一个项目n_estimators从50加到200F1从0.89涨到0.91但推理延迟从2毫秒涨到8毫秒。最后选了100棵树F1 0.90延迟4毫秒性价比最高。特征数量也是影响因素。TF-IDF的max_features从5万降到1万推理速度能提升一倍多精度可能只掉1到2个百分点。如果延迟压力大可以优先砍特征。还有一个技巧是用模型蒸馏。用大模型或复杂模型生成软标签训练一个小模型。我试过用XGBoost蒸馏到逻辑回归精度损失很小但推理速度快了一个数量级。5.4 版本管理与可复现性模型上线后版本管理很重要。我一般会把每次训练的配置、数据版本、模型文件、评估结果都记录下来。用MLflow或简单的文件目录管理都行。关键是要能复现。同样的数据、同样的参数、同样的代码训练结果应该一致。随机种子要固定依赖库版本要锁定。我踩过因为scikit-learn版本升级导致TF-IDF行为变化的坑后来所有项目都用requirements.txt锁版本。模型文件也要版本化。线上服务加载模型时指定版本号回滚时直接切版本。我一般保留最近5个版本方便对比和回退。6. 什么时候该考虑混合方案纯非LLM方案不是万能的。有些场景下混合方案效果更好。我做过一个客服工单分类类别有几十个边界模糊。纯XGBoost方案F1只有0.78纯LLM方案F1能到0.88但延迟太高。最后用了两级方案第一级用快速模型做粗分类把工单分到几个大类第二级只对置信度低或容易混淆的样本调LLM做细分类。整体延迟控制在50毫秒以内F1提升到0.86。这种级联方案的关键是第一级的召回率要高不能把正确类别漏掉。我一般会设一个较低的置信度阈值低于阈值的才走第二级。这样大部分请求走快速路径只有少数走LLM兼顾速度和精度。另一个思路是用LLM做数据增强或标注辅助。用LLM生成一些合成样本或者用LLM预标注再人工修正能大幅降低标注成本。但要注意LLM生成的样本可能有偏需要人工审核。还有一个混合点是特征层面。用LLM的embedding作为特征喂给XGBoost。这样既有LLM的语义表示能力又有树模型的推理速度。我试过用sentence-transformers生成句向量降维后和TF-IDF特征拼接效果比纯TF-IDF好不少。缺点是embedding计算本身有延迟适合离线场景。说到底技术选型没有银弹。LLM有LLM的适用场景非LLM模型有它的价值。关键是想清楚你的约束条件是什么延迟、成本、数据隐私、精度要求。把这些列清楚答案往往就出来了。我个人的经验是大部分分类任务非LLM方案都能做到足够好而且快得多、便宜得多。只有在语义理解确实复杂、且延迟和成本不是瓶颈时才值得上LLM。