简介这是一份面向计算机与软件工程专业毕业设计、课程设计及NLP入门实践的文档型资料主题为基于Python的文本相似度计算系统适合需要完成文本处理类项目、撰写系统实现论文或了解相似度系统设计的人群参考。压缩包内共1个docx文件大小约749KB内容以毕业论文/设计说明为主体涵盖文本清洗与分词、TF-IDF关键词提取、词向量构建、余弦相似度计算及Django可视化界面等模块并涉及Python、Django、MySQL等技术栈的选型说明。文档还包含可行性分析、系统架构、实验评估与结论展望可帮助读者梳理从需求分析到算法验证的完整写作与实现思路。目前已有138人学习适合用作毕业设计框架参考、功能模块拆解和相似度算法实现思路的对照材料。1. 相似度算出来 0.87 却答不上为什么文本相似度计算系统要解决的真问题做毕业设计选到「文本相似度计算系统」这个题多数人第一反应是调一行cosine_similarity就收工真正跑起来才发现问题不在公式而在工程两段八竿子打不着的文本算出来 0.87老师随口一问「这个分数凭什么」就卡住了。这套基于 Python 的文本相似度计算系统把文本清洗、中文分词、停用词过滤、特征向量化、相似度计算到可视化展示串成一条完整链路Web 层用 Django数据落 MySQL前端 Bootstrap 加 Ajax同时提供文本对文本、文件对语料库两种比对入口。它适合正在做文本处理方向毕业设计或课程设计的人直接对照落地也适合要给搜索、推荐、问答类功能做文本匹配原型的开发者参考模块边界。下面按需求、架构、算法、踩坑、验收的顺序把这套东西拆开讲透。2. 需求拆解与技术选型Python Django MySQL 为什么适合这个题2.1 三类使用者和五条硬功能这套系统的使用者其实分得很清楚。第一类是普通用户打开网页往两个文本框里各粘一段新闻点「开始计算」要的是一个分数加一段可视化结果第二类是内容维护者负责往库里录新闻标题和正文这些数据构成了文件分析时的比对语料第三类是系统管理员管用户、看操作记录、盯首页那四个统计数字——总用户数、总新闻数、总操作次数、当前月份。从这三类人身上能抽出五条必须实现的功能文本预处理清洗、分词、去停用词、关键词与词向量构建、多算法相似度计算余弦、Jaccard、编辑距离、欧氏与曼哈顿距离、结果可视化展示、文件分析上传 txt 后与库内新闻做匹配返回最相似的一条。这五条不是拍脑袋列的它们对应一条完整的请求链路输入进来先洗洗完切词切完转向量向量进算法分数回前端。需求里还有几条容易被忽略但答辩必问的约束响应时间要控制在一秒内、要能同时处理中英文、要能扛住长时间不中断运行、要保证用户输入的数据不被越权访问。这几条落到设计上分别对应索引设计、编码选择、异常兜底和登录鉴权后面章节会逐个对上。提示功能需求写成「能算相似度」是过不了需求分析这一关的要写成「输入两段中文文本选择余弦算法1 秒内返回 0 到 1 之间的分数并展示」。可测量才叫需求。2.2 技术栈取舍为什么走 Python 全栈而不混搭 JSP关键词里常出现 JSP、Java 这类词很多学校的课程体系也确实以 Java Web 为主于是有人想前端 JSP、算法 Python搞成两套服务。我的建议是这个题不要混搭。原因很实际文本处理生态几乎全在 Python 这边jieba、nltk、scikit-learn、numpy 一条线打通你为了「技术栈丰富」硬拆一个 Java 服务出来等于给自己加一层 HTTP 调用、一套序列化协议和一份部署脚本答辩时还得多解释一遍为什么这么干。真正合理的选型是 Python 3.8 以上 Django 3.x MySQL 5.7/8.0。Django 自带 ORM、表单、模板、认证、后台管理新闻录入这种 CRUD 几乎零成本MySQL 存文本正文、计算记录、操作日志足够配合 utf8mb4 能覆盖绝大多数中文场景前端用 Bootstrap 加少量原生 JavaScript表单提交走 Ajax避免整页刷新打断演示节奏。如果课程硬性要求 Java 技术栈也不是不能平移把engine这一层的预处理、向量化、度量三个模块照着同样的接口用 Java 重写Django 那部分换成 Spring Boot 的 Controller 和 Thymeleaf 模板数据库表结构一行不用改。模块边界设计对了换语言只是换壳。2.3 性能指标怎么落到可验收的数字上「响应快」「能处理大数据」这种描述在验收时没法判定。我在做这类系统时习惯把指标翻译成三条可测的线指标目标值测量方式单次文本比对响应时间2000 字以内 ≤ 1s浏览器 Network 面板看 TTFB单文件分析≤2MB txt≤ 3s后端打点记录cost_ms并发可用性10 并发无明显超时本地压测脚本循环请求结果可复现性同一输入重复计算分数一致固定随机种子与语料第一条决定了你不能在每次请求里重新fit一遍向量器第二条决定了文件大小必须设上限第三条决定了数据库连接池和分词器初始化要放在启动阶段第四条决定了语料库必须固定不能随着新闻新增而让历史分数漂移。这四条约束在后面第四章的代码里会一一体现现在先记住它们的存在。3. 总体架构与数据模型B/S 三层结构加六张核心表的职责划分3.1 前后端分层与一次请求的完整链路系统走的是标准 B/S 结构前端负责输入与展示后端负责计算与存储中间靠 HTTP 通信。真正值得说清楚的是后端内部的分层因为这一层决定了你后期改算法时会不会牵一发动全身。整个后端拆成四块预处理层、特征提取层、相似度计算层、结果呈现层。预处理层只干一件事——把任意脏文本变成干净 token 列表它不知道后面要用什么算法特征提取层把 token 列表变成数值向量它不知道这些向量会被拿去算余弦还是算 Jaccard计算层只接收两个向量或两个集合返回一个 0 到 1 的分数它不知道数据从哪来呈现层只负责组装模板上下文。四层之间用纯 Python 函数通信不跨进程。这种切法的好处在联调阶段特别明显。某次我发现中文分词的停用词表漏了几个高频虚词只需要改preprocess.py一个文件向量化和计算层完全不动回归测试也只跑预处理那几个用例。要是当初把清洗、切词、算分写在一个视图函数里这一改就得整体重测。一次完整的文本比对请求链路是这样走的浏览器提交表单 → Django URL 路由到视图 → 表单校验通过 → 调用SimilarityEngine→ 预处理 → 向量化语料库向量在启动时已建好→ 计算 → 分数写入similarity_record表 → 渲染模板返回。文件分析走的是同一条链路只是入口换成文件解析比对对象换成库内全部新闻。3.2 六张核心表与字段设计数据库这块我的原则是能用 Django ORM 定义就不手写 SQL但表结构本身要想清楚。系统里真正承载业务的有六张表用户表复用 Django 自带auth_user、新闻表、相似度计算记录表、操作日志表、新闻分类表、上传文件表。表名作用关键字段auth_user登录与权限username、password、is_staffnews语料库主体title、content、category_id、created_atsimilarity_record每次计算的留痕text_a、text_b、algorithm、score、cost_msoperation_log操作审计user_id、action、target、created_atnews_category新闻分类name、sortupload_file文件分析记录filename、size、matched_news_id、score新闻表和计算记录表是重点。新闻表的content用MEDIUMTEXT因为新闻正文可能上万字title上加前缀索引因为文件分析时要按标题做快速召回字符集统一utf8mb4这一点在第五章会重点讲用utf8迟早翻车。计算记录表看起来可有可无实际是答辩时的护身符——老师问「你这个结果可复现吗」你直接SELECT score FROM similarity_record WHERE text_a? AND text_b?给他看历史记录比嘴上解释一百句都管用。CREATE TABLE news ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, content MEDIUMTEXT NOT NULL, category_id INT DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_title (title(64)), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci; CREATE TABLE similarity_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, text_a TEXT NOT NULL, text_b TEXT NOT NULL, algorithm VARCHAR(16) NOT NULL DEFAULT cosine, score DECIMAL(6,4) NOT NULL, cost_ms INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;上面两段 DDL 里DECIMAL(6,4)是刻意的——相似度只可能是 0 到 1 之间的小数四位小数足够表达用FLOAT会出现 0.30000000000000004 这种展示尴尬。idx_user_time这个联合索引服务于「某用户最近的计算记录」这类查询属于典型的时间倒序翻页场景。text_a和text_b直接存原文而不是存哈希是为了让记录表本身能当复盘素材代价是占空间本地演示规模完全扛得住。3.3 目录结构与模块边界工程目录按 Django 的 app 划分一个大原则是算法不进视图text_sim/ ├── manage.py ├── text_sim/ # 配置settings、urls、wsgi ├── similarity/ # 核心计算应用 │ ├── engine/ │ │ ├── preprocess.py # 清洗、分词、停用词 │ │ ├── features.py # 词袋 / TF-IDF 向量化 │ │ └── metrics.py # 余弦、Jaccard、编辑距离 │ ├── forms.py │ ├── models.py │ ├── views.py │ └── urls.py ├── news/ # 新闻语料管理 ├── accounts/ # 用户与个人信息 ├── static/ └── templates/engine这个包不 import 任何 Django 模块纯 Python意味着它可以脱离 Web 框架单独跑测试。这一点在做对比实验时特别省事写个脚本from similarity.engine.metrics import cosine直接对着一批样本算不用启动服务、不用构造请求。很多同学把算法写在views.py里结果想批量测十组数据还得构造十个 HTTP 请求纯属给自己找麻烦。4. 算法链路实现从 jieba 分词、TF-IDF 向量化到四种相似度计算4.1 文本预处理清洗、分词、去停用词的可复现代码预处理是整条链路的入口也是最容易被低估的一环。输入文本可能带着 HTML 标签、URL、全角标点、表情符号、连续空白这些东西如果直接进分词会产生大量无意义 token把相似度分数拉偏。清洗的取舍要想清楚中英文都保留数字和符号统一替换为空格。为什么留数字如果比对的是带年份、带编号的新闻数字其实是有效特征如果你的场景里数字纯属噪音把它也过滤掉即可。import re import jieba def clean_text(text: str) - str: 去标签、去链接、保留中英文其余字符压成空格 text re.sub(r[^], , text) # HTML 标签 text re.sub(rhttps?://\S, , text) # URL text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) # 只留中英文数字 return re.sub(r\s, , text).strip()分词这块中文用jieba.lcut英文用正则切分后接 Porter 词干化。停用词表自己维护一份文本文件一行一个词加载时转成set查找复杂度 O(1)。这里有个细节len(t) 1这个过滤条件会把单字词全部丢掉中文里「爱」「恨」这种单字其实有语义但它们在短文本相似度里贡献极小且噪音大我在这个系统里选择丢弃换来的是分数更聚焦在实词上。def tokenize(text: str, lang: str zh) - list: text clean_text(text).lower() if lang zh: tokens jieba.lcut(text) stop load_stopwords(stopwords_zh.txt) else: tokens re.findall(r[a-z], text) stop set(stopwords.words(english)) # 过滤停用词、单字和纯数字减少噪音 return [t for t in tokens if len(t) 1 and t not in stop and not t.isdigit()]lang参数是从表单里透传下来的用户在前端选择「中文 / 英文」后端据此走不同分支。如果你的系统要支持中英混合别指望自动语种识别直接按中文字符占比做启发式判断就行超过 30% 走中文分支简单可靠。停用词表本身值得花半小时整理。网上随手搜的表动辄一两千词里面塞满了「我们」「他们」「可以」这类在特定语料里其实是关键词的词。我的做法是先跑一批真实新闻统计词频把明显无区分度的高频词删掉最后留下三四百个。这一步做完同一对文本的分数通常会更接近人的直觉。4.2 特征提取词袋与 TF-IDF 的取舍初稿里常见的写法是词袋模型把整个语料库的词汇做成一个词表每篇文本对应一个等长向量出现过的词标 1没出现的标 0。这个方案实现简单、解释直观但它有两个硬伤。第一个硬伤是长文本天然占优。一篇两千字的文章词袋里命中 1 的维度远多于一段五十字的短句余弦相似度算出来必然偏高哪怕两者主题完全不同。第二个硬伤是常见词权重过大。「的」「是」「在」虽然被停用词过滤了一部分但像「新闻」「记者」这种在语料里高频出现的词在词袋模型里和生僻专有名词的权重完全一样。TF-IDF 正好解决第二个问题词频乘以逆文档频率一个词在越多文档里出现它的区分度越低权重就被压下去。实现上直接用 scikit-learn 的TfidfVectorizer把自定义的tokenize函数塞进tokenizer参数注意lowercaseFalse否则它会二次小写化。from sklearn.feature_extraction.text import TfidfVectorizer def build_vectorizer(corpus_tokens): 语料库固定后 fit 一次后续 transform 复用保证向量空间一致 vectorizer TfidfVectorizer( tokenizerlambda x: x, # 传入的已经是 token 列表 lowercaseFalse, max_features5000, # 控制维度避免稀疏矩阵过大 token_patternNone ) vectorizer.fit(corpus_tokens) return vectorizer这里最关键的一点是fit只能在语料库上做一次。我见过不少实现是在每次请求里把「文本 A 文本 B」临时拼成语料再fit_transform这样算出来的余弦相似度永远偏高因为向量空间只有两个维度两者只要有一个公共词夹角就会很小。正确做法是启动时用全部新闻正文 fit 出一个固定的向量器存成全局对象或缓存请求来时两段文本各自transform落进同一个 5000 维空间里再比较。词袋模型也不是完全弃用。我在系统里保留它作为可切换的向量化方式答辩时正好做对照实验同一组样本词袋算出 0.62TF-IDF 算出 0.41然后解释为什么后者更贴近语义判读。这组对比数据本身就是加分项。4.3 相似度计算四种度量的封装与适用边界计算层封装成统一接口输入两个对象输出 0 到 1 的分数。四种度量各有各的适用场景不能只会余弦。余弦相似度衡量的是向量夹角对长度不敏感长文本短文本比较不会被长度带偏是默认选项。Jaccard 看的是两个 token 集合的交并比适合短文本、关键词匹配这种场景比如判断两条新闻标题是不是同一事件。编辑距离衡量的是把 A 改成 B 最少要几步适合错别字容忍、拼写纠错但它对长度差极其敏感两段长度差一倍的中文几乎必然分数很低。欧氏距离和曼哈顿距离本质是距离需要再转成相似度通常用1 / (1 d)归一化。import numpy as np def cosine(vec_a, vec_b) - float: a, b np.asarray(vec_a, dtypefloat), np.asarray(vec_b, dtypefloat) denom np.linalg.norm(a) * np.linalg.norm(b) if denom 0: # 空向量直接判 0绝不允许除零 return 0.0 return float(np.dot(a, b) / denom) def jaccard(tokens_a, tokens_b) - float: sa, sb set(tokens_a), set(tokens_b) if not sa or not sb: # 任一方为空交集必为空 return 0.0 return len(sa sb) / len(sa | sb)denom 0这个判断看着不起眼它是整个系统最容易被漏掉的一行。空文本、纯符号文本、全部被停用词过滤掉的文本在transform之后会得到一个全零向量np.linalg.norm返回 0接下来就是ZeroDivisionError前端拿到 500 错误页。第五章还会专门说这个坑。编辑距离用difflib.SequenceMatcher就够不必引第三方库from difflib import SequenceMatcher def edit_similarity(text_a: str, text_b: str) - float: # ratio 内部按最长匹配子序列计算返回 0~1 return SequenceMatcher(None, text_a, text_b).ratio()视图层把这几个函数统一包进一个引擎类按前端传来的algorithm参数分派。这样加算法只要在类里加一个分支不用动路由和模板。4.4 Django 视图与文件分析接口视图层要做三件事接表单、调引擎、落记录。表单类这里有个初稿里常见的毛病——只定义了一个text字段可界面上明明是「两个输入框加一个按钮」一个字段装不下两段文本提交后校验直接失败。正确写法是两个字段加一个算法选择# similarity/forms.py from django import forms class TextSimilarityForm(forms.Form): text_a forms.CharField( label文本 A, widgetforms.Textarea(attrs{class: form-control, rows: 6})) text_b forms.CharField( label文本 B, widgetforms.Textarea(attrs{class: form-control, rows: 6})) algorithm forms.ChoiceField( choices[(cosine, 余弦), (jaccard, Jaccard), (edit, 编辑距离), (euclid, 欧氏距离)], initialcosine)widget里指定的form-control是 Bootstrap 的类名这样后端渲染出来的输入框和前端样式表能对上不用手写一堆 CSS。algorithm用ChoiceField而不是让前端传自由字符串是为了避免用户传个cosinexxx进来导致分派失败。# similarity/views.py from django.shortcuts import render from django.contrib.auth.decorators import login_required from .forms import TextSimilarityForm from .engine.metrics import cosine, jaccard, edit_similarity from .models import SimilarityRecord login_required def index(request): result None if request.method POST: form TextSimilarityForm(request.POST) if form.is_valid(): a form.cleaned_data[text_a] b form.cleaned_data[text_b] algo form.cleaned_data[algorithm] # 分派到对应度量空输入由 metrics 层兜底返回 0 if algo jaccard: score jaccard(tokenize(a), tokenize(b)) elif algo edit: score edit_similarity(a, b) else: va, vb transform_pair(a, b) # 复用全局向量器 score cosine(va, vb) SimilarityRecord.objects.create( user_idrequest.user.id, text_aa, text_bb, algorithmalgo, scoreround(score, 4)) result {score: round(score, 4), algorithm: algo} else: form TextSimilarityForm() return render(request, similarity/index.html, {form: form, result: result})login_required这个装饰器对应需求里的「保护用户数据」未登录直接跳登录页不需要自己写判断。SimilarityRecord.objects.create这一行是异步无关的但要注意它会在主请求里多花几毫秒写库如果追求极限响应可以改成消息队列本地演示规模没必要。文件分析接口的差异在于入口是request.FILES且比对对象是库里全部新闻。这里必须做两层校验扩展名必须是.txt同时读前 512 字节判断是不是可解码文本。只校验扩展名不够把.docx改名成.txt一样能传上来读出来是二进制乱码分词结果全是噪音最后匹配出一条毫不相干的新闻用户还以为系统坏了。login_required def analyze_file(request): f request.FILES.get(file) if not f or not f.name.endswith(.txt) or f.size 2 * 1024 * 1024: return JsonResponse({code: 400, msg: 仅支持 2MB 以内的 txt}) raw f.read() try: content raw.decode(utf-8) except UnicodeDecodeError: content raw.decode(gbk, errorsignore) # 兼容旧编码 tokens tokenize(content) best match_news(tokens) # 与库里全部新闻算相似度取最大 return JsonResponse({code: 0, data: best})这段代码里f.size 2 * 1024 * 1024对应第二章那条「单文件分析 ≤ 3s」的指标超过 2MB 的文本光分词就要好几秒不如直接拒掉。decode(gbk)这个兜底是血泪经验Windows 下导出的 txt 大概率是 GBK不处理就是满屏乱码。5. 避坑与排查五个最容易翻车的环节5.1 相似度分数人人都是 0.9现象随便拿两段不相关的新闻去算余弦相似度都在 0.8 以上前端展示出一条几乎拉满的进度条看起来系统坏了。原因有两层。表层是停用词表太小虚词没滤干净两段文本共享大量「我们」「这个」「表示」之类的词向量在这些维度上高度重合。深层原因是向量器在每次请求里重新fit向量空间只由这两段文本构成维度塌缩到几十个任何共享词都会把夹角压得很小。解决停用词表按语料词频重新筛一遍删掉无区分度的高频词向量器改成全局单例启动时用全部新闻fit一次请求里只transform再加一条兜底分数低于 0.1 和高于 0.95 的记录打日志抽样看确认不是系统性偏差。改完之后同一组样本的分数会从 0.87 落到 0.35 左右这才符合人的直觉。5.2 空文本触发除零异常现象用户不填内容直接点计算页面报 500后端日志是ZeroDivisionError: float division by zero。原因空字符串经过清洗和分词得到空列表向量化后是全零向量模长为 0余弦公式的分母直接变零。这个坑在正常测试里几乎撞不到因为大家测试时都会老老实实填内容。解决在metrics层做双重兜底一是算分母前判断denom 0返回 0二是在视图入口判断len(tokens) 2就返回友好提示而不是继续往下走。前端也加一层 JavaScript 校验输入框为空时禁用按钮。三层防护听着夸张但这类边界用例恰恰是答辩时老师最爱点的。5.3 中文乱码和表情符号存不进去现象存进去的新闻正文在列表页显示成问号或者用户粘贴带表情符号的文本时MySQL 直接抛Incorrect string value异常。原因数据库或表建的时候用了utf8字符集。MySQL 的utf8实际是三个字节的变长编码存不下四字节的字符表情符号和部分生僻汉字都会被拒。解决建库建表统一utf8mb4Djangosettings.py里DATABASES的OPTIONS加上charset: utf8mb4连接串也确认一遍。已经建好的表用ALTER TABLE news CONVERT TO CHARACTER SET utf8mb4迁移迁移前先备份。这个坑不改等到演示当天用户粘一段带 emoji 的文本就当场翻车。5.4 文件分析接口把请求拖死现象上传一个 5MB 的 txt页面转圈十几秒最后超时浏览器报 504。原因文件分析要把上传文本和库里每一条新闻逐一算相似度新闻一千条就是一千次向量化加一千次余弦计算全在请求线程里同步跑。再加上如果向量器没做缓存每次transform都重新加载词典时间还会翻倍。解决接口层限制文件 2MB 并做前置校验新闻向量在启动时批量算好缓存起来比对时只算上传文本的向量然后做矩阵乘法一次性出全部分数一千条新闻的比对能压到几十毫秒新闻量再大就上分页或者只比对标题做粗召回再对 Top 20 做正文精算。5.5 同一组输入两次算出不同分数现象演示时先算了一遍 0.42讲了两句再点一次变成 0.38老师当场质疑结果不可信。原因语料库在两次计算之间被改动过——后台新录了一条新闻或者向量器因为某种原因被重新fit。TF-IDF 的 IDF 值依赖整个语料库语料一变同一段文本的向量就变了分数自然漂移。解决把语料快照和向量器版本绑在一起每次fit后把vectorizer和语料 id 列表序列化落盘请求时加载固定版本同时similarity_record表里记下当时的vectorizer_version复盘时有据可查。演示前锁库不要在现场加新闻这条听着像废话但每年都有人栽在上面。6. 验收与答辩一套可复现的测试用例和十分钟演示脚本6.1 功能验收清单与测试用例验收这件事靠「我点了几下没报错」是过不了的。我在做这类系统时习惯先写一张用例表把正常路径、边界路径、异常路径各覆盖几条跑完打勾才算交付。编号输入预期结果判定TC-01两段完全相同的文本分数 1.0000通过TC-02两段主题无关的新闻分数 0.20通过TC-03两段空文本提示「请输入内容」不抛 500通过TC-04纯标点符号返回 0 并给出提示通过TC-052000 字中文切换四种算法均返回 0~1响应 ≤ 1s通过TC-06上传 2MB 以内 txt正常解析并返回最相似新闻通过TC-07把 docx 改名成 txt 上传拒绝并提示格式错误通过TC-08未登录访问计算页跳转登录页通过TC-01 和 TC-02 是一对一个验上限一个验下限缺了下限那条你无法证明系统不是「什么都算得很像」。TC-03 到 TC-05 是边界TC-06 到 TC-08 是异常和权限。八条用例跑完大概五分钟建议录屏存一份答辩现场网络出问题时直接放录像。6.2 现场演示的十分钟脚本演示最忌讳临场发挥我会提前把每一步的顺序和话术写死第 0 到 2 分钟登录进首页指着四个统计数字说清楚系统在管什么——总用户、总新闻、总操作、当前月份顺便点开操作日志证明每次计算都留了痕。第 2 到 5 分钟进文本分析页粘两段同主题新闻默认余弦点计算得到 0.6 左右然后把算法切到 Jaccard分数掉到 0.3 左右再切到编辑距离分数更低。每切一次解释一句「余弦看方向、Jaccard 看词集重合、编辑距离看字面改动」这一段是整个演示的信息密度高点。第 5 到 7 分钟进文件分析上传一个准备好的 txt展示返回的最相似新闻标题和分数再点进去看原文证明匹配结果说得通。第 7 到 9 分钟进后台新闻管理加一条新闻回前台重算说明语料变化对分数的影响顺势引出「向量器版本固定」这个设计。第 9 到 10 分钟打开数据库客户端展示similarity_record里刚才那几条记录收尾。6.3 答辩常被追问的三个点第一个问题几乎必问为什么用余弦不用欧氏距离。回答要落到场景上——文本向量是高维稀疏的长度差异大欧氏距离对长度敏感两篇长短悬殊但主题一致的文章会被判得很远余弦只看夹角对长度不敏感更适合文本。第二个问题词袋和 TF-IDF 差在哪。直接拿演示时那组对比数据说话同一对文本两种方式算出的分数差多少差在哪个词上比背定义有说服力。第三个问题系统能处理多长的文本瓶颈在哪。老实回答瓶颈在分词和向量化2MB 的文件要几秒再大就得上分批和缓存然后把第五章那套缓存方案讲一遍。承认边界比吹嘘无限能扛更能拿分。从那以后我每次带人做这个题都强制先跑一遍空文本、纯符号、docx 改名这三条异常用例再谈功能演示——顺序错了后面全是补救。希望这套结构和清单能帮到你照着搭一遍再按自己的语料把停用词表和用例表改一改就是一份能交差的完整方案。本文还有配套的精品资源点击获取