这次我们看的是一个很典型的“毕业设计升级版”技术项目:用 LangChain 编排 LLM 大语言模型,再叠加上传统机器学习分类模型,共同完成垃圾邮件检测与分析。和直接调一个现成接口不同,这个系统要自己处理数据、训练模型、设计置信度判断,再让大模型介入“疑难邮件”复核,最后通过接口或 Web 页面把整条链路暴露出去。对于计算机毕业设计、网络安全方向的综合实战、或者想入门 LangChain 应用开发的人来说,这套架构能把“机器学习 + LLM 应用”两个热门点一次性串起来。
这个项目最值得关注的点有三个:一是传统机器学习负责快速粗筛,延迟低、可解释性强;二是 LLM 负责深度语义复核,能给出检测理由,弥补关键词过滤在语义绕过上的短板;三是把数据预处理、模型训练、LangChain 编排、FastAPI 接口和批量任务整合成完整系统,而不是一堆零散的 notebook 代码。纯 CPU 环境可以完成机器学习的训练和推理,LLM 部分可以选择调用 API,也可以在本地跑一个小模型,硬件门槛完全取决于你选哪条链路。
下面我会按照“规格速览 → 系统设计 → 环境准备 → 数据预处理 → 机器学习训练 → LangChain 增强 → 接口与批量任务 → 性能观察 → 排错 → 毕设落地”的顺序,把整个系统从零到一讲清楚,代码部分可以直接照着改。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 垃圾邮件检测分析系统(毕业设计 / 综合实战项目) |
| 技术栈 | LangChain、LLM 大语言模型、Scikit-learn、Pandas、FastAPI |
| 检测模式 | 机器学习粗筛 + LLM 深度复核的双层检测 |
| 主要功能 | 垃圾邮件二分类、语义理解、检测理由生成、批量检测、API 服务、分析报表 |
| 推荐硬件 | 机器学习部分 CPU 可跑;LLM 部分可选云端 API 或本地模型 |
| 显存需求 | 不固定;选用本地 LLM 时按模型参数量和量化方式实测 |
| 支持平台 | Windows / Linux / macOS |
| 启动方式 | 命令行启动、Web UI、API 服务 |
| 批量任务 | 支持,可按目录批量导入或接口批量提交 |
| 适合场景 | 毕业设计、邮件内容安全分析、LLM 应用开发练习 |
从表格能看出来,这个项目并不是纯“调包炼丹”,而是更接近一个能演示、能答辩、能扩展成论文原型的完整系统。
2. 适用场景与使用边界
这套系统适合三类人:第一类是计算机相关专业的学生,需要一个既能体现机器学习基础、又能展示大模型应用能力的毕业设计题目;第二类是做网络安全或内容安全方向的初学者,希望理解垃圾邮件检测的基本流程和纵深防御思路;第三类是后端开发,想用最短路径把 LangChain 接入到实际业务链路里。
它解决的问题也很具体:邮件内容中大量重复、群发、诱导性文本需要被自动识别;同时攻击者会通过变体拼写、图片文字、伪装正常句式来绕过传统规则。传统机器学习可以快速识别“已知垃圾模式”,LLM 则更适合识别“语义上是垃圾但关键词不明显”的样本。两者的结合,能明显改善单一模型的鲁棒性。
但使用边界也要说清楚:这个项目适合做检测分析与教学演示,不适合直接当作生产级邮件安全网关。它不负责邮件传输层的 SPF/DKIM/DMARC 校验,也不处理附件木马分析,更无法替代完整的反钓鱼体系。另外,凡是涉及真实邮件数据的场景,必须遵守数据来源授权、用户隐私保护和内容展示规范,不能把真实用户邮件库直接公开或拿去训练商用模型。演示环境建议使用公开数据集或自建脱敏语料。
3. 系统架构与整体设计
整个系统可以拆成五个层次:数据层、特征层、机器学习层、LLM 增强层、服务层。
数据层负责读取邮件文本、标注数据和原始邮件目录,并做基础清洗。特征层把中文或英文邮件文本转成 TF-IDF 向量、邮件头特征、URL 特征、文本长度特征等。机器学习层使用朴素贝叶斯或逻辑回归等模型做粗筛,输出是否为垃圾邮件的概率。LLM 增强层接收低置信度样本,通过 LangChain 组装 Prompt,要求大模型输出判断结果和简短理由。服务层用 FastAPI 暴露/predict、/batch等接口,并提供一个简单的 Web 页面用于人工复核。
处理流程如下:
邮件输入 -> 文本清洗 -> 特征提取 -> 机器学习粗筛 -> 高置信度:直接输出结果 -> 低置信度:LangChain + LLM 复核 -> 合并结果,返回标签、置信度、检测理由这样设计的核心原因只有一个:成本控制。LLM 调用比传统模型昂贵且耗时,如果每封邮件都走大模型,批量场景根本扛不住。先让机器学习处理大部分明显样本,只把边界样本送进 LLM,既能提升整体准确率,又能把系统延迟控制在可接受范围内。
4. 环境准备与前置条件
4.1 基础软件清单
建议使用 Python 3.10 或 3.11,Windows、Linux、macOS 均可。项目本质上是 Python 生态应用,不需要依赖特定操作系统。机器学习部分建议安装 Scikit-learn、Pandas、Numpy、Jieba(处理中文分词时需要)。LLM 部分可选安装 LangChain 和对应模型接入包。服务层需要 FastAPI、Uvicorn。
虚拟环境是必需品,避免把全局 Python 环境搞乱。首次安装时如果网络条件一般,可以考虑使用国内镜像源加速。
# 创建虚拟环境 python -m venv .venk # Windows 激活虚拟环境 .venk\Scripts\activate # Linux / macOS 激活虚拟环境 source .venk/bin/activate # 升级 pip python -m pip install --upgrade pip4.2 核心依赖安装示例
pip install pandas numpy scikit-learn jieba pip install fastapi uvicorn pip install langchain langchain-openai如果计划使用本地 Ollama 模型,还需要额外安装langchain-ollama。这类指令在不同版本下依赖差异较大,建议以官方文档为准。
4.3 硬件与显存说明
机器学习训练阶段不强制要求 GPU,几万条邮件样本的 TF-IDF 和逻辑回归在普通 CPU 上通常可以接受。LLM 部分有两种选择:云端 API 方式不要求本地显存;本地部署方式则需要根据模型大小准备对应显存和内存。本地跑 7B 级别量化模型时,建议先用nvidia-smi观察实际占用,不同量化等级、上下文长度和并发数会带来成倍差异,不能只凭模型参数量推断。
4.4 端口规划
FastAPI 服务默认使用 8000 端口,如果本机端口被占用,可以在启动时手动指定。这里建议固定一个端口写入配置,方便后续接口联调。
5. 数据集准备与文本预处理
5.1 数据集选取
公开可用的垃圾邮件数据集有 SpamAssassin、Enron-Spam 等,中文场景有 trec06c 这类学术数据集。使用前必须确认数据集的版权许可,并在论文或 README 中标注来源。自建数据集时,建议从公开语料中抽样,而不是直接导出个人邮箱数据。
数据集建议采用两列结构:
| 字段 | 说明 | 示例 |
|---|---|---|
| text | 邮件正文或主题行 | “恭喜您获得 9980 元大礼包” |
| label | 0 表示正常邮件,1 表示垃圾邮件 | 1 |
这里需要注意类别不平衡问题。真实场景中垃圾邮件比例往往不低,但正常邮件和垃圾邮件的具体比例随数据集变化。训练前先统计两类的数量,避免出现全部预测为多数类的情况。
5.2 文本清洗流程
垃圾邮件的文本噪声非常大,包含 HTML 标签、URL、数字、特殊符号、繁体字、异体字和拼写变体。清洗的核心目的是把有效语义信息保留下来,去掉过程性噪声。
完整的清洗模块可以这样写:
import re import jieba STOP_WORDS = set(["的", "了", "是", "在", "我", "有", "和", "就", "不", "人"]) def clean_text(text: str) -> str: if not isinstance(text, str): return "" # 去掉 HTML 标签 text = re.sub(r"<[^>]+>", "", text) # 去掉 URL text = re.sub(r"http[s]?://\S+", " ", text) # 去掉邮箱地址 text = re.sub(r"\S+@\S+", " ", text) # 去掉数字和特殊符号,英文场景可改保留字母 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z\s]", " ", text) # 转小写 text = text.lower() return text def tokenize(text: str) -> list[str]: cleaned = clean_text(text) words = jieba.lcut(cleaned) words = [w.strip() for w in words if w.strip() and w not in STOP_WORDS] return words这一步会给后面的特征工程省下大量麻烦。分词质量直接影响 TF-IDF 向量质量,中文场景建议把自定义词典和停用词表单独保存成文件,便于后续维护。
5.3 训练集与测试集划分
按 8:2 或 7:3 的比例划分训练集和测试集,并固定随机种子,保证结果可复现。邮件数据常常存在时间相关性,更严谨的做法是按照时间先后划分,防止未来信息泄漏到训练集。
from sklearn.model_selection import train_test_split train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df["label"] )使用stratify做分层抽样,能保证训练集和测试集中正负样本比例接近。
6. 机器学习模型训练与评估
6.1 特征工程
文本特征最常用的是 TF-IDF 向量,简单且稳定。可以开启ngram_range=(1, 2)让模型捕捉短语特征,再限制max_features防止维度爆炸。
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( tokenizer=tokenize, ngram_range=(1, 2), max_features=20000, min_df=2, max_df=0.95 ) X_train = vectorizer.fit_transform(train_df["text"]) X_test = vectorizer.transform(test_df["text"]) y_train = train_df["label"] y_test = test_df["label"]这里要注意:测试集只能transform,不能fit,否则就发生了数据泄漏。很多初学者在这里踩坑,得到一个虚高的准确率,换新数据立刻崩掉。
6.2 模型选择与训练
垃圾邮件检测常用的传统模型有朴素贝叶斯、逻辑回归、线性 SVM 和随机森林。从实际效果看,逻辑回归和朴素贝叶斯在该任务中表现稳定,训练速度快,可解释性也好。下面以逻辑回归为例:
from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix model = LogisticRegression(max_iter=1000, class_weight="balanced") model.fit(X_train, y_train) y_pred = model.predict(X_test) print(classification_report(y_test, y_pred, target_names=["正常邮件", "垃圾邮件"])) print(confusion_matrix(y_test, y_pred))class_weight="balanced"会在类别不平衡时自动调整权重,通常能带来明显收益。如果数据集较大,可以尝试随机森林,但训练和推理成本更高,收益不一定匹配。
6.3 评估与调优重点
评估时不要只看 accuracy,重点看三个指标:精确率、召回率、F1。垃圾邮件检测场景里,召回率低意味着漏放垃圾邮件,精确率低意味着误杀正常邮件。具体偏重哪个指标,取决于系统定位:偏安全的系统更看重召回率,偏用户体验的系统更看重精确率。
调优路线建议:
| 调优方向 | 操作建议 |
|---|---|
| 特征维度 | 调整max_features、ngram_range |
| 类别不平衡 | 使用class_weight或过采样/欠采样 |
| 模型选择 | 对比逻辑回归、朴素贝叶斯、SVM |
| 阈值调整 | 不一定要用默认 0.5,可以根据验证集调整概率阈值 |
下面这段代码演示如何自定义分类阈值。把概率低于阈值的样本判定为低置信度,交给后续 LLM 处理。
proba = model.predict_proba(X_test)[:, 1] threshold = 0.6 y_rule = (proba >= threshold).astype(int) # 置信度区间:不适合直接下结论的样本 low_confidence_mask = (proba >= 0.4) & (proba < 0.6)7. LangChain + LLM 增强检测模块
7.1 为什么需要 LLM 增强
传统模型对拼写变体和隐晦语义的识别能力有限。攻击者会把“免费”写成“免'费”、“发财”写成“fa cai”,或者把整个话术包装成正常商务邮件。单纯靠 TF-IDF 很难捕捉这种语义层面的垃圾特征。LLM 的长处在于,它能理解整段文本的意图,即使关键词被改写也能做出判断。
但这个增强模块不能每封邮件都调用,所以在系统设计里增加了一个“置信度分流”机制:模型输出概率在 0.4 到 0.6 之间的样本,认为是边界样本,交给 LLM;概率大于 0.6 直接判为垃圾邮件;概率小于 0.4 直接判为正常邮件。阈值数值需要按实际验证集调整。
7.2 LangChain Prompt 设计
LangChain 在这里的作用是管理 Prompt 模板、组织少样本示例、构建立场化的输出格式。最简单的用法是ChatPromptTemplate加一个输出解析器。
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser prompt = ChatPromptTemplate.from_messages([ ("system", "你是一名邮件内容安全分析专家,负责判断邮件是否为垃圾邮件。" "垃圾邮件包括广告推销、诈骗、钓鱼、恶意诱导等内容。" "只输出 JSON:{{\"label\": 0 或 1, \"reason\": \"简要判断理由\"}}"), ("human", "请分析以下邮件:\n{email_text}") ]) llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, api_key="your-api-key", base_url="https://your-api-endpoint" # 兼容 OpenAI 格式的服务 ) chain = prompt | llm | StrOutputParser()如果不方便使用云端 API,也可以把ChatOpenAI换成本地模型接入方式,比如配合 Ollama 中的开源模型。具体模型选择不在这里展开,原则是优先选指令跟随能力好、支持中文的小模型。
7.3 与机器学习结果合并
实际调用时,不要把 LLM 结果直接覆盖机器学习结果,而是把它作为“复核意见”合并。最终标签、置信度、理由都保留在结果对象里。
def final_decision(ml_label: int, proba: float, llm_text: str): """ ml_label: 机器学习预测标签 proba: 机器学习预测为垃圾邮件的概率 llm_text: LLM 返回的 JSON 字符串 """ if 0.4 <= proba < 0.6: # 边界样本,以 LLM 复核结果为准 result = parse_llm_output(llm_text) return { "label": result["label"], "source": "llm", "reason": result["reason"], "ml_proba": round(proba, 4), } return { "label": ml_label, "source": "ml", "reason": "机器学习模型高置信度判断", "ml_proba": round(proba, 4), }在实际项目中,这里还需要加异常处理。LLM 调用可能超时、返回非 JSON 内容、或者因限流失败,系统必须能在 LLM 不可用时回退到机器学习结果,保证主流程不中断。
7.4 少样本与多轮能力
如果发现某个类型的垃圾邮件漏检严重,可以在 Prompt 中加入几条少样本示例,帮助模型理解任务边界。LangChain 支持从示例列表中动态构建消息,这一点比直接拼字符串更优雅。
8. Web 服务、接口 API 与批量任务
8.1 FastAPI 服务搭建
为了方便演示和二次开发,系统服务层使用 FastAPI 实现。下面是一个最小可运行示例,包含健康检查、单条预测、批量预测三个接口。
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI(title="垃圾邮件检测分析系统") class EmailInput(BaseModel): text: str class BatchInput(BaseModel): items: list[EmailInput] @app.get("/health") def health(): return {"status": "ok"} @app.post("/predict") def predict(email: EmailInput): text = email.text # 业务中需要替换为完整检测流程:清洗 -> 特征 -> 模型 -> LLM 增强 result = { "text": text, "label": 1, "reason": "演示结果,请接入完整检测流程", "source": "demo" } return result @app.post("/batch") def batch_predict(batch: BatchInput): results = [] for item in batch.items: results.append(predict(item)) return {"total": len(results), "results": results}启动服务:
uvicorn main:app --host 127.0.0.1 --port 8000 --reload启动后访问http://127.0.0.1:8000/docs就能看到 Swagger 文档,可以直接在页面里发送测试请求。FastAPI 自带交互式文档,在接口调试和毕设答辩演示里很好用。
8.2 接口调用示例
可以使用 Python 的requests测试接口:
import requests url = "http://127.0.0.1:8000/predict" payload = { "text": "恭喜您获得万元大奖,点击链接立即领取!" } response = requests.post(url, json=payload, timeout=30) print(response.json())也可以用 curl 直接调:
curl -X POST "http://127.0.0.1:8000/batch" \ -H "Content-Type: application/json" \ -d '{"items": [{"text": "免费领取优惠券"}, {"text": "周四下午项目评审会议室见"}]}'8.3 批量任务设计
批量检测如果做成同步接口,文件较大或样本较多时容易超时。更合理的做法是:上传文件后生成任务 ID,后端异步处理,前端轮询任务状态。这个模式也能让毕设增加一个“任务队列”的亮点。
流程可以简化为:
上传邮件文件 -> 生成 task_id -> 后台线程逐条读取 -> 清洗 -> 模型判断 -> LLM 复核 -> 写结果文件 -> 更新任务状态批量任务最容易忽略的问题是失败重试。单条数据可能因为格式异常、编码问题、LLM 限流而失败。建议每条记录都记录独立状态:成功、失败、跳过,失败原因写入日志。全部处理完后汇总统计文件,方便用户查看哪些邮件被判定为垃圾邮件以及理由。
import csv from pathlib import Path OUTPUT_DIR = Path("./outputs") OUTPUT_DIR.mkdir(exist_ok=True) def write_result(task_id: str, rows: list[dict]): output_file = OUTPUT_DIR / f"{task_id}.csv" with open(output_file, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=["text", "label", "source", "reason", "ml_proba"]) writer.writeheader() writer.writerows(rows)9. 资源占用与性能观察
这是一个需要重点观察的模块。部署完成后,建议先跑小批量数据,记录三个核心数据:训练耗时、单条预测耗时、LLM 复核耗时。
9.1 CPU 与内存观察
传统机器学习的训练过程会占用较多 CPU,但推理阶段非常快,单条预测通常只有几毫秒。问题主要集中在 TF-IDF 向量化阶段:max_features设置过大、ngram_range过大、文本过长,都会显著拉高内存。观察方式很简单,任务运行时打开系统监视器或使用top/htop。
9.2 显存观察
如果 LLM 采用本地部署,观察显存占用是必要的。推理过程中可以另开终端执行:
nvidia-smi -l 2这里要强调,显存占用不是固定值,它和模型量化等级、上下文长度、批处理大小、并发请求数都有关系。不要套用别人机器的数据,一定要用自己机器跑一遍。如果显存吃紧,优先降低上下文长度、减小批量数,或者换更小的量化模型。
9.3 延迟与成本控制
单条邮件如果走“机器学习 + LLM 复核”,耗时会从毫秒级跳到秒级。因此系统要重点关注“LLM 调用占比”。占比越高,整体延迟越高,成本也越高。调优方向是:提高机器学习置信度阈值,让更多样本直接出结果;或者把低置信度区间压缩。
9.4 并发与限流
FastAPI 本身是异步框架,可以处理一定并发,但 LLM 接口通常有速率限制。批量任务必须做简单的并发控制,比如限制同时最多 5 个 LLM 请求,避免触发限流导致大量失败。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 依赖安装失败 | Python 版本不兼容或网络原因 | 查看 pip 报错信息 | 换用 3.10/3.11 虚拟环境;使用镜像源 |
| 中文乱码或分词结果异常 | 文件编码不是 UTF-8 | 检查数据文件编码 | 统一转为 UTF-8 后重新读取 |
| 准确率低,几乎全预测为多数类 | 类别不平衡或特征没做清洗 | 打印类别分布和分类报告 | 使用class_weight、调整阈值、补充数据 |
| 测试集效果不错,新数据很差 | 数据泄漏或数据分布不一致 | 检查是否对测试集执行了fit | 确认只对测试集执行transform |
| LLM 接口调用超时 | 网络波动或服务端限流 | 查看日志和响应码 | 增加超时时间、重试机制、并发控制 |
| LLM 返回内容无法解析 | 模型输出了非 JSON 内容 | 打印原始输出 | 使用强约束的 Prompt,增加回退解析逻辑 |
| 批量任务中途卡住 | 异常数据导致线程阻塞 | 查看任务日志,定位具体批次 | 逐条增加 try/except,记录失败原因 |
| 服务启动后页面打不开 | 端口被占用或服务未启动 | 检查终端日志和本机端口 | 更换端口或重启服务 |
这里有一个比较隐蔽的问题需要单独提醒:模型文件、输出目录和日志目录最好在项目初始化时自动创建,否则批量任务跑到一半可能因为目录不存在直接崩掉。
11. 毕业设计落地与答辩建议
11.1 模块划分与工作量展示
这个项目非常适合做毕业设计,因为它的模块划分天然对应论文结构:需求分析、系统设计、数据集构建、模型训练、LLM 增强、系统实现、测试评估。建议在 README 里画一张简单的系统架构图,把五个层次和核心数据流标清楚,答辩时直接围绕这张图讲。
11.2 答辩高频问题准备
答辩时大概率被问到几个问题:“为什么用了机器学习还要用大模型?”“大模型调用成本那么高,怎么保证系统可用性?”“数据集从哪里来,是否存在隐私问题?”“如何处理中文邮件和英文邮件的差异?”
回答思路分别是:机器学习适合快速处理常规样本,LLM 适合处理语义模糊样本,两者是“成本与效果”的平衡;系统通过置信度分流控制 LLM 调用占比,并做了失败回退;数据集全部采用公开语料或人工脱敏数据;中文和英文分别使用不同分词与清洗策略。
11.3 效果展示建议
答辩演示时不要只跑一条邮件,建议准备一个本地测试集,包含明显垃圾邮件、正常邮件、带变体的垃圾邮件、伪装成正常邮件的钓鱼邮件四类。重点展示前两类由机器学习快速判断,后两类触发 LLM 复核并给出理由。这个对比最能体现系统价值。
11.4 后续扩展方向
如果时间和精力充足,可以在现有系统上加三个方向:垃圾邮件多分类(广告、诈骗、钓鱼)、邮件附件危险链接分析、基于知识库的钓鱼邮件特征库。这些方向都能把项目深度再做一层。
12. 最佳实践与合规提醒
代码工程层面,建议从一开始就把配置项抽离出来。API Key、模型名称、端口、阈值、文件路径都放配置文件,不要直接写死在代码里。模型训练完的vectorizer和model用joblib或pickle保存,接口启动时加载一次,避免每次请求重新训练。
安全合规方面,这是这篇博客里最需要重视的一部分。垃圾邮件检测项目天然涉及大量邮件内容,真实邮件属于个人隐私数据,使用前必须确保数据来源合法,匿名化处理后再进入实验环境。模型文件、数据集、日志都包含文本内容,不能随意公开。涉及 LLM 的调用,也要注意不要把未经脱敏的邮件内容直接发送到外部 API。如果是在企业内部或毕设演示中使用,建议使用本地模型或专门审批过的 API 服务。
另外,系统如果发布成在线服务,建议增加简单的访问认证和限流。FastAPI 中实现一个简单的 API Key 校验并不复杂,成本低,但能防止接口被别人恶意刷量。
from fastapi import Header, HTTPException API_KEY = "your-secret-token" def verify_api_key(x_api_key: str = Header(default="")): if x_api_key != API_KEY: raise HTTPException(status_code=401, detail="Invalid API Key")13. 总结与下一步
这个项目最值得尝试的点,是把“传统机器学习”和“LangChain + LLM”组合到一个真实的检测任务里,而不是为了用大模型而用大模型。整套系统跑通之后,你会发现它比单一模型更容易解释,也比纯 Prompt 方案的稳定性更好。
建议先验证的功能是:机器学习模块能否在测试集上稳定复现分类指标,以及置信度分流是否能把边界样本挑出来。最容易踩的坑有两个:一个是测试集上做fit导致数据泄漏,一个是 LLM 调用没有做异常回退,导致单点故障拖垮整个批量任务。
后续可以继续扩展的方向包括:把邮件头信息、发件人信誉、URL 检测接入特征层;用向量数据库构建钓鱼邮件知识库,让 LLM 复核时能参考历史案例;或者在批量任务里引入消息队列,把吞吐量再提上去。无论你是做毕业设计,还是想入门 LangChain 应用开发,这套系统都值得动手跑一遍。