☰
用Python与本地模型打造自动文件归档系统:语义分类与全量落地
2026/10/7 4:45:17 网站建设 项目流程

1. 这个系统到底解决了什么问题

1.1 乱成一锅粥的笔记库

三个月前我打开自己的笔记目录时,差点以为电脑被“格式化过一轮”——127 个大小文件夹,三千多个文件名叫“新建 文档.pdf”,还有不知道从哪年存下来的截图、扫描件、报表、会议纪要和截图,内容五花八门:技术笔记、产品需求、发票照片、菜谱截图,甚至还有我早忘了的几篇半成品文章。翻一页找一份文件的平均时间,比我当年在图书馆翻纸档还慢。

当时正好在做一个 Python 自动化的小案子,顺手把整理笔记这件事也提上了日程。最初的方案很简单:按文件后缀分一下,PDF 归 PDF、图片归图片。结果一跑就发现纯粹是掩耳盗铃——PDF 里混着合同扫描件、技术手册和剪贴板导出的网页,图片里更乱。真正需要的是“看懂内容再分类”,而不是“看扩展名分类”。

于是我把方案升级成 Python 调本地模型做语义分类,让每个文件都被自动读一遍、归纳一个类别、生成一个干净标题,然后移动进对应的归档目录。整套系统从 1 月中旬开始跑,到现在整整跑了 3 个月,3747 份文件全部完成自动归档,中途基本没怎么人工干预。这篇文章就是把这三个月的完整实操过程摊开来讲,包括踩过的坑、调过的参、以及本地模型方案最让人头疼的“稳定性”问题。

如果你和我一样,有一个越来越乱的本地知识库,或者经常下载文件后随手一扔,那么这套思路可以直接照搬。你用不着一开始就做得很全,只要掌握“Python 遍历文件 + 本地模型分类 + 脚本归档”这个最小闭环,就能把这事跑起来。

1.2 为什么是“Python + 本地模型”而不是别的组合

先回应一个必然被问的问题:你直接调云端大模型 API 不就行了?我当时确实先试过调 API,效果很好,分类准确率比本地模型还高一点,但两个现实问题让我最终放弃:

  • 隐私问题。笔记里有大量我自己的技术笔记、客户沟通记录和财务票据,这些东西传到一个云端接口去“读一遍”,哪怕别人承诺不上云不留存,我心理上也过不去。本地模型能在完全不出内网的情况下完成同样的工作,这是一个不可替代的价值。
  • 成本问题。3747 份文件,每份文件哪怕只喂给模型 1500 字的前缀内容,累计 token 也是一个不小的数字。API 按 token 计费跑完整档,虽然不至于破产,但够买一个不错的外接硬盘了。而本地模型跑在自己机器上,电费远低于 API 费用。

那用 Python 是不是唯一选择?也不是。Node.js、Go 都能写文件遍历脚本,但 Python 的优势在于生态太成熟:openai客户端直接支持 LM Studio 和 Ollama 的本地接口,pandas可以批量处理文件名,sqlite3用来记录归档状态,easyocr还能顺带处理扫描图片。这一套组合下来,不需要额外造轮子,几乎每个环节都有现成库可用。

至于纯规则脚本,比如“按后缀分类”“按文件夹路径分类”“按关键词正则匹配分类”,我都试过,全部败在语义理解这个坎上。规则能识别出标题里带“报告”“发票”的文件,但识别不出一段没写“总结”二字的会议纪要。本地模型虽然不至于像人一样理解内容,但它的语义判断能力已经足够覆盖日常归档需求,这就够了。

2. 整体架构与关键选型

2.1 三件套架构:源目录、分类表、归档区

这个系统的整体架构可以概括为“三个目录 + 一个状态库”:

  • 源目录:所有待整理的笔记、截图、PDF、扫描件先统一丢到E:\Inbox里,也就是一个“垃圾进库”的入口。文件一多,人就没有精力逐个分拣,全部放到这里让脚本扫。
  • 归档区:整理完成后,文件按分类被移动到E:\Archive\{分类}\{年月}\下面。比如技术笔记归到tech/2026/01/,财务票据归到finance/2026/01/,再加上日期前缀,文件一眼就知道是什么。
  • 分类表:不是指数据库表,而是一份categories.json文件,里面定义了 7 个顶层分类,每个分类对应一组关键词和一句描述。LLM 的提示词里会引用这张表。

状态库我用的是archive_state.db这个 SQLite 文件,记录每个文件是否已经被处理过、处理时间、原始路径、新路径、分类结果。这样脚本可以反复运行,不需要每次全量扫描判断。

整体流程一句话:Python 遍历源目录里所有文件,读取内容之后把前 2000 字摘出来,拼接一个分类提示词发给本地模型,拿到 JSON 返回结果后解析出分类和标题,最后移动文件到归档区,并把状态写入 SQLite。整个过程完全在本地完成。

2.2 本地模型的取舍:为什么不用云端 API

前面提到云端 API 在分类准确率上略强,本地模型也并非没有短板,但这场对比的关键不在“谁更强”,而在“能否稳定跑完 3 个月”。

本地模型最强的优势是确定性成本。API 按 token 计费,虽然便宜,但你要是连续跑几千份文件,账单上的金额就是一份长期负担,而且每一轮吞吐都依赖对方服务器的并发配额;本地模型虽然前期要花时间配置,但部署完之后,每整理一份文件的边际成本几乎为零,电费和显存占用是固定开销,跑多少文件都不用看价格表。

其次是隐私和合规。我在公司做过类似的文档整理,客户合同、内部纪要这类内容,别说上传到外部 API,连内网以外都不允许碰。本地模型把推理过程完全锁在机器内部,这个特性对于技术人员做自己的知识库整理,尤其是涉及个人隐私的内容,是个决定性的加分项。

但要注意,本地模型也有明显的代价:需要一块像样的显卡,或者愿意牺牲速度用 CPU 跑量化模型。以我的配置,一块 RTX 3090 24GB 就能轻松跑 7B 到 8B 量级模型,14B 模型也能在较大量化下勉力运行。如果你的机器没有独显,用 Ollama 跑 4bit 量化后的 7B 模型,虽然会慢不少,但仍然是可行的方案。

2.3 模型与运行时选型备忘

我最终用的模型是Qwen2.5-7B-Instruct,在 LM Studio 里加载了 Q4_K_M 量化版本。选择这个组合的原因很直接:7B 级模型在消费级显卡上推理速度足够快,Q4 量化后显存占用约 4.5GB,3090 跑起来毫无压力;更重要的是 Qwen 系列对中文的语义理解能力明显好于同参数规模的 Llama 系模型,而我的笔记内容是中文占绝大多数。

如果你用的是 Ollama,启动命令更简单:

ollama run qwen2.5:7b-instruct-q4_K_M

Ollama 会同时暴露一个 OpenAI 兼容接口,默认地址是http://localhost:11434/v1,所以 Python 代码里只要改一下base_url就行。

我自己的运行环境如下:

OS : Windows 11 CPU : i7-13700K GPU : RTX 3090 24GB 模型服务 : LM Studio 0.2.24 模型 : Qwen2.5-7B-Instruct Q4_K_M 上下文长度 : 8192 单条最大推理 : 256 tokens

这里有一个容易被新手忽略的点:上下文长度不是越大越好。我把上下文设成 8192,但实际每次请求只喂一两千字,因为文件内容越长,推理时间越长,内存占用越高,而且文件里大段无意义空白反而会干扰分类结果。先截断再送模型,是保证全流程吞吐量的关键习惯。

3. 核心实现:分类结果如何“靠谱”又不翻车

3.1 提示词设计:一次说清楚分类规则

本地模型能不能给出可用的分类结果,八成取决于提示词怎么写。我用一个很简单的模板解决了这个问题:把分类表(category)、每个分类的说明(description)和几个示例文件的关键词(examples),连同文档内容一起发给模型,让它直接输出 JSON。

先看分类表的结构:

{ "categories": [ {"name": "tech", "description": "技术笔记、代码片段、软件配置、开发文档", "examples": ["python", "git", "docker", "数据库"]}, {"name": "work", "description": "会议纪要、工作计划、项目进度、同事协作内容", "examples": ["会议", "项目", "排期", "需求"]}, {"name": "finance", "description": "发票、账单、报销单、合同、银行流水", "examples": ["发票", "账单", "报销", "合同"]}, {"name": "life", "description": "个人生活记录、菜谱、旅行攻略、健康内容", "examples": ["菜谱", "旅行", "体检", "日程"]}, {"name": "research", "description": "行业研究报告、论文、出版物、竞品分析", "examples": ["报告", "论文", "研究", "竞品"]}, {"name": "misc", "description": "无法归入其他类别的杂项", "examples": []} ] }

提示词模板长这样:

你是一个文件归档助手。请根据下面给出的文件内容,判断它最合适的分类。 候选分类如下:{categories_json} 规则: 1. 只输出一个 JSON 对象,不要输出任何其他文字。 2. JSON 格式必须如下: {"category": "tech", "title": "不超过20字的简洁标题", "reason": "一句话说明判断依据"} 文件内容: {truncated_content}

这里有个关键心得:规则里那句“不要输出任何其他文字”必须加。本地模型和人一样,你让它“回答”,它就忍不住先说“好的,我来分析一下”再输出 JSON,规则写得越死,解析就越省心。

分类表里给出 examples 也很有必要。模型看到“技术笔记、代码片段”这种描述可能还很模糊,但看到“python、git、docker”这些具体词之后,判断就明显更稳定。3 个月跑下来,我在“tech”和“work”两个类别上做过一次抽样复核,准确率大概在九成左右,对于自动归档这个场景足够用了。

3.2 让模型输出 JSON 并做健壮性兜底

LM Studio 和 Ollama 其实都支持response_format={"type": "json_object"}这种 OpenAI 兼容写法。在 OpenAI 提供 JSON mode 的情况下,模型会尽量保持输出结构合法。但在本地部署场景下,我依然默认它会返回脏数据,所以 Python 侧必须有一个兜底解析函数。

我的核心调用代码长这样:

import json import datetime from openai import OpenAI client = OpenAI(base_url="http://localhost:1234/v1", api_key="local") def classify_document(content: str, categories: dict) -> dict: prompt = build_prompt(content, categories) for attempt in range(3): try: resp = client.chat.completions.create( model="qwen2.5-7b-instruct", messages=[{"role": "user", "content": prompt}], temperature=0.2, max_tokens=256, response_format={"type": "json_object"}, ) text = resp.choices[0].message.content.strip() text = text.replace("```json", "").replace("```", "").strip() parsed = json.loads(text) if "category" in parsed and "title" in parsed: return parsed except Exception as exc: print(f"第 {attempt + 1} 次调用失败: {exc}") return {"category": "misc", "title": "未识别", "reason": "解析失败"}

这里有一个细节值得一提:temperature 设置成 0.2,而不是默认的 1.0。分类任务需要的是确定性,不是创造性。如果 temperature 太高,模型可能对着同一份内容每次给出不同的分类结果,这在本地模型上表现得特别明显。0.2 是我实测下来比较稳的数值,再低到 0 也可以,但有时反而会让模型过度死板,个别边缘文档会硬套一个最不像的类别。

万一三次重试都失败,misc就是最后的兜底。这类文件占比很低,三个月里大概只有二三十份,单独留在 Inbox 里等人工处理,完全不影响整体流程。

3.3 重命名、移动与防重复归档

文件归档不只是移动,重命名同样重要。我从一开始就定了一个规则:新文件名 = 日期 + 模型生成的标题 + 原扩展名。比如:

Inbox/20260115_094323.png → Archive/tech/2026/01/20260115_094323_笔记系统架构草图.png

为什么要加日期?因为 20 字的模型标题在长文件场景里很可能看不出是哪一天生成的,加上日期之后,按时间线浏览、搜索、回溯都方便得多。

移动文件时有个坑:重名冲突。两张截图在同一个分类、同一天整理,就可能生成完全相同的标题。我的策略很简单,如果目标路径已经存在,就在文件名末尾追加一个递增序号:

def unique_path(path): if not os.path.exists(path): return path base, ext = os.path.splitext(path) for i in range(1, 10000): new_path = f"{base}_{i}{ext}" if not os.path.exists(new_path): return new_path raise FileExistsError(f"无法生成唯一路径: {path}")

移动文件之前,我还会先做一次“内容哈希去重”。虽然这个场景里重复文件不多,但跑久了总会碰到同一份文件被存了两份的情况。hashlib.md5可以快速判断两个文件是否完全相同,重复文件不移动,只在日志里标记 duplicate,然后直接丢弃。这一步防止了归档区里出现一半以上的垃圾副本。

4. 落地运行与稳定性调节

4.1 扫描频率、幂等设计与运行日志

整套系统不是一次性脚本,而是要长期跑的服务。我用 Windows 自带的任务计划程序设置了一个定时任务,每天上午 10 点和下午 4 点各跑一次,对应“睡前攒一批、上班后自动清空”的使用习惯。

运行流程里最关键的设计是幂等性。所谓幂等,就是脚本重复执行多少次,最终状态都一样。我靠两样东西保证这一点:

  • SQLite 状态库:记录每个文件的abs_path和status。扫描目录时先查状态库,已经处理过的文件直接跳过;处理完一个文件立刻更新状态,即使脚本跑到一半崩溃,重启后也不会重复处理。
  • 先移动后记录:文件被移动成功后,才写入状态“ARCHIVED”。如果移动失败,状态保持“PENDING”,脚本下次会自动重试。

运行日志单独输出到archive.log,每次处理记录文件名、推理耗时、分类结果、错误信息。我跑到现在,这个日志已经成了排障最宝贵的依据:

[2026-01-15 10:03:27] OK Ok? extra/20260115_094323.png -> tech/2026/01/20260115_094323_笔记系统架构草图.png (0.8s) [2026-01-15 10:05:41] ERR Inbox/新建文档(17).pdf : JSON parse fail at attempt 3 -> misc [2026-01-15 10:07:02] DUP Inbox/截图_20251228.jpg => remove duplicate

刚开始那几天,我每天都会翻一遍日志,看看有没有奇怪的分类或失败记录。后期分类准确率和稳定性上来了之后,改成一周一看,纯粹是为了确保没有新文件在 Inbox 里积压。

4.2 三类最容易踩的稳定性坑

跑了三个月,说几个真实踩过的坑,希望你能绕开。

第一个坑:长时间运行后,模型服务假死。LM Studio 连续跑几百个文件之后,可能出现“接口能通但响应越来越慢,甚至直接卡住”的现象。原因多半是显存泄漏或服务进程长时间未释放资源。我最终的办法是写了一个“健康检查 + 自动重启”的辅助函数:每处理 100 个文件,就调用一次轻量请求,如果响应时间超过 30 秒,则通过subprocess杀掉 LM Studio 进程并重新启动它。虽然粗暴,但三个月里确实避免了至少五六次长任务卡死的风险。

第二个坑:大批量文件同时涌入导致内存飙升。刚开始我为了省事,用os.walk一次性把所有文件路径读进内存遍历,几千条路径不算什么,但如果你同时在读文件内容、截取前 2000 字、编码成 prompt,内存占用很可观,CPU 和内存双双拉满。后来改成“边遍历边处理边写状态”,一次只处理一个文件,内存占用降下来一大截,整个系统也稳定多了。

第三个坑:断电或中断导致状态不完整。文件处理到一半突然断电,SQLite 里状态不一致,下次扫描就会存在“文件已移动但状态还是 PENDING”的情况,导致重复处理同一个文件。解决办法很简单,在移动文件之前先把源文件和目标文件都记录到一个pending_ops临时表,移动成功后清掉临时表;下次启动时先扫描临时表,替上次中断的操作收尾。

4.3 性能与硬件成本记录

硬件没什么特殊,核心就是那块 RTX 3090。以 Qwen2.5-7B 的推理速度来说,单份文件平均耗时大约 0.6 到 0.9 秒,如果文件内容很长、上下文超过 3000 token,耗时可能到 1.5 秒。3747 份文件累加下来,总耗时大概在 40 分钟左右,分摊到三个月里绰绰有余。

这里有一个显存优化的经验:模型的上下文长度和批处理数量要精确控制。一开始我把n_ctx拉满到 32K,一个批四个请求同时塞进去,3090 的 24GB 显存直接顶满,速度反而不见得快。后来把上下文压回 8192、批处理改成单线程串行,每次显存占用不超过 6GB,速度却快了将近一倍。原因是显存满了之后模型会频繁做 swap,反而拖慢吞吐。

如果你用的是 8GB 显存的显卡,建议直接用 Q5_K_M 量化的 7B 模型,上下文设置 4096,运行起来也能有不错的速度。实在没有独立显卡,可以考虑 CPU 上用 Ollama 跑,虽然一份文件可能要花 10 到 20 秒,但至少能做,适合偶尔整理一批文件而不是长期批量跑的场景。

5. 三个月流程复盘

5.1 3747 份文件的最终效果

三个月跑完,我把 SQLite 状态库导出来做了一次统计,最终归档分布如下:

分类文件数量占比说明
tech143238.2%技术笔记、代码片段、软件文档
work82622.1%会议纪要、项目排期、协作文档
finance51213.7%发票、账单、合同、报销单
life40310.8%菜谱、旅行、健康、生活随手拍
research2988.0%行业报告、论文、竞品资料
misc2767.4%无法分类或解析失败的杂项

看到这个分布的时候我有点意外,技术笔记占比这么高,说明我之前一直高估了“分类能力”带来的工作量,其实把文件从一堆文件夹里捞出来并不难,真正难的是靠人去翻内容判断每一份文件是什么。而这类重复劳动恰恰最适合模型来做,模型不需要多聪明,只要能稳定地把 7 个类别分对,就能节省大把时间。

misc这一档里有 276 份,占 7.4%,说实话比我预想的高一些。抽样翻了翻,里面大量是正文里根本没有文字的图片、损坏的 PDF、以及一些只有标题的超短网页保存文件。这类文件人工处理也就一秒的事,不用纠结优化到零,只要保证它们没有被脚本误删,都已经留在了单独的misc目录里等待抽查。

5.2 这套方案什么时候适合你,什么时候不适合

看你自己的场景,用我的结论做个对照:

这套方案很适合以下情况:

  • 本地笔记、文档、截图越积越多,人工整理已经变成每周必做的“家务活”;
  • 文件内容以中文为主,对分类准确性要求不像正式系统那么变态;
  • 对隐私敏感,或者工作环境不允许把文档内容传给外部接口;
  • 有一块至少 8GB 显存的显卡,或者愿意接受 CPU 推理的慢速度。

这套方案不太适合的情况:

  • 文件总量不超过两三百份,一次性手工整理也就一晚上,没必要上系统;
  • 需要绝对精确的分类,比如财务报销流程依赖“所有发票必须在 finance 且不能误分类”这样严格的规则,本地模型偶尔的一次误判就会引发连锁问题;
  • 目标文件都是几百 MB 的大型视频、压缩包,这些内容没法塞进 LLM 分类,用文件名后缀规则更快。

另外提醒一句,模型分类的“准确率”需要你亲自抽样验证。每个笔记库的内容风格差异很大,我这边准不代表你那边准。最初两周你可以只读日志,定期检查几个关键分类,随时调整分类表和提示词,稳定之后再开全自动模式。

写在最后的实操心得

这个项目对我来说最大的收获不是“整理完了 3747 份文件”,而是意识到本地模型做语义分类这件事,已经不需要什么稀奇硬件或者复杂框架了。一个 Python 脚本、一份分类表、一个本地模型服务,就能把过去需要半天才能做完的整理工作全部自动化。

如果你也要搭一套类似的系统,我的建议是起步时先圈一个小目录试跑:放二三十个文件进去,盯着日志看两天,把分类表、提示词、重命名规则都调到满意了,再滚动扩大到全部文件。不要一上来就扫全盘,否则第一次跑完发现分类表设计不合理,你还要花双倍时间去返工。

最后分享一个小技巧:做这个系统时,我在提示词里专门让模型输出一个reason字段。虽然归档目录不会用到它,但排查“这个文件为什么被分到 tech”时特别管用,省去了很多猜测。给模型加一个“解释自己判断依据”的要求,次要输出能在调试阶段帮你省下大量时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询