一只阿木木

Lint 审计: 给你的数字大脑做一次体检

给你的数字大脑做一次体检

Lint 审计:让知识系统保持健康的完整操作手册

作者:一只阿木木 | 数字大脑摆渡人 🌊


前四篇,我们建好了架构,写好了规则,学会了提取。

你的 /wiki 里已经有了几十个页面, 知识网络开始生长,系统开始运转。

但今天,我要告诉你一件没人喜欢听的事:

你的系统,正在悄悄生病。

不是因为你做错了什么。 是因为所有知识系统,都会随着时间老化。 而大多数人,从来不给它做体检。


一个让我后背发凉的发现

我记得那天清晨,我打开系统,提了一个我觉得很基础的问题:

"RAG 和向量搜索的关系是什么?"

系统的回答,让我愣住了——

它给了我两个完全相反的答案,分别来自两个不同的 wiki 页面,都标注着"置信度:high"。

一个页面说:向量搜索是 RAG 的核心基础设施,缺少向量数据库,RAG 无法运行。

另一个页面说:在个人知识库规模下,LLM Wiki 证明了向量搜索并不是必须的,纯 Markdown 索引就足够了。

两句话,都是"对的"——只是在不同语境下。

但我的系统,把它们当成了同级别的绝对真理,并排放在那里,没有任何一个标记说它们存在张力。

这就是典型的「知识幻觉固化」——

不是 AI 临时编造了一个错误答案,而是两个有条件的真理,被系统当成了无条件的事实,并且互相矛盾地共存着。

这件事让我意识到:

一个没有定期体检的知识系统,不只是"不够好",它可能在某些时刻主动误导你。

这一篇,我们来谈谈如何防止这件事发生。


第一部分:为什么知识系统会"生病"?

要理解为什么需要 Lint,先要理解知识系统退化的根本原因。

1 有一个对 LLM Wiki 模式值得认真对待的批评:因为 LLM 将来源压缩为 wiki 页面,存在幻觉被固化为"事实"的风险。在纯 RAG 系统里,一个错误答案只是一个错误答案。但在 LLM Wiki 里,一个小误解可能会悄悄传播到关联页面。这正是 Karpathy 强调 Lint 步骤的原因——定期审计。

但幻觉传播,只是系统生病的原因之一。

知识系统退化,有四种主要的"病",每一种都有不同的症状、危害和治疗方式:


病 1:幽灵链接病(Dead Links)🔗

什么是它:

你的 wiki 页面里有 [[某个概念]] 的链接,但那个页面已经被重命名、移动或删除了。链接还在,但指向的东西不存在了。

为什么会发生:

你在迭代 Schema 的时候重新命名了几个页面。或者 AI 在创建链接时打错了目标页面的名字(kebab-case 里差一个字母)。

危害:

3 Lint 操作运行结构性检查:孤儿页面(wiki 中存在但 schema 中未定义的文件)、缺失页面(schema 中有 slug 但没有对应文件)、损坏的交叉引用([[slug]] 链接指向不存在的页面)。

当你提问时,AI 想要追踪这条链接,结果撞上一堵墙。它可能开始用通用知识填补这个空白——这正是你最不想要的。

治疗: 程序化扫描,秒级修复。


病 2:孤儿页面病(Orphan Pages)🏝️

什么是它:

2 定期健康检查应当寻找:页面之间的矛盾、已被更新来源取代的过时声明、没有入链的孤儿页面、被提及但缺少独立页面的重要概念、缺失的交叉引用,以及可以通过网络搜索填补的数据缺口。

孤儿页面,就是那些"没有任何其他页面链接到它"的页面。

为什么会发生:

新建了一个页面,但当时没有及时在其他相关页面里添加反向链接。时间久了,这个页面就成了一个信息孤岛。

危害:

你的 AI 在回答问题时,是从 index.md 出发,通过链接关系找到相关页面的。2两个特殊文件帮助 LLM(和你)随着 wiki 成长来导航。它们有不同的用途:index.md 是内容导向的,是 wiki 中所有内容的目录——每个页面都列出了链接、一行摘要和可选的元数据。按类别组织。LLM 在每次 Ingest 时更新它。在回答查询时,LLM 先读取索引找到相关页面,然后深入其中。

一个孤儿页面,在这个导航体系里是隐形的。它存在,但永远不会被找到。里面的知识,等于消失了。

治疗: 程序化扫描发现孤儿,语义审计决定是保留并重新链接,还是归档。


病 3:知识矛盾病(Contradictions)⚡

什么是它:

两个不同的页面,对同一个问题给出了互相冲突的答案,但都没有标注这个矛盾的存在。

为什么会发生:

3 当你添加一个来源,它对某个概念有显著更新时,路由步骤应该捕捉到它并触发重新综合。但如果该页面的 schema 描述模糊或不一致,路由 LLM 可能无法识别其相关性,导致页面变得陈旧。定期的 Lint 检查和周期性的全量重新摄入可以缓解这个问题,但需要主动维护。

简单说:你摄入了新素材,但旧页面没有被正确更新,新旧知识开始打架。

危害: 这是最危险的一种病。AI 不知道该相信哪个,可能给你一个置信度虚高的混合答案。你也不知道该信哪个,系统失去了作为「可靠知识源」的基本价值。

治疗: 必须用 LLM 进行语义审计,程序化脚本无法识别语义矛盾。


病 4:认知僵化病(Stale Content)📦

什么是它:

页面的内容停留在你两年前的认知水平,但你已经通过后来的阅读更新了相关判断——问题是 wiki 里的旧页面还不知道这件事。

为什么会发生:

5 在快速变化的领域,综合页面在来源之前就变得陈旧,而 Lint 检查并不能可靠地捕捉到这一点。

系统不能自动检测「你的认知已经进化了」这件事,它只能检测「这个页面很久没被更新了」这个代理指标。

危害: 表面上系统在正常工作,但给出的答案是过时的。更糟糕的是,高置信度的旧结论会让你停止搜索更新的证据。

治疗: 设置时效性阈值(超过 6 个月未更新的 high 级页面自动降级),并定期对核心领域触发重新 Ingest。


第二部分:Lint 是什么?Karpathy 的原始定义

在我们讲「怎么做」之前,先把 Karpathy 对 Lint 的原始定义讲清楚。

2 Lint 操作:定期请 LLM 对 wiki 进行健康检查。寻找:页面间的矛盾、被更新来源取代的过时声明、没有入链的孤儿页面、被提及但缺少独立页面的重要概念、缺失的交叉引用,以及可以通过网络搜索填补的数据缺口。 1 wiki 保持健康,是因为 LLM 做了没有任何人类愿意做的维护工作。

这句话,是理解 Lint 最重要的一把钥匙。

Lint 不是你的工作,是 LLM 的工作。

你不需要手动找矛盾,不需要手动检查每一个链接,不需要记得每个页面的更新日期。

你只需要定期触发 Lint,然后阅读报告,做人类最擅长的那件事:判断。

这是 LLM Wiki 整套系统里,分工最清晰的地方:

text

程序化脚本 → 找结构性问题(死链、孤儿、格式)
LLM        → 找语义性问题(矛盾、过时、缺口)
你          → 看报告,做决定,不做执行

9 每周定期运行。审计矛盾、过时内容、孤儿页面和缺失的交叉引用。报告问题但不自动修复——由你决定如何处理。


第三部分:双层 Lint 架构——精确分工,高效运转

我在实践中把 Lint 分成了两层,每层负责不同类型的问题,用不同的工具来解决。

这个分层不是我发明的,而是从社区实践中观察到的最佳分工:

19 每次 Lint 运行检查损坏的链接、孤儿页面、格式错误的 frontmatter,以及(可选地,使用 LLM 进行)页面之间的矛盾。


Layer A:程序化 Lint(脚本,5 分钟跑完)

负责: 一切可以被精确定义、用逻辑判断的结构性问题。

特点: 秒级运行,零 API 成本,结果确定性 100%,可自动修复。

检查项目:

text

✅ 死链检查
   扫描所有 [[wikilink]],验证目标文件是否存在
   → 输出:死链列表 + 正确路径建议

✅ 孤儿页面检查
   扫描 /wiki 下所有文件,找出没有任何入链的页面
   → 输出:孤儿列表 + 该页面曾被哪些文件"提到但未链接"

✅ YAML Frontmatter 完整性检查
   验证每个页面是否包含:title, type, created, updated, sources, confidence
   → 输出:缺失字段的文件列表

✅ 索引漂移检查
   比较 /wiki/index.md 的条目数量 vs /wiki 目录下的实际文件数量
   → 输出:index.md 中缺失的文件、index.md 中指向不存在文件的条目

✅ 时效性标记
   扫描所有 confidence: high 的页面
   检查 updated 日期是否超过阈值(默认 180 天)
   → 输出:「待复查」列表,不自动降级,等待人工确认


Layer B:语义 Lint(LLM 参与,30 分钟)

负责: 一切需要「理解意思」才能判断的语义性问题。

特点: 需要 LLM 参与,有 API 成本,结果是「建议」而非「判决」,需要人工决策。

检查项目:

text

🔍 跨页面矛盾检测
   LLM 读取全量 wiki,寻找在不同页面里对同一问题存在明显冲突的声明
   → 输出:矛盾清单(A 页面的观点 vs B 页面的观点 + 建议处理方式)

🔍 过时声明识别
   对照最近新增的素材,检查现有页面中被新来源「超越」的旧结论
   → 输出:可能需要更新的声明列表

🔍 合并候选识别
   寻找两个页面覆盖了高度重叠内容的情况
   → 输出:合并建议(哪两个页面可以合并,保留哪些内容)

🔍 知识缺口发现
   寻找在多个页面中被频繁引用,但没有独立 wiki 页面的概念
   → 输出:「建议新建」列表(附上该概念在哪些页面里被提及)

🔍 置信度合规审查
   检查是否有只有单一来源支撑但被标注为 high 的页面
   → 输出:置信度疑似虚高的页面列表


第四部分:Layer A 实现——你可以直接运行的脚本

以下是一个可以在 macOS / Linux 直接运行的 Python 脚本。

不需要任何外部依赖,不消耗 API,运行一次约 5-30 秒(取决于你的 wiki 体量)。

将以下内容保存为 /schema/scripts/lint_layer_a.py:

Python

#!/usr/bin/env python3
"""
阿木木数字大脑 · Layer A Lint 脚本
程序化健康检查:死链 / 孤儿 / Frontmatter / 索引漂移 / 时效性标记
运行方式:python3 schema/scripts/lint_layer_a.py
"""

import os
import re
import yaml
from datetime import datetime, timedelta
from pathlib import Path

# ─────────────────────────────────────────────
# 配置区(根据你的目录结构调整)
# ─────────────────────────────────────────────
WIKI_DIR = "wiki"
INDEX_FILE = "wiki/index.md"
REPORT_DIR = "wiki/lint-reports"
STALE_THRESHOLD_DAYS = 180          # 超过多少天视为「时效待复查」
REQUIRED_FRONTMATTER = [            # 每个页面必须有的 YAML 字段
    "title", "type", "created", "updated", "sources", "confidence"
]

# ─────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────

def get_all_wiki_files(wiki_dir):
    """获取 /wiki 下所有 .md 文件(排除系统文件)"""
    system_files = {"index.md", "log.md", "glossary.md"}
    files = {}
    for path in Path(wiki_dir).rglob("*.md"):
        rel = path.relative_to(wiki_dir)
        stem = path.stem  # 文件名不含扩展名
        if path.name not in system_files:
            files[stem] = path
    return files

def extract_wikilinks(content):
    """从 Markdown 内容中提取所有 [[wikilink]]"""
    return re.findall(r'\[\[([^\]|#]+?)(?:\|[^\]]*)?\]\]', content)

def parse_frontmatter(content):
    """解析 YAML frontmatter"""
    if not content.startswith("---"):
        return {}
    end = content.find("---", 3)
    if end == -1:
        return {}
    try:
        return yaml.safe_load(content[3:end]) or {}
    except yaml.YAMLError:
        return {}

def date_from_str(date_val):
    """将 frontmatter 中的日期字段转为 datetime"""
    if isinstance(date_val, datetime):
        return date_val
    if isinstance(date_val, str):
        for fmt in ("%Y-%m-%d", "%Y/%m/%d"):
            try:
                return datetime.strptime(date_val, fmt)
            except ValueError:
                continue
    return None

# ─────────────────────────────────────────────
# 检查模块
# ─────────────────────────────────────────────

def check_dead_links(wiki_files):
    """检查 1:死链检测"""
    dead_links = []
    for stem, path in wiki_files.items():
        content = path.read_text(encoding="utf-8")
        links = extract_wikilinks(content)
        for link in links:
            # 处理路径分隔符,取最后一段作为 stem
            link_stem = Path(link).stem if "/" in link else link
            if link_stem not in wiki_files:
                dead_links.append({
                    "source_file": str(path),
                    "dead_link": link,
                    "link_stem": link_stem
                })
    return dead_links

def check_orphan_pages(wiki_files):
    """检查 2:孤儿页面检测"""
    # 统计每个页面被链接的次数
    inlink_count = {stem: 0 for stem in wiki_files}
    for stem, path in wiki_files.items():
        content = path.read_text(encoding="utf-8")
        links = extract_wikilinks(content)
        for link in links:
            link_stem = Path(link).stem if "/" in link else link
            if link_stem in inlink_count and link_stem != stem:
                inlink_count[link_stem] += 1

    orphans = [stem for stem, count in inlink_count.items() if count == 0]
    return orphans

def check_frontmatter(wiki_files):
    """检查 3:Frontmatter 完整性"""
    issues = []
    for stem, path in wiki_files.items():
        content = path.read_text(encoding="utf-8")
        fm = parse_frontmatter(content)
        missing = [field for field in REQUIRED_FRONTMATTER if field not in fm]
        if missing:
            issues.append({
                "file": str(path),
                "missing_fields": missing
            })
    return issues

def check_index_drift(wiki_files):
    """检查 4:索引漂移"""
    if not os.path.exists(INDEX_FILE):
        return {"error": "index.md 不存在"}

    index_content = Path(INDEX_FILE).read_text(encoding="utf-8")
    # 从 index.md 中提取所有 wikilink
    index_links = set(extract_wikilinks(index_content))
    wiki_stems = set(wiki_files.keys())

    not_in_index = wiki_stems - index_links   # 文件存在但 index 没有
    ghost_in_index = index_links - wiki_stems  # index 有但文件不存在

    return {
        "not_in_index": sorted(not_in_index),
        "ghost_in_index": sorted(ghost_in_index)
    }

def check_stale_pages(wiki_files):
    """检查 5:时效性标记(只标记,不自动降级)"""
    stale = []
    threshold = datetime.now() - timedelta(days=STALE_THRESHOLD_DAYS)

    for stem, path in wiki_files.items():
        content = path.read_text(encoding="utf-8")
        fm = parse_frontmatter(content)
        confidence = fm.get("confidence", "")
        updated = fm.get("updated")

        if confidence == "high" and updated:
            updated_dt = date_from_str(updated)
            if updated_dt and updated_dt < threshold:
                stale.append({
                    "file": str(path),
                    "last_updated": str(updated),
                    "days_since_update": (datetime.now() - updated_dt).days
                })
    return stale

# ─────────────────────────────────────────────
# 报告生成
# ─────────────────────────────────────────────

def generate_report(dead_links, orphans, frontmatter_issues, index_drift, stale_pages):
    today = datetime.now().strftime("%Y-%m-%d")
    lines = [
        f"# Lint Report · Layer A · {today}",
        f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
        "",
        "---",
        "",
        "## 总览",
        "",
        f"| 检查项 | 发现问题数 | 状态 |",
        f"|--------|-----------|------|",
        f"| 死链 | {len(dead_links)} | {'⚠️ 需处理' if dead_links else '✅ 正常'} |",
        f"| 孤儿页面 | {len(orphans)} | {'⚠️ 需处理' if orphans else '✅ 正常'} |",
        f"| Frontmatter 缺失 | {len(frontmatter_issues)} | {'⚠️ 需处理' if frontmatter_issues else '✅ 正常'} |",
        f"| 索引漂移 | {len(index_drift.get('not_in_index', [])) + len(index_drift.get('ghost_in_index', []))} | {'⚠️ 需处理' if (index_drift.get('not_in_index') or index_drift.get('ghost_in_index')) else '✅ 正常'} |",
        f"| 时效待复查 | {len(stale_pages)} | {'📋 建议复查' if stale_pages else '✅ 正常'} |",
        "",
        "---",
        "",
    ]

    # 死链详情
    lines.append("## 1. 死链(可自动修复)")
    lines.append("")
    if dead_links:
        for item in dead_links:
            lines.append(f"- **文件:** `{item['source_file']}`")
            lines.append(f"  - 死链:`[[{item['dead_link']}]]`")
            lines.append(f"  - 建议:检查是否有拼写相近的页面,或删除该链接")
            lines.append("")
    else:
        lines.append("✅ 无死链,状态正常。")
        lines.append("")

    # 孤儿页面详情
    lines.append("## 2. 孤儿页面(需人工判断)")
    lines.append("")
    if orphans:
        lines.append("以下页面没有任何其他页面链接到它们。")
        lines.append("处理选项:A) 在相关页面中添加链接  B) 归档此页面  C) 删除此页面")
        lines.append("")
        for stem in orphans:
            lines.append(f"- `[[{stem}]]`")
        lines.append("")
    else:
        lines.append("✅ 无孤儿页面,状态正常。")
        lines.append("")

    # Frontmatter 问题
    lines.append("## 3. Frontmatter 缺失字段(可自动修复)")
    lines.append("")
    if frontmatter_issues:
        for item in frontmatter_issues:
            lines.append(f"- **文件:** `{item['file']}`")
            lines.append(f"  - 缺失字段:`{', '.join(item['missing_fields'])}`")
            lines.append("")
    else:
        lines.append("✅ 所有页面 Frontmatter 完整。")
        lines.append("")

    # 索引漂移
    lines.append("## 4. 索引漂移(建议更新 index.md)")
    lines.append("")
    not_in_index = index_drift.get("not_in_index", [])
    ghost_in_index = index_drift.get("ghost_in_index", [])
    if not_in_index:
        lines.append("**文件存在但未在 index.md 中记录:**")
        for stem in not_in_index:
            lines.append(f"- `[[{stem}]]`")
        lines.append("")
    if ghost_in_index:
        lines.append("**index.md 中有条目但对应文件不存在(幽灵条目):**")
        for stem in ghost_in_index:
            lines.append(f"- `[[{stem}]]`")
        lines.append("")
    if not not_in_index and not ghost_in_index:
        lines.append("✅ 索引与文件系统完全同步。")
        lines.append("")

    # 时效待复查
    lines.append("## 5. 时效待复查(建议重新 Ingest 相关素材)")
    lines.append("")
    if stale_pages:
        lines.append(f"以下页面标注为 `confidence: high`,但超过 {STALE_THRESHOLD_DAYS} 天未更新:")
        lines.append("")
        for item in stale_pages:
            lines.append(f"- **文件:** `{item['file']}`")
            lines.append(f"  - 最后更新:`{item['last_updated']}`({item['days_since_update']} 天前)")
            lines.append(f"  - 建议:投入该领域的新素材,触发重新评估")
            lines.append("")
    else:
        lines.append("✅ 所有 high 级页面均在时效范围内。")
        lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("**下一步:**")
    lines.append("1. 将此报告发给 Claude Code,请它修复标注为「可自动修复」的问题")
    lines.append("2. 人工判断「孤儿页面」和「时效待复查」的处理方式")
    lines.append("3. 将需要语义审计的问题,传入 Layer B Lint Prompt")
    lines.append("")
    lines.append(f"*报告生成于 {datetime.now().strftime('%Y-%m-%d %H:%M')}*")

    return "\n".join(lines)

# ─────────────────────────────────────────────
# 主程序
# ─────────────────────────────────────────────

def main():
    print("🔍 阿木木数字大脑 · Layer A Lint 正在运行...")
    print(f"   扫描目录:{WIKI_DIR}")
    print("")

    wiki_files = get_all_wiki_files(WIKI_DIR)
    print(f"   发现 wiki 页面:{len(wiki_files)} 个")
    print("")

    print("   [1/5] 检查死链...")
    dead_links = check_dead_links(wiki_files)

    print("   [2/5] 检查孤儿页面...")
    orphans = check_orphan_pages(wiki_files)

    print("   [3/5] 检查 Frontmatter...")
    frontmatter_issues = check_frontmatter(wiki_files)

    print("   [4/5] 检查索引漂移...")
    index_drift = check_index_drift(wiki_files)

    print("   [5/5] 标记时效待复查...")
    stale_pages = check_stale_pages(wiki_files)

    print("")
    print("   生成报告...")

    # 生成报告
    report = generate_report(dead_links, orphans, frontmatter_issues, index_drift, stale_pages)

    # 保存报告
    os.makedirs(REPORT_DIR, exist_ok=True)
    today = datetime.now().strftime("%Y-%m-%d")
    report_path = f"{REPORT_DIR}/lint-layer-a-{today}.md"
    with open(report_path, "w", encoding="utf-8") as f:
        f.write(report)

    # 控制台摘要
    total_issues = len(dead_links) + len(orphans) + len(frontmatter_issues)
    total_issues += len(index_drift.get("not_in_index", [])) + len(index_drift.get("ghost_in_index", []))

    print(f"✅ Layer A Lint 完成!")
    print(f"   发现问题:{total_issues} 个(另有 {len(stale_pages)} 个时效待复查)")
    print(f"   报告已保存至:{report_path}")
    print("")
    print("   下一步:")
    print(f"   1. 用 Obsidian 打开 {report_path} 查看详情")
    print("   2. 将报告发给 Claude Code,执行自动修复")
    print("   3. 运行 Layer B 语义 Lint 进行深度检查")

if __name__ == "__main__":
    main()


第五部分:Layer B 实现——语义 Lint 的完整 Prompt

Layer A 搞定了所有「结构性问题」。

但真正影响知识质量的,是那些只有「理解意思」才能发现的问题——矛盾、过时、缺口。

这些,需要 LLM 来做。

把以下 Prompt 发给 Claude Code,触发一次完整的语义审计:


Layer B 语义 Lint 主 Prompt

text

你正在执行一次 wiki 语义健康检查(Layer B Lint)。

目标目录:/wiki
Layer A 程序化 Lint 已完成,报告位于:/wiki/lint-reports/lint-layer-a-[今日日期].md

请按以下顺序执行语义审计:

──────────────────────────────────────
STEP 1:读取全局索引
──────────────────────────────────────
读取 /wiki/index.md,获取所有页面的完整列表。
这是你本次审计的工作清单。

──────────────────────────────────────
STEP 2:跨页面矛盾扫描
──────────────────────────────────────
从 index.md 中,选取置信度标注为 contested 或近期有「⚡矛盾标记」的页面。
逐一深入阅读,寻找以下类型的矛盾:

类型 A:直接矛盾
  A 页面说「X 是 Y」,B 页面说「X 不是 Y」

类型 B:条件性矛盾
  A 页面的结论在条件 P 下成立,B 页面的结论在条件 Q 下成立,
  但两个页面都没有标注各自的适用条件

类型 C:时间性矛盾
  A 页面引用了旧来源的旧结论,B 页面引用了新来源的新结论,
  两者并列但未标注时间上的演进关系

输出格式(每个矛盾一条):
【矛盾 N | 类型:A/B/C】
页面 1:[[page-name]] → 立场摘要(不超过 30 字)
页面 2:[[page-name]] → 立场摘要(不超过 30 字)
矛盾性质:[解释为什么这是矛盾]
建议处理方式:
  方案 1:[如果来源质量 A > B,保留 A 的结论,将 B 的结论移入「历史观点」节]
  方案 2:[如果两者均有道理,创建综合分析页,显式标注适用条件]
  方案 3:[如果信息不足判断,两个页面均标注「contested」,等待更多来源]
等待用户决策:是

──────────────────────────────────────
STEP 3:过时声明识别
──────────────────────────────────────
检查 /raw 目录中最近 30 天新摄入的素材(通过 /wiki/log.md 确认日期)。
对每篇新素材,检查它是否「超越」了某个现有 wiki 页面的核心结论:

判断标准:
- 新素材明确提出对旧观点的更新或反驳
- 新素材提供的数据/案例比现有页面的来源更新、更有说服力
- 现有页面引用的工具/方法已有新版本,旧版本的结论不再完全适用

输出格式(每个过时声明一条):
【过时声明 N】
现有页面:[[page-name]],相关段落:「[30 字以内摘要]」
超越来源:raw/[文件路径],相关内容:「[30 字以内摘要]」
建议操作:更新该页面的对应段落,追加新来源,调整置信度(如适用)

──────────────────────────────────────
STEP 4:合并候选识别
──────────────────────────────────────
扫描 index.md,寻找可能覆盖高度重叠主题的页面对。

判断标准:
- 两个页面的 related 字段互相引用
- 两个页面的核心定义有超过 50% 的概念重叠
- 其中一个页面的内容可能是另一个页面的子集

输出格式(每个合并候选一条):
【合并候选 N】
页面 A:[[page-name-a]](一行摘要)
页面 B:[[page-name-b]](一行摘要)
重叠分析:[解释重叠的具体内容]
合并建议:
  - 保留页面:[[page-name-a 或 b]](建议保留哪个)
  - 重定向处理:被合并的页面改为重定向文件,保留 slug
  - 需要保留的独特内容:[列出被合并页面中不重叠的部分]
等待用户决策:是

──────────────────────────────────────
STEP 5:知识缺口扫描
──────────────────────────────────────
扫描所有 wiki 页面,寻找「被多次引用但没有独立页面的概念」:

具体操作:
1. 统计所有 [[wikilink]] 中,指向不存在页面的链接频率
2. 频率 ≥ 3 次的「缺失概念」,列入「建议新建」清单

同时检查:
3. 在页面正文中被多次提到(不是以 [[link]] 形式)但没有独立页面的重要术语

输出格式:
【知识缺口 N】
缺失概念:[概念名称]
被引用次数:N 次
来源页面:[[page-a]], [[page-b]], ...
建议操作:新建 /wiki/concepts/[kebab-case].md
  核心内容建议:[该概念应包含哪些核心内容]

──────────────────────────────────────
STEP 6:置信度合规审查
──────────────────────────────────────
抽查 20 个标注为 confidence: high 的页面(优先选最近创建的)。
验证:每个 high 级页面是否真的有 3 个以上高质量、互相独立的来源支持。

发现不合规的情况:
- 只有单一来源但标注为 high → 建议降为 medium
- 来源都指向同一个人/机构 → 建议降为 medium(来源不独立)
- 来源超过 12 个月旧 → 建议降为 medium,标注「待更新来源」

──────────────────────────────────────
STEP 7:生成 Layer B 报告
──────────────────────────────────────
将上述所有发现整合为一份 Markdown 报告,保存至:
/wiki/lint-reports/lint-layer-b-[今日日期].md

报告结构:
## 执行摘要
(总共发现 N 个矛盾、M 条过时声明、K 个合并候选、J 个知识缺口)

## 需要立即处理(高优先级)
(直接影响答案质量的矛盾 + 置信度严重不合规)

## 建议处理(中优先级)
(过时声明 + 合并候选)

## 等待更多素材(低优先级)
(知识缺口 + 单来源 high 页面)

## 下次 Ingest 建议
(基于知识缺口,推荐搜索和摄入哪些类型的素材)


第六部分:Lint 报告出来了——然后呢?

很多人走到这一步,拿到了报告,然后不知道该怎么办。

Lint 报告,不是让你自己去修复所有问题的。

记住你的角色:你是策展人和决策者,不是执行者。

这是收到 Lint 报告后的标准处理流程:


第一步:阅读执行摘要(5 分钟)

看总数,不看细节。

问自己两个问题:

  • 有没有「高优先级」的矛盾,可能正在影响我的日常使用?
  • 有没有「知识缺口」,说明我在某个重要领域的素材严重不足?

第二步:处理自动修复项(10 分钟)

把 Layer A 报告发给 Claude Code,说这一句话:

"请读取这份 Lint 报告,对所有标注为「可自动修复」的问题执行修复,包括:修复死链(如有明确对应页面)、补全缺失的 Frontmatter 字段、更新 index.md 的漂移条目。修复完成后,告诉我做了哪些修改。"

你不需要看过程,只看结果报告。


第三步:处理矛盾(需要你的判断,每条 3-5 分钟)

对每一个矛盾,Layer B 报告已经给了你三个处理方案。

你的工作是选择哪个方案,然后告诉 Claude Code 去执行:

"关于矛盾 2(rag vs llm-wiki 关于向量搜索的矛盾), 请采用方案 2:创建综合分析页 rag-vs-llm-wiki-on-vector-search, 在两个原始页面添加「⚡矛盾已解决,参见综合分析页」的标注。"

Lint 操作明确检查页面之间的矛盾、被更新来源取代的过时声明,以及缺失的连接。这是最大的缺口——当新信息到来时,应该检查它是否与现有内容矛盾并标记冲突,而不是静默覆盖。

静默覆盖是质量最大的敌人。 矛盾被显式标记,比矛盾被悄悄掩盖,要好一千倍。


第四步:处理知识缺口(每月一次,作为 Ingest 选题来源)

Layer B 报告里的「知识缺口」,是你下一个月 Ingest 工作的选题库。

它告诉你:你的知识网络里有哪些节点是空的,但被其他节点反复引用着。

把知识缺口清单复制进你的 seeds 文件夹,作为下个月阅读计划的输入。

这是整个系统最优雅的闭环之一:

Lint 发现缺口 → 缺口变成阅读选题 → 阅读产生素材 → 素材 Ingest 填补缺口 → wiki 更完整 → 下次 Lint 发现更深的缺口 → 循环

LLM 善于建议新的研究问题和新的素材来源。这让 wiki 随着成长保持健康。

第五步:不处理的,写进决策日志(2 分钟)

对于那些你看了、但决定暂时不处理的问题——

在 /wiki/log.md 里追加一条记录:

text

## 2026-05-23 | Lint 决策日志
矛盾 3([[compilation]] vs [[build-systems]]):暂缓处理,等待摄入更多软件工程领域素材后再做综合判断。
知识缺口([[system-prompt-learning]]):已加入下月 Ingest 优先清单。

知识溯源日志——每次 Ingest 的时间序列记录——记录了什么内容进入知识库、什么时间、来自哪个来源、影响了哪些页面。可以用 grep 快速搜索。简单但对调试和复查极有价值。

这条记录很重要:它让你三个月后再看 log.md 时,知道为什么这个矛盾当时没有被解决,而不是认为它被遗漏了。

第七部分:你的数字大脑季度体检清单

把以下清单存进你的系统。这是每个周期应该做的最小必要动作:

text

📅 每月体检(约 45 分钟)

【第一步:运行 Layer A 脚本(5 分钟)】
□ 运行 python3 schema/scripts/lint_layer_a.py
□ 打开报告,浏览执行摘要
□ 将报告发给 Claude Code,执行自动修复

【第二步:处理明显错误(10 分钟)】
□ 修复所有死链
□ 为孤儿页面决定:补链接 / 归档 / 删除
□ 更新 index.md 的漂移条目

【第三步:记录决策(5 分钟)】
□ 将暂缓处理的问题记录进 /wiki/log.md
□ 将新发现的知识缺口加入下月 Ingest 选题

---

📅 每季审计(约 2.5 小时)

【第一步:运行 Layer A + Layer B(30 分钟运行 + 等待)】
□ 运行 Layer A 脚本
□ 发送 Layer B 语义 Lint Prompt,等待报告

【第二步:处理矛盾(60 分钟)】
□ 逐一阅读所有矛盾,选择处理方案
□ 发给 Claude Code 执行
□ 验证处理结果

【第三步:处理过时声明(30 分钟)】
□ 对每条过时声明,确认是否需要更新
□ 对「时效待复查」的页面,决定是否重新 Ingest 相关素材

【第四步:知识缺口转化(15 分钟)】
□ 将 Top 5 知识缺口转化为下季度的 Ingest 优先清单
□ 每个缺口对应一篇「种子文章」,存入 /raw/seeds/

【第五步:Schema 回顾(15 分钟)】
□ 回顾本季度最常触发矛盾的领域——Schema 的矛盾检测规则是否需要更新?
□ 回顾孤儿问题最多的页面类型——命名规范是否需要调整?
□ 如有改进,更新 CLAUDE.md 并追加到 LEARNED.md

---

📅 年度大审(约 半天)

□ 评估 CLAUDE.md 整体是否需要架构性升级
□ 统计全年 Lint 发现的问题类型分布——找规律,优化 Schema
□ 检查知识网络的「枢纽节点」——这些节点代表你理解最深的领域,是否和你的核心价值主张一致?
□ 清理「永久孤儿」:超过 1 年没有被链接到的页面,考虑归档
□ 备份整个 /wiki 目录到外部存储


第八部分:一个更大的视角——Lint 不是维护,是进化

我想在结束之前,说一件超越「操作手册」范畴的事。

很多人听到「体检」「维护」「审计」这些词,就觉得这是负担——是系统给你增加的额外工作量。

但我越用这套系统,越觉得:

Lint 不是负担,是系统给你的礼物。

Karpathy 的 Gist 揭示了我的生产级系统里一些我曾经视而不见的缺口——因为我是一点一点增量搭建起来的,用我的学习循环作为指引。我的系统没有全局的 Lint 操作,没有孤儿检测,没有死链扫描,没有过时内容识别。

这是真正走在前面的实践者的诚实反馈:没有 Lint 的知识系统,是带着蒙眼布在成长。

你不知道它的盲点在哪里,不知道它的矛盾在哪里,不知道哪些「确信」已经过期了。

而当你第一次拿到 Layer B 报告,看到系统找到了一个你自己从未意识到的矛盾——

那个矛盾的存在,说明了一件事:

你的知识系统,已经聪明到能够发现你自己的认知盲点了。

这才是「数字大脑」真正应有的样子。

不是一个更好的收藏夹,不是一个更整齐的笔记本,而是一个能够挑战你、更新你、帮你看到你自己看不到的东西的外部认知系统。

Lint,是这个系统保持这种能力的机制。

LLM Wiki 解决了维护问题——wiki 能保持维护,是因为维护成本接近于零。LLM 在每次 Ingest 时自动创建和更新交叉引用。人类专注于真正重要的事——决定读什么和提什么问题。

你专注于好奇心和判断力。系统负责一切的记录、连接、和健康维护。

这,才是知识工作者在 AI 时代真正的分工。


尾声:给你的系统安排第一次体检

读完这篇文章,我希望你做一件事:

今天,给你的系统安排第一次体检。

不需要等到系统很大。哪怕你现在只有 20 个 wiki 页面,跑一次 Layer A 脚本,都可能发现你没注意到的问题。

而「第一次发现问题」,是整个 Lint 流程里最有价值的时刻——

因为它会改变你看待自己知识系统的方式:

从「我有一堆笔记」,到「我有一个需要持续维护、持续进化、但不需要我亲自维护的知识引擎」。

只需要四步:

text

第一步:把本文第四部分的脚本保存到 schema/scripts/lint_layer_a.py
第二步:在你的 Vault 根目录打开终端,运行:
        python3 schema/scripts/lint_layer_a.py
第三步:打开报告,看看发现了什么
第四步:把报告发给 Claude Code,说:
        "请读取这份 Lint 报告,修复所有「可自动修复」的问题"

就这四步。

然后,你的系统会比今天更健康一点。

下次,再健康一点。

一次一次的 Lint,就是你的知识系统,不断进化的过程。

下一篇预告:

系统建好了,能运转了,也能自我维护了。

下一篇,我要讲的是整个系列里最前沿的内容——

LEARNED.md:让你的数字大脑自己「长脑子」。

当你的系统开始把自己踩过的坑记录下来, 开始自己更新自己的编译规则, 它就从一个「工具」进化成了一个「正在成长的认知伙伴」。

这,是 Karpathy 指向但还没有完全解决的那个方向。 而我们,会尝试走得比他更远一点。


关注一只阿木木,别让这个系列在你的收藏夹里睡觉。去做,才是真的学。🌊


本文参考资料:Andrej Karpathy,「LLM Wiki」,GitHub Gist,2026 年 4 月;社区实践参考:karpathy-llm-wiki(Astro-Han)、obsidian-llm-wiki(green-dalii)、llm-knowledge-bases(rvk7895),2026 年 4-5 月。



我是【一只阿木木】,AI 知识系统架构师,坐标杭州。
用 Obsidian + Claude + LLM Wiki 范式,帮普通人搭建由 AI 自动编译、自我进化的个人知识系统。
你只需要保持好奇——阅读、思考、提问;AI 负责所有苦活——总结、归档、交叉引用、维护。
我相信:在 AI 时代,每个普通人都该拥有一个自动生长的知识系统

扫码加入行动营👇获取更多Obsidian + AI数字大脑实践

Image

关注一只阿木木,别让这个系列在你的收藏夹里睡觉。去做,才是真的学。🌊