ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

结合机器学习与LangChain的垃圾邮件检测与解释系统

结合机器学习与LangChain的垃圾邮件检测与解释系统 垃圾邮件检测系统是毕业设计里非常常见的选题但很多同学做完之后会发现一个问题功能太单薄答辩时讲不出东西。如果项目标题里同时出现了 LangChain、LLM 和机器学习那这个项目就不能只做一个分类器了。更合理的做法是把传统机器学习的检测结果作为主基线再用 LangChain 编排大语言模型对检测结果做复核、解释和异常线索提取最终形成一个“可检测、可解释、可交互”的分析系统。这篇文章就按这条链路拆开讲从数据、特征、模型训练到 LangChain 接入 LLM再到批量检测、接口封装和答辩准备。1. 先搞清楚这个项目里三个模块各自干什么很多同学一看到“LLM 大模型”就想着把邮件直接丢给大模型判断结果发现调用慢、成本高、还容易飘。真正合理的结构不是让大模型当主力而是让每个模块做自己擅长的事。1.1 传统机器学习模型负责主要分类垃圾邮件检测的核心任务还是分类一封邮件进来判断它是正常邮件还是垃圾邮件。这个任务用传统机器学习算法就能做得很好。常见的选择是朴素贝叶斯、逻辑回归、随机森林配上 TF-IDF 特征向量化。这类方案训练快、推理快、可解释性强CPU 环境就能跑。毕业设计里这部分是完全可以拿出来讲清楚的因为公式、原理、评估指标都有成熟资料。1.2 LLM 与 LangChain负责复核、解释和流程编排LLM 在这个项目里的价值不是替代分类器而是做两件机器学习模型做不到的事第一对分类结果做解释。比如模型判定某封邮件是垃圾邮件概率是 92%但为什么ML 模型给不出让普通人理解的理由。LLM 可以阅读邮件内容提炼出“促销链接”“免费领取”“紧急转账”这类关键线索生成一段自然语言说明。第二对边界样本做复核。比如模型判定某封邮件是垃圾邮件的概率只有 55%这个判断不太可靠LLM 可以结合内容语义再判断一轮给出倾向性意见。LangChain 在这里的角色是流程编排层。它负责把“邮件内容读取→ML 模型预测→构造提示词→调用 LLM→解析输出→写入结果”这一整条流程串起来。如果你用原生的 API 调用也能做但代码会越来越乱。LangChain 的优势就是把提示词模板、模型调用、输出解析、链式组合这些重复工作规范化。注意不要把 LLM 当成主力分类器。垃圾邮件检测需要的是稳定、低成本、可复现这恰恰是传统机器学习模型的优势。LLM 只处理需要语言理解能力的部分。2. 环境准备和目录设计先把最小可运行版本搭起来我见过太多项目一开始就追求大而全结果环境还没配好就放弃了。做毕业设计也好做简历项目也好第一步永远是先把最小可运行的流程跑通。2.1 硬件与依赖CPU 就可以起步这个项目对硬件要求不高。普通笔记本CPU 环境16GB 内存基本就够了。不需要 GPU因为传统机器学习的训练和推理在 CPU 上都非常快。真正可能拖慢性能的是 LLM 调用但那属于网络请求主要看接口服务的响应速度。依赖方面核心是这么几组功能模块依赖库说明数据处理pandas、numpy读取 CSV、做统计分析特征提取scikit-learn 的 TfidfVectorizer把邮件文本转成数值特征模型训练scikit-learn朴素贝叶斯、逻辑回归、随机森林评估scikit-learn 的 metrics精确率、召回率、F1、混淆矩阵LLM 编排langchain、langchain-openai也可用其他模型服务的封装包Web 接口FastAPI 或 Flask简单起一个 HTTP 服务这里不要急着把所有库都装上。建议先建一个虚拟环境装最小依赖跑通训练脚本再逐步加 LangChain 和 Web 接口。环境越干净后面排查问题越容易。2.2 目录结构从第一天就按项目规范来很多同学的毕业设计项目只有一个 Jupyter Notebook所有代码和数据都堆在一起。这样做不是不行但到后面写论文、做 PPT、演示系统的时候会非常痛苦。建议至少按这个结构组织spam_detection/ ├── data/ │ ├── raw/ # 原始邮件数据 │ ├── processed/ # 清洗后的数据 │ └── samples/ # 小样本测试数据 ├── notebooks/ │ ├── 01_eda.ipynb # 探索性数据分析 │ ├── 02_train_baseline.ipynb # 训练基线模型 │ └── 03_llm_review.ipynb # LLM 复核实验 ├── src/ │ ├── data_process.py │ ├── train_model.py │ ├── predict.py │ └── llm_review.py ├── models/ # 保存训练好的模型文件 ├── outputs/ # 预测结果和日志 ├── .env # 环境变量不要提交到代码仓库 └── requirements.txt这个结构的好处是数据、代码、模型、输出完全隔离。训练的时候不会不小心覆盖原始数据写论文的时候每个文件都有明确用途跑批量的结果有地方查。2.3 环境变量和密钥调用 LLM 前先理清楚如果项目要接入真正的 LLM 接口调用密钥是绕不开的。这里要养成一个习惯密钥放到.env文件里代码里用os.getenv()读取不要把密钥硬编码到 Python 文件里。# .env 示例 LLM_API_KEYyour_key_here LLM_MODEL_NAMEyour_model_name LLM_BASE_URLyour_base_url具体模型名、接口地址、密钥申请方式每个人能接触到的服务不一样以你自己实际能申请到的为准。示例里写your_model_name只是为了占位。原始材料没有给出明确版本建议落地时先确认 langchain 相关库的版本不同版本的 API 差异比较明显。我的建议是装完依赖后先跑一个最简单的调用确认本机能连上接口再做后面的链路。3. 机器学习检测部分先让结果落在一个可评估的基线上LLM 是加分项但整个系统的地基仍然是机器学习分类模型。地基不牢LLM 再花哨也没用。3.1 数据获取与预处理先看是否划分训练集、验证集和测试集垃圾邮件检测常用的公开邮件数据集有 SpamAssassin 公共邮件集、Enron 邮件数据集还有一些竞赛平台提供的中文邮件分类数据集。具体用哪个要看你的项目是英文场景还是中文场景。原始材料里没有指定我建议先选一个你已经能稳定下载的数据集跑通再说。数据预处理阶段最基础的四步去重邮件数据里经常有重复样本。标签检查确认正常邮件和垃圾邮件的数量比例。内容清理去掉 HTML 标签、URL、多余空白但要注意保留对判断有帮助的链接特征。划分数据集训练集、验证集、测试集。这个顺序不能乱。很多项目评分虚高就是因为没有严格划分数据特征提取时把测试集信息也用了。3.2 特征向量化TF-IDF 参数怎么选文本不能直接丢给机器学习模型需要先转成数值向量。最常用的方法就是 TF-IDF。TF-IDF 的核心思想是一个词在某一封邮件里出现次数越多同时在其他邮件里出现次数越少它对这封邮件的区分能力就越强。使用TfidfVectorizer时最常调的几个参数max_features保留多少特征。建议从 5000 到 20000 之间试。太小可能丢失信息太大会让训练变慢还可能出现过拟合。ngram_range是否保留词组。(1, 2)表示保留单个词和两个词的组合。垃圾邮件里“免费领取”“点击链接”这类短语有区分度所以(1, 2)是常见选择。stop_words去停用词。“的、了、是”这类词对分类没有贡献去掉可以降低维度。3.3 模型训练先从朴素贝叶斯和逻辑回归开始第一次跑基线不要直接上随机森林或者集成模型。先用朴素贝叶斯因为它在文本分类里表现稳定训练快而且很适合作为对比基准。示例代码如下from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # X_texts 是邮件文本列表y_labels 是 0/1 标签 X_train, X_test, y_train, y_test train_test_split( X_texts, y_labels, test_size0.2, random_state42 ) pipeline Pipeline([ (tfidf, TfidfVectorizer(max_features8000, ngram_range(1, 2))), (clf, MultinomialNB(alpha0.1)) ]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) print(classification_report(y_test, y_pred, target_names[normal, spam]))这里要注意random_state。同一个固定值保证每次跑的结果可复现写论文的时候也能对得上。逻辑回归也是很好的选择它比朴素贝叶斯更灵活而且在特征量大的时候表现不差。随机森林可以放到后面做对比但不要一开始就追着跑。3.4 评估指标垃圾邮件场景里精确率、召回率和 F1 怎么取舍很多项目只报准确率Accuracy但在垃圾邮件检测里这是不够的。因为正常邮件和垃圾邮件比例往往不平衡很可能把全部邮件都判成正常邮件准确率依然很高但垃圾邮件一封都没拦住。更合理的评估方式精确率Precision判定为垃圾邮件的样本里有多少是真的垃圾邮件。召回率Recall真正的垃圾邮件里有多少被模型抓出来了。F1 值精确率和召回率的调和平均。在垃圾邮件场景里漏报和误报的代价是不同的。漏报一封垃圾邮件用户会看到骚扰信息误报一封正常邮件用户可能错过一个重要通知。实际使用时你可以根据业务场景调分类阈值而不是永远用默认的 0.5。建议画一条混淆矩阵同时打印 classification report再试几个不同的判定阈值。这部分内容是答辩时非常有说服力的实验材料。4. LangChain 接入 LLM让检测结果多一重解释能力而不是硬凑大模型机器学习模型能告诉我们“这封邮件是垃圾邮件的概率是 86%”但给不出解释。LLM 能给出解释但如果直接让 LLM 判断成本和稳定性都是问题。所以这两个要结合而不是互相替代。4.1 为什么要放在 ML 模型之后我的建议是把 LLM 放在 ML 模型之后而不是之前。原因很简单如果所有邮件都先经过 LLM每一封都要调用一次接口既慢又贵。更合理的方式是先用 ML 模型做一轮粗筛只有两种情况才走 LLM预测概率处于边界区间。比如垃圾邮件概率在 40% 到 70% 之间模型自己也不确定。需要生成解释文本。比如用户想看到“为什么判定这封邮件是垃圾邮件”的说明。这样整个系统的平均成本很低体验反而更好。4.2 完整流程分类、拼接、复核、结构化输出用 LangChain 组织起来的流程大致是这样读取一封邮件的标题和正文内容。用训练好的 ML 模型预测得到 spam_prob 和 label。判断是否需要 LLM 参与。如果不需要直接返回结果。如果需要把邮件内容、模型预测结果、置信度拼接到提示词模板里。LLM 返回解释文本用输出解析器提取关键字段。把 ML 预测结果和 LLM 解释合并写入最终输出。这个流程的核心点在于ML 模型是主判官LLM 是解释员和复核员。4.3 一个可落地的调用示例下面是一个简化示例假设你已经安装好 langchain 相关依赖也配置好了模型接口。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ ( system, 你是邮件安全分析助手。请根据邮件内容和模型预测概率判断该邮件是否为垃圾邮件 并输出 JSON 格式的结果包含 is_spam、confidence、reason 三个字段。 ), ( human, 邮件标题{subject}\n 邮件内容{content}\n 机器学习模型预测概率{prob}\n 请你进行复核并解释关键线索。 ) ]) messages prompt.format_messages( subject免费领取VIP会员, content点击链接立即领取限时优惠, prob0.92 )这里有一个细节prompt.format_messages只是构造消息真正调用模型还需要模型对象和invoke方法。为了不让示例变成无法运行的伪代码我建议你把 LLM 调用单独封装成一个函数比如review_email(subject, content, prob)返回解析后的 JSON 字典。如果模型返回的是纯文本而不是 JSON可以用 LangChain 的JsonOutputParser或者自己写一段简单的解析逻辑。第一个版本先保证能拿到结果再考虑解析健壮性。4.4 关键参数temperature、max_tokens、timeout、retryLLM 调用嵌入到系统链路之后有几个参数必须提前控制temperature建议设成 0 或者接近 0。我们要的是复核和解释不是创意写作。温度太高会让输出不稳定同一封邮件第二次调用的解释可能完全不同。max_tokens限制响应长度。垃圾邮件解释一般不需要很长几百个 token 足够。不限制的话偶尔会遇到超长输出白白浪费时间和费用。timeout设置请求超时。网络接口不可能永远稳定超时时间建议设成 20 到 60 秒。retry自动重试次数。LLM 服务偶尔会出现瞬时错误重试 1 到 2 次是合理的。但要注意重试超过 3 次意义不大反而把错误时间拉长。另外邮件正文可能非常长。LLM 接口通常有上下文长度限制全文塞进去不一定合适。建议在拼接提示词之前先截断正文。比如只取前 500 到 1000 个字符或者提取正文里与链接、免费、账户、转账相关的关键片段。5. 从单条样例到批量检测把这个系统真正用起来训练好模型、接好 LLM 之后项目还只是“能演示”。要让它成为一个真正可用的检测分析系统必须能处理批量邮件并且能稳定输出结果。5.1 先跑单条确认链路完整不管你的最终形态是 Web 系统还是命令行工具第一步永远是跑单条样例。单条测试时重点检查四件事ML 模型能不能正常加载输出概率是否合理。LLM 接口能不能通响应时间多长。拼接后的提示词是否合理有没有把字段传错。返回内容能不能被解析解析后写入的结果格式是否正确。这一阶段不要开并发、不要开批量。单条链路稳定了后面的批量才有意义。5.2 批量检测的输入输出设计批量检测最常见的输入格式是 CSV每行包含邮件 ID、标题、正文等字段。输出建议再生成一个新的 CSV保留原始字段同时追加三列预测标签、预测概率、LLM 解释。我建议的输出列结构id subject content prediction spam_prob llm_reason model_version time加了model_version和time之后后续做实验对比会非常方便。比如你换了特征参数或换了模型可以横向对比两批结果。批量处理流程用 pandas 组织起来非常自然import pandas as pd df pd.read_csv(data/raw/mail_samples.csv) results [] for idx, row in df.iterrows(): # 先调用 ML 预测得到 prob # 再根据规则决定是否调用 LLM # 把结果写入 results 列表 pass result_df pd.DataFrame(results) result_df.to_csv(outputs/predict_result.csv, indexFalse, encodingutf-8-sig)注意一个细节CSV 编码建议使用utf-8-sig这样用 Excel 打开中文不会乱码。这是一个很小但很实用的经验。5.3 并发控制、失败重试与日志排查批量任务真正跑起来之后关注点就从“能不能跑”变成“稳不稳定”。LLM 接口通常有并发限制。不要一上来就开几十个并发。建议从 1 个并发开始逐步增加到 3 到 5 个。如果接口经常报限流错误就降回去。同时批量任务必须处理失败的情况。一封邮件调用 LLM 超时了不能导致整个程序崩溃。更稳妥的做法是记录失败样本的 ID 和错误信息。跳过失败样本继续处理后续邮件。全部跑完后单独把失败样本重新跑一遍。日志信息要包含样本 ID、阶段、耗时、错误类型。不要只打印一个笼统的“调用失败”你后面排查时会疯掉。批量跑完不等于任务结束先检查失败样本数量再检查输出结果的字段完整性。如果失败率超过 5%优先看接口限流和超时设置而不是盲目加大并发。6. 毕业设计怎么包装接口、界面、创新点和常见坑做完整条链路之后还需要解决一个问题怎么把这个项目呈现成一份像样的毕业设计或简历项目。6.1 做一个小接口或轻量界面纯脚本项目在答辩时不好演示。建议加一层简单的接口或者界面。如果只是做接口用 FastAPI 就能搞定。设计一个/detect接口接收 POST 请求参数为邮件标题和正文返回结果包含预测标签、置信度和 LLM 解释。这个接口可以直接演示也可以作为后续 Web 前端的数据来源。如果想让界面更直观用 Streamlit 可以很快搭一个页面左侧输入邮件标题和正文右侧展示预测结果和解释。Streamlit 的优点是开发快不需要写前端代码。缺点是自定义程度有限但作为毕设演示足够了。6.2 答辩时怎么讲清这个项目的创新点这个项目的创新点最好不要说“我用到了大模型”而要说“我把机器学习分类器的可解释性缺点用 LLM 的语言理解能力补上了”。这样听起来是经过思考的设计而不是单纯堆技术。具体可以从三个角度讲成本控制只有边界样本才调用 LLM不是每封邮件都走大模型。可解释性ML 模型给出概率LLM 给出人话解释。工程化批量任务、失败重试、日志记录、接口封装。这三个点已经比很多毕设项目有内容了。如果以后想继续深入还可以扩展两个方向。一是 RAG构建一个邮件安全规则知识库当用户询问某条规则的含义时让 LLM 基于知识库回答减少幻觉。二是 LangGraph如果把检测链路做实包含多个分支和人工审核环节LangGraph 会比普通 LangChain 链更适合管理有状态的长流程。这两个方向可以作为“未来展望”写在论文结论里。6.3 常见报错和排查顺序我把这个项目里最容易踩的坑整理成一张排查表现象优先排查项说明训练准确率很高但测试效果差数据泄露、特征覆盖检查是否在训练前就用了整体数据的统计信息中文邮件效果差分词、编码、停用词中文文本需要先分词英文数据集直接按空格切分不够LLM 返回结果解析失败提示词、输出格式先让模型输出纯 JSON再做解析不要让它输出 MarkdownLLM 调用超时网络、timeout、并发先单条测再逐步加并发不要改动多个变量批量任务中断输出覆盖、没有断点续跑输出文件名加时间戳失败样本先记录再重跑Web 接口启动失败端口占用、依赖版本换一个端口确认 FastAPI 和相关库版本兼容排查问题的时候顺序很重要。先看现象再看输入数据再看环境和依赖最后才是模型和参数。不要一上来就怀疑模型选错了很多时候只是路径写错、编码不对、密钥没加载。6.4 可扩展的方向这个项目能扩展的地方其实很多。比较现实的方向有这么几个多模型对比实验把朴素贝叶斯、逻辑回归、随机森林、XGBoost 都跑一遍做一个完整的实验对比表格。这对于写论文非常有用。阈值调优画出精确率-召回率曲线选择一个适合你业务场景的阈值。增量更新设计一个简单的反馈机制用户标记错误的样本可以重新进入训练集。可视化分析对垃圾邮件做词云、高频词、链接域名统计让分析系统看起来更完整。最后说点实际的东西这个项目真正落地时最该盯住的不是功能列表而是输入格式、资源占用和失败重试。ML 模型训练部分只要数据干净基本不会拖太久LLM 部分才是最难稳定的。先把单条链路跑顺再上批量再上 Web 界面这样每一步出问题都知道该查哪里。踩过几次之后你会发现很多问题不是工具能力不够而是前置环境和输入材料没有处理干净。目录结构、数据编码、密钥配置、日志规范这些看起来不起眼的小事恰恰决定了整个项目能不能顺利收尾。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进