ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AI智能联系人管理毕设指南:从语义搜索到重复检测的实现

AI智能联系人管理毕设指南:从语义搜索到重复检测的实现

简介:面向毕业设计场景的Python+AI智能联系人管理项目,以Python为主要开发语言,借助自然语言处理与机器学习实现联系人高效管理、智能检索及行为预测推荐,覆盖添加、编辑、删除、搜索分类与导入导出等功能,适合计算机相关专业学生作为课程设计或毕业论文实践参考。整套资源共50个文件,压缩包约141.21MB,包含7个py源码与10个pyc编译文件、8个ui界面文件、6个html页面及js脚本、3个xml配置、2个xlsx数据表格,另有doc使用说明、pdf申请指南、exe可执行程序及jpg/png图片素材,从源码阅读到直接运行的完整链路都齐备。开发文档详解系统设计理念、架构与测试案例,可执行程序让非开发人员免环境直接体验,模板素材用于优化界面观感。已有612人学习下载,对于想掌握AI技术如何落地到实际软件的学生而言,这份资源既能巩固Python编程能力,也能提供完整的工程实践范本。

1. 这个题目真正难的不是通讯录,而是「AI」两个字怎么交差

做过毕业设计的人都有体会:导师扫一眼题目,「AI智能联系人管理」里真正会被追问的,不是联系人表怎么建,而是这个「AI」到底体现在哪。只用 LIKE 模糊搜索加一个增删改查,撑不起题目里的「智能」;但真要接一个大模型接口,论文解释不了、演示环境又不稳定。我见过不止一个同学卡在这:功能做完了,答辩时被一句「你的 AI 算法在哪里」问住。这条路的正解,是用轻量但完整的算法把「智能」做成系统里可解释、可演示、可写进论文的模块——语义搜索、重复联系人自动合并、标签自动生成,这些技术单拆出来都不难,合在一起就是一个能站住脚的毕设核心。这篇文章就按这个思路,从选型到代码,再到开发文档怎么写、哪些坑必须躲,完整拆一遍。

2. 技术选型:为什么是 Flask + SQLite + 轻量算法,而不是 Django + MySQL + 大模型

2.1 三个能落地的「AI 能力」选型:语义搜索、重复合并、自动标签

毕设联系人系统的「智能」要想在有限工期里既做得出、又讲得清,就得把 AI 落在这类场景依赖少、效果肉眼可见的任务上。第一是语义搜索:搜「做芯片的张总」能命中备注里写着「张某某,芯片行业,深圳」的联系人,而不是靠关键词硬匹配;第二是重复联系人检测:同一人存了两个号码、称呼不一致时,系统能识别「李强」和「李强(北京)」是同一个人并提示合并;第三是自动标签:根据邮箱域名、公司字段、联系频率自动生成「商务」「高校」「活跃」等分组。这三个能力各自只需要一个经典算法——TF-IDF 加余弦相似度、编辑距离加并查集、规则引擎——计算量小,本地就能跑,而且每一个都能在论文里画出清晰的算法流程图。相比之下,接入大模型 API 做智能问答或摘要看似更「AI」,但答辩现场网络一断、模型接口一限流,整个 Demo 就塌了,不划算。

2.2 框架选型:Flask + SQLite 为什么比 Django + MySQL 更适合这个题

框架选择上,常见做法是 Flask 配 SQLite,而不是 Django 配 MySQL。原因有三层:第一,联系人管理的数据量在毕设场景下就是几千条,SQLite 单文件存储足够支撑,还能省掉 MySQL 的安装配置,评审老师把项目拷到别的机器上也能直接跑;第二,Flask 的代码结构直观,ORM 用 SQLAlchemy 独立封装,写论文时「表示层—业务层—数据层」三层架构一目了然,这是毕业设计评分的加分点;第三,Django 自带 Admin 后台和 ORM 确实省事,但它的「全家桶」结构会让论文里的系统设计章节很难写出自己主导的架构感,而且对新手来说,Django 的中间件、信号、迁移机制本身就是一个新的学习负担。如果你的选题另外要求了「高并发」或「分布式」,那另当别论;但就「AI智能联系人管理」这个题目而言,Flask + SQLite 是把精力留给 AI 模块的最优解。

2.3 系统整体模块划分与数据流

按照上面的思路,整个系统逻辑上拆成四个模块:联系人管理(增删改查、分组、导入导出)、AI 服务(语义搜索、重复检测、自动标签、亲密关系提醒)、用户界面(网页端)、数据存储(SQLite)。调用关系是:用户在前端发起请求,业务层调用联系人管理模块做常规操作,涉及搜索、合并、打标签时再进入 AI 服务模块,AI 模块读取数据存储中的联系人数据做计算,把结果返回给界面。血缘关系上,普通字段的改动只走业务层,AI 模块只做「读数据 + 算结果 + 回写标记」,这样的设计让两边可以并行开发,也能在论文里分别论述。下面这张表是依赖版本的一个参考组合,实际以你自己环境为准:

依赖版本建议用途
Python3.9 或 3.10运行环境,兼容性最稳
Flask2.xWeb 框架
SQLAlchemy1.4 或 2.xORM 与数据库操作
jieba0.42.x中文分词,语义搜索的前提
scikit-learn1.xTF-IDF 与余弦相似度计算
difflib标准库重复名称相似度计算

开发环境建议直接用虚拟环境:python -m venv venv,然后把依赖写进 requirements.txt。这一步很多人跳过,结果演示前在教室的机器上装依赖装了四十分钟,血泪经验,后文避坑章节会单独讲。

3. 核心代码落地:从数据模型到四个 AI 功能的完整实现

3.1 联系人数据模型:为什么用关联表存标签,而不是一个字段存字符串

数据模型是后面所有 AI 功能的地基,这里容易犯错的地方是把标签存成「游戏,同学,球友」这样一个逗号分隔字符串。短期内省事,但你要做「按标签统计人数」「找出同时属于两个标签的人」这类功能时,全部退化成 LIKE 模糊匹配,效率低且答辩时会被问住。正确做法是标准的多对多模型:联系人表和标签表,通过中间关联表相连。用 SQLAlchemy 建模型代码如下:

from datetime import datetime from sqlalchemy import Column, Integer, String, Text, DateTime, Table, ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() # 多对多中间表:一个联系人可以有多个标签,一个标签下可以有多个联系人 contact_tag = Table( 'contact_tag', Base.metadata, Column('contact_id', Integer, ForeignKey('contacts.id'), primary_key=True), Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True), ) class Contact(Base): __tablename__ = 'contacts' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(50), nullable=False, index=True) # 姓名,必填 phone = Column(String(20), index=True) # 手机号 email = Column(String(120)) # 邮箱 company = Column(String(120)) # 公司 address = Column(String(200)) # 地址 remark = Column(Text) # 备注,参与语义搜索 created_at = Column(DateTime, default=datetime.now) # 创建时间 last_contact_at = Column(DateTime, nullable=True) # 最后联系时间 contact_count = Column(Integer, default=0) # 联系总次数 favorite = Column(Integer, default=0) # 是否星标,0/1 tags = relationship('Tag', secondary=contact_tag, backref='contacts') class Tag(Base): __tablename__ = 'tags' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(30), unique=True, nullable=False)

这段代码有两个关键决策。第一是last_contact_at和contact_count这两个字段,它们不为普通展示服务,而是为后面的「亲密关系提醒」和「活跃标签」提供数据来源,建表时留好,免得后期加字段还要改迁移脚本。第二是remark和tags必须进模型,因为语义搜索要把它们一起作为检索内容,缺一个字段,搜索效果都会打折。索引方面,name和phone加了index=True,常规查询走索引,数据量到万级也不会卡。

3.2 语义搜索:用 TF-IDF 加余弦相似度替代 LIKE 模糊查询

语义搜索是整个系统里最像「AI」的部分,也是答辩时最能讲算法的模块。原理一句话:把每个联系人的姓名、公司、备注、标签拼成一段文本,用 jieba 分词,再用 TfidfVectorizer 把文本转成向量,查询时把用户的输入也转成向量,最后用余弦相似度排序。这样搜「做芯片的张总」,哪怕备注里写的是「张某某在深圳做半导体芯片,行业峰会认识的」,也能靠「芯片」「半导体」的语义关联命中。核心代码实现如下:

import jieba import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def build_corpus(contact_list): """把联系人对象列表转成可检索的文本语料""" corpus = [] for c in contact_list: # 每个字段用空格隔开,避免 jieba 把跨字段的词错误合并 terms = [] terms.append(c.name or '') if c.company: terms.append(c.company) if c.remark: terms.append(c.remark) if c.email: terms.append(c.email) for tag in c.tags: terms.append(tag.name) corpus.append(' '.join(terms)) return corpus def semantic_search(contact_list, query, top_k=5, min_score=0.05): """语义搜索:按相关度返回 [(联系人, 分数)]""" corpus = build_corpus(contact_list) # tokenizer=jieba.lcut 让 sklearn 使用 jieba 分词结果 vectorizer = TfidfVectorizer(tokenizer=jieba.lcut) tfidf_matrix = vectorizer.fit_transform(corpus) query_vec = vectorizer.transform([' '.join(jieba.lcut(query))]) scores = cosine_similarity(query_vec, tfidf_matrix).flatten() top_idx = np.argsort(scores)[::-1][:top_k] results = [] for idx in top_idx: score = float(scores[idx]) if score < min_score: continue # 低于阈值的结果视为无关 results.append((contact_list[idx], score)) return results

逻辑说明:build_corpus把每个联系人的多个字段拼成一个文档,创建向量时TfidfVectorizer会统计词频并计算逆文档频率,高频但无区分度的词(如「的」「了」)会被自动降权;查询时query_vec转成同一个特征空间里的向量,cosine_similarity计算两个向量夹角的余弦值,越接近 1 越相似。min_score这个阈值要结合实际数据调:如果数据里备注文案普遍短,0.05 偏低会出现无关结果,可以调到 0.1;如果备注普遍长,0.05 又能保住部分弱相关的合理结果。分词的坑在于 jieba 默认词库对「张总」「王工」这类称呼容易切碎,简单办法是在系统启动时加载一次自定义词典,把常见称呼和行业词加进去。

3.3 重复联系人检测:编辑距离加并查集,处理「一个人被存了两次」

联系人系统里最高频的脏数据问题,是同一个人被以不同形式存了两条以上:「李强」和「李强(北京)」,或者同一手机号挂了两个不同姓名。重复检测要做两件事:算出两两之间的相似度,再把相似的联系人聚类成一组。选择并查集而不是简单两两标记,是因为「A 和 B 相似、B 和 C 相似」时,A、B、C 三者应该归为一组,这本质是连通分量问题。代码实现如下:

from itertools import combinations from difflib import SequenceMatcher def name_similarity(a: str, b: str) -> float: """基于编辑距离的归一化相似度:1 表示完全相同,0 表示完全不同""" return SequenceMatcher(None, a, b).ratio() def find_duplicate_groups(contacts, name_threshold=0.85): """返回并查集 parent 数组:parent[i] 相同则属于同一重复组""" n = len(contacts) parent = list(range(n)) def find(x): # 路径压缩,保证合并时复杂度接近 O(1) while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x def union(x, y): rx, ry = find(x), find(y) if rx != ry: parent[ry] = rx # 两两比较,满足任一条件即视为同一人 for i, j in combinations(range(n), 2): ci, cj = contacts[i], contacts[j] same_phone = ci.phone and ci.phone == cj.phone same_name = name_similarity(ci.name, cj.name) >= name_threshold if same_phone or same_name: union(i, j) return parent

参数要说明白:name_threshold=0.85是个经验值,对中文姓名来说,「李强」和「李强(北京)」的相似度约 0.73,但在工整数据里「李强」和「李莉」的相似度只有 0.5 左右,0.85 能覆盖「带了后缀/前缀」的情况又不至于把同姓不同名的人误合并。same_phone加了ci.phone非空判断,避免两个没有手机号的人因为都为空就被判成同一人。实际使用中要保留一份「合并建议列表」让用户确认,而不是自动删除数据——毕业设计里「自动合并」直接改数据是高风险操作,数据一旦误合并且没提供撤销,演示当场翻车。

3.4 自动标签与亲密关系提醒:不靠 AI 模型的规则实现

自动标签的功能定位是「让新录入的联系人自动进入合适的分组」,常见做法是规则引擎加少量启发式逻辑。与前面两个模块不同,这部分不使用机器学习模型,而是基于业务规则的可解释规则系统,好处是每条规则都能在论文里列表说明,答辩时完全可控。核心实现如下:

import re from datetime import date, datetime def auto_generate_tags(contact): """根据联系人字段自动推断标签,返回标签名列表""" tags = set() email = contact.email or '' name = contact.name or '' # 规则 1:邮箱域名识别身份 m = re.search(r'@([\w.-]+)', email) if m: domain = m.group(1).lower() if domain.endswith('.edu.cn'): tags.add('高校') elif domain in ('qq.com', '163.com', '126.com'): tags.add('个人邮箱') else: tags.add('商务') # 规则 2:公司字段存在则视为工作联系人 if contact.company: tags.add('工作') # 规则 3:最近 30 天内联系过,视为活跃 if contact.last_contact_at: days_since = (datetime.now() - contact.last_contact_at).days if days_since <= 30: tags.add('活跃') # 规则 4:联系次数超过 10 次,视为高频 if contact.contact_count >= 10: tags.add('高频') return list(tags)

auto_generate_tags在联系人新增或编辑后调用,把返回的标签名与现有 Tag 表做 get-or-create 再关联。这里有意不用机器学习,因为标签的「标准答案」本身主观,规则系统 100% 可控且一眼能看懂。规则阈值是按业务预设的,比如「30 天」「10 次」可以在界面上做成可配置项,也能写进论文做参数分析。「亲密关系提醒」同理:用一个查询找出last_contact_at距今超过 90 天、且历史contact_count >= 5的联系人,在首页提示「你可能很久没联系某人了」,这就是可写进文档的 AI 应用场景之一。

4. 开发文档怎么写,才能让源码和论文互相撑住

4.1 毕业设计文档集的标准构成与篇幅参考

带「开发文档说明」的题目,评审重点不只是代码跑不跑得通,还包括文档是否完整、是否与源码一致。一套能过审的文档集通常包含六份:开题报告(含选题背景与可行性)、需求规格说明书、概要设计说明书、详细设计说明书、数据库设计说明、测试报告。正文里写文档,要记住一个原则:每份文档解决一个特定问题,不要在一份文档里既写需求又写实现。篇幅上,需求规格 8 到 12 页、概要设计 10 到 15 页、详细设计 15 到 20 页是比较稳妥的区间,核心目标是让一个没看过你代码的人,仅靠文档就能理解系统在做什么、模块怎么协作、核心算法流程是什么。

4.2 需求规格与概要设计:先画图、再写字,用例图必须先于数据表

文档最容易犯的错误是一上来就贴数据库建表语句。正确顺序是:先写用例图,把「联系人管理」「智能搜索」「重复检测」「标签管理」「提醒」这几个用例画清楚,每个用例配上三到五行文字说明;再画数据流图,标出数据从界面层到业务层再到存储层的流转;最后才是 E-R 图和建表语句。写作时常见做法是每张图下配一段 200 字左右的解释,把图中元素对应到代码模块,比如「用户发起语义搜索请求时,请求经 ContactsController 转发至 AIService,AIService 调用向量化模块计算出相似度排名」。这样的写法在查重和答辩追问时都站得住,因为图和代码是逐一对应的。

4.3 详细设计文档要跟源码函数逐一对上,别写成项目简介

详细设计是工作量所在,也是很多同学偷工减料的地方。合格的标准是:源码里每一个关键函数,文档里都要能找到对应说明,包括输入参数、输出结果、核心逻辑、异常处理。比如语义搜索模块,详细设计里要画一张流程图或写清四步流程:分词、向量化、相似度计算、阈值过滤,然后贴核心接口定义和关键代码片段。写这部分的态度直接决定评审老师的判断,如果文档里出现的函数名在源码里找不到,会被认为「文档和程序属于两套东西」,这是毕设最伤的评价。

5. 调试与避坑:联系人系统最容易翻车的 5 个点

5.1 中文分词不准,语义搜索效果约等于随机排序

现象:搜索「王老师」返回了一堆无关联系人,甚至搜「芯片」出来的结果里没有备注里真正写过芯片的人。原因:jieba 默认词典对单字人名和行业简称分词不稳定,比如把「张总」切成了「张」和「总」,导致查询向量和文档向量匹配不上。解决:系统启动时加载自定义词典,把常见称呼、行业词、学校名放进去,代码用jieba.load_userdict('dict.txt'),一行搞定。这个问题属于典型的不踩不知道、一踩全乱套,建议在开发初期就准备一份词典,边测边补。

5.2 SQLite 并发写入导致界面卡死或报错

现象:同时开两个页面,一个在导入联系人,一个在编辑标签,界面转圈很久,甚至报database is locked。原因:SQLite 同一时刻只允许一个写事务,Flask 开发模式下多个线程并发写就会冲突。解决:给 SQLAlchemy 的连接配置connect_args={'timeout': 10}增加等待时间,同时把写操作集中到一个服务函数里,避免琐碎写入频繁发生。如果并发写是应用的常态,就说明 SQLite 已经不满足需求,但毕设演示场景下加大 timeout 足够。

5.3 重复检测阈值调不好:要么漏掉重复,要么把手足误判成同一人

现象:阈值设为 0.9 时,「李强」和「李强(北京)」没有合并;阈值降到 0.6 后,「李强」和「李莉」又被标记为可能重复。原因:单一阈值无法同时满足所有命名情况。解决:把判断逻辑改为双条件兜底——只要手机号完全相同,无论姓名相似度高低都直接标记;仅当双方都没有手机号时,才用姓名相似度阈值。同时阈值不要写死在代码里,做成一个可配置项,答辩时现场调整对比效果反而加分。

5.4 演示环境依赖装不上,程序在教室电脑上跑不起来

现象:换了一台电脑,pip install -r requirements.txt报错,或者 sklearn 装完后 import 报错找不到动态库。原因:没有锁版本,或者在有系统 Python 的环境里直接装了依赖,破坏了原有环境。解决:开发阶段就建虚拟环境并生成 requirements.txt,生成时保留顶层依赖的版本号;换机器时先建虚拟环境再安装,不要图省事全用--user装到系统目录。演示前至少在一台干净机器上完整跑一遍安装流程,这一步能救很多人。

5.5 开发文档和源码版本不一致,答辩被追问时圆不回来

现象:评阅老师照着详细设计文档里的接口描述提问,源码里根本没有这个参数。原因:文档是中期写的,后来改代码没同步文档,最后仓促交稿。解决:把「文档更新」写进开发计划,每次功能迭代完成时顺手更新对应章节,用半小时改文字,不要攒到最后一天熬夜同步。特别要注意函数名、数据表字段名这类细节,一个不对都可能被当成整体态度问题。

6. 交付前的最后一步:把 Demo 效果验证做扎实,而不是靠运气

很多人的毕设演示是「现场随便找一条数据搜一下」,这完全是把评分交给随机性。更稳妥的做法是准备一套可复现的演示数据脚本,固定随机种子,让每次运行的搜索结果、重复检测结果完全一致,这样你能提前排练演示文稿里的每一个操作步骤。演示数据里要刻意准备三组效果明显的样例:一条备注里写了行业但姓名不含该词的(验证语义搜索)、两条手机号相同但姓名不完全一致的(验证重复合并)、一条长期未联系但历史联系频繁的(验证提醒功能)。验证脚本可以这样写:

import random from datetime import datetime, timedelta # 固定随机种子,保证每次生成的演示数据集一致 random.seed(42) def build_demo_contacts(): demo_data = [ {"name": "张伟", "phone": "13800001111", "company": "华芯科技", "remark": "负责芯片采购,深圳展会认识的", "contact_count": 15, "last_contact_at": datetime.now() - timedelta(days=120)}, {"name": "张伟(北京)", "phone": "13800001111", "company": "华芯科技", "remark": "", "contact_count": 3, "last_contact_at": datetime.now() - timedelta(days=10)}, {"name": "李莉", "phone": "13900002222", "company": "", "remark": "大学同学,目前在高校任教", "contact_count": 2, "last_contact_at": datetime.now() - timedelta(days=5)}, ] return demo_data

固定random.seed(42)的意义在于,任何涉及随机采样的逻辑(比如抽样展示)每次跑结果都一样,演示时不会出现「刚才能搜出来,现在搜不出来」的尴尬。对于语义搜索这类算法,我还会额外做一个小验证:准备 5 个查询词,每个词手工标注期望命中的联系人,跑一遍脚本统计 Top-1 命中率,把结果写进测试报告。这个习惯帮我提前发现了分词问题,而不是在答辩现场被评委的一句「换个词再搜一下」打乱阵脚。当年我因为偷懒没做演示数据脚本,演示时正好撞上一个分词失败的关键词,当场卡了半分钟,才有了后来的这套固定化验证流程。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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