1. “知识健康”到底是个什么问题如果你和我一样用本地 Markdown 笔记记了几年东西知识库会慢慢长成一个失控的杂物间。我见过不少人的笔记目录是这样的新建文档 2023-01、未命名 12、final_really_final_v3同一个概念在十几个笔记里反复出现链接指向的文件早就被改名或删掉了还有一些笔记写了八百字开头之后就再也没动过。这还不算什么。真正让人头疼的是当你想基于这些积累做一个完整输出的时候你会发现你不知道哪些笔记是可信的、哪些是过期的、哪些内容其实可以合并成一个更大的主题。你只知道“我记了很多”但完全不知道“我记的东西健不健康”。我管这个叫知识库的“健康度”问题。它和人体健康很像关注体重不代表关注体脂关注笔记数量也不代表关注知识状态。一个知识库健康与否应该看它内部有没有断链、有没有重复、有没有陈旧信息、有没有孤立主题、有没有长期不更新的“僵尸笔记”以及各主题之间的连通性。这些指标单独拎出来都不起眼合在一起却能反映知识库的真实状态。之所以想做一个专门关注“知识健康”的开源项目“玉衡Yuheng”就是因为市面上常见的笔记工具核心能力基本都集中在“记”和“找”这两个环节。你会看到各种花哨的块引用、双向链接、全文搜索但几乎没有工具会告诉你“你的笔记库里有 87 个断链”、“你有 23 篇笔记高度相似”、“有 41 篇笔记超过一年没更新了其中 12 篇的内容大概率已经过时”。玉衡想做的恰恰是补上这个缺口像体检一样扫描知识库输出一份可读的健康报告给出具体到文件路径的修复建议并且持续追踪知识库的健康趋势。它不是一个新笔记软件不抢现有工作流的戏份而是安安静静地站在一边告诉你这片“知识田”该浇水还是该除虫。适合它的用户也蛮清晰笔记积累超过几百篇的人、用 Markdown 或纯文本管理知识库的人、对“整理笔记”有长期需求但又不想靠纯手工维护的人、以及想给自己建一套“自动化内容质量保障体系”的折腾党。小白也能用它跑一次全库体检哪怕只是看一眼报告都会对“知识库健康”这个概念有完全不一样的认识。2. 玉衡该怎么理解这个需求2.1 名字的来历与产品定位“玉衡”是北斗七星中的第五颗星古人观星时用来定方向、辨季节。我起这个名字的时候想得很简单一个知识库也应该有一颗“玉衡”帮你测定方向——哪些内容在支撑你的知识体系哪些内容已经偏航了。先说说产品定位。我一开始有过几个选项做成 Obsidian 插件和具体工具绑定太深换个笔记软件就废了。做成“笔记整理 AI 助手”太重不符合“轻量、透明、可解释”的初衷。做成一个命令行工具 可读报告跨平台、跨场景任何用 Markdown/纯文本存知识的人都能用。我最后选了第三条路。玉衡本质上是一个静态分析器不是常驻后台的监控进程。它跑完一次生成一份报告剩下的事情交给用户去决策。这样做有好处不干扰你写笔记时的节奏不占内存也不会偷偷扫描你的敏感内容所有计算都在本地完成。2.2 核心设计原则不打扰、可解释、可行动从第一版原型开始我就给自己定了三条铁律。第一条不打扰。玉衡不做成常驻服务也不在你写笔记时弹提示框。你需要它时手动跑一次或者挂个定时任务让它自动跑。原因很简单知识管理工具最大的敌人是用户的使用负担负担一重工具就会被弃用。第二条可解释。每一个健康指标都必须能回溯到具体的文件、具体的行、具体的规则。不能说“你的知识库不够健康”就完了要说“你的笔记库中约 12% 的链接指向不存在的文件其中最集中的三个目录是 xxx、yyy、zzz”。我拒绝在早期版本里引入“AI 判分”因为 AI 打的分说不清楚用户不放心出错了也没法修。第三条可行动。报告里每个问题都要带修复建议。死链就告诉你是哪篇笔记指向哪个失效目标重复内容就告诉你相似度有多高、建议保留哪篇陈旧内容就标记出最后修改时间。光发现问题不能叫健康管理能推动你去做整理才算。2.3 技术选型与数据模型技术栈上我选的是Python 3.10CLI 框架用 TyperMarkdown 解析用 python-frontmatter 加正则辅助历史趋势数据存 SQLite报告输出为 Markdown 文件。为什么是 Python因为这类“扫描文本 统计规则 生成报告”的场景Python 的标准库和生态最省事。你不需要编译改完代码立刻能跑对贡献者来说门槛也低。Typer 会让你写 CLI 的体验非常舒服参数解析、帮助文档、补全这些都不用自己造轮子。数据模型其实很朴素核心就是一张“文件信息表”和一张“链接关系表”dataclass class DocInfo: path: str title: str word_count: int created_at: datetime | None modified_at: datetime | None tags: list[str] outbound_links: list[str] dataclass class LinkEdge: source: str target: str link_type: str # wiki / md / url有了这两个基础结构死链、孤岛、连通度、陈旧度这些指标都能算出来。SQLite 表结构也不复杂存三类数据文件快照、链接关系快照、历次扫描报告摘要。通过对比两次快照就能看出知识库是在变好还是变坏。3. 核心功能拆解与实现思路3.1 知识库体检维度与评分模型玉衡把“知识库健康”拆成了六个维度每个维度都有明确的定义和检测方式。我第一次设计时有八个维度后来砍到六个因为发现多一个维度就多一层理解成本用户反而不容易上手。现在的维度如下健康维度指标定义检测方式典型问题链接健康链接总数中无效链接的占比解析所有 Markdown 链接并检查目标是否存在死链、指向改名文件的旧链接内容冗余相似度超过阈值的文档对数计算正文的 SimHash 并做近似去重重复笔记、同一素材反复抄写结构完整空文档、无标题文档、超长文档的占比解析 Frontmatter 和正文结构只有标题没有正文的“空壳笔记”时效状态超过 N 个月未更新的文档占比基于文件修改时间统计过期信息、僵尸笔记主题连通孤立节点无入链也无出链的占比构建有向图计算节点度数散落的碎片内容没有上下文元数据质量缺少 Frontmatter 或 Tags 的文档占比检查 YAML Frontmatter无法聚合、检索的信息孤岛每个维度先算出“问题发生率”再用加权公式转成 0~100 分。总分不是简单平均而是链接健康占 30%、内容冗余 20%、结构完整 15%、时效状态 15%、主题连通 10%、元数据质量 10%。链接健康权重最高是因为知识库一旦断链各种自动化流程和阅读路径都会受影响修复成本也最高。这个评分模型我调过很多轮。最初版本是越细越好每个维度都给了比较平均的权重结果报告出来后用户反馈“哪里都有问题不知道先弄哪个”。后来改成突出核心维度报告里也按“严重 / 中等 / 轻微”分级展示这直接解决了一半的“看过报告不知道该干嘛”的问题。3.2 死链、孤岛、重复、过期的检测实现死链检测。Markdown 里的链接主要有三种形态[文字](相对路径.md)、[[双链]]、以及裸路径引用。我做了两层解析第一层用正则把所有链接模式抓出来第二层对相对路径做“规范化解析”得到目标绝对路径再判断文件是否存在。import re from pathlib import Path WIKI_LINK_RE re.compile(r\[\[([^\]|#]?)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]) MD_LINK_RE re.compile(r\[[^\]]*\]\(([^)])\)) def extract_markdown_links(text: str, base_dir: Path): links set() for raw in WIKI_LINK_RE.findall(text): target resolve_wiki_target(raw.strip(), base_dir) if target: links.add(target) for raw in MD_LINK_RE.findall(text): if raw.startswith(http://) or raw.startswith(https://): continue target (base_dir / raw).resolve() links.add(target) return links这里有个细节[[链接]]里的目标可能不带.md后缀也可能带/分隔路径解析时要尝试几种常见补全方式——先补.md找不到就补/index.md再找不到再看有没有同名目录。我们讨论过要不要支持“智能匹配同名不同目录文件”最后还是没做。启发式匹配越多误报率越高我宁愿多报几个“可疑链接”让用户自己确认也不能悄悄把坏链接修成错误目标。孤岛检测。我构建了一张有向图节点是全部 Markdown 文件边是链接关系。每个节点的“入度”是别的笔记通过链接指向它的次数“出度”是这篇笔记指向其他笔记的次数。入度和出度都为零的节点就是孤立节点。这个计算用纯 Python 跑几千个文件也就几秒钟不需要额外引入图数据库。重复检测。完全重复的笔记好办算个 MD5 就行。真正麻烦的是“内容高度相似但措辞不同”的笔记。我用的是 SimHash 加汉明距离先把正文做分词然后计算每个文档的 64 位指纹两两比较时看汉明距离是否小于某个阈值。SimHash 的好处是对局部修改不敏感两篇笔记如果有 80% 的内容重叠它们的指纹距离会非常近。阈值我默认设 3实际使用中可以根据自己笔记的风格调。def simhash(text: str, hash_bits: int 64) - int: weights [0] * hash_bits tokens tokenize(text) for token in tokens: h hash_token(token) for i in range(hash_bits): mask 1 i if h mask: weights[i] 1 else: weights[i] - 1 fingerprint 0 for i in range(hash_bits): if weights[i] 0: fingerprint | 1 i return fingerprint这个算法是经典的 SimHash 实现思路不是我的原创但在“判断笔记是否重复”这个场景下特别好用。需要注意的是中文切词不能按空格来早期版本直接用 jieba 分词后来为了减少依赖换成了“按标点拆句 提取 2~4 个连续字符的 n-gram”方案。实测下来对中文内容的重复检测效果完全够用而且少了 jieba 这个重依赖安装体积小了不少。过期检测。这个最简单直接读取文件的st_mtime和当前时间做差。但要注意一个矛盾有些笔记是“永久笔记”内容长期不变反而说明它稳定健康。我给出的补偿机制是如果文档带有status: evergreen或status: done这样的 Frontmatter 标记就跳过过期判定。也就是说玉衡不会一刀切地认为“不更新 有问题”而是允许你告诉它哪些内容不需要频繁更新。3.3 报告生成与趋势追踪扫描完之后玉衡会在指定输出目录默认是知识库根目录下的.yuheng/生成三样东西report-YYYY-MM-DD.md完整的健康报告。health.json机器可读的指标摘要方便接入其他工具。history.dbSQLite 数据库记录历次扫描的核心指标。报告的结构我花了不少心思。为了让用户快速定位最要紧的问题报告开头是一个“总体结论”比如“本次体检综合评分 72 分较上次下降 3 分主要原因是链接健康度从 88 分降到了 79 分”。然后是详细的维度表格、问题清单、修复建议。问题清单里每条都标注文件路径和具体位置路径直接写成代码格式方便复制进编辑器跳转。趋势追踪往往被人忽视但我觉得这是“健康管理”和“一次体检”的根本区别。身体体检如果年年做真正有价值的不是单次报告而是曲线血压是稳了还是在爬升知识库也一样。你跑了第一次扫描把评分基线建立起来然后再过一个月跑第二次、第三次看到健康度曲线往上走这种反馈带来的整理动力比任何“待办清单”都管用。3.4 自动化修复与“养护清单”有些人以为健康报告看完就该去手动修。但我觉得能自动化的就不要让人做重复劳动。玉衡提供了一组半自动修复命令每条命令在执行前都会把“将要对哪些文件做什么修改”列出来你需要确认后才执行。这是很刻意的设计知识库是你的私有资产工具不能代替你做判断只能在可信的场景下帮你动手。目前支持的修复动作修复死链当链接目标不存在但发现“改名前的旧名称恰好对应某个现有文件”时自动把链接文字更新为新文件名。清理重复把重复度超过阈值的文档列出来按修改时间排序建议删旧留新不直接删除只生成待删除清单。标记过期给超过 12 个月未更新、且没有status标记的文档自动插入status: stale字段。补充 Frontmatter给缺失标题、缺失 tags 的文档自动生成最小化 Frontmatter。这些修复动作全部是幂等的跑两遍不会产生二次副作用。日志也写得很明确每个文件动了哪里都会在终端里打出来。我还设想了一个“养护清单”功能把最值得优先处理的十个问题汇总成一个todo.md放在知识库根目录。你可以把它当作笔记打开一勾一个处理处理完跑一次玉衡评分涨了成就感也有了。这个功能本质上是用“任务管理”的思路来驱动知识库整理实测下来比直接扔几百个问题清单给人要有效得多。4. 实操把玉衡跑起来4.1 安装与初始化配置安装很简单如果 Python 环境已经准备好了直接pip install yuheng需要Python 3.10或更高版本。装完先跑一下yuheng init命令会在当前目录生成一个配置文件默认文件名是yuheng.yaml。yuheng init yuheng config --show配置文件长这样knowledge_base: root: ~/Documents/Notes # 你的笔记根目录 exclude_dirs: - .obsidian - .git - node_modules - .yuheng extensions: - .md scoring: weights: link_health: 30 content_redundancy: 20 structure_completeness: 15 freshness: 15 connectivity: 10 metadata_quality: 10 stale_after_months: 12 report: output_dir: .yuheng include_suggestions: trueexclude_dirs这行一定要留意。如果你用 Obsidian.obsidian目录里全是配置和缓存文件不排除掉它们扫描结果会非常难看。类似的还有.git里面塞满了历史快照纯属噪声。如果你有其他二进制资源目录比如附件图片目录建议也加进去玉衡目前只关注 Markdown 文本文件不会去解析 PDF 或图片。4.2 执行一次全库体检初始化后跑一次完整扫描yuheng scan如果你只改了少量文件想快速看有没有新问题可以加--incremental参数。增量模式会通过对比文件修改时间只重新扫描变过的文件。第一次全量扫描可能比较慢我拿一个五千多篇笔记的库测过大概需要 20 秒左右主要是 SimHash 计算会吃一点 CPU。如果是几百篇的规模几秒钟就跑完了。扫描结束后终端会显示最简单的汇总本次扫描共发现 1234 个文件4892 条链接。 综合健康评分76.4 / 100 链接健康68.2 内容冗余81.0 结构完整88.5 时效状态72.3 主题连通79.1 元数据质量70.8 报告已生成.yuheng/report-2025-03-09.md第一次跑完报告以后我的建议是不要急着修问题。花半小时把报告从头到尾翻一遍感受一下自己的知识库在全貌上是什么状态。很多人在这个环节会有一种“啊原来我的库长这样”的顿悟感。这时候你再决定先动哪一块比工具引导你动哪一块更符合你的真实需求。4.3 看懂你的健康报告报告的主体分四个部分总体概览、维度明细、问题清单、修复建议。我们挑一段实际报告来看。## 2. 维度明细 ### 2.1 链接健康评分 68.2 - 有效链接4721 条 - 无效链接171 条 - 无效链接比例3.5% 其中指向“疑似已改名文件”的链接有 28 条。最常见的失效目标集中在 - notes/projects/旧项目复盘.md - notes/ideas/产品灵感笔记.md链接健康这块报告里会直接把无效目标路径列出来。你一眼就能看到哪些链接指向不存在的文件然后可以去笔记里搜“旧项目复盘”这几个字把链接文字改成现在实际存在的文件名。再看重复内容部分### 2.2 内容冗余评分 81.0 发现 13 组高相似度文档其中 3 组相似度超过 90%。建议优先处理 - notes/idea/关于本地优先软件的想法.md 与 notes/pkm/本地优先工具思考.md 相似度 94%最近修改时间分别为 2024-03-02 和 2024-12-18 建议保留后者将前者的差异部分合并进去后删除。这里有个小技巧玉衡列出的“建议保留”是根据修改时间推算的默认保留较新的那篇。但你自己的真实情况未必如此可能旧笔记里有一段早就忘了的深入思考反而更有价值。所以报告里只给建议不自动删说实话这是有意为之。4.4 定时体检与 CI 集成如果你希望知识库健康度能长期被跟踪定时任务是个好选择。macOS/Linux 下用 cron 一行就能搞定# 每周一早上 9 点跑一次体检 0 9 * * 1 cd /path/to/notes /usr/local/bin/yuheng scan --quiet--quiet是给脚本用的模式不输出彩色日志只写报告文件。Windows 上可以开“任务计划程序”或者直接用 GitHub Actions。如果你的笔记仓库本来就推到 Git 上我推荐用 GitHub Actions 跑每周体检这样健康趋势直接跟着仓库走。name: weekly-knowledge-health-check on: schedule: - cron: 0 9 * * 1 jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.12 - run: pip install yuheng - run: yuheng scan --quiet - run: git add .yuheng git commit -m weekly health report --allow-empty这套东西跑起来之后你就拥有了一份随仓库更新的知识库健康档案。想回看三个月前知识库是什么状态翻 Git 历史即可。5. 常见问题与排查技巧实录5.1 扫描速度与增量更新第一次全量扫描偏慢是最常见的抱怨。五千篇笔记 20 秒对命令行工具来说不算慢但用户体感上会觉得“卡了一下”。如果笔记量更大比如几万篇我建议把扫描拆成分批任务。玉衡没有内置分布式能力但好在它支持对子目录单独扫描你完全可以手动拆成几个指定目录跑。增量更新的逻辑很简单每份文件扫描后会在history.db里记录mtime。下次扫描时文件mtime没变就跳过。但这个机制依赖系统文件时间戳如果你用 Git 管理笔记并且git checkout切换分支所有文件的时间戳都可能会变导致增量失效触发一次全量重扫。这不是 bug是文件系统的正常行为知道就好。遇到这种情况直接跑一次全量扫描即可不用做额外操作。5.2 误报处理与规则调优误报率是这类工具的命门。我总结过几类典型误报图片和附件路径误报为死链比如。玉衡默认只认.md扩展名但链接到图片的路径如果你希望它检查却不想把图片本身当文档统计就需要在配置里明确让图片路径“跳过存在性校验”。锚点链接误报[[note#章节]]指向的章节名变了文件还在链接逻辑上也还成立。玉衡默认不校验锚点是否存在只校验文件是否存在原因很简单解析标题和锚点的对应关系复杂度会指数级上升。大小写问题Windows 路径不区分大小写而 Linux 区分。如果你在 Windows 上建库、Linux 上跑扫描可能会出现一批“假死链”。玉衡提供了一个case_sensitive: false配置项在跨平台场景下把它关掉能消除这类误报。误报本身不可怕可怕的是误报率太高导致用户不再信报告。所以我的建议是宁可配置项多一些也要让用户有能力把规则调到自己信任的状态。5.3 中文内容与编码问题中文场景有几个特有的坑。先说编码。Markdown 文件大多数是 UTF-8但有些老笔记是 GBK 或 GB18030 编码用默认方式打开会直接报错。玉衡在读取文件时会先尝试 UTF-8失败就尝试 GB18030再不行就跳过并记录“不可读文件”。这不算完美方案但起码不会让整个扫描挂掉。再说分词和相似度。SimHash 的效果强依赖 token 的切分方式英文按空格切分很自然中文如果整句不切就会导致相似的句子因为字符顺序差异被判定为不相似。玉衡目前的做法是提取 2-gram 和 3-gram 的字符序列组合。举个例子“知识健康检测”和“检测知识健康”这两个短语虽然词序不同但 2-gram 重叠度其实很高SimHash 识别起来也不难。如果你觉得重复检测不够敏感或过度敏感可以在配置里调simhash_threshold。5.4 与不同笔记工具协作的注意事项玉衡不绑定某个笔记软件但不同工具的目录结构和文件规范会影响扫描结果。如果你用的是带.obsidian目录的本地笔记库记得把它排除掉。如果你用的是某些带数据库的笔记工具核心内容是一个很大的数据库文件而不是.md文本那玉衡目前对它无能为力因为玉衡只处理纯文本 Markdown。这是刻意保持的边界我不想为了兼容数据库格式而牺牲工具本身的简单性。如果你在笔记里用了很多自定义的魔法语法比如#todo、%%高亮这些玉衡通通不认识但不影响它工作。它只关心链接、Frontmatter 和文件时间其它内容默认当作普通文字处理。6. 我在使用中的一些体会6.1 健康度从 69 分到 87 分的真实变化我自己有一个记了三年的知识库启动这个项目之前第一次扫描的综合评分只有 69 分。最扎眼的数据是171 条死链、23 组高相似文档、14 篇超过一年没动的“僵尸笔记”以及 31 个孤立节点。我当时没有立刻去修复所有问题而是按照报告里“严重程度”排序先处理了死链最集中的三个目录。这三个目录里有大量“原来写过但后来改名了”的笔记链接还指向老名字。玉衡列出的“疑似改名文件”帮了大忙我只需要确认映射关系然后批量执行yuheng fix broken-links --apply。光这一步链接健康分就从 68 跳到了 86。后来我把重复文档一篇篇处理完再给所有“永久性笔记”打上status: evergreen标记评分稳定在了 87 分上下。没有哪个单次操作是惊天动地的但每次跑完扫描看到曲线往上走一点确实比任何提醒事项都更能维持整理动力。有了这段实操经历后我才更加确信知识管理这件事真正稀缺的不是“多记一笔”的冲动而是“知道现状在变好”的反馈。玉衡就是这个反馈机制的载体。6.2 下一步想做的功能与社区协作方向这个项目目前还很轻我接下来的计划有几个方向。一是做语义相似度检测用本地 embedding 模型替代 SimHash这样能识别“表述完全不同但含义高度重合”的笔记但前提是模型要够轻、能在普通电脑上跑起来隐私和安全我是不会让步的。二是做“知识结构图”可视化把孤岛节点和聚类结果画成一张图让用户一眼看到自己的知识版图长什么样。三是完善规则库把更多笔记风格纳入兼容范围。说到底玉衡的定位不是“终极解决方案”而是一个可以按自己需求揉捏的基础工具。如果你也想贡献规则、插件或者新的体检维度欢迎直接在仓库里开 Issue 或 PR。做过类似工具的人都知道一个人能维护的规则始终有限一群人的笔记习惯凑在一起这个工具才能真正长成大家都顺手的样子。如果你现在正面对一个越积越乱的知识库我建议今晚就跑一次玉衡先别想着怎么改就看看报告。很多时候看见问题的瞬间解决路径自己就会浮现出来。