这次我们聊一个经常被问歪的问题:LLM 出来后,经典机器学习是不是该淘汰了?从生产系统的角度看,答案恰恰相反。LLM 更强的地方不是“独立做决策”,而是把非结构化文本变成结构化信号,然后把最终判断交给 XGBoost、逻辑回归这类经典 ML 模型。这类架构有个很直白的描述:LLMs Don't Replace Classical ML – They Feed It,LLM 不是替代经典机器学习,而是在喂它。
这种模式不是过渡方案,而是当前很多稳定业务的落地结构。LLM 负责理解文本、生成标签、抽取实体、产出 embedding,经典 ML 负责打分、排序、分类和回归。前者解决“懂不懂”,后者解决“稳不稳”。
这篇文章会拆开讲清楚这个混合架构怎么设计、怎么落地:先说核心能力,再说适用边界,然后给出一套 LLM 特征生成 + 经典 ML 训练/推理的完整工程示例,包括批量任务、接口封装、性能观察和排查思路。如果你的团队正在纠结“要不要用 LLM 重写所有模型”,这篇可以直接当讨论底稿。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 架构核心 | LLM 作为特征生成器/标注器,Classical ML 作为最终决策器 |
| 主要功能 | 文本分类、意图识别、情感分析、实体抽取、标签生成、排序特征构造 |
| 硬件门槛 | 经典 ML 部分 CPU 即可;LLM 部分取决于模型规模,可用 API 或本地量化模型 |
| 启动方式 | LLM 可用 API 或本地推理服务;经典 ML 模型可单独封装为 REST 服务 |
| 接口能力 | LLM 特征生成可批量调用;经典 ML 推理服务可对下游业务开放 |
| 批量任务 | 支持。LLM 离线批量生成特征,经典 ML 在线/离线批量推理 |
| 可解释性 | 经典 ML 提供特征重要性和 SHAP 解释,弥补 LLM 黑盒问题 |
| 适合场景 | 非结构化文本多、标注成本高、需要稳定可解释决策的业务 |
| 不适合场景 | 对毫秒级延迟和成本极其敏感、文本理解需求简单的规则场景 |
从这张表能看出来,这个架构的核心价值不是“哪个模型更强”,而是把两类模型放到各自擅长的位置上。LLM 做特征层,经典 ML 做决策层。
2. 适用场景与使用边界
2.1 适合解决的问题
先说适合的场景。最典型的一类是非结构化文本到结构化标签的转换。例如客服工单分类、评论情感分析、舆情事件识别、简历初筛、合同条款抽取。这些任务过去依赖人工标注和关键词规则,成本高、覆盖率低。现在可以用 LLM 把文本变成“是否包含投诉意图”“是否涉及退款纠纷”“语气是否负面”等离散特征。
第二类是搜索和推荐中的文本理解。query 很短,歧义大,用户搜“苹果”可能是水果也可能是手机。LLM 可以先判断领域,生成“query_领域=数码”这样的特征,再交给排序模型使用。经典 ML 负责最终的 CTR 预估或相关性排序。
第三类是文档处理流水线。PDF、邮件、工单进入系统后,LLM 抽取摘要、主题、关键实体,经典 ML 基于这些特征做路由、优先级判断或风险分级。
2.2 不适合的边界
不是所有场景都需要这样绕一圈。
如果任务本身可以用简单规则解决,比如“包含‘退款’两个字的工单标记为退款类”,直接上正则更划算。LLM 调用会引入延迟和成本,没必要。
如果业务对单次推理延迟有毫秒级硬要求,例如广告实时竞价,那么在链路上额外插入一次 LLM 调用会非常危险。更稳妥的做法是 LLM 离线生成特征,在线只跑经典 ML。
如果数据完全不能出内网,又没有足够的 GPU 部署本地 LLM,那么需要重新评估。经典 ML 部分对硬件要求很低,但 LLM 部分如果靠 CPU 跑大模型,吞吐量会很难看。这种情况下可以优先考虑小参数量的量化模型,或者干脆只在离线场景使用 LLM。
3. 架构设计:LLM 怎么“喂”经典 ML
在动手写代码之前,先明确三种常见的喂法。
第一种:离线特征标注。LLM 对历史文本批量打标,生成训练数据集。这些标签可以是类别,也可以是理由、实体列表、情感分数。生成结果进入经典 ML 的训练流程,替代部分人工标注。这种模式成本低,适合冷启动。
第二种:在线特征抽取。请求进来后,业务系统先调用 LLM,把文本转化为一组结构化特征,再传给经典 ML 模型打分。经典 ML 模型像以前一样做决策,只是输入从“原始文本”变成了“LLM 抽取后的特征 + 原有业务特征”。
第三种:Embedding 稠密特征。LLM 输出文本向量,作为经典 ML 模型的稠密特征。这种方式比较灵活,但可解释性会弱一些。建议把离散标签和 embedding 同时作为特征,兼顾效果和可解释性。
无论哪种方式,核心原则一致:LLM 只负责把不可计算的文本变成可计算的特征,最终决策交给经典 ML。这样做的直接收益是可解释性。XGBoost 可以输出 feature importance,可以算 SHAP,但 GPT 系列模型很难说清楚“为什么把这个工单判为高优先级”。业务审计、风控合规、模型上线评审都需要这部分解释能力。
另一个收益是稳定。LLM 版本升级后,同一句话可能输出不同标签。但经典 ML 的输入只要还是那组特征,行为就不会突然跳变。即使 LLM 特征分布变化,也可以通过监控发现,而不是整个线上预测结果失控。
4. 环境准备与前置条件
这一节不绑定某个具体项目,给的是通用检查清单。以 Python 环境为例。
4.1 操作系统与语言
建议 Linux 或 macOS 作为开发环境,Windows 也能跑,但要注意路径和编码问题。Python 版本建议 3.10 或更高。LLM 调用如果走 HTTP API,对 Python 版本要求不高;如果本地加载模型做推理,需要 PyTorch 版本与 CUDA 版本匹配。
4.2 LLM 部分
LLM 可以是云服务 API,也可以是本地部署的推理服务。如果是本地部署,常见方案包括 vLLM、Ollama、llama.cpp 等。项目里只需要用一个 OpenAI 兼容的 HTTP 接口,这样换服务商、换本地模型都不用改业务代码。
# 以 OpenAI 兼容接口为例,通过环境变量配置访问地址 export LLM_BASE_URL="http://127.0.0.1:8000/v1" export LLM_API_KEY="local-test-key" export LLM_MODEL="qwen2.5-7b-instruct"如果是纯 API 场景,LLM_BASE_URL换成对应服务的地址,LLM_API_KEY换成真实密钥。为了安全,密钥不要写死在代码里。
4.3 经典 ML 部分
经典 ML 训练库用xgboost、scikit-learn,推理服务用FastAPI。特征处理用pandas。
python -m venv .venv source .venv/bin/activate pip install -U pip pip install pandas scikit-learn xgboost fastapi uvicorn requests4.4 项目目录结构
建议把 LLM 特征生成和经典 ML 训练分成两个模块,避免互相污染。
llm_feed_ml/ ├── config/ │ └── settings.yaml ├── data/ │ ├── raw/ │ └── processed/ ├── features/ │ ├── llm_feature_generator.py │ └── schema.py ├── models/ │ ├── train.py │ └── predict.py └── services/ └── api.pydata/raw放原始文本,data/processed放 LLM 生成的特征和最终训练集,features放提示词和解析逻辑,models放训练和推理代码,services放 REST 服务。这样 LLM 相关的依赖和经典 ML 相关的依赖可以分开管理。
5. 安装部署与启动方式
5.1 启动本地 LLM 推理服务
如果选择本地部署,先启动一个 OpenAI 兼容的推理服务。不同框架启动命令不同,这里给一个通用思路:
# 示例:启动一个兼容 OpenAI 接口的本地服务 # 实际命令以所选推理框架的文档为准 python -m vllm.entrypoints.openai.api_server \ --model /path/to/local/model \ --served-model-name my-llm \ --port 8000启动后,可以用下面的命令验证服务是否可用:
curl http://127.0.0.1:8000/v1/models \ -H "Authorization: Bearer local-test-key"如果返回模型列表,说明 LLM 服务已经就绪。这一步的关键是确认接口协议是否兼容 OpenAI 格式,不兼容的话后续代码要改请求体。
5.2 验证 LLM 特征生成
在跑批量任务之前,先用一条文本验证输出格式。这里定义一个简单的特征生成函数,输入文本,输出 JSON:
# features/schema.py from pydantic import BaseModel class TextFeature(BaseModel): is_complaint: bool is_urgent: bool category: str sentiment: str reason: str# features/llm_feature_generator.py import os import json from openai import OpenAI client = OpenAI( base_url=os.getenv("LLM_BASE_URL"), api_key=os.getenv("LLM_API_KEY"), ) SCHEMA_PROMPT = """你是文本特征抽取器。 请从用户文本中抽取以下字段: - is_complaint: 是否包含投诉意图 - is_urgent: 是否紧急 - category: 分类,只能是【账单, 退换货, 物流, 售后, 其他】之一 - sentiment: 情感,只能是【正面, 中性, 负面】之一 - reason: 一句话说明判断理由 只输出 JSON,不要输出其他内容。 """ def generate_feature(text: str) -> dict: response = client.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=[ {"role": "system", "content": SCHEMA_PROMPT}, {"role": "user", "content": text}, ], temperature=0, response_format={"type": "json_object"}, ) content = response.choices[0].message.content parsed = json.loads(content) return parsed注意,response_format参数并非所有本地推理服务都支持。如果不支持,需要把“只输出 JSON”写进提示词,并在解析失败时做重试。这里只是一个通用示例,实际字段名和提示词要根据业务调整。
5.3 训练经典 ML 模型
LLM 把原始文本转换成结构化特征后,把这些特征和标签拼成一张表,训练 XGBoost 分类器。
# models/train.py import pandas as pd import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report df = pd.read_csv("data/processed/train_features.csv") # 这里假设特征列已经通过 LLM 生成 feature_cols = ["is_complaint", "is_urgent", "sentiment_score"] X = df[feature_cols] y = df["label"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = xgb.XGBClassifier( n_estimators=300, max_depth=4, learning_rate=0.05, eval_metric="logloss", use_label_encoder=False, ) model.fit( X_train, y_train, eval_set=[(X_test, y_test)], verbose=False, ) print(classification_report(y_test, model.predict(X_test))) # 保存模型 model.save_model("models/classifier.json")这里的sentiment_score需要把类别特征转成数值,具体方式取决于模型和特征类型。如果 LLM 输出的是文本标签,比如“负面”,需要先做映射。更稳妥的做法是在特征生成阶段直接输出数值分数,减少下游转换。
6. 功能测试与效果验证
6.1 LLM 输出稳定性测试
先测最基础的一件事:同一句话,多次调用 LLM,输出是否稳定。
# 测试同一句话多次调用的输出差异 text = "我买的东西三天没发货,再不发我就投诉" results = [generate_feature(text) for _ in range(5)] for r in results: print(r)将temperature设为 0,通常可以显著减少随机性。如果仍然不稳定,说明提示词约束不够,或者模型能力不足。这时可以加 few-shot 示例,或者在解析层对多次采样结果做投票。
6.2 特征有效性测试
LLM 生成的特征到底有没有用,不能靠感觉。用最简单的逻辑回归或决策树跑一版 baseline,然后做特征重要性排序。如果 LLM 生成的特征排在最前面,说明确实有信息量;如果排在后面,说明提示词设计有问题,或者这个特征对当前任务不敏感。
import xgboost as xgb model = xgb.XGBClassifier() model.load_model("models/classifier.json") importance = sorted( zip(model.feature_names_in_, model.feature_importances_), key=lambda x: x[1], reverse=True, ) for name, score in importance: print(f"{name}: {score:.4f}")6.3 对照实验
建议做三组对照:
| 方案 | 特征来源 | 决策模型 | 预期收益 |
|---|---|---|---|
| A | 关键词匹配 | 逻辑回归 | 成本最低,效果一般 |
| B | LLM 结构化特征 | XGBoost | 理解能力更强,效果可能最好 |
| C | 纯 LLM 直接输出标签 | 无 | 灵活,但成本高、不稳定 |
如果 B 没有显著优于 A,说明业务文本用关键词就能解决,不需要引入 LLM。如果 B 和 C 效果接近但 B 更稳定,那这个架构就是合理的。这里重点看的是成本和稳定性,不只是准确率。
6.4 失败场景模拟
特征生成最怕两种情况:解析失败、输出不符合 schema。测试时故意输入超长文本、空文本、混合中英文、包含特殊字符的文本,确认解析层能兜住。
test_cases = [ "", "你好", "退退退退退退退退退退" * 200, "Order #12345 hasn't arrived, please refund", ] for case in test_cases: try: feat = generate_feature(case) print(feat) except Exception as e: print(f"解析失败: {e}")空文本和超长文本需要特殊处理。空文本可以直接跳过 LLM 调用,否则浪费 token;超长文本需要截断或分段,否则可能超出模型上下文窗口。
7. 接口 API 与批量任务
7.1 LLM 特征批量生成
生产环境中不可能一条条手动调用,需要写一个批量脚本。输入是一个 CSV,每行一条文本,输出是带特征列的 parquet 文件。
# scripts/batch_generate_features.py import pandas as pd from tqdm import tqdm from features.llm_feature_generator import generate_feature df = pd.read_csv("data/raw/tickets.csv") texts = df["content"].tolist() rows = [] for text in tqdm(texts, desc="generating features"): try: feat = generate_feature(text) rows.append(feat) except Exception as e: rows.append({ "is_complaint": None, "is_urgent": None, "category": None, "sentiment": None, "reason": f"error: {e}", }) feat_df = pd.DataFrame(rows) output = pd.concat([df, feat_df], axis=1) output.to_parquet("data/processed/train_features.parquet")批量任务必须做断点续跑。更简单的做法是每条文本生成一个哈希,存在缓存表里。下次再跑时先查缓存,已经跑过的直接跳过。
7.2 经典 ML 推理服务
经典 ML 模型封装成 REST 服务后,下游业务只需要传文本和基础特征,服务内部负责调用 LLM 生成特征,再返回最终预测结果。
# services/api.py import os import pandas as pd import xgboost as xgb from fastapi import FastAPI from pydantic import BaseModel from features.llm_feature_generator import generate_feature app = FastAPI() model = xgb.XGBClassifier() model.load_model("models/classifier.json") class PredictRequest(BaseModel): text: str user_level: int = 0 @app.post("/predict") def predict(req: PredictRequest): feat = generate_feature(req.text) # 组装经典 ML 需要的一维特征 feature_row = pd.DataFrame([{ "is_complaint": int(feat["is_complaint"]), "is_urgent": int(feat["is_urgent"]), "sentiment_score": sentiment_to_score(feat["sentiment"]), "user_level": req.user_level, }]) prob = model.predict_proba(feature_row)[0][1] return {"score": float(prob), "category": feat["category"]} def sentiment_to_score(s: str) -> float: return {"正面": 1.0, "中性": 0.0, "负面": -1.0}.get(s, 0.0)启动服务:
uvicorn services.api:app --host 127.0.0.1 --port 80017.3 curl 调用示例
curl -X POST http://127.0.0.1:8001/predict \ -H "Content-Type: application/json" \ -d '{"text": "我买的东西三天没发货,再不发我就投诉", "user_level": 2}'如果返回{"score": 0.92, "category": "物流"},说明整条链路已经通了。这个接口可以直接接到自己的工单系统或消息队列里。
8. 资源占用与性能观察
8.1 观察哪些指标
这类架构的瓶颈通常不在经典 ML,而在 LLM 调用环节。需要重点观察四类指标:
- LLM 调用耗时:单条文本的 P50、P95 耗时。如果 P95 远超 P50,说明有长文本或模型排队。
- Token 消耗:输入 token 和输出 token 分开计。提示词越长,成本越高。
- 特征生成成功率:LLM 返回的 JSON 能否被解析,解析失败率过高说明提示词要调整。
- 经典 ML 推理性能:模型打分本身通常是毫秒级,可以直接记录接口 P99。
8.2 降本手段
先缓存。相同的文本不要重复调用 LLM,尤其是工单这类重复度较高的场景,可以用文本哈希做 key。
再批量。离线任务不要把单条文本并发请求打到 LLM 服务上,批量填充可以减少调度开销。
然后是精简提示词。提示词越长,输出延迟越高。把不必要的前缀、解释、示例去掉,只保留 schema 和必要约束。
最后是量化或换小模型。如果本地部署的 7B 模型已经够用,不要强行上 70B。Llama、Qwen、DeepSeek 等系列都有量化版本,显存占用更低,推理速度更快。具体占用要以实际模型和推理框架为准,不同 batch size 下差别很大。
8.3 显存与 CPU 观察
经典 ML 推理基本不依赖 GPU。真正吃资源的是本地 LLM 服务。如果在 GPU 上部署,可以用nvidia-smi观察显存占用;如果走 API,就只需要关心调用延迟和成本。不要看到 LLM 就默认必须买大显卡,很多业务场景用一个小模型跑离线特征生成就足够。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| LLM 返回内容解析失败 | 模型输出了额外文字,或 JSON 格式不完整 | 打印原始 content 内容 | 在提示词中加强约束,解析失败时重试一次 |
| 同一句话多次打标结果不同 | temperature 过高 | 检查调用参数 | 将 temperature 设为 0,必要时多次采样投票 |
| 特征列大量为空 | 模型对某些文本不理解,或提示词分类范围不够 | 统计 None 占比 | 增加未知类别兜底,记录失败样本 |
| 经典 ML 效果没有提升 | LLM 特征与标签无强关联,或样本太少 | 查看 feature importance | 重新设计提示词,检查特征与任务相关性 |
| LLM 调用延迟很高 | 文本过长或模型排队 | 拆开统计单条耗时 | 截断文本、批量调用、换小模型 |
| 本地 GPU 显存不足 | 模型太大或 batch size 太大 | nvidia-smi 观察 | 缩小 batch size、使用量化模型、切到 API |
| API 服务启动后请求超时 | 端口被占用或 LLM 服务未启动 | 检查日志和端口 | 更换端口,确认 LLM 服务健康 |
| 批量任务中断后重跑全量 | 没有缓存或断点续跑 | 查看任务日志 | 增加结果缓存,按文本哈希跳过已完成项 |
10. 最佳实践与合规提醒
10.1 工程上的建议
第一,LLM 特征生成结果要落盘,不要只存最终标签。保留原始文本、LLM 原始输出、解析后的特征、模型版本、调用时间。这样一旦线上效果异常,可以回溯是哪一层出了问题。
第二,特征 schema 要版本化。业务字段一旦变化,比如新增一个“是否涉政敏感”字段,旧的训练集就不能继续用。建议把 schema 定义和提示词放在同一个目录下,跟随代码一起做版本管理。团队可以借鉴 LLM wiki 的思路:把提示词模板、特征定义、调用示例沉淀成团队知识文档,而不是散落在各个 notebook 里。
第三,模型要定期重训。LLM 生成的特征本身会变,经典 ML 基于旧特征训练出的决策边界也会过期。建议建立数据漂移监控,比如每周统计 LLM 生成标签的分布,一旦分布明显变化就触发告警。
第四,接口服务要限制访问范围。如果只在内网使用,FastAPI 可以绑定127.0.0.1;如果要开放给其他团队,建议加鉴权和限流。
10.2 合规提醒
在真实业务中引入 LLM 特征生成,需要特别注意三点。
一是数据授权。文本数据如果是用户产生的,要确认是否有权用于模型分析。尤其涉及客服对话、邮件、医疗记录时,要先做脱敏处理,再进行 LLM 调用。能本地部署就本地部署,不要把敏感数据直接送到外部 API。
二是版权合规。LLM 生成的摘要、理由、标签,本质是模型输出。如果这些内容要对外展示或商用,要确认模型服务条款是否允许,并做人工抽检。
三是人工复核机制。LLM 特征被用于风控、审核等决策链路时,不应完全自动化。建议对分数处于边界区间的样本保留人工复核通道,对 LLM 抽取的特征字段提供可视化解释。
11. 总结与下一步
这个架构最值得尝试的点在于:不推翻现有机器学习系统,就能把 LLM 的理解能力接进来。经典 ML 承担最终决策,LLM 提供文本理解特征。两者各干各的活,风险被隔离在特征层,而不是整个预测链路。
如果你现在想验证这套方案,建议先做三件事。第一,选一个小数据集,用 LLM 批量生成特征,统计解析成功率和标签分布。第二,用 XGBoost 跑一个 baseline,对比“只用关键词特征”和“加入 LLM 特征”的效果差异。第三,把缓存加上,观察同文本重复调用是否下降。
最容易踩的坑有两个:一个是过于相信 LLM 输出,不校验 JSON、不统计失败率,结果下游模型吃到一堆脏特征;另一个是直接让 LLM 输出最终标签,而不经过经典 ML,导致线上行为不可控、不可解释。这两个问题的解法都在第 6 节和第 10 节里。
下一步可以扩展的方向包括:用 LLM Agent 串联多个特征抽取步骤,自动生成候选标签;把 LLM 特征和 embedding 同时接入排序模型;或者训练一个较小的蒸馏模型,复现 LLM 的特征抽取能力,从而把离线特征生成搬到线上低延迟推理。建议先收藏这篇文章,实际落地时按第 9 节的排查表逐项对照,能少走很多弯路。