如果你是从上一篇跟过来的读者应该还记得我留了个尾巴纯规则和经典机器学习方法在真实文本上遇到“苹果在店里卖苹果”这种场景就直接投降。苹果到底是公司还是水果靠 TF-IDF 和词向量猜不出来因为词向量是静态的——不管上下文里出现了“手机”还是“削皮”它们的向量都长一个样。2024 年的 Python NLP 已经很少有人从头训练模型了行业里默认的打法是用在大规模语料上预训练好的模型在自己的标注数据上做微调需要召回资料或者希望模型输出自带来源时再叠加检索增强RAG。这套组合拳绕开了三个老大难问题标注数据不够、训练成本太高、模型知识太旧。这篇我按实际项目的推进顺序来写先讲为什么旧方法到了天花板然后逐个拆文本分类、实体识别、语义检索、RAG 的落地细节最后把评估和生产环境里最容易翻车的点单独拎出来说。适合已经掌握 Python 基础、想把手头 NLP 任务从 demo 做成工程的人。1. 静态词向量的天花板为什么 2024 年必须换打法1.1 词向量是“一本静态词典”预训练模型是“会翻词典的人”我见过不少团队抱着 Word2Vec 和 GloVe 不撒手理由是“当年的论文就是这么写的”。这话在 2016 年没错放到 2024 年就有点刻舟求剑了。Word2Vec 做的是把“词”映射到一个固定向量好处是训练快、内存小坏处是这个词无论出现在什么语境里向量永远不变。你可以把它理解为一本只有一条释义的词典“苹果”下面永远只写着“一种水果”。预训练语言模型则是另一套玩法。它拿到一句话之后会根据上下文现场决定每个词“此刻”是什么意思——“苹果店”里的苹果和“苹果削皮”里的苹果在模型内部是两套表征。这个差别在新闻情感判断、电商评论分析、医疗病历结构化这类任务里基本都是决定质量的胜负手。我自己的经验是把项目从 TF-IDF 迁移到预训练模型之后很多以前怎么调参都上不去的指标第一次出现了质的提升。当然不是说 Word2Vec 就该被扔进垃圾桶。在一些对延迟极其敏感、数据量又特别小的场景里静态词向量仍然有它的位置。但如果你做的是通用 NLP 任务2024 年的默认选项一定是预训练模型这个没有太多讨论余地。1.2 自注意力在做什么一场全员参与的讨论会Transformer 的自注意力机制听着唬人拆开看其实是一个很朴素的思路。我习惯给刚入行的朋友打这个比方每个词在开会会议规则是每个人都必须发言说明自己“现在最关心什么”Query同时挂出一块牌子介绍“自己带了什么信息”Key再把手头真正的内容Value放在桌上。接下来所有人两两配对算一遍“你俩这次讨论的相关程度”相关度高的内容就被放大相关度低的基本被忽略。这正是传统模型最缺的能力。RNN 系模型处理长句子时信息从第一个词传到最后一个词已经衰减得差不多了而自注意力让任意两个位置直接建立关联距离不再是问题。“这杯咖啡虽然凉了但香气确实不错”里“香气”和“咖啡”隔着五个词注意力机制依然能稳稳地把它们绑在一起。理解到这个程度对工程实践已经够了。你再往下追会发现多头注意力不过是把“开会”分成几个不同话题的小组位置编码是给词加个座位号残差连接和层归一化是防止训练崩掉的保险丝。这些细节不需要背但能帮你判断报错信息到底出在模型的哪一层。1.3 迁移学习改变了项目启动姿势预训练模型本质上是“别人花巨额成本积累出的语感”。模型在大规模语料上学会了通用语法、常识关联和表达习惯你手头那几千条标注数据只需要教它“你这个业务里正样本长什么样”。这就是微调。有个朋友的项目只有两千条标注样本做客服工单分类。要是从头训一个深度网络基本是个笑话用预训练模型微调上线 F1 直接跑到 90% 上下。这就是迁移学习的价值你不用替模型买“通识教育”的账单只需要付“职业培训”的学费。所以在 2024 年我的结论很直接除非你是做研究写论文否则主力模型直接选开源预训练权重别再考虑自己训练这件事。2. 文本分类微调一套在 2024 年仍然最常用的流程文本分类是 NLP 里需求最刚、套路最成熟的任务。无论是舆情正负面、工单打标、评论维度识别还是垃圾内容过滤核心流程几乎一致。这一节我把从环境准备到评估保存的完整过程写出来照着抄基本能跑。2.1 先把版本钉死transformers 的 API 变得比业务还快先说环境。我 2024 年上半年在多个环境里跑稳定的组合是这样的virtualenv nlp_env source nlp_env/bin/activate pip install transformers4.38.2 datasets2.19.0 evaluate0.4.1 accelerate0.29.3版本号一定要锁。Transformers 库的 API 变脸速度不慢比如evaluation_strategy这个参数在 4.38 之后被改名为eval_strategy你搜到一篇老教程直接抄参数名就会报一个莫名其妙的 TypeError。锁版本的另一个好处是网上搜到的报错和答案能跟你本地环境对得上排查效率高一大截。另外建议把scikit-learn一起装好后面算 F1 要用。如果机器有 NVIDIA 显卡顺手装对应版本的 PyTorch没有显卡也能跑就是慢一些后面我会讲吃亏的地方。2.2 数据准备三件事不做后面全白搭第一件是看标签分布。做情感四分类要是负样本只有 3%你不做任何处理直接训模型学到的全是“蒙一个中性”也能拿高分。我习惯新建项目后第一件事就是打印value_counts占比低于 5% 的类要么补充数据要么换评估指标别用 accuracy 自欺欺人。第二件是看文本长度分布。画个简单的直方图看看 80% 的样本集中在多少个字以内这个数字直接决定后面的max_length设置。第三件是看脏数据空文本、纯标点、网页乱码、重复记录。别觉得这是小事——重复记录不清掉等模型训练完你都不知道它到底在学规律还是在背答案。2.3 微调脚本逐段拆解直接给一份可以跑的代码数据只需要两列text和label。import pandas as pd from datasets import Dataset from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments, ) from sklearn.metrics import accuracy_score, precision_recall_fscore_support df pd.read_csv(train_data.csv) train_ds Dataset.from_pandas(df[[text, label]]) checkpoint bert-base-chinese # 也可以换成其它中文预训练权重 tokenizer AutoTokenizer.from_pretrained(checkpoint) def preprocess(batch): return tokenizer( batch[text], truncationTrue, paddingmax_length, max_length128, ) train_ds train_ds.map(preprocess, batchedTrue) def compute_metrics(pred): preds pred.predictions.argmax(-1) p, r, f1, _ precision_recall_fscore_support( pred.label_ids, preds, averagemacro ) return { accuracy: accuracy_score(pred.label_ids, preds), macro-f1: f1, } model AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels4) args TrainingArguments( output_dircls_result, learning_rate2e-5, per_device_train_batch_size32, per_device_eval_batch_size64, num_train_epochs3, weight_decay0.01, eval_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelmacro-f1, ) trainer Trainer( modelmodel, argsargs, train_datasettrain_ds, eval_dataseteval_ds, compute_metricscompute_metrics, ) trainer.train()几个参数说下我的习惯。max_length128对中文来说大约是 128 个字如果你的业务文本大多是长文就往上加到 256 或 512代价是训练变慢如果文本普遍很短64 就够别浪费显存。learning_rate从 1e-5 到 5e-5 都可以先试预训练模型微调一般不用太大。per_device_train_batch_size看显存32 跑不动就降到 16 或 8或者打开梯度累积。load_best_model_at_endTrue一定要开不然最后保存的不一定是最优模型。2.4 容易翻车的三个小细节第一标签映射要写清楚。用AutoModelForSequenceClassification时最好显式设置model.config.id2label和label2id否则保存下来的模型目录里只有 0/1/2/3线上排查问题时你对不上业务含义会很痛苦。第二随机种子。同一个训练脚本跑两遍结果可能就是 88% 和 91% 的差别。在 TrainingArguments 里配上seed42不仅是为了复现更是为了让你在调参时不至于把“随机性”误判成“模型变好了”。第三训练和推理必须用同一个 tokenizer。这不是废话——有人训练时用的是带paddingmax_length的预处理推理时换了个简易写法结果长度不一致模型产出飘忽不定。保存模型时把 tokenizer 一起save_pretrained到同一个目录推理时整个目录加载最稳。3. 实体识别从 BIO 标注到可用的信息抽取实体识别NER是信息抽取的地基电商要抽商品词医疗要抽症状和药品金融要抽公司名和金额。2024 年做实体识别有两条明显不同的路线先认清再动手。3.1 序列标注和提示抽取先选路线再动手路线 A 是微调一个序列标注模型把每个 token 打上实体标签。优点是单条推理成本几乎可以忽略标签固定可控适合百万级文本批处理。缺点是实体类型一变就要重新准备数据、重新训练。路线 B 是把文本丢给大模型通过提示词让它按 JSON 格式返回实体。优点是零训练样本也能开工实体类型调整只改提示词。缺点是按条计费、延迟高而且输出稳定性需要花力气去约束。我自己的选择标准是实体类型固定、数据量大、预算紧张老老实实走 A实体类型三天两头变、要做链式抽取或者业务根本不缺算力钱走 B。很多项目的真实状态介于两者之间那就两条腿走路高频稳定实体用微调模型低频新增实体用提示抽取兜底。3.2 BIO 标注体系与一份可复制的数据格式序列标注里最常见的标注体系是 BIOB 表示实体开始I 表示实体延续O 表示非实体。举个例子原文李雷在北京工作标签B-PERI-PEROB-LOCI-LOCOO更精细的 BIOES 体系还会把最后一个 token 标成 E以及单字实体标成 S。选哪种取决于标注工具和下游需求BIOES 在边界预测上略占便宜但标注难度也略高。我的建议是团队刚起步就用 BIO把数据量做大比体系花哨更重要。数据格式我强烈建议每一行一个 token用空格或制表符分隔李 B-PER 雷 I-PER 在 O 北 B-LOC 京 I-LOC 工 O 作 O这种格式直接能被开源标注工具读取也方便写脚本转成模型输入别自创妖格式。3.3 微调序列标注的关键标签对齐和 -100训练代码看起来跟分类差不多用AutoModelForTokenClassification但真正的坑在标签对齐。中文的字符级模型还好英文和 BPE 分词会把一个词切成多个子 token你标注的标签是词级别的必须决定切出来的子 token 谁继承标签。业界标准做法是第一个子 token 继承标签剩余子 token 设成 -100因为 PyTorch 的交叉熵损失函数会自动忽略值为 -100 的位置。def tokenize_and_align_labels(batch): tokens batch[tokens] # 已经按词切好的列表 labels batch[ner_tags] # 与 tokens 一一对应的标签 ID enc tokenizer(tokens, truncationTrue, is_split_into_wordsTrue) aligned [] word_ids enc.word_ids() previous None for wid in word_ids: if wid is None: aligned.append(-100) # 特殊 token 不计损失 elif wid ! previous: aligned.append(labels[wid]) # 第一个子 token 继承标签 previous wid else: aligned.append(-100) # 剩余子 token 忽略 enc[labels] aligned return enc训练开始时用tokenizer(李雷在北京工作, paddingmax_length)同样的坑也要注意中文预训练模型会把每个汉字当做一个 token所以中文本体不需要对齐但只要你混入英文、数字或者特殊符号对齐函数依然不能省。实体识别和分类评估不同别只用准确率。实体边界差一个字就算错所以我一般直接算实体级别的 precision、recall、F1而不是 token 级别的。seqeval这个包就是干这个的把预测结果和真实标签喂进去就能拿到实体级指标。3.4 规则兜底与标注质量复核模型不是万能的。数字、身份证号、邮箱、特定业务编号这类高确定性实体我通常会在模型前面加一层正则抽取命中就直接输出模型只负责模型擅长的事情。规则和模型不是对立关系规则帮模型减负模型帮规则兜底合作起来最舒服。NER 数据的质量检查比分类麻烦得多。我给自己立了一条不成文的规矩每批标注数据抽 10% 做双人复核不一致率达到 5% 以上就整批打回重标。标签错一对模型的边界就学歪一点这种错误在分类任务里是噪音在序列标注任务里会被放大成系统性偏差。4. 语义相似与向量检索让机器看懂“意思差不多”文本分类和实体识别是判别式任务而语义相似度是另一类高频需求岗位去重、客服知识库命中、相似文章聚合、商品标题匹配。这类需求的核心问题不再是“这句话属于哪一类”而是“这两句话是不是同一个意思”。4.1 字面匹配为什么撑不住“意思差不多”最早做相似度匹配大家习惯用关键词重合、编辑距离、TF-IDF 向量余弦。这些方法对“字面相似”的问题有效但遇到近义词就抓瞎“怎么退换货”和“退款流程是什么”在字面上几乎没有重合词可它们明明是一类问题。再往深处说“电脑”和“笔记本”字面毫无关系语义却高度相关这是字面匹配永远跨不过去的坎。把文本 encode 成语义向量的思路就能解决这个问题。模型把“怎么退换货”和“退款流程是什么”映射到向量空间里相近的位置余弦相似度就会很高。这就是为什么 2024 年的检索系统普遍用稠密向量替代稀疏的 TF-IDF 特征。4.2 句向量模型别拿 [CLS] 直接当句向量这里有个最常见的误区直接用 BERT 最后一层的[CLS]向量当句向量。不是说完全不能用而是普通 BERT 预训练时没专门优化过句向量表征直接拿出来经常出现“各向异性”问题——不同句子的向量都挤在一个狭窄的锥形区域里相似度区分度很差。行业普遍的做法是用专门训练过的句向量模型常见的有 BGE 系、M3E 系这些开源权重。它们在语义相似度任务上明显比通用 BERT 的[CLS]好用而且用起来特别简单不需要自己写编码逻辑from sentence_transformers import SentenceTransformer model SentenceTransformer(./models/bge-large-zh) sentences [怎么退换货, 退款流程是什么, 商品保修期多久] vectors model.encode(sentences, normalize_embeddingsTrue)normalize_embeddingsTrue是个好习惯。向量归一化之后余弦相似度可以直接用点积计算省掉一次除法检索时也能少踩几个坑。4.3 相似度计算与阈值校准计算两个向量相似度最直观的方式就是余弦相似度但注意句向量模型输出的向量维度可能上千暴力算两两相似度先搞清复杂度。我用过一段时间的朴素写法是这样import numpy as np def cosine_sim(v1, v2): return float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))) query_vec model.encode(怎么退换货) doc_vecs model.encode([退换货流程说明, 商品保修期政策, 发票开具指引]) for idx, score in enumerate(cosine_sim(query_vec, doc_vecs)): print(score, docs[idx])真正要下功夫的是阈值选择。很多人拿着 0.9 这种拍脑袋的阈值当标准结果上线后发现要么召回太多垃圾要么把该命中的全挡在门外。正确做法是准备一批已经标好“应该命中/不该命中”的样本对把相似度从低到高扫一遍画一条精确率和召回率都还过得去的分界线这个阈值才对你的业务有意义。同一个模型在不同业务下合理阈值可能从 0.6 到 0.85 之间横跳不存在通用答案。4.4 数据量变大以后的检索选型条数在一万以下直接用 numpy 暴力算余弦没问题单条查询也就几毫秒。但到了百万级甚至亿级暴力遍历就不现实了需要近似最近邻ANN索引或者专门的向量数据库。我判断要不要上向量库的粗标准是十万级以下内存里暴力算百万级以上再考虑 ANN 和向量库。别一上来就整重架构很多时候你缺的不是性能而是数据量。向量检索的工程细节也很考验人索引的构建频率、增量更新的策略、筛选条件比如“只检索某个分类下的文档”怎么在向量检索前后过滤。这些先想清楚再选具体产品不迟。5. RAG 落地大模型输出要有凭有据2024 年如果你还没在项目里碰过 RAG那确实有点说不过去了。RAG 全称是“检索增强生成”思路一句话就能说清让模型在回答之前先从一个外部知识库里检索相关内容把检索结果作为参考资料一起交给模型让模型“读了资料再回答”。5.1 RAG 解决的是三件大模型单独搞不定的事第一是知识陈旧。通用大模型的训练数据有截止日期你跟它聊 2024 年 6 月才有的政策它只能瞎猜。第二是私有知识缺失。企业内部的制度文件、产品手册、售后记录大模型根本没见过你再怎么提示词引导也没用。第三是幻觉。模型并不清楚自己“不知道什么”没人给它参考资料它就胡编乱造。RAG 的原理相当于给模型配了一个随叫随到的图书馆和一位图书管理员你来提问管理员先到书架里把相关书页抽出来模型照着书页作答。这样输出不再是空口无凭而是有出处、可追溯的。5.2 最小可用管道五步接出一个带来源的回答完整流程就五步文档清洗切分、段落向量化入索引、查询向量化检索、组装提示词、生成回答。我先写一个最简版本重点看检索和提示词组装def retrieve(query, top_k3): q_vec embed_model.encode(query) scores [cosine_sim(q_vec, vec) for vec in corpus_vecs] top_idx np.argsort(scores)[-top_k:][::-1] return [(corpus[i], scores[i]) for i in top_idx] def build_prompt(query, contexts): ctx_text \n.join(f[{i1}] {text} for i, text in enumerate(contexts)) return f请根据下面的参考资料回答问题。 如果资料里没有答案请直接说不知道不要编造。 参考资料 {ctx_text} 问题{query} 回答这个管道的最小版本跑通之后再逐步加上来源追踪、多路召回、重排序。不要一上来就设计一个复杂的编排框架先让端到端链路跑起来你才知道瓶颈到底在检索还是在生成。5.3 切分策略检索质量的第一道关口RAG 项目里最容易被低估的环节是文档切分。很多人拿一个固定字数咔咔切完就向量化结果检索出来的内容语义断裂、上下文残缺模型再强也补不回来。切分策略直接决定检索质量没有万能的参数只有场景适配。策略做法适合场景注意点固定长度切分按固定字数切加重叠窗口无结构的 FAQ、公告、短文案必须加重叠否则容易切断句子结构感知切分按标题、段落、列表切分说明书、规章制度、有层级的长文保留标题层级信息检索时一并返回语义切分按向量相似度自动聚类分界内容跨度大、主题切换频繁的长文成本高切分边界不稳定固定长度切分最大的坑是“把一句话从中间剪断”。中文尤其明显模型拿到的检索片段里全是半句话回答自然磕磕绊绊。所以哪怕用固定长度重叠窗口也得留出上下文余量。结构感知切分则要注意检索结果能不能把“这是第几节第几条”这些引用信息一起返回这决定了你的回答能不能做到每个结论都带出处。5.4 幻觉压制提示词、引用和后置校验我做了几个 RAG 项目之后的体会是幻觉不是一次性能压干净的而是三层防线叠加的结果。第一层是提示词层面明确告诉模型“资料里没有答案就直说不知道禁止编造”。别小看这一句实测能显著降低强行编造的比例。第二层是引用机制。要求模型在生成答案时每个关键论断都标注来源编号比如“根据资料[2]”。这样即使模型说错用户和系统也能定位到是哪份资料出了问题方便追责和修复。第三层是后置校验。用一个独立的检查步骤判断模型输出的内容是否能从检索到的资料里找到依据。这层可以交给一个小模型做分类也可以再让大模型“复述一遍依据”两边对不上就打回重生成。越重要的业务越要加这一层。6. 如何评估你的 NLP 项目别让准确率骗了你评估这件事我在项目里踩的坑比在算法上踩的坑多得多。很多项目线下分数很好看一上线就现原形十有八九是评估链路出了问题。6.1 准确率在类别失衡时是个陷阱情感分类里负面样本占 3%你全预测成中性准确率也有 97%。这个数字看着漂亮对你的业务却毫无意义。所以在分类项目里我几乎只看宏平均 F1macro-F1它把每个类别的 F1 单独算出来后取平均小类被忽视的情况就会被暴露出来。指标计算方式适合场景准确率正确预测数 / 总数类别均衡且各类错误代价相同精确率TP / (TPFP)你更在意“预测为正的样本确实为正”召回率TP / (TPFN)你更在意“别漏掉真正的正样本”宏平均 F1各类别 F1 的算术平均类别不均衡各类一样重要加权 F1各类别 F1 按样本量加权大类比小类重要时对于实体识别别只看 token 级别的准确率实体级别的 F1 才是业务关心的。一个实体识别错了一个字token 准确率可能只掉一点点实体级别的 F1 会摔得很难看。6.2 评估集要模拟真实世界的分布最常见的错误是“同源随机切分”。比如做电商标题分类同一个商品有好几条记录随机切分后同一条记录既出现在训练集又出现在测试集模型等于提前看到了答案线下分数虚高。正确做法是先按业务主键去重再去切分。另一个更隐蔽的问题是时间漂移。业务数据永远在变化训练集是今年上个月的上线时面对的是这个月的新文本。如果线下评估只是从同一个月里随机抽你永远不知道模型对“新表达”的适应力有多差。我越来越习惯做时间切分用前几个月的数据训练用最后一个月的评估这样评估结果才接近真实上线体验。6.3 大模型当判卷老师新方法的天花板对于文本生成、摘要、对话这类任务传统的客观指标很难衡量质量。近几年流行的做法是让一个大模型当“判卷老师”按照你写好的评分标准给候选答案打分。这个方法好用但也有明显缺陷判卷老师存在位置偏好同样的答案放前面还是放后面分数可能不一样它还可能对“文风像自己”的答案更友好。我的应对是评测时把候选答案的顺序随机打乱多跑几轮取平均分并且固定使用同一个判卷模型和同一套提示词保证不同批次之间的可比性。把大模型当工具用而不是当真理用这一点在任何评估环节都成立。7. 生产环境里的五个血泪教训前面讲的都是方法和代码最后这部分是我最想让你看到的——项目上线之后踩过的坑比跑通 demo 时遇到的问题多得多。7.1 数据泄漏97% 的线下分数上不了线一个电商标题分类项目线下准确率 97%上线后只有 82%团队差点把责任全推到微调环节。后来排查发现同一商品的多条记录在随机切分时同时进了训练集和测试集模型在线下见过这些标题线上遇到新标题自然露馅。这个教训让我记住一件事任何项目开始之前先做数据血缘和去重检查而不是直接开训练。重复样本、相似样本、同一个用户产生的多条文本都可能造成不同程度的数据泄漏。7.2 模型体积、推理延迟、硬件成本要先算账BERT-base 大概是 1.1 亿参数一张 T4 卡能做中等规模的批量推理但如果你只有 CPU 环境一条文本跑几十到几百毫秒是常有的事。做生产方案时先算清你的延迟预算和吞吐量再决定用什么规模的模型。很多团队犯的错是贪大。明明一个蒸馏版小模型就能满足业务非上大模型结果硬件成本翻倍、延迟超标、运维复杂度上升。模型不是越大越好是“够用就好”。实在不行先用大模型离线打标生成训练数据再蒸馏成小模型上线这套路在工业界已经非常成熟。7.3 该用规则的时候就别硬上模型我见过最离谱的需求是用模型抽“日期和电话号码”还要求准确率 99%。这类高确定性、格式稳定的抽取正则表达式一行就能解决硬上模型等于给自己找不痛快。我现在的判断标准是如果规则能达到 99% 的覆盖率且后续维护成本低就别上模型只有语义复杂、规则写不清楚的部分才交给模型。规则和模型分层治理既省钱又省心。7.4 模型上线与回滚留好退路模型一旦上线就成了系统的一部分就不再是“一个文件”的事了。我建议每个模型目录保存时都带上版本号和训练日期推理服务加载时固定到本地路径而不是每次从线上拉取。回滚这件事尤其重要。新模型上线前先在暗环境跑一段时间把线上真实请求喂进去对比新旧模型的输出差异。一旦新模型的输出异常要能一键回退到旧版本。很多团队把模型训练和模型发布混在一起出了问题只能“停机修模型”这是生产事故的前兆。最后说点个人的体会。做 NLP 项目这几年我最大的转变是以前拿到任务第一反应是找模型现在第一反应是问清楚“怎么算做对”。模型选型再花哨评估和生产链路立不住上线就是给自己挖坑。你先把评估集和指标定义清楚再去碰预训练权重项目成功率会高很多。下一篇如果时间允许我打算专门写文本生成类的落地案例把解码策略、长度控制这些坑再展开说说。