最近在做一个文本风控项目时,团队里争论了一个很有意思的问题:既然大语言模型(LLM)已经能读懂长文本、能写摘要、能做情感分析,那我们为什么还要保留 XGBoost、逻辑回归这些经典机器学习模型?干脆全换成 LLM 不就行了?
实际做下来,结论恰恰相反。LLM 并没有“取代”经典机器学习,反而在很多场景里是 LLM 在给经典模型“喂数据”“喂特征”。本文会围绕这个主题展开,先讲清楚 LLM 与 Classical ML 的定位差异,再通过两个完整实战案例,演示如何用 LLM 做特征生成、Embedding 提取,然后把结果输入 XGBoost、逻辑回归等经典模型完成分类任务。
本文适合三类读者:
- 已经在用 LLM 做业务,但觉得 API 成本高、效果不稳定的人;
- 熟悉经典机器学习,想了解怎么把 LLM 融入现有特征工程的人;
- 正在做技术选型,纠结“全上 LLM”还是“LLM + 经典模型”的人。
读完你会掌握:LLM 与经典模型的边界划分、四种常见的 LLM 辅助经典模型的方式、可直接运行的 Python 代码,以及一套混合架构的工程落地建议。
1. 背景:LLM 与 Classical ML 不是“替代关系”
1.1 什么是 Classical ML
经典机器学习通常指依赖人工特征工程和统计学习算法的模型,比如线性回归、逻辑回归、决策树、随机森林、XGBoost、LightGBM、SVM、KNN 等。
这类模型有几个共同特点:
- 输入是结构化特征:数值、类别、稀疏向量,而不是原始的整段自然语言;
- 训练依赖高质量标签:需要人工标注或者规则抽取得到标签;
- 推理速度快、资源占用低:单条样本预测通常在毫秒级别,甚至可以部署在低配 CPU 上;
- 可解释性相对好:树模型可以输出特征重要性,线性模型可以直接看系数。
经典模型最大的短板是“吃不到非结构化信息”。比如一段售后工单,里面有大量自由文本,如果只靠人工抽几个关键词当特征,信息损失很大。过去解决这个问题的办法是 TF-IDF、Word2Vec、主题模型,但这些方法对语义的理解都比较浅。
1.2 什么是 LLM
大语言模型(Large Language Model,LLM)是基于 Transformer 架构、在海量文本上预训练的深度神经网络模型,比如 ChatGPT、ChatGLM、Qwen、LLaMA 等。
LLM 的能力特点是:
- 理解自然语言:能做摘要、翻译、情感分析、实体抽取、意图识别;
- 零样本 / 少样本学习:给几条示例就能完成新任务,不需要专门训练;
- 生成能力:可以写文案、生成 SQL、生成合成样本;
- 复杂推理:能够根据上下文进行多步推理。
但 LLM 也有明显问题:
- 推理成本高:单次调用消耗大量算力,尤其是长文本;
- 响应不稳定:同一个问题,温度参数调高后每次输出可能不一样;
- 延迟高:单次推理可能几百毫秒到几秒,不适合高并发低延迟场景;
- 幻觉风险:模型会生成看起来合理但事实错误的内容;
- 依赖外部 API 或 GPU 资源:部署和运维成本比经典模型高一个量级。
1.3 为什么说 LLM 在“喂养”经典机器学习
回到我开头的项目。我们的目标是判断一段客服工单是否属于“高危投诉”。一开始尝试直接用 LLM 做分类,Prompt 写得再详细,效果在测试集上也只有 92% 左右,而且每次调参、换版本结果都有波动。更麻烦的是,我们每秒要处理几十条工单,如果全部走 LLM,成本完全扛不住。
后来我们换了一个思路:用 LLM 把非结构化文本转成结构化特征,再用 XGBoost 做最终分类。结果准确率提升到 96%,推理延迟从几百毫秒降到十几毫秒。
这就是“LLM 喂养经典模型”的核心含义:LLM 不直接承担最终预测任务,而是充当一个“特征引擎”或“数据加工厂”,把原始文本变成经典模型能理解的特征、标签、样本或者向量。经典模型负责最终的高频、低成本、可解释预测。
可以这样理解两者的关系:
- LLM 负责“理解”:消化非结构化信息,输出结构化结果;
- Classical ML 负责“决策”:基于结构化特征做稳定、快速、可解释的预测。
在后面的章节里,我会把这套思想拆解成具体方案和代码。
2. 环境准备与版本说明
2.1 运行环境
本文的示例代码使用 Python 编写,推荐环境如下:
- 操作系统:Windows 10/11、macOS 或 Linux 均可;
- Python 版本:3.9 或以上;
- 内存:至少 8GB,推荐 16GB;
- 网络:需要能访问 LLM API 或本地推理服务。
如果你本地没有可用的 LLM API,也可以使用 OpenAI 兼容接口的本地框架(如 vLLM、Ollama)启动本地模型。本文代码里所有调用 LLM 的地方都会封装成一个函数,方便你替换成自己的服务地址和模型名称。
2.2 安装依赖
需要安装以下 Python 库:
| 库名 | 用途 | 安装命令 |
|---|---|---|
| pandas | 数据处理 | pip install pandas |
| scikit-learn | 经典模型与评估 | pip install scikit-learn |
| xgboost | XGBoost 分类器 | pip install xgboost |
| requests | 调用 LLM HTTP 接口 | pip install requests |
| openai | 调用 OpenAI 兼容接口 | pip install openai |
建议先创建一个独立的虚拟环境:
python -m venv llm_classical_env source llm_classical_env/bin/activate # Windows 下使用 llm_classical_env\Scripts\activate然后批量安装:
pip install pandas scikit-learn xgboost requests openai2.3 项目结构
实战部分会用到以下文件结构:
llm_feed_classical/ ├── config.py # 全局配置,包含 API 地址、模型名、参数 ├── llm_client.py # LLM 调用封装 ├── feature_extractor.py # 用 LLM 提取结构化特征 ├── embedding_utils.py # 用 LLM 生成文本 Embedding ├── train_xgb.py # 训练 XGBoost 模型 ├── train_embedding_clf.py # 训练 Embedding + 逻辑回归模型 └── data/ ├── samples.csv # 原始文本样本 └── features.csv # 生成的中间数据3. LLM 为 Classical ML 提供能力的四种方式
在动手写代码之前,先梳理清楚 LLM 到底能为经典模型提供哪些能力。根据我实践中的经验,最常见的有四种模式。
3.1 特征工程:把非结构化文本变成结构化特征
这是最直接、也最常用的一种方式。
经典模型无法直接读取“用户投诉说订单一直不到货,客服也不理人”这样的文本。传统做法是 TF-IDF 或关键词匹配,但语义理解非常有限。
LLM 可以承担这个转换过程。比如让 LLM 输出:
{ "是否涉及物流": 1, "是否涉及客服态度": 1, "是否提及退款": 0, "情绪强度": 0.8, "紧急程度": 0.7 }这段 JSON 就是经典模型的标准输入特征。LLM 在这里扮演的是一个“语义特征抽取器”。
这种方式的优点是特征含义明确、可解释性强;缺点是调用成本与文本长度成正比,所以建议只对原始文本做一次离线或准实时的特征生成,然后缓存结果。
3.2 标签生成与数据标注
经典机器学习非常依赖高质量标签,但人工标注成本高、速度慢。
LLM 可以辅助生成标签候选。比如先让 LLM 给一批无标签文本打标,再用人工抽检的方式修正错误样本。这样做的本质是“LLM 先标注,人来复核,经典模型再训练”。
其中有一个关键点需要特别注意:LLM 生成的标签不能直接当 gold label 用,必须经过抽样评估。因为 LLM 存在系统性偏差,比如对不同类别可能有偏好,直接采用会把偏差带入训练集。
3.3 数据增强与合成数据
当某一类别的样本太少时,经典模型很容易过拟合。传统办法是 SMOTE 之类的采样方法,但作用有限。
LLM 可以用来生成同分布的合成样本。例如,现有 100 条“物流投诉”样本,可以让 LLM 基于这些样本改写生成 500 条新的、语义相似但不重复的样本,然后合并进训练集。
这种方式适合文本分类任务,但生成的数据需要做质量过滤,建议用相似度计算或人工抽检来控制生成质量。
3.4 嵌入向量(Embedding)作为特征输入
LLM 的倒数第二层输出可以看作一段文本的向量表示,也就是 Embedding。这个向量保留了文本的语义信息,通常有几百到几千维。
经典模型可以直接把 Embedding 当作特征向量输入。例如:
- 把用户问题转成 1536 维向量;
- 输入逻辑回归或 XGBoost 进行分类。
这种方式不需要 LLM 输出结构化 JSON,只需要调用 API 的 embeddings 接口,速度比完整生成式调用快,成本也更低。
它的缺点是特征维度较高,需要配合降维或正则化处理;同时 Embedding 本身的可解释性不如“是否涉及物流”这种离散特征。
3.5 选型建议
| 业务需求 | 推荐方案 | 原因 |
|---|---|---|
| 需要明确解释为什么不通过 | LLM 特征 + XGBoost | 特征含义清晰,树模型可输出重要性 |
| 高并发、低延迟、文本语义强 | Embedding + 逻辑回归 | Embedding 接口快,逻辑回归稳定 |
| 正样本太少,无法训练 | LLM 生成合成样本 | 扩充少数类,缓解类别不平衡 |
| 完全没有标注数据 | LLM 打标 + 人工复核 | 降低标注成本,快速启动 |
4. 完整实战:LLM 特征生成 + XGBoost 二分类
下面进入完整实战。我们的任务是:判断一段客服工单是否属于“高危投诉”。
“高危投诉”定义为:用户表达了强烈不满,并且可能升级到媒体曝光、监管投诉或法律途径。
4.1 场景说明
原始数据是客服工单的文本记录,例如:
"你们的快递放驿站也不通知,我找了两天才找到,气死我了,再这样我就去12315投诉了!"我们希望输出一个二分类标签:1 表示高危投诉,0 表示普通投诉。
整体流程分三步:
- 调用 LLM,从工单文本中抽取结构化特征;
- 用特征训练 XGBoost 分类器;
- 在测试集上评估效果。
4.2 全局配置
先写配置文件config.py:
# 文件路径:llm_feed_classical/config.py LLM_API_BASE = "http://localhost:8000/v1" LLM_API_KEY = "EMPTY" LLM_MODEL_NAME = "Qwen2.5-7B-Instruct" # 特征抽取时使用的温度参数,越低越稳定 FEATURE_TEMPERATURE = 0.1 # 分类模型超参数 XGB_PARAMS = { "n_estimators": 300, "max_depth": 4, "learning_rate": 0.05, "subsample": 0.8, "colsample_bytree": 0.8, "eval_metric": "logloss", "use_label_encoder": False, "random_state": 42, }这里说明两点:
LLM_API_BASE使用 OpenAI 兼容地址,可以替换为任意兼容服务,也可以换成https://api.openai.com/v1之类的线上接口;XGB_PARAMS中的use_label_encoder在新版本 XGBoost 中可能不需要,如果报错可以去掉。
4.3 封装 LLM 客户端
新建llm_client.py,统一封装 LLM 调用:
# 文件路径:llm_feed_classical/llm_client.py import json from openai import OpenAI from config import LLM_API_BASE, LLM_API_KEY, LLM_MODEL_NAME, FEATURE_TEMPERATURE class LLMClient: def __init__(self): self.client = OpenAI(base_url=LLM_API_BASE, api_key=LLM_API_KEY) self.model = LLM_MODEL_NAME def extract_features(self, text: str) -> dict: """从一段文本中抽取结构化特征,返回 JSON 字典。""" prompt = f""" 你是一个文本特征抽取器。请阅读下面的客服工单文本,并输出一个 JSON 对象。 JSON 字段说明: - is_logistics_issue: 是否涉及物流问题,0 或 1 - is_service_attitude: 是否涉及客服态度问题,0 或 1 - is_refund_issue: 是否涉及退款问题,0 或 1 - emotion_intensity: 情绪强度,0 到 1 之间的小数 - urgency: 紧急程度,0 到 1 之间的小数 - contains_threat: 是否包含投诉威胁(如投诉、曝光、法律等),0 或 1 只输出 JSON,不要输出其他内容。 工单文本: {text} """ resp = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=FEATURE_TEMPERATURE, ) content = resp.choices[0].message.content.strip() # 兼容模型输出可能包含 ```json 包裹的情况 if content.startswith("```"): content = content.strip("`") if content.startswith("json"): content = content[4:] try: result = json.loads(content) except json.JSONDecodeError: # 解析失败时返回空特征,后续可以用默认值兜底 return {} return result这里需要注意几点:
- 温度参数设成 0.1,是为了让输出尽量稳定。
- 我在 Prompt 里明确要求“只输出 JSON”,但不同模型可能仍然会多输出一些解释文字,所以代码里做了兼容处理。
- 解析失败时返回空字典,后续代码会用默认值兜底,避免单条数据异常导致整个流程中断。
4.4 批量生成特征
新建feature_extractor.py,读取原始文本,批量调用 LLM 生成特征并保存:
# 文件路径:llm_feed_classical/feature_extractor.py import time import pandas as pd from llm_client import LLMClient def build_feature_dataset(input_path: str, output_path: str, max_samples: int = None, batch_sleep: float = 0.2): df = pd.read_csv(input_path) if max_samples: df = df.head(max_samples) client = LLMClient() records = [] for idx, row in df.iterrows(): text = row["text"] features = client.extract_features(text) record = { "id": row.get("id", idx), "text": text, "label": row.get("label", None), "is_logistics_issue": features.get("is_logistics_issue", 0), "is_service_attitude": features.get("is_service_attitude", 0), "is_refund_issue": features.get("is_refund_issue", 0), "emotion_intensity": features.get("emotion_intensity", 0.0), "urgency": features.get("urgency", 0.0), "contains_threat": features.get("contains_threat", 0), } records.append(record) # 避免请求太快触发限流 time.sleep(batch_sleep) result_df = pd.DataFrame(records) result_df.to_csv(output_path, index=False) print(f"生成完成,共 {len(result_df)} 条,保存至 {output_path}") return result_df if __name__ == "__main__": # 示例:先对训练集前 100 条生成特征 build_feature_dataset( input_path="data/samples.csv", output_path="data/features.csv", max_samples=100, )这里建议对 LLM 调用做缓存。如果任务失败重跑,可以先把已生成的特征保存好,下次跳过已处理的数据,避免重复花钱。
4.5 训练 XGBoost 模型
新建train_xgb.py:
# 文件路径:llm_feed_classical/train_xgb.py import pandas as pd from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score import xgboost as xgb from config import XGB_PARAMS FEATURE_COLUMNS = [ "is_logistics_issue", "is_service_attitude", "is_refund_issue", "emotion_intensity", "urgency", "contains_threat", ] def main(): df = pd.read_csv("data/features.csv") # 丢弃没有标签的样本 df = df.dropna(subset=["label"]) X = df[FEATURE_COLUMNS].fillna(0.0) y = df["label"].astype(int) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) model = xgb.XGBClassifier(**XGB_PARAMS) model.fit(X_train, y_train) y_pred = model.predict(X_test) y_proba = model.predict_proba(X_test)[:, 1] print("=== 分类效果 ===") print(classification_report(y_test, y_pred)) print("AUC: {:.4f}".format(roc_auc_score(y_test, y_proba))) # 输出特征重要性 importance = pd.Series(model.feature_importances_, index=FEATURE_COLUMNS) print("\n=== 特征重要性 ===") print(importance.sort_values(ascending=False)) # 保存模型 model.save_model("xgb_hazard_model.json") print("\n模型已保存至 xgb_hazard_model.json") if __name__ == "__main__": main()这段代码做的事情:
- 读取上一步生成的
features.csv; - 剔除没有标签的行;
- 用 6 个 LLM 生成的特征训练 XGBoost;
- 在测试集上输出精确率、召回率、F1 和 AUC;
- 输出特征重要性,方便我们验证 LLM 抽取的特征是否真的有用。
4.6 运行与验证
执行顺序如下:
cd llm_feed_classical python feature_extractor.py python train_xgb.py如果一切正常,你会看到类似这样的输出:
=== 分类效果 === precision recall f1-score support 0 0.95 0.96 0.95 60 1 0.90 0.88 0.89 40 accuracy 0.93 100 macro avg 0.92 0.92 0.92 100 weighted avg 0.93 0.93 0.93 100 AUC: 0.9712 === 特征重要性 === contains_threat 0.35 emotion_intensity 0.27 urgency 0.18 is_logistics_issue 0.09 is_service_attitude 0.07 is_refund_issue 0.04注意:这里的数字只是示例,实际结果取决于你的数据集、模型版本和 Prompt 设置。
4.7 结果说明
从特征重要性可以看出,“是否包含投诉威胁”和“情绪强度”这两个由 LLM 生成的特征,对“高危投诉”的判断贡献最大。这是符合业务直觉的:用户提到“12315 投诉”“曝光”“起诉”,往往是高危信号。
这套方案的价值在于:
- 最终模型只有 6 维特征,XGBoost 训练和推理都非常快;
- 特征有明确业务含义,可以解释为什么某条工单被判为高危;
- 后续如果 LLM 升级,只需要重新生成特征,不需要重新设计整个分类流程。
5. 实战进阶:Embedding + 逻辑回归分类
第二种实战方案是用 LLM 的 Embedding 接口生成文本向量,然后输入逻辑回归模型。
5.1 为什么用 Embedding
结构化特征抽取适合“特征含义明确”的场景,但有些场景里我们不知道哪些特征是关键的。比如一条工单里可能隐含了复杂的语义关系,很难用几个离散字段覆盖。
Embedding 可以保留更完整的语义信息。缺点是特征维度高,而且不好直接解释。所以这里选用逻辑回归,它训练快、对高维稀疏特征有一定鲁棒性,也方便加 L2 正则化。
5.2 生成 Embedding
新建embedding_utils.py:
# 文件路径:llm_feed_classical/embedding_utils.py import time import pandas as pd from openai import OpenAI from config import LLM_API_BASE, LLM_API_KEY, LLM_MODEL_NAME class EmbeddingGenerator: def __init__(self): self.client = OpenAI(base_url=LLM_API_BASE, api_key=LLM_API_KEY) self.model = LLM_MODEL_NAME def get_embedding(self, text: str): resp = self.client.embeddings.create( model=self.model, input=text, ) return resp.data[0].embedding def generate_embeddings(input_path: str, embedding_path: str, max_samples: int = 200, batch_sleep: float = 0.1): df = pd.read_csv(input_path) if max_samples: df = df.head(max_samples) generator = EmbeddingGenerator() vectors = [] for idx, row in df.iterrows(): vec = generator.get_embedding(row["text"]) vectors.append(vec) time.sleep(batch_sleep) if (idx + 1) % 20 == 0: print(f"已处理 {idx + 1} 条") embedding_df = pd.DataFrame(vectors) embedding_df.insert(0, "label", df["label"].values) embedding_df.insert(0, "text", df["text"].values) embedding_df.to_pickle(embedding_path) print(f"Embedding 已保存至 {embedding_path},维度:{len(vectors[0])}") if __name__ == "__main__": generate_embeddings( input_path="data/samples.csv", embedding_path="data/embeddings.pkl", max_samples=200, )注意:Embedding 的维度与 LLM 模型有关。比如某些模型的 Embedding 是 1024 维、1536 维或者 4096 维,这与普通向量数据库里的向量维度含义一致。如果你的模型输出维度不稳定,请先打印一下len(vectors[0])确认。
5.3 训练逻辑回归分类器
新建train_embedding_clf.py:
# 文件路径:llm_feed_classical/train_embedding_clf.py import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score from sklearn.pipeline import Pipeline def main(): df = pd.read_pickle("data/embeddings.pkl") y = df["label"].astype(int).values X = df.drop(columns=["label", "text"]).values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 逻辑回归对特征尺度敏感,建议先标准化 pipeline = Pipeline([ ("scaler", StandardScaler()), ("clf", LogisticRegression(C=1.0, max_iter=1000, random_state=42)), ]) pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) y_proba = pipeline.predict_proba(X_test)[:, 1] print("=== Embedding + 逻辑回归效果 ===") print(classification_report(y_test, y_pred)) print("AUC: {:.4f}".format(roc_auc_score(y_test, y_proba))) if __name__ == "__main__": main()这里使用StandardScaler做标准化,是因为逻辑回归使用梯度下降优化,特征尺度差异太大会影响收敛速度。
5.4 对比实验
在实际项目中,建议同时跑三套基线,对比效果:
| 方案 | 特征 | 优点 | 缺点 |
|---|---|---|---|
| 基线:TF-IDF + XGBoost | TF-IDF 稀疏向量 | 无需外部依赖,成本低 | 语义理解弱 |
| 方案一:LLM 特征 + XGBoost | 6 维结构化特征 | 可解释性好、推理快 | 依赖 LLM 抽取质量 |
| 方案二:Embedding + LR | 高维稠密向量 | 语义信息完整 | 可解释性差 |
判断一个方案是否适合上线,不能只看离线指标,还要看:
- 线上推理延迟能否满足要求;
- 单个样本的处理成本;
- 特征漂移后如何快速恢复;
- 出现误判时,能否快速定位原因。
在我的实践中,方案一适合需要解释的业务,方案二适合文本语义差异大、又要求自动化程度高的场景。
6. 常见问题与排查思路
在写这套代码和上线过程中,比较容易踩到下面几个坑。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| LLM 返回的内容不是 JSON,解析失败 | 模型指令遵循能力不足,或温度过高 | 降低温度;Prompt 中给出 JSON 示例;增加解析兜底逻辑 |
| 特征全部为 0,模型效果和随机差不多 | LLM 接口返回异常,或字段名不匹配 | 打印原始返回内容,检查字段名是否与 Prompt 一致 |
| 调用 LLM 速度太慢,批量生成要跑几小时 | 单条串行调用,且没有做并发 | 使用线程池或异步并发;对已处理样本做缓存 |
| Embedding 维度不同导致训练报错 | 换了模型,向量维度变化 | 固定模型版本;保存向量时确认维度 |
| LLM 生成标签有偏差,模型学偏 | 未做人工复核,直接把生成标签当 ground truth | 抽样检查,结合置信度过滤,必要时人工修正 |
| 线上成本过高 | 每条请求都调用 LLM,且输入文本过长 | 对输入做截断;对相同或相似文本做缓存;只对不确定样本走 LLM |
| API 请求超时 | 网络不稳定或服务端负载高 | 增加重试机制和超时时间;使用本地推理服务降低延迟 |
这里重点说一下 LLM 调用失败时的兜底策略。
千万不要让 LLM 返回异常直接中断整个流程。我的做法是:
# 文件路径:llm_feed_classical/llm_client.py 中的重试示例 import time from openai import OpenAI def extract_features_with_retry(self, text: str, max_retries: int = 3): for attempt in range(max_retries): try: return self.extract_features(text) except Exception as e: print(f"第 {attempt + 1} 次调用失败:{e}") if attempt < max_retries - 1: time.sleep(2 ** attempt) return {}这个逻辑使用指数退避重试,最多尝试 3 次,最后一次失败时返回空字典,由下游用默认值填充。这样可以保证特征生成任务不会因为单条数据失败而整体失败。
另一个容易忽略的问题是“缓存”。
假设你有 100 万条历史工单需要生成特征,全部调用 LLM 是一笔不小的费用。如果中途失败重跑,已经成功的结果就浪费了。建议在特征生成前,先检查id是否已经在输出文件中存在,存在则跳过。
def load_existing_ids(output_path: str): import os if not os.path.exists(output_path): return set() df = pd.read_csv(output_path, usecols=["id"]) return set(df["id"].tolist())在循环里加上判断:
existing_ids = load_existing_ids(output_path) for idx, row in df.iterrows(): if row["id"] in existing_ids: continue # 继续处理这个改动看起来简单,但能省下大量重复调用成本。
7. 最佳实践与工程建议
7.1 明确 LLM 的职责边界
在混合架构里,一定要想清楚“什么任务交给 LLM,什么任务交给经典模型”。
我的经验是:
- 适合 LLM 的任务:从非结构化文本中提取结构化信息、生成标签候选、生成合成样本;
- 适合经典模型的任务:高频预测、低延迟决策、强解释性要求、资源受限环境。
一个比较合理的分工:LLM 离线加工特征,经典模型在线实时预测。这样既利用了 LLM 的语义理解能力,又保证了线上服务稳定可控。
7.2 严格控制 LLM 输出质量
LLM 的输出天然带有不确定性,不能默认它每次都能给出正确结果。
建议做到:
- 温度参数尽量低,特征抽取任务建议 0 到 0.2;
- 每次输出都做格式校验,解析失败要有兜底;
- 定期抽检特征质量,观察字段分布是否漂移;
- 对关键业务字段,设置合法值域检查,例如“0 或 1”的字段不允许出现 0.5。
在实践中,我发现最有效的做法是:先抽 100 条数据人工核对 LLM 输出特征,确认准确率达标后再全量生成。全量上线后,每周再随机抽 50 条做质量巡检。
7.3 特征一致性是工程核心
LLM 特征生成有一个容易被忽视的问题:不同时间、不同模型版本、不同 Prompt 版本生成的特征分布可能不一致。
比如上个月用 7B 模型,这个月换成了 70B 模型,emotion_intensity的分布可能完全不同。这样会导致未来数据送入旧模型时预测偏移。
解决办法:
- 固定模型版本和 Prompt 版本,任何变更走版本管理;
- 特征生成结果落库,记录模型名、Prompt 版本、生成时间;
- 每次模型或 Prompt 升级,重新生成训练集,重新训练经典模型;
- 线上监控特征的均值、方差、缺失率,发现异常及时报警。
7.4 成本与性能优化
LLM 调用是这套架构里最大的成本项。优化方向主要有四个:
- 批量处理:能离线算的特征不要在线算;
- 缓存:相同或相似文本直接命中缓存;
- 截断:只保留文本关键段落,减少 Token 消耗;
- 混合路由:简单文本用规则或 TF-IDF 处理,只有复杂文本才走 LLM。
这里给出一个简单的混合路由思路:
def route_to_llm(text: str, keyword_rules: dict) -> bool: """如果命中关键词规则,则直接走规则,不调用 LLM。""" for keyword_set in keyword_rules.values(): if any(kw in text for kw in keyword_set): return False return True比如“包含 12315 / 投诉 / 曝光 / 起诉”等强关键词时,直接标记为候选高危,不需要再调用 LLM 抽取特征。这样可以大幅降低 LLM 调用量。
7.5 可解释性与模型监控
经典模型相对容易解释,这在风控、金融、医疗等场景是很大的优势。如果你选择了 LLM + 经典模型的方案,建议把“解释能力”做进去:
- 树模型输出特征重要性;
- 关键特征(如 contains_threat)单独记录并可视化;
- 误判样本定期复盘,看是 LLM 特征错了,还是经典模型决策错了。
这个排查步骤非常重要:如果发现是 LLM 特征错了,需要调整 Prompt;如果是经典模型决策错了,需要调整阈值或换模型。两者的问题处理方式完全不同。
7.6 安全与权限边界
如果你在项目中使用 LLM API,需要注意几个安全事项:
- API Key 不要明文写在代码里,使用环境变量或配置中心管理;
- 发送给 LLM 的文本如果包含用户隐私,要做脱敏处理;
- 对 LLM 返回内容做校验,防止模型被 Prompt 注入后输出恶意内容;
- 涉及生产环境的数据处理链路,先在小规模测试集验证,再灰度上线。
特征生成属于数据加工链路,建议对输入的敏感字段(手机号、身份证、地址等)先做脱敏,再调用外部或内部 LLM 服务,避免隐私数据外泄。
8. 总结与下一步学习方向
本文的核心观点是:LLM 与经典机器学习不是替代关系,而是上下游关系。LLM 可以承担“理解文本、生成特征、产出标签、合成样本”这些经典模型不擅长的工作,而经典模型可以承担“高频、低成本、可解释”的最终预测任务。
围绕这个思路,我们完整跑通了两套方案:
- 第一套:LLM 抽取结构化特征(物流问题、客服态度、情绪强度、紧急程度等),输入 XGBoost 做分类;
- 第二套:LLM 生成文本 Embedding,输入逻辑回归做分类。
同时,我整理了这套架构在工程落地时最需要注意的几个问题:LLM 输出不稳定、特征一致性、调用成本、缓存设计、模型监控和安全边界。
下一步,你可以继续深入研究这几个方向:
- 特征抽取 Prompt 优化:用 few-shot 示例提升抽取准确率;
- 多模态扩展:把语音转写文本、图片 OCR 文本也纳入特征生成链路;
- 自动路由:用一个小模型判断“是否需要调用 LLM”,进一步降低成本;
- 在线学习:经典模型上线后,结合新标注数据做增量更新。
我建议你拿自己手头的数据先跑一遍第 4 节的完整流程,哪怕只有几百条样本,也能直观感受到“LLM 特征 + 经典模型”相比“直接用 LLM 分类”在成本、稳定性和可解释性上的差异。技术在迭代,但“让合适的模型做合适的事”这个原则,短期之内不会变。