Files
llm_wiki/tools/docs/LLM-Wiki-v2升级技术方案.md
T
hehaiguang1123 a6f05ab2d5 Phase 0-2: Schema cleanup, typed relations, event-driven automation
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd)
- Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py)
- Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks
- Fix .gitignore to track tools/ and .githooks/
- Fix git remote URL (remove plaintext token)
- New wiki pages: 504 pages, 34 raw sources
2026-07-01 08:05:43 +08:00

28 KiB
Raw Blame History

categories, tags, created, type
categories tags created type
Tools
Documentation
wiki
planning
architecture
2026-06-30 reference

LLM Wiki v2 升级 — 技术实施方案

基于 Karpathy v1 + Rohit v2 理念的本仓库升级方案。

评估基线:v1 实现度 75%,v2 实现度 15%,总体成熟度 48% → 目标 85%+


目录


Phase 0 — 即时修复(1-2h

目标: 清理 Schema 债务,提升基础数据质量

任务 A-0: 修复 AGENTS.md 冗余

问题 文件 行号 操作
Karpathy quote 重复 x3 AGENTS.md 1294, 1297 删除第 1297 行的重复 quote,保留第 1294 行
编号冲突(两个第 5 节) AGENTS.md 777, 887 重编号:WorkBuddy → ## 5.,工具目录 → ## 5.5 保留,代码风格 → ## 6. 并调整后续编号
qmd 说明附录末尾 AGENTS.md 末尾 整合到 ## 6. 工具链

具体操作:

  1. 删除重复 quote

    Location: ~L1294-L1297
    old: > "这种个性化方式..."
    new: (delete line L1297)
    
  2. 重新编号冲突节:

    ~L887: "## 5. 代码风格与工具" → "## 6. 代码风格与工具"
    ~L891: "### 5.1 脚本规范" → "### 6.1"
    ~L898: "### 5.2 Lint" → "### 6.2"
    

    同时触发后续引用更新:AGENTS.md 内部引用的锚点需更新。

  3. 将 qmd 说明(当前末尾)移入 ## 6. 工具链

    • 当前末尾的 qmd 说明(## opencode-mem 之前的块)拆出
    • ### 6.2 Lint 工具脚本 之后,新增 ### 6.3 qmd 本地搜索引擎

任务 B-0: 提高行号标注覆盖率

当前: 880 条 [raw:…] 引用中,492 条有行号(55.9%)

工具: tools/scripts/fix-raw-citations.py(新建)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
fix-raw-citations.py — 扫描 wiki/ 页面,对缺失行号的 [raw:file] 引用
自动从原始 raw 文件中查找匹配文本并补上行号。
"""
import re, os, glob, sys
from pathlib import Path

VAULT = Path(r"D:\Applications\app\kepano-obsidian-main")
WIKI_DIR = VAULT / "wiki"
RAW_DIR = VAULT / "raw"

def extract_context(text: str, keyword: str, context_lines: int = 3) -> str:
    """在 raw 文件中找到 keyword 所在的行号范围"""
    lines = text.splitlines()
    for i, line in enumerate(lines, 1):
        if keyword in line:
            start = max(1, i - context_lines)
            end = min(len(lines), i + context_lines)
            return f"{start}-{end}"
    return ""

def process_file(filepath: Path):
    content = filepath.read_text(encoding="utf-8")
    # Match [raw:filename] without line number
    pattern = r'\[raw:([^:\]]+)\]'  # [raw:filename]
    matches = list(re.finditer(pattern, content))
    if not matches:
        return content, 0

    raw_text_cache = {}
    fixes = 0
    for m in reversed(matches):  # iterate backwards to preserve offsets
        raw_filename = m.group(1)
        if raw_filename not in raw_text_cache:
            raw_path = RAW_DIR / f"{raw_filename}.md"
            if raw_path.exists():
                raw_text_cache[raw_filename] = raw_path.read_text(encoding="utf-8")
            else:
                continue

        # Get text around the citation (find keyword in surrounding context)
        start = max(0, m.start() - 200)
        end = min(len(content), m.end() + 200)
        context = content[start:end]

        # Try to extract a key phrase from context to search in raw
        # Simple approach: get 3 words before the citation
        before = content[max(0, m.start()-50):m.start()]
        words = re.findall(r'[\w\u4e00-\u9fff]+', before)
        keyword = " ".join(words[-5:]) if len(words) >= 5 else " ".join(words)

        line_range = extract_context(raw_text_cache[raw_filename], keyword)
        if line_range:
            old = m.group(0)
            new = f"[raw:{raw_filename}:{line_range}]"
            content = content[:m.start()] + new + content[m.end():]
            fixes += 1

    return content, fixes

if __name__ == "__main__":
    total_fixes = 0
    for md_file in sorted(WIKI_DIR.glob("*.md")):
        if md_file.name in ("index.md", "log.md"):
            continue
        content, fixes = process_file(md_file)
        if fixes:
            md_file.write_text(content, encoding="utf-8")
            print(f"{md_file.name}: +{fixes} line numbers")
            total_fixes += fixes
    print(f"\nTotal: {total_fixes} citations updated")

运行:

$env:PYTHONIOENCODING="utf-8"
python tools/scripts/fix-raw-citations.py

预期效果: 行号覆盖率从 56% → 75%+

任务 C-0: 更新 index.md 指标

当前问题: 声称 478+ wiki 页面,实际 506 页

操作: 运行统计后手动编辑 index.md

# 统计当前指标
python -c "
import os
wiki = r'D:\Applications\app\kepano-obsidian-main\wiki'
files = [f for f in os.listdir(wiki) if f.endswith('.md') and f not in ('index.md','log.md')]
print(f'Wiki pages: {len(files)}')
raw = r'D:\Applications\app\kepano-obsidian-main\raw'
raw_files = [f for f in os.listdir(raw) if f.endswith('.md') and not f.startswith('_')]
print(f'Raw sources: {len(raw_files)}')
# Type breakdown
types = {}
for f in files:
    content = open(os.path.join(wiki, f), encoding='utf-8').read()
    m = __import__('re').search(r'type:\s*(\S+)', content)
    if m: t = m.group(1); types[t] = types.get(t, 0) + 1
for t, c in sorted(types.items(), key=lambda x: -x[1]):
    print(f'  {t}: {c}')
"

更新 wiki/index.md 中概览表格的数字。


Phase 1 — 短期升级(2-4 周)

目标: 实现 v2 两个核心特性——Typed relationships 和内存生命周期

任务 A-1: Typed relationships

1.1 前端:Frontmatter 扩展

AGENTS.md 中定义新的 frontmatter 字段:

# 新增可选字段
relations:
  - type: depends_on | conflicts_with | supersedes | caused_by | supports | extends | part_of | example_of
    target: "[[页面名称]]"
    description: "关系说明(可选)"  # NEW
    confidence: 1-5                  # NEW

边类型一览

类型 含义
depends_on A 理解依赖 B 三湾改编 depends_on 兵为将有
conflicts_with A 与 B 矛盾的声明 RAG vs 持久化知识库 conflicts_with 纯 RAG 方案
supersedes A 替代了 B(新知识覆盖旧知识) LLM Wiki v2 supersedes LLM Wiki v1
caused_by A 由 B 导致 八月失败 caused_by 杜修经
supports A 提供证据支持 B ALEKS 研究 supports AI 个性化学习
extends A 扩展了 B 的概念 AIEOU extends ETEE 生命周期框架
part_of A 是 B 的一部分 支部建在连上 part_of 三湾改编
example_of A 是 B 的一个实例 Duolingo example_of AI 教育应用

1.2 工具:relations 编辑脚本

# tools/scripts/manage-relations.py
"""
用法:
    python manage-relations.py add <page> --type supersedes --target "旧页面"
    python manage-relations.py list <page>
    python manage-relations.py graph <page>    # 输出 DOT 格式(Graphviz
"""

核心逻辑 — 添加关系

import yaml, re, sys, json
from pathlib import Path

WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")

def read_frontmatter(filepath):
    content = filepath.read_text(encoding="utf-8")
    m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not m: return {}, content
    try:
        front = yaml.safe_load(m.group(1)) or {}
    except:
        front = {}
    return front, content

def write_frontmatter(filepath, front, body):
    new_yaml = yaml.dump(front, allow_unicode=True, default_flow_style=False, sort_keys=False)
    filepath.write_text(f"---\n{new_yaml}---\n{body.lstrip()}", encoding="utf-8")

def add_relation(page, rel_type, target, desc="", confidence=3):
    fp = WIKI / f"{page}.md"
    if not fp.exists():
        print(f"ERROR: {page}.md not found"); return
    front, body = read_frontmatter(fp)
    if "relations" not in front: front["relations"] = []
    # dedup
    for r in front["relations"]:
        if r.get("type") == rel_type and r.get("target") == target:
            print(f"  Already exists: {rel_type} [[{target}]]")
            return
    entry = {"type": rel_type, "target": target}
    if desc: entry["description"] = desc
    if confidence != 3: entry["confidence"] = confidence
    front["relations"].append(entry)
    write_frontmatter(fp, front, body)
    print(f"  Added: {page} --{rel_type}--> [[{target}]]")

1.3 增量迁移策略

不要一次性给所有 506 页添加 relations。新页面优先

  1. 新创建的 wiki 页面必须(使用 AGENTS.md 规范要求)包含 relations
  2. 对现有的 high-traffic 页面(被最多其他页面引用的 top-50)按批次添加
  3. Lint 报告 relations: none 的页面数量

优先级排序

# 找出被引用最多的 Top 50 页面
grep -roh "\[\[[^]]*\]\]" wiki/*.md | sort | uniq -c | sort -rn | head -50

方案: 无需新搜索引擎。在 qmd 的基础上加一层关系扩展:

# tools/scripts/graph-search.py
"""
用法:
    python graph-search.py "查询词"
    
流程:
    1. qmd vsearch "查询词" → top 10 结果
    2. 对每个结果,读取 relations → 获取相邻节点
    3. 去重后返回 (直接关联 + 关系扩展后的) top 15
"""

核心代码:

import subprocess, json, yaml, re
from pathlib import Path

QMD = r'node "C:\Users\hhhh2024\AppData\Roaming\npm\node_modules\@tobilu\qmd\dist\cli\qmd.js"'
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")

def qmd_search(query: str, count=10):
    result = subprocess.run(
        f'{QMD} vsearch "{query}" -c wiki -n {count}',
        capture_output=True, text=True, shell=True
    )
    return [line.strip() for line in result.stdout.splitlines() if line.strip()]

def get_relations(page_name: str):
    fp = WIKI / f"{page_name}.md"
    if not fp.exists(): return []
    content = fp.read_text(encoding="utf-8")
    m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not m: return []
    try:
        front = yaml.safe_load(m.group(1)) or {}
    except:
        return []
    return front.get("relations", [])

def expand_results(results):
    expanded = list(results)
    for r in results:
        rels = get_relations(r)
        for rel in rels:
            target = rel.get("target", "").strip("[]")
            if target and target not in expanded:
                expanded.append(target)
    return expanded[:15]

if __name__ == "__main__":
    query = sys.argv[1]
    results = qmd_search(query)
    print("=== Direct matches ===")
    for r in results: print(f"  [[{r}]]")
    expanded = expand_results(results)
    new_items = [e for e in expanded if e not in results]
    if new_items:
        print("\n=== Graph-expanded (via relations) ===")
        for e in new_items: print(f"  [[{e}]]")

任务 B-1: 内存生命周期

2.1 Frontmatter 扩展

# 新增可选字段
confidence: 3           # 1-5: 1=推测, 2=单源未验证, 3=已验证, 4=多源一致, 5=无可争议
status: active          # active | superseded | deprecated | tentative | needs-review
superseded_by: "[[新页面]]"  # 仅 status=superseded 时
last_reviewed: 2026-06-30
review_interval_days: 180   # 默认 180 天(技术类建议 90 天)

2.2 时效检测脚本

# tools/scripts/check-staleness.py
"""
扫描 wiki/ 中所有页面的 last_reviewed 字段,
计算是否超过 review_interval_days,输出过时页面列表。
"""
from datetime import date, timedelta
import yaml, re, sys
from pathlib import Path

WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
TODAY = date.today()
DEFAULT_INTERVAL = 180

stale = []
no_review = []

for md_file in sorted(WIKI.glob("*.md")):
    if md_file.name in ("index.md", "log.md"):
        continue
    content = md_file.read_text(encoding="utf-8")
    m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not m: continue
    try:
        front = yaml.safe_load(m.group(1)) or {}
    except:
        continue

    last_str = front.get("last_reviewed", "")
    interval = front.get("review_interval_days", DEFAULT_INTERVAL)

    if not last_str:
        no_review.append(md_file.stem)
        continue

    try:
        last = date.fromisoformat(last_str)
        if (TODAY - last).days > interval:
            stale.append((md_file.stem, last_str, (TODAY - last).days - interval))
    except:
        no_review.append(md_file.stem)

print(f"=== Stale pages (overdue): {len(stale)} ===")
for name, last, overdue in sorted(stale, key=lambda x: -x[2]):
    print(f"  {name}: last reviewed {last}, overdue by {overdue} days")

if no_review:
    print(f"\n=== No review date: {len(no_review)} ===")
    for n in no_review[:20]:
        print(f"  {n}")
    if len(no_review) > 20:
        print(f"  ... and {len(no_review)-20} more")

2.3 Lint 集成

将时效检测和矛盾检测集成到现有的 Lint 工作流:

# tools/scripts/check-staleness.ps1
$env:PYTHONIOENCODING="utf-8"
python tools/scripts/check-staleness.py

矛盾检测逻辑:

def detect_conflicts():
    """扫描所有页面的 relations 字段中的 conflicts_with 对,
    确认双方的声明是否都还引用对方。"""
    conflicts = []
    for md_file in WIKI.glob("*.md"):
        if md_file.name in ("index.md", "log.md"): continue
        content = md_file.read_text(encoding="utf-8")
        m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
        if not m: continue
        try: front = yaml.safe_load(m.group(1)) or {}
        except: continue
        for rel in front.get("relations", []):
            if rel["type"] == "conflicts_with":
                target = rel["target"].strip("[]")
                # Check if target also references us
                conflicts.append((md_file.stem, target))
    return conflicts

2.4 AGENTS.md 规范更新

在 wiki 页面模板(AGENTS.md 第 2.4 节)增加:

# 更新所有模板,增加:
relations:
  # - type: depends_on | supersedes | etc.
  #   target: "[[页面]]"
confidence: 3
status: active
last_reviewed: "{{today}}"
review_interval_days: 180

Phase 2 — 中期自动化(1-2 月)

目标: 事件驱动自动化 + Consolidation tiers

任务 A-2: 事件驱动自动化

1.1 Git hooks 配置

# .githooks/pre-commit
#!/bin/sh
# Pre-commit hook: 验证被修改的 wiki 页面 frontmatter

CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep '^wiki/.*\.md$')
if [ -z "$CHANGED" ]; then exit 0; fi

python tools/scripts/validate-frontmatter.py --files $CHANGED
if [ $? -ne 0 ]; then
    echo "ERROR: Frontmatter validation failed. Commit rejected."
    exit 1
fi
# Config:
git config core.hooksPath .githooks

1.2 validate-frontmatter.py 脚本

# tools/scripts/validate-frontmatter.py
"""
校验 wiki 页面的 frontmatter 完整性(pre-commit 用)。
检查项:
- 必须包含 categories (含 [[LLM Wiki]])
- 必须包含 tags (含 wiki)
- 必须包含 type
- 如果 status != active,必须说明 superseded_by/deprecated 原因
- relations 的 target 必须指向存在的页面
"""

REQUIRED_CATEGORIES = ["[[LLM Wiki]]"]
REQUIRED_TAGS = ["wiki"]
VALID_TYPES = ["concept", "entity", "tool", "reference", "place",
               "institution", "method", "knowledge-card", "synthesis",
               "index", "log", "research-report", "lesson"]

def validate(filepath):
    errors = []
    content = filepath.read_text(encoding="utf-8")
    m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not m: return ["No frontmatter"]
    try: front = yaml.safe_load(m.group(1)) or {}
    except: return ["YAML parse error"]

    cats = front.get("categories", [])
    if not any(c.strip("[]") == "LLM Wiki" for c in cats):
        errors.append("Missing [[LLM Wiki]] in categories")
    tags = front.get("tags", [])
    if "wiki" not in tags:
        errors.append("Missing 'wiki' in tags")
    if not front.get("type"):
        errors.append("Missing type")
    elif front["type"] not in VALID_TYPES:
        errors.append(f"Invalid type: {front['type']}")
    if not front.get("source"):
        errors.append("Missing source")
    for rel in front.get("relations", []):
        target = rel.get("target", "").strip("[]")
        if target and not (WIKI / f"{target}.md").exists():
            errors.append(f"Relation target [[{target}]] not found")
    return errors

1.3 Windows 定时任务

# 安装每周 Lint 定时任务
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File D:\Applications\app\kepano-obsidian-main\tools\scripts\weekly-lint.ps1"

$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 10:00PM

Register-ScheduledTask -TaskName "ObspVault-WeeklyLint" `
    -Action $action -Trigger $trigger -Description "Weekly LLM Wiki health check"
# tools/scripts/weekly-lint.ps1
$env:PYTHONIOENCODING="utf-8"
$logFile = "D:\Applications\app\kepano-obsidian-main\tools\data\lint-report-$(Get-Date -Format 'yyyy-MM-dd').log"

Write-Output "=== Weekly Lint $(Get-Date) ===" > $logFile

# 1. Orphan detection
Write-Output "`n=== Orphans ===" >> $logFile
python tools/scripts/check-orphans.py >> $logFile 2>&1

# 2. Broken links
Write-Output "`n=== Broken Links ===" >> $logFile
& ".\tools\scripts\wiki-lint-broken-v2.ps1" >> $logFile 2>&1

# 3. Staleness
Write-Output "`n=== Staleness ===" >> $logFile
python tools/scripts/check-staleness.py >> $logFile 2>&1

# 4. Discrepancies
Write-Output "`n=== Conflicts ===" >> $logFile
python tools/scripts/detect-conflicts.py >> $logFile 2>&1

Write-Output "`nDone: $(Get-Date)" >> $logFile

1.4 session-end 结晶机制

AGENTS.md 增加自动化指引:

## 结晶机制

每次 LLM 对话结束时,Agent 必须:

1. 检查对话中是否产生了**可复用的知识**(新概念、新的实体信息、经验教训)
2. 如果产生,创建或更新对应的 wiki 页面
3. 在 wiki/log.md 追加条目
4. 执行 `git add + git commit`(自动提交)

### 自动判断标准

| 信号 | 动作 |
|------|------|
| 用户问了从未问过的问题 | 若回答中包含新知识,创建新页面 |
| 引用了外部来源 | 存入 raw/,创建 wiki 页面 |
| 修复了错误或过时信息 | 更新对应页面,设置为 superseded |

任务 B-2: Consolidation tiers

2.1 层级定义

wiki/
├── working/        ← NEW: 临时笔记、对话草稿(TTL 7 天)
├── semantic/       ← 当前 wiki/ 大部分页面
├── procedural/     ← 操作手册、最佳实践、工作流
└── archive/        ← 已有的过时页面归档

迁移规则

来源 → 目标 条件 自动化程度
root level → working/ 任何新建的零散 .md 手动
working/ → semantic/ 有 source 字段 + type + 完成度 > 70% 脚本辅助
semantic/ → procedural/ 面向操作的 knowledge(how-to、配方、操作指南) 手动
active → archive/ status=deprecated OR superseded 自动(Lint 时)

2.2 层级提升脚本

# tools/scripts/promote-knowledge.py
"""
检查 working/ 中的页面,分析是否可以提升到 semantic/。
标准:
- 有完整的 frontmattercategories, tags, type, source
- 有 body 内容(>100 字)
- 创建时间 > 7 天
"""
from datetime import date, timedelta
import re, yaml, shutil
from pathlib import Path

WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")

def check_promotable(filepath: Path) -> tuple[bool, list[str]]:
    """返回 (是否可提升, 原因列表)"""
    content = filepath.read_text(encoding="utf-8")
    m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not m: return False, ["No frontmatter"]
    try: front = yaml.safe_load(m.group(1)) or {}
    except: return False, ["YAML parse error"]

    body = content[m.end():].strip()
    reasons = []
    if not front.get("categories"): reasons.append("Missing categories")
    if not front.get("tags"): reasons.append("Missing tags")
    if not front.get("type"): reasons.append("Missing type")
    if not front.get("source"): reasons.append("Missing source")
    if len(body) < 100: reasons.append("Body too short (<100 chars)")
    return len(reasons) == 0, reasons

Phase 3 — 长期进阶(3-6 月)

目标: 图遍历搜索 + 高级自修正

任务 A-3: 图遍历搜索

1.1 图存储层(轻量级起步)

从 Neo4j 方案改为 Sqlite + JSON 混合方案以降低复杂度:

# tools/scripts/knowledge-graph.py
"""
维护 wiki/ 的 typed relationships 图。
存储:tools/data/knowledge-graph.json + Sqlite 索引
"""

GRAPH_DB = Path(r"D:\Applications\app\kepano-obsidian-main\tools\data\knowledge-graph.json")

def build_graph():
    """重建完整图(从所有 wiki 页面的 relations 字段)"""
    graph = {"nodes": [], "edges": []}
    seen_nodes = set()
    for md_file in sorted(WIKI.glob("*.md")):
        if md_file.name in ("index.md", "log.md"): continue
        content = md_file.read_text(encoding="utf-8")
        m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
        if not m: continue
        try: front = yaml.safe_load(m.group(1)) or {}
        except: continue
        name = md_file.stem
        if name not in seen_nodes:
            graph["nodes"].append({
                "id": name,
                "type": front.get("type", "unknown"),
                "confidence": front.get("confidence", 3)
            })
            seen_nodes.add(name)
        for rel in front.get("relations", []):
            target = rel.get("target", "").strip("[]")
            if not target: continue
            if target not in seen_nodes:
                graph["nodes"].append({
                    "id": target,
                    "type": "unknown",  # will be resolved
                    "confidence": 3
                })
                seen_nodes.add(target)
            graph["edges"].append({
                "source": name,
                "target": target,
                "type": rel.get("type", "related"),
                "confidence": rel.get("confidence", 3)
            })
    GRAPH_DB.write_text(json.dumps(graph, ensure_ascii=False, indent=2), encoding="utf-8")
    return graph

1.2 图遍历 — 证据链查询

def find_evidence_chain(target_page: str):
    """从 target 反向追溯所有 caused_by/supports 关系的源头"""
    graph = json.loads(GRAPH_DB.read_text(encoding="utf-8"))
    adj = {}
    for e in graph["edges"]:
        adj.setdefault(e["target"], []).append((e["source"], e["type"]))
    
    chain = []
    visited = set()
    def dfs(node, depth=0):
        if node in visited or depth > 5: return
        visited.add(node)
        if depth > 0:
            chain.append(("  " * depth) + f"← {node}")
        for src, rel in adj.get(node, []):
            dfs(src, depth + 1)
    
    dfs(target_page)
    return chain

1.3 qmd 集成(graph-expansion 模块)

在 qmd 搜索结果后,用 graph 做扩展:

def graph_expanded_search(query: str) -> list[dict]:
    """融合搜索:qmd BM25/向量结果 → graph 关系扩展 → 去重重排序"""
    direct = qmd_search(query)  # list of page names
    graph = json.loads(GRAPH_DB.read_text(encoding="utf-8"))
    
    # Build node -> adjacent nodes
    adj = {}
    for e in graph["edges"]:
        adj.setdefault(e["source"], []).append((e["target"], e["type"], e["confidence"]))
        adj.setdefault(e["target"], []).append((e["source"], "inverse_" + e["type"], e["confidence"]))
    
    expanded = list(direct)
    for page in direct:
        for target, rel_type, conf in adj.get(page, []):
            if target not in expanded and conf >= 3:
                expanded.append(target)
    
    return [{"page": p, "relevance": "direct" if p in direct else "graph"} for p in expanded]

任务 B-3: 高级自修正

3.1 LLM 评审器

# tools/scripts/review-pages.py
"""
使用本地 LLM(通过 Ollama/llama.cpp)审核 wiki 页面的内容准确性。
可检测:
- 声明与 raw 来源不匹配(数据偏差)
- 声明之间的矛盾(跨页面)
- 过时的信息(last_reviewed 太久)

用法:
    python review-pages.py --stale  # 审查过时页面
    python review-pages.py --random 5  # 随机抽 5 页
"""

import subprocess, json, re, random
from pathlib import Path

OLLAMA_MODEL = "qwen2.5:7b"  # 本地模型

def review_page(page_name: str) -> dict:
    """用 LLM 审核给定页面"""
    fp = Path(r"D:\Applications\app\kepano-obsidian-main\wiki") / f"{page_name}.md"
    content = fp.read_text(encoding="utf-8")
    
    prompt = f"""请审核以下 wiki 页面,输出 JSON 格式的审核结果:
{{
    "accuracy_score": 1-5,
    "issues": ["问题1", "问题2"],
    "confidence_match": true/false,
    "suggested_updates": ["建议1"]
}}

页面内容:
{content[:4000]}  # truncate to fit context
"""
    result = subprocess.run(
        ["ollama", "run", OLLAMA_MODEL, prompt],
        capture_output=True, text=True, timeout=120
    )
    try:
        return json.loads(result.stdout)
    except:
        return {"accuracy_score": 3, "issues": ["Parse failed"], "confidence_match": False}

3.2 矛盾自动标注

当检测到跨页面矛盾时,自动在双方页面添加 callout:

> [!WARNING] 可能矛盾
> 本页声称「A = 3」,但 [[其他页面]] 声称「A = 4」。
> 来源对比:[[raw/source1]]:42  vs [[raw/source2]]:87
> 需要人工复核。

3.3 低置信度自动建议

def auto_suggest_low_confidence():
    """对 confidence <= 2 的页面,自动生成"需要更多证据"提示"""
    for md_file in WIKI.glob("*.md"):
        if md_file.name in ("index.md", "log.md"): continue
        content = md_file.read_text(encoding="utf-8")
        m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
        if not m: continue
        try: front = yaml.safe_load(m.group(1)) or {}
        except: continue
        conf = front.get("confidence", 3)
        if conf is not None and conf <= 2:
            print(f"Suggestion: {md_file.stem} (confidence={conf}) — needs more evidence")

附录:文件清单

新建文件

文件 阶段 用途
tools/scripts/fix-raw-citations.py P0 行号自动标注
tools/scripts/manage-relations.py P1 Typed relationships 管理
tools/scripts/graph-search.py P1 图感知搜索
tools/scripts/check-staleness.py P1 时效检测
tools/scripts/detect-conflicts.py P1 矛盾检测
tools/scripts/validate-frontmatter.py P2 Pre-commit frontmatter 验证
tools/scripts/weekly-lint.ps1 P2 定时 Lint 入口
tools/scripts/promote-knowledge.py P2 层级提升
tools/scripts/knowledge-graph.py P3 图存储和遍历
tools/scripts/review-pages.py P3 LLM 评审器
.githooks/pre-commit P2 Git hook
.githooks/post-merge P2 Git hook
tools/data/knowledge-graph.json P3 图数据库文件

修改文件

文件 阶段 变更
AGENTS.md P0/P1 修复冗余 + 新增 v2 特性定义 + 更新模板
wiki/index.md P0 更新指标数字
wiki/log.md P0/P1/P2 追加每次操作日志

依赖安装

pip install pyyaml  # 已安装,确认版本
# Phase 3 可能需要:
pip install networkx  # 轻量图分析(替代 Neo4j