简介:这份毕业设计资料包面向计算机相关专业学生与Python初学者,围绕AI智能联系人管理程序的完整实现,提供源代码、开发文档说明与可直接运行的可执行程序。项目以Python完成联系人增删改查、按姓名电话邮箱检索分类及CSV/VCF导入导出,并尝试结合自然语言处理理解查询、用机器学习分析行为以做联系人推荐,适合用作课程设计、毕设参考或桌面应用入门练手。压缩包为zip格式,共50个文件、约141.21MB,包含7个py源程序、8个ui界面文件、10个pyc字节码、6个html图表页面,以及doc/pdf说明文档、xlsx与dat数据文件、exe可执行程序和界面图片素材,结构上兼顾源码阅读、界面设计与答辩展示。已有615人学习下载。读者可据此梳理项目分层与模块调用关系,理解AI功能如何嵌入联系人管理流程,也能借助开发文档和可执行程序完成功能验证、界面调整与二次开发,积累从需求到交付的软件工程实践。
1. 通讯录翻三页还没找到人,这套 Python 智能联系人管理程序把它做成能跑的桌面软件
答辩前一晚程序起不来,排查半天发现是数据库路径写死在某台电脑的 D 盘——这类翻车在毕业设计里太常见。这份「Python 源代码 + 开发文档说明」的 AI 智能联系人管理程序,恰好把这类问题规避在结构里:界面用 Tkinter 起桌面窗口,数据落在本地 SQLite,检索支持“上个月加的设计师张工”这种口语化输入,重复联系人能自动判重合并,标签分组、生日提醒、CSV/vCard 导入导出都是现成可点的功能。它对标的是完整可运行的交付物,而不是一篇《基于深度学习的智能通讯录设计》式的空论文。
适合三类人:拿它当毕业设计底稿的在校生,改改字段和界面就能交;想练一套“GUI + 数据库 + 算法层”完整分层的 Python 中级开发者;以及需要一份能快速改成内部通讯录原型的开发者。下面从技术选型、目录结构一路讲到核心检索链路、文档说明怎么对照、常见坑怎么排、二次开发往哪改,照着走能跑起来,也能动手改。
2. 技术选型与目录结构:Tkinter + SQLite 的分层桌面架构为什么比 Web 全家桶更合适
2.1 技术栈逐项拆解与选型理由
这套程序的依赖清单不长,但每一项都在承担明确职责。Tkinter 做界面而不是 PyQt5,核心原因是 Python 标准库自带,pip install少一个几十兆的包,答辩现场换台机器也不会因为 Qt 动态库缺失起不来。SQLite 替代 MySQL,是因为单机通讯录的数据量在几千条量级,嵌入式库省掉了“装数据库、配账号、导 SQL 脚本”这一整套环境流程,拷贝一份data/contacts.db就完成了数据迁移。
AI 部分是本项目最容易被误读的地方。标题写着“AI 智能”,实际并没有拉一个联网大模型进来,而是三块轻量算法叠出来的智能感:
| 能力 | 实现方案 | 依赖 | 作用位置 |
|---|---|---|---|
| 口语化查询解析 | 词典匹配 + 正则槽位抽取 | 标准库 re | core/nlu.py |
| 模糊检索与拼音兜底 | 字段加权打分 + 拼音转换 | pypinyin | core/search_engine.py |
| 重复联系人判定 | 编辑距离 + 拼音相似度 + 号码归一化 | difflib | core/dedupe.py |
| 批量导入导出 | DataFrame 读写 | pandas | io/importer.py |
| 统计图表 | 生日/公司分布柱状图 | matplotlib | ui/stats_view.py |
这套组合的好处是全程离线、结果可解释。评委问“你的 AI 体现在哪”,你能指着打分函数说清每一分的来源,比甩一个黑盒接口稳得多。常见做法是把相似度阈值、字段权重都提到config.py里做成常量,调参时不用翻业务代码。
依赖清单大致长这样:
# requirements.txt pandas>=1.3 # CSV/vCard 批量导入导出 pypinyin>=0.49 # 中文姓名转拼音,做检索兜底和排序 jieba>=0.42 # 备注、公司名等长文本分词 matplotlib>=3.5 # 统计视图 python-docx>=0.8 # 导出联系人报表为 docx pytest>=7.0 # 测试用例提示:
pandas和matplotlib体积不小,如果只是交课程作业,可以把统计视图和批量导入摘掉,依赖能砍到两个,启动速度明显提升。
2.2 目录结构与各层职责
项目按“界面 / 核心逻辑 / 数据读写”三层切开,界面层不直接写 SQL,核心层不 import 任何 Tkinter 组件,这条边界守住了,后面拆成 Web 版或者接命令行都好办。
smart_contacts/ ├── main.py # 入口:建库、建窗口、挂事件 ├── config.py # 路径、权重、阈值、分页大小等常量 ├── requirements.txt ├── app/ │ ├── ui/ │ │ ├── main_window.py # 主窗口:列表面板 + 搜索框 + 工具栏 │ │ ├── contact_form.py # 新增/编辑弹窗,含字段校验 │ │ ├── stats_view.py # 统计图表窗口 │ │ └── widgets.py # 可复用控件:带占位符的输入框、标签选择器 │ ├── core/ │ │ ├── db.py # 连接管理、建表、事务封装 │ │ ├── models.py # Contact 数据类,负责字段清洗 │ │ ├── repository.py # 增删改查,返回 dict 而不是裸 tuple │ │ ├── search_engine.py # 加权打分检索 │ │ ├── nlu.py # 口语查询解析 │ │ ├── dedupe.py # 判重与合并策略 │ │ └── reminder.py # 生日/跟进提醒计算 │ └── io/ │ ├── importer.py # CSV / vCard 解析 │ └── exporter.py # 导出,含 utf-8-sig 处理 ├── data/ │ ├── contacts.db # 运行时自动创建,不入版本库 │ └── stopwords.txt # 分词停用词 ├── tests/ │ ├── test_dedupe.py │ └── test_search.py └── docs/ ├── 需求说明.md ├── 数据库设计.md ├── 接口说明.md └── 测试用例.mdrepository.py返回 dict 而非 sqlite3 的 Row 元组,是因为上层打分函数要按字段名取值,元组一改列顺序就全乱。这个细节在二次开发时省很多事,我一般会让所有查询结果统一走一个row_to_dict()。
2.3 环境搭建与首次启动
跑起来只需要三步,关键是把路径问题挡在入口处。config.py里的根目录判断做了两件事:开发态用源码目录,打包成 exe 后用sys._MEIPASS,这样换机器、换路径都不会因为写死盘符崩掉。
# config.py import sys from pathlib import Path def app_root() -> Path: """打包后资源根目录会变,必须动态取,不能写死绝对路径""" if getattr(sys, "frozen", False): # PyInstaller 打包态 return Path(sys._MEIPASS) return Path(__file__).resolve().parent # 源码运行态 BASE_DIR = app_root() DATA_DIR = BASE_DIR / "data" DB_PATH = DATA_DIR / "contacts.db" SEARCH_WEIGHTS = {"name": 1.0, "pinyin": 0.8, "company": 0.6, "title": 0.5, "remark": 0.3} DUP_THRESHOLD = 0.75 # 相似度超过该值提示重复,低于 0.9 由用户确认合并建表语句集中在db.py的init_db()里,字段设计上有个小取舍:tags用逗号拼接的字符串而不是单独一张关联表。理由是标签数量少、查询以“包含某个标签”为主,关联表会带来额外 JOIN,而字符串方案配合LIKE '%设计%'足够用;真要统计标签分布时再在内存里split(',')。索引加在姓名、拼音、手机号上,这三个是检索命中率最高的字段。
-- db.py 中 init_db() 执行的核心建表语句 CREATE TABLE IF NOT EXISTS contact ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, pinyin TEXT, -- 建记录时同步写入,检索时免去实时转换 phone TEXT, email TEXT, company TEXT, title TEXT, tags TEXT, -- 逗号分隔,如 "客户,设计" birthday TEXT, -- 'YYYY-MM-DD',用于提醒计算 remark TEXT, created_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE INDEX IF NOT EXISTS idx_contact_name ON contact(name); CREATE INDEX IF NOT EXISTS idx_contact_pinyin ON contact(pinyin); CREATE INDEX IF NOT EXISTS idx_contact_phone ON contact(phone);首次启动命令很朴素,先把依赖装上再跑入口:
cd smart_contacts python -m venv .venv && source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt python main.pymain.py里做的事有顺序:先init_db()建表建索引,再实例化主窗口,最后mainloop()。如果启动报no such table: contact,基本是DB_PATH指到了别处,打一行print(DB_PATH)就能定位。
3. 核心模块拆解:一句“上个月加的设计师”从输入到出结果的完整链路
3.1 意图解析层:把口语拆成可查询的槽位
搜索框里用户不会规规矩矩输“姓名=张”,而是“上个月加的那个做设计的”。nlu.py的思路是词典加正则,先抽时间和职业,剩下的词交给检索层,不追求通用 NLU,追求对通讯录场景够用。
# app/core/nlu.py import re TIME_WORDS = {"上个月": 30, "上周": 7, "最近": 30, "昨天": 1, "今年": 365} ROLE_WORDS = ["设计", "产品", "运营", "开发", "销售", "测试", "财务"] def parse_query(text: str) -> dict: """把一句口语化查询拆成 {keywords, window_days, roles} 三个槽位""" slots = {"keywords": [], "window_days": None, "roles": []} text = text.strip() # 1) 时间槽:命中词典换算成天数,交给 repository 做 created_at 过滤 for word, days in TIME_WORDS.items(): if word in text: slots["window_days"] = days text = text.replace(word, " ") break # 2) 职业槽:只做标记,实际打分时与 title / remark / tags 交叉命中 for role in ROLE_WORDS: if role in text: slots["roles"].append(role) text = text.replace(role, " ") # 3) 剩余片段当关键词,过滤单字和停用词 slots["keywords"] = [w for w in re.split(r"[\s,,。、]+", text) if len(w) >= 1] return slots逐段看这段代码的意图。TIME_WORDS是正向映射,把模糊时间词转成可计算的整数天数,window_days为None时表示不做时间过滤,这个约定让上层判断只有一种分支,不用写if window_days is not None and window_days > 0双重判断。职业词从文本里剔除后再切关键词,是为了避免“设计”同时出现在关键词列表和roles里,导致同一份信息被打两次分,排序被污染。
参数上最值得调的是ROLE_WORDS。它本质是一个业务词典,对着自己院系或公司的岗位名称堆一遍就行,改词典比改算法性价比高得多。len(w) >= 1这个阈值故意放松到单字,因为中文姓名只有一个字的情况真实存在(比如“李”),代价是“的”“了”这类虚词会漏进来,靠停用词表在打分阶段再过滤一道。
3.2 检索打分层:字段加权加上拼音兜底
这是整个项目最像“搜索引擎”的一块。思路是给每个字段一个权重,命中位置越靠前分越高,姓名字段额外做一次拼音匹配,解决“知道怎么念不知道怎么写”的经典问题。
# app/core/search_engine.py from pypinyin import lazy_pinyin from config import SEARCH_WEIGHTS def to_pinyin(name: str) -> str: """张伟 -> zhangwei,全拼形式,小写无分隔""" return "".join(lazy_pinyin(name or "")).lower() def score_row(row: dict, keyword: str) -> float: """单条记录对单个关键词的打分,返回值直接用于排序""" kw = keyword.lower().strip() if not kw: return 0.0 total = 0.0 for field, weight in SEARCH_WEIGHTS.items(): value = str(row.get(field) or "").lower() if not value: continue if value == kw: total += weight * 1.2 # 完全相等给额外加成 elif value.startswith(kw): total += weight * 1.0 # 前缀命中,输入体验最自然 elif kw in value: total += weight * 0.7 # 包含命中,降权 # 姓名走拼音兜底:输入 "zhangwei" 也能命中 "张伟" if field == "name" and kw in to_pinyin(value): total += weight * 0.5 return round(total, 4)三个加成系数1.2 / 1.0 / 0.7构成了排序的主干逻辑,完全相等压过前缀,前缀压过包含,符合人的直觉:搜“张”,先出“张伟”再出“张伟明”,最后才是“小张伟”。拼音命中单独给 0.5,权重要低于汉字命中,因为它存在歧义——zhangwei可能对应“张伟”“张玮”“章巍”,只应作为兜底把候选拉进来,不该让拼音结果顶掉汉字精确匹配。
to_pinyin在每次打分时实时调用,看起来有性能隐患。实测两千条记录、单关键词查询在毫秒级,因为只对姓名一个字段做转换;如果记录数上到十万级,就得像建表时那样把拼音预算好存进pinyin字段,检索时直接读列。这也是数据库设计里保留pinyin列的原因,预留了升级路径。
上层调用顺序是先按window_days用 SQL 做粗筛,再对结果集逐条打分排序,最后截断分页。SQL 负责能走索引的条件,Python 负责需要算分的条件,两边各干各的。
# app/core/repository.py 中检索入口的核心片段 def search(conn, keyword: str, window_days: int | None = None, limit: int = 50): sql = "SELECT * FROM contact WHERE 1=1" params = [] if window_days: # 时间条件交给数据库,能用上 created_at sql += " AND created_at >= datetime('now', ?)" params.append(f"-{window_days} days") rows = [dict(r) for r in conn.execute(sql, params).fetchall()] ranked = sorted(((r, score_row(r, keyword)) for r in rows), key=lambda x: x[1], reverse=True) return [r for r, s in ranked if s > 0][:limit]limit默认 50,界面上一页展示 20 条,多取一倍是为了给“加载更多”留缓冲。s > 0这个过滤不能省,否则关键词没命中任何字段时也会返回一堆零分记录,看起来像没过滤。
3.3 去重合并层:编辑距离与拼音相似度组合判定
导入的外贸名录、老手机备份、自己手输的零散记录,同一个人出现两三次是常态。dedupe.py的判定不只看字面,还把拼音相似度和手机号归一化拉进来投票。
# app/core/dedupe.py from difflib import SequenceMatcher from pypinyin import lazy_pinyin def norm_phone(p: str) -> str: """去掉 +86、空格、横线,只留后 11 位数字""" digits = "".join(ch for ch in (p or "") if ch.isdigit()) return digits[-11:] def similar(a: dict, b: dict) -> float: """两条记录的综合相似度,0~1,越大越可能是同一个人""" name_sim = SequenceMatcher(None, a["name"], b["name"]).ratio() py_sim = SequenceMatcher(None, "".join(lazy_pinyin(a["name"])), "".join(lazy_pinyin(b["name"]))).ratio() score = 0.5 * max(name_sim, py_sim) # 汉字或拼音任一接近即可 pa, pb = norm_phone(a.get("phone")), norm_phone(b.get("phone")) if pa and pa == pb: score += 0.4 # 手机号相同,强证据 if a.get("email") and a.get("email").lower() == (b.get("email") or "").lower(): score += 0.2 # 邮箱相同,辅助证据 return round(min(score, 1.0), 4)这段逻辑的关键是“证据叠加 + 上限截断”。姓名相似度占 0.5 的底分,手机号相同加 0.4,邮箱相同加 0.2,理论上能到 1.1,所以必须min(score, 1.0)封顶。系数设置上要防一个误判:同名不同人。“张伟”和“张伟”字面相似度是 1.0,光看姓名就 0.5 分,达不到DUP_THRESHOLD = 0.75的阈值,不会被自动判定重复,需要用户手动确认——这是有意的保守设计,宁可漏判也不能误合并,联系人合错了比多一条记录麻烦得多。
norm_phone只保留后 11 位,是为了吃掉+86前缀和138-0013-8000这类格式差异,同一个号码不同写法能对上。如果号码不足 11 位(座机、分机),[-11:]会原样返回,此时靠姓名和邮箱加权,逻辑仍然成立。
批量判重的调用方式是两两比较,复杂度 O(n²)。几千条以内能接受,超过一万条要换成按姓名首字母分桶,只在同桶内比较,把复杂度压到接近线性。
4. 开发文档说明怎么读:需求条目、ER 图和测试用例与代码的对照方法
4.1 文档四件套与代码的映射关系
随源码附带的开发文档不是装饰,四份文档各对应代码里的一类东西,读的顺序决定你改代码的效率。对着下面这张表看,找任何一处实现都能三步定位。
| 文档 | 关键内容 | 对应代码 | 读它的目的 |
|---|---|---|---|
| 需求说明 | 功能点编号、输入输出、边界条件 | ui/下各窗口 | 确认某个按钮该干什么 |
| 数据库设计 | ER 图、字段类型、索引说明 | core/db.py建表语句 | 加字段前先看约束 |
| 接口说明 | 函数签名、参数含义、异常约定 | core/repository.py、dedupe.py | 调用前知道返回什么 |
| 测试用例 | 用例编号、前置条件、预期结果 | tests/下 pytest 文件 | 改完跑一遍验证没坏 |
最容易踩的误区是跳过需求说明直接读代码,读完发现界面上一堆按钮不知道哪些是必须的、哪些是锦上添花。文档里功能点带编号,比如 F-03 对应“按标签筛选”,代码里main_window.py的on_tag_filter会带同样的编号注释,用编辑器全局搜F-03能直接把需求、实现、测试用例串起来。这套约定在二次开发时特别省时间,加了新功能顺手补个编号,别人接手不用猜。
ER 图对照建表语句要重点看两处:一是tags为什么不是独立表(前面解释过,标签查询以包含匹配为主);二是pinyin为什么冗余存储(为了检索时免实时转换)。这两处是最容易被答辩追问的设计取舍,文档里写了理由,读一遍能少被问住。
4.2 用例驱动的验证:把测试用例写成 pytest
文档里的手工测试用例表抄进tests/变成可执行断言,是这类项目性价比最高的一步改进。原来“输入带 +86 的号码应正确判重”是人眼看结果,现在一条命令跑完。
# tests/test_dedupe.py import pytest from app.core.dedupe import norm_phone, similar @pytest.mark.parametrize("raw, expect", [ ("+86 138-0013-8000", "13800138000"), ("138 0013 8000", "13800138000"), ("", ""), ]) def test_norm_phone(raw, expect): assert norm_phone(raw) == expect def test_same_phone_should_be_duplicate(): a = {"name": "张伟", "phone": "13800138000", "email": ""} b = {"name": "张玮", "phone": "+86 138 0013 8000", "email": ""} # 姓名相近 + 号码一致,应超过 0.75 的判重阈值 assert similar(a, b) >= 0.75 def test_same_name_diff_phone_not_duplicate(): a = {"name": "李娜", "phone": "13900000001", "email": ""} b = {"name": "李娜", "phone": "13700000002", "email": ""} assert similar(a, b) < 0.75第三个用例是回归防线,专门防“同名人被误合并”这个血泪坑。参数化那条("", "")覆盖空号码分支,很多人写norm_phone忘了空值会抛异常。pytest -q跑出来全绿再动手改代码,心里有底。
4.3 五分钟答辩演示路径
文档最后一节通常给了演示脚本,实话说这一节比前面所有技术内容都影响评分。推荐的演示顺序是:先展示一条完整的新增流程(含字段校验提示),再输一句“最近加的做设计的”看检索结果,接着导入一份格式混乱的 CSV 触发判重弹窗,最后打开统计视图看图表。四个动作分别命中增删改查、NLP 解析、算法判重、可视化四类能力,每个动作控制在 60 秒内,中间别去解释代码,评委问什么再翻对应模块。
演示前务必准备一份干净数据库和一份“脏”数据文件。用带重复记录的数据现场触发合并弹窗,效果比口头讲算法强很多。
5. 常见问题排查:GUI 假死、SQLite 锁库与中文乱码这些坑
5.1 界面与线程类问题
现象:点“批量导入”后窗口变灰,标题栏显示“未响应”,几百条数据要等半分钟。原因是 Tkinter 的界面刷新全在主线程,导入逻辑直接写在按钮回调里,主线程被 pandas 解析和逐条入库占死,mainloop拿不到执行权就没法重绘。解决是把耗时任务挪到工作线程,但工作线程不能碰任何界面组件,只往队列里塞结果,主线程用after轮询取。
# app/ui/async_task.py import threading, queue def run_async(widget, task, on_done): """task 在工作线程执行,on_done 回主线程执行,widget 任意 tk 组件用来挂 after""" q = queue.Queue() def worker(): try: q.put(("ok", task())) except Exception as e: q.put(("err", e)) # 异常必须带出来,否则界面永远转圈 threading.Thread(target=worker, daemon=True).start() def poll(): try: status, payload = q.get_nowait() except queue.Empty: widget.after(100, poll) # 100ms 轮询,不能用 sleep 阻塞主线程 return on_done(status, payload) widget.after(100, poll)daemon=True让程序退出时不被残留线程卡住,on_done里根据status分支处理成功和失败,失败要弹messagebox而不是静默吞掉——导入失败什么都不提示,是新手最容易留的坑。
现象:中文姓名排序全乱,“赵”排在“安”前面。原因是 SQLite 默认按 UTF-8 字节序排列,汉字编码顺序不等于拼音顺序。解决是按pinyin列排序,或者查询后取出用lazy_pinyin转换再sorted。
-- 按拼音排序,pinyin 列在写入记录时已补全 SELECT id, name, pinyin FROM contact ORDER BY pinyin ASC;现象:PyInstaller 打包后启动报FileNotFoundError,找不到词典或 db 文件。原因是打包后资源被塞进临时解压目录,代码里Path.cwd()或相对路径都指不到。解决是统一走第 2 章那个app_root(),所有资源路径都从它拼出来,并在打包命令里用--add-data "data;data"显式声明。
5.2 数据与环境类问题
现象:导入几百条后程序报sqlite3.OperationalError: database is locked。原因是多个线程共用了同一个sqlite3.Connection对象。SQLite 的连接不是线程安全的,一个线程写入持锁时另一个线程读写就被挡。解决是每个线程各自sqlite3.connect(DB_PATH),用完关掉;同时在建库后开一次 WAL 模式提升并发读写能力。
# app/core/db.py import sqlite3 def get_conn(): """每线程独立连接,主线程和导入线程互不干扰""" conn = sqlite3.connect("data/contacts.db", timeout=5) conn.row_factory = sqlite3.Row # 支持按列名取值 conn.execute("PRAGMA journal_mode=WAL") # 允许读写并发 conn.execute("PRAGMA synchronous=NORMAL") return conntimeout=5表示锁等待 5 秒再抛异常,短事务场景下能自动消化掉大部分争抢。批量导入时分批executemany加commit,比一条一条提交快一个数量级。
现象:导出的 CSV 用 Excel 打开,中文全是乱码。原因是 Excel 默认按 GBK 解码,而文件写的是不带 BOM 的 UTF-8。解决是写文件时指定encoding="utf-8-sig",加 BOM 头 Excel 就能认。
import pandas as pd def export_csv(rows: list[dict], path: str): df = pd.DataFrame(rows) # utf-8-sig 带 BOM,Excel 双击直接打开不乱码 df.to_csv(path, index=False, encoding="utf-8-sig")注意:从 Excel 导入时也要显式指定
encoding,先尝试utf-8-sig,捕获UnicodeDecodeError后再回退gbk,这两个编码覆盖了绝大多数中文 CSV。
6. 二次开发改造思路:从关键词打分升级到语义检索,再给通讯录加一层 HTTP 接口
这套代码最容易接到需求的地方是检索层。现在的加权打分依赖关键词字面命中,用户搜“帮我找上次那个做品牌的人”,分词后得到的“品牌”如果没出现在任何字段里,结果就是空的。改造方向是把search_engine.py的打分替换或叠加一层向量检索,用句向量算语义相似度,字面不匹配但意思接近的记录也能被召回。
# app/core/semantic.py —— 需要额外 pip install sentence-transformers import numpy as np from sentence_transformers import SentenceTransformer model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") def build_index(rows: list[dict]): """把每条记录拼成一句话再编码,字段拼接顺序影响召回质量""" texts = [f'{r["name"]} {r["company"]} {r["title"]} {r["tags"]} {r["remark"]}' for r in rows] vecs = model.encode(texts, normalize_embeddings=True) return texts, np.asarray(vecs) def semantic_search(query: str, texts, vecs, topk: int = 10): """归一化后点积即余弦相似度,省掉一次除法""" q = model.encode([query], normalize_embeddings=True)[0] sims = vecs @ q order = np.argsort(-sims)[:topk] return [(texts[i], round(float(sims[i]), 4)) for i in order]落地时我建议保留原来的关键词打分做粗排,向量结果做补充召回,两路结果按加权融合排序,语义分权重给 0.6、字面分给 0.4。纯向量检索在姓名这种短文本上反而不如字面匹配准,混着用最稳。索引要落盘缓存,每次启动重编码几千条记录大概几秒,加个文件缓存能把冷启动压到一秒以内。
其他几个改造点的投入产出比列在下面,按需要挑:
| 改造点 | 做法 | 工作量 | 主要风险 |
|---|---|---|---|
| HTTP 接口 | 用 FastAPI 包一层repository | 半天 | 连接管理要改成依赖注入 |
| 头像与附件 | 存文件路径,数据库只存引用 | 1 天 | 路径迁移、文件丢失 |
| 多端同步 | 加updated_at做增量拉取 | 2 天 | 冲突合并策略要先定 |
| 姓名分词优化 | 换带自定义词典的 jieba | 2 小时 | 词典维护成本 |
改造时有两个验证习惯值得坚持:每次改动前先pytest -q跑一遍拿到绿色基线,改完再跑一遍对比;新增字段必须同步改docs/数据库设计.md和建表语句,并且写一个ALTER TABLE ... ADD COLUMN的迁移函数,别直接删库重建——用户手里的数据删了就找不回来。之前有次我图快,直接删了contacts.db让程序重建,结果测试同事录了两天的样本全没了,从那以后我每次动表结构都先cp data/contacts.db data/contacts.db.bak再执行迁移,这个动作已经变成肌肉记忆。希望这份拆解帮到你,把项目跑起来只是开始,改出自己那版才算真吃透。
本文还有配套的精品资源,点击获取