
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.5GB3090 跑起来毫无压力更重要的是 Qwen 系列对中文的语义理解能力明显好于同参数规模的 Llama 系模型而我的笔记内容是中文占绝大多数。如果你用的是 Ollama启动命令更简单ollama run qwen2.5:7b-instruct-q4_K_MOllama 会同时暴露一个 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_urlhttp://localhost:1234/v1, api_keylocal) def classify_document(content: str, categories: dict) - dict: prompt build_prompt(content, categories) for attempt in range(3): try: resp client.chat.completions.create( modelqwen2.5-7b-instruct, messages[{role: user, content: prompt}], temperature0.2, max_tokens256, 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”时特别管用省去了很多猜测。给模型加一个“解释自己判断依据”的要求次要输出能在调试阶段帮你省下大量时间。