docs/design/memory-link-design.md
日期: 2026-04-23 状态: Draft 分支: feat/memory_isolation
Memory Link 是在现有记忆体系(profile/preferences/entities/events 等)之上建立的统一关系层,将分散的记忆文件通过有向链接互联。
核心目标:
设计原则:
.relations.json 目录级 sidecar 文件中,content 保持纯净,检索时按需渲染定位: 个人知识大脑,AI agent 在每次响应前读取、对话后写入的持久化知识库。
Skill 执行模型: GBrain = CLI 工具(TypeScript,确定性操作如 search/put/sync/embed)+ Skillpack(29 个 fat markdown 指令文件)。Skill 是指令不是代码——告诉宿主 agent 什么时候调什么 CLI、什么条件下调 LLM,但不含运行时,没有 hook。
gbrain skillpack install --all 将 skill 文件复制到 agent workspace。
flowchart TB
gb_msg[用户对话消息] --> gb_signal[signal-detector\n行为指令,异步调子代理]
gb_meeting[Meeting 转录] --> gb_mtingest[meeting-ingestion skill]
gb_media[链接/文章/视频/Email] --> gb_ingest[idea-ingest / media-ingest / email-to-brain]
gb_git[Git 仓库] --> gb_sync[gbrain sync\n增量导入]
gb_signal --> gb_write
gb_mtingest --> gb_write
gb_ingest --> gb_write
gb_sync --> gb_write
subgraph gb_write[写入操作(CLI)]
gb_page[put_page\n覆写 compiled truth\n+ auto-link hook 写 links]
gb_addlink[add_link\nLLM 手动建链]
gb_timeline[add_timeline_entry\n追加 timeline]
end
gb_page --> gb_pages[pages 数据库表\ncompiled_truth + timeline]
gb_page --> gb_links[links 数据库表]
gb_addlink --> gb_links
gb_timeline --> gb_pages
gb_page --> gb_dream[Dream Cycle\n每夜四阶段]
gb_dream --> gb_page2[整理产物\n重编译页面 + 富化实体]
gb_page --> gb_embed[写入时\n分块 + embedding + 全文索引]
gb_links --> gb_search[搜索 + backlink boost]
gb_pages --> gb_search
gb_brainops[brain-ops\n每次响应前查 brain] --> gb_search
style gb_signal fill:#ff9,stroke:#333
style gb_dream fill:#cfc,stroke:#333
style gb_brainops fill:#9cf,stroke:#333
输入源:
| 输入 | 机制 | 负责 Skill |
|---|---|---|
| 对话消息 | CLAUDE.md / RESOLVER.md 写入"signal-detector on every inbound message",用户每条消息触发子代理异步执行:①检测原创想法→brain/originals/(原文逐字记录)②gbrain search检测实体引用→已有页面追加 timeline / 无页面则创建③回链铁律:实体页面补写回链④gbrain sync | signal-detector |
| Meeting 转录 | 拉取完整转录(非 AI 摘要),创建会议页,传播到所有参会者/公司的 timeline,双向建链 | meeting-ingestion |
| 链接/文章/Tweet | 用户分享链接 → 创建页面 + 作者人物页 + 交叉链接 | idea-ingest |
| 视频/音频/PDF | 转录 → 实体提取 → 回链传播 | media-ingest |
| 确定性脚本拉取邮件 → LLM 判断实体/行动项 → 更新 brain 页面 | email-to-brain recipe | |
| Git 仓库 | gbrain sync 增量导入变更文件,逐文件走 put_page | CLI |
对话读取机制(brain-ops,行为指令):每次响应前先查 brain(gbrain search / gbrain get),形成 read-enrich-write 循环。同样是 LLM 行为指令,非程序级 hook。
数据模型: pages 表存储页面,每个页面由 --- 分隔为两部分:
---
title: Pedro Franceschi
type: person
tags: [ceo, brex]
---
Pedro is CEO of Brex. Previously co-founded Cognito.
---
- 2026-04-10 | Presented Q1 numbers [Source: board meeting, 2026-04-10]
- 2026-04-05 | Met for coffee, discussed Series D [Source: conversation, 2026-04-05]
--- 上方):agent 对该实体的当前认知摘要,每次更新整体覆写。类似维基条目的当前版本--- 下方):带日期 + source attribution 的证据链,只追加不覆写。类似 git log,记录"谁在什么时候说了什么"核心事实(基于 GBrain v0.12 源代码分析):
put_page 覆写add_timeline_entry 写入更新路径:
| 路径 | 触发 | 计算方式 | 说明 |
|---|---|---|---|
| 1. 实时更新 | 用户对话中 agent 主动调用 | LLM | agent 读取现有 compiled_truth + timeline,重新生成新 compiled_truth,调用 put_page 覆写 |
| 2. stale 页面更新 | Dream Cycle 未实现 | LLM(未实现) | 设计文档提到的 "T+1 compile" 概念,代码中未实现 |
stale-link reconciliation:
put_page 时自动移除不再存在的链接(基于内容重新提取)写入方式(两条路径,都会写 links 表):
路径 A:put_page 内 auto-link hook(runAutoLink,零 LLM,每次 put_page 自动执行,gbrain sync 也会触发):
Entity-ref 提取:从 compiled_truth 中提取 Markdown 链接([张三](../people/zhangsan.md))和裸 slug 引用(people/zhangsan),自动剥离 code fence 避免代码块误提取
类型推断级联:对 compiled_truth 做正则匹配 + 页面元数据启发式,推断 link_type(零 LLM)。从强到弱级联,同一对页面(from_slug, to_slug)只保留一条边、优先级最高的类型:
| 内容模式 | link_type | 示例 |
|---|---|---|
"founded"/"co-founded" | founded | "Alice founded Acme AI" → founded |
"invested in" | invested_in | "Bob invested in Acme AI" → invested_in |
"advises"/"advisor" | advises | "Carol advises Acme AI" → advises |
"CEO of"/"CTO of" 等 | works_at | "Alice, CEO of Acme AI" → works_at |
| 会议页(type=meeting)+ 人物引用 | attended | 会议页提到 [Alice](people/alice) → attended |
| partner-bio 语言 | invested_in | VC 机构人物页提到某公司 → invested_in |
Within-page 去重:同一页面内指向同一目标的多个引用只保留一条链接
路径 B:LLM agent 手动调用 add_link:meeting-ingestion 等 skill 指令调 add_link CLI。两页面间只有一条边,重复调用覆盖
| # | 路径 | 触发 | 处理摘要 |
|---|---|---|---|
| 1 | put_page | CLI / MCP / Agent | 解析 frontmatter → SHA256 去重 → 分块(300词) → embedding → 旧值快照 → 覆写 pages → 对账 tags → runAutoLink(auto-link hook,写 links 表 + stale-link reconciliation) |
| 2 | gbrain sync | CLI / cron / --watch | git pull → git diff → 按 A/M/D/R 分类 → 逐文件走 put_page |
| 3 | add_link | LLM agent 按 skill 指令调用 | INSERT ON CONFLICT DO UPDATE(两页面间一条边) |
| 4 | add_timeline_entry | LLM agent 按 skill 指令调用 | 参数:slug + date(YYYY-MM-DD,严格校验)+ summary + detail(可选)+ source(可选)→ 追加单条 timeline_entries 记录,不动 compiled_truth;DB trigger 自动刷新 search_vector(timeline 内容参与全文搜索)和 updated_at |
| 5 | extract links | 首次启用 / 版本升级 | 遍历所有页面 → 提取实体引用 → 类型推断级联 → 写入 links。支持 --since 增量、--dry-run |
| 6 | check-backlinks | CLI / maintain skill | 正则扫描正文 Markdown 链接 → check 报告缺失 / fix 补写 timeline |
Dream Cycle,通过 cron + skills 实现的 6 阶段维护管道(从 GBrain v0.12 源代码分析得出):
| 阶段 | 输入 | 计算方式 | 输出 |
|---|---|---|---|
| Phase 1: Lint | 所有页面 | CLI(无 LLM) | lint 报告,检查缺失字段等结构问题 |
| Phase 2: Backlinks | 所有页面 | CLI(无 LLM) | 检查回链完整性,调用 check-backlinks fix |
| Phase 3: Sync | Git 仓库 | CLI(无 LLM) | gbrain sync 拉取并导入变更 |
| Phase 4: Extract | 新增/变更页面 | CLI + auto-link post-hook(零 LLM) | 调用 gbrain extract links 批量提取链接 |
| Phase 5: Embed | 嵌入过期的页面 | CLI(无 LLM) | gbrain embed --stale 重新计算向量 |
| Phase 6: Orphans | 孤立实体 | CLI(无 LLM) | 识别无入边的实体页面 |
核心事实:
put_page 时自动执行,Extract 阶段用于批量回填历史页面1. Query → 意图分类器(entity/temporal/event/general)
2. 多查询扩展(llm 生成 2 个query + 原始query 共3路)
3. 向量搜索(HNSW cosine)+ 关键词搜索(tsvector)→ RRF 融合
4. compiled truth boost: compiled_truth 权重是timeline的两倍
5. backlink boost: r.score *= (1.0 + 0.05 * Math.log(1 + count)) 被更多页面链接的实体分数更高(P@5 +5.4pts,Recall@5 +11.5pts)
6. cosine 重评分: const blended = 0.7 * normRrf + 0.3 * cosine
7. 去重逻辑:
(1)同一页面只保留top3 chunks
(2)Jaccard相似度大于 0.85 trunk去重。
(3)同一页面类型不超过结果60% (比如 person)
(4)同一页面只保留top2 chunks
Backlink boost:被更多页面链接的实体排名更高。BrainBench v1(240 页):P@5 +5.4pts,Recall@5 +11.5pts。
基于链接提供了图遍历的CLI供模型调用(基于postgresql实现)
例 1:谁在 Acme 工作?(入边 + 类型过滤)
gbrain graph-query companies/acme --type works_at --direction in
例 2:谁投资了 Acme?(入边 + 不同类型)
gbrain graph-query companies/acme --type invested_in --direction in
例 3:两跳关系——Alice 通过会议间接见过谁?
gbrain graph-query people/alice --type attended --depth 2
提供了BrainBench 用于评测 https://github.com/garrytan/gbrain-evals
定位: 开源自托管 AI 个人助理平台,插件架构,memory 是一个特殊 plugin slot。
输入源: 对话(memory-core 和 memory-wiki 分别消费对话)。
memory-wiki 为独立于memory-core的记忆加工与存储模块
memory-core 在线写入(攒批提取): 对话达到阈值时 LLM flush 写入,生成每日笔记 history/date.md。SQLite 索引做搜索。
memory-wiki 在线写入(工具调用写入): LLM 在对话中调用 wiki_apply 工具,写入带 Claims 的 wiki 页面(YAML frontmatter)。memory-wiki 是独立子系统,通过跨插件资源同步从 memory-core 导入整理产物(MEMORY.md、history/date.md)作为资源输入。
OpenClaw 最核心的设计是 Claims 体系,将页面中的事实声明结构化,支持可信度评估和矛盾检测。
Claim 结构: 每个页面可包含多个 claim,每个 claim 有:
text:声明内容(如 "Alpha uses PostgreSQL")status:supported / contested / contradicted / refuted / supersededconfidence:0~1 可信度evidence:证据数组,每条证据指向源页面 + 行号范围 + 权重三层链接机制:
id 出现在不同页面时,自动聚类检测矛盾矛盾检测与健康评估: 按 claim ID 聚类检测矛盾,按更新时间计算 freshness(fresh < 30天 / aging 30-89天 / stale ≥ 90天)。Dashboard 展示 Open Questions、Contradictions、Low Confidence 等报告。
flowchart TB
subgraph oc_core[memory-core 插件]
direction TB
oc_in[对话] --> oc_flush[攒批提取\n对话达到阈值时 flush]
oc_flush --> oc_daily[每日笔记\nhistory/date.md]
oc_in --> oc_search[memory_search\n副作用记录 recall]
oc_search --> oc_recall[recall 跟踪 JSON]
oc_daily --> oc_dream1[定时整理 Phase 1 Light\n每 6h 规则切片]
oc_recall --> oc_dream1
oc_dream1 --> oc_dream2[Phase 2 Deep\n每日 3AM\n6维评分提升]
oc_dream2 --> oc_dream3[Phase 3 REM\n每周日\n关键词频率标记主题]
oc_dream3 --> oc_memory[整理产物\nMEMORY.md\nhistory/date.md]
end
subgraph oc_wiki_plugin[memory-wiki 插件 — 3 个数据来源]
direction TB
oc_src1[来源1: wiki_apply\nLLM 工具调用写入] --> |syntheses/ 页面| oc_compile[wiki 编译器\n全量编译\n零LLM]
oc_src2[来源2: 跨插件资源同步\nMEMORY.md + history/date.md] --> |sources/ 页面| oc_compile
oc_src3[来源3: wiki.ingest\n本地文件导入] --> |sources/ 页面| oc_compile
oc_compile --> oc_related[页面 Related 块\nSources / Referenced By]
oc_compile --> oc_dashboard[5 个 Dashboard\n矛盾 / 低置信度 / 过期]
oc_compile --> oc_digest[agent-digest.json\n+ claims.jsonl]
oc_compile --> oc_index[6 个索引页]
end
oc_memory -.-> oc_src2
style oc_compile fill:#ff9,stroke:#333
style oc_src1 fill:#9f9,stroke:#333
style oc_src2 fill:#9cf,stroke:#333
style oc_src3 fill:#9cf,stroke:#333
style oc_dream1 fill:#cfc,stroke:#333
style oc_dream2 fill:#fcf,stroke:#333
style oc_dream3 fill:#ccf,stroke:#333
memory core部分
Phase 1 Light Sleep 规则切片示例:
输入 history/2026-04-05.md:
## 运维
- 重启了网关,auth 偏移导致的。
- Token 现在对齐了。
## 小王
- 她喜欢直接定时间,不喜欢"改天再说"。
- 最好给一个具体时间段。
切片产出(写入 short-term-recall.json):
| chunk | 行号 | snippet | 分数 |
|---|---|---|---|
| 1 | 2-3 | 运维: 重启了网关,auth 偏移导致的。; Token 现在对齐了。 | 0.62(硬编码) |
| 2 | 6-7 | 小王: 她喜欢直接定时间,不喜欢"改天再说"。; 最好给一个具体时间段。 | 0.62(硬编码) |
切片规则:按 ## heading 分组 → 列表项用 ; 拼接 → 每组最多 4 行/280 字符 → 不调 LLM → 分数硬编码(daily=0.62, session=0.58)
Phase 2 Deep Sleep 评分提升示例:
假设"小王"这条 snippet 在接下来几天被搜索召回了 4 次,跨 3 天、2 个不同 query:
| 维度 | 权重 | 值 | 加权 |
|---|---|---|---|
| frequency | 0.24 | log1p(4)/log1p(10) = 0.66 | 0.158 |
| relevance | 0.30 | avgScore = 0.78 | 0.234 |
| diversity | 0.15 | max(2,3)/5 = 0.60 | 0.090 |
| recency | 0.15 | exp(-ln2/14 × 2) = 0.91 | 0.137 |
| consolidation | 0.10 | max(spread=0.55, grounded=0) = 0.55 | 0.055 |
| conceptual | 0.06 | 2tags/6 = 0.33 | 0.020 |
| 总分 | 0.694 |
门控条件:signalCount(4) ≥ 3 ✓ | diversity(3) ≥ 3 ✓ | score(0.694) < 0.8 ✗ → 未通过,不提升
若后续又被召回 2 次(共 6 次),分数升至 0.86,则通过门控,追加到 MEMORY.md:
## Promoted From Short-Term Memory (2026-04-08)
<!-- openclaw-memory-promotion:memory:memory/2026-04-05.md:6:7:a1b2c3d4e5f6 -->
- 小王: 她喜欢直接定时间,不喜欢"改天再说"。; 最好给一个具体时间段。
[score=0.86 recalls=6 avg=0.80 source=memory/2026-04-05.md:6-7]
Phase 3 REM Sleep 关键词频率标记主题示例:
关键词来源:对每个 snippet 做规则分词(词表匹配 + 复合 token 正则 + Intl.Segmenter 分词),过滤停用词和太短的词,最多 8 个关键词。不调 LLM。
假设 short-term-recall.json 中有 20 条 snippet,关键词频率统计:
| 关键词 | 出现次数 | 总条目数 | strength | 标记主题? |
|---|---|---|---|---|
| 网关 | 3 | 20 | min(1, 3/20×2) = 0.30 | ✗ (< 0.75) |
| 定时间 | 12 | 20 | min(1, 12/20×2) = 1.0 | ✓ |
超过阈值(0.75)的关键词输出为标记主题:
### 标记主题
- Theme: `定时间` kept surfacing across 12 memories.
- confidence: 1.0
- evidence: memory/2026-04-05.md:6-7, memory/2026-04-06.md:2-3, ...
### Possible Lasting Truths
- 小王: 她喜欢直接定时间。[confidence=0.72 evidence=memory/2026-04-05.md:6-7]
标记主题不直接提升记忆,只是发现高频关键词;candidate truths 列出的条目会被 REM 写入 phase-signals.json(remHits += 1),后续 Deep Sleep 评分时通过 phaseBoost(0.09 × remStrength × remRecency)间接加分,仍需通过 6 维评分门控才能提升到 MEMORY.md。注意这不是关键词共现分析,只是单个关键词的频率统计。
memory core最终输出的为MEMORY.md及history/2026-04-26.md
memory-wiki 部分 — 3 个数据来源,写入后均触发全量编译
来源1: wiki_apply(LLM 工具调用写入)
触发:LLM 在对话中调用 wiki_apply 工具
输入:JSON,包含 op + title + body + sourceIds + claims + contradictions + questions
处理:写入 wiki 页面(YAML frontmatter + markdown body)→ 触发全量编译
输出:wiki 页面 .md 文件 + 编译产物
wiki_apply 只支持两种操作:create_synthesis(写入 syntheses/xxx.md)和 update_metadata(更新已有页面 frontmatter)。不支持创建 entity/concept 页面。sourceIds 和 claims 均由 LLM 填写,编译器只做被动 Map 查找。
输入示例(LLM 发送的工具调用):
{
"op": "create_synthesis",
"title": "小王运维手册",
"body": "小王负责生产环境 Kubernetes 集群的日常运维,包括滚动升级和故障排查。",
"sourceIds": ["source.小王周报", "source.oncall-log"],
"claims": [
{
"id": "claim.小王.k8s",
"text": "小王是生产环境 K8s 集群的一线运维负责人",
"status": "supported",
"confidence": 0.92,
"evidence": [{"sourceId": "source.小王周报", "lines": "3-7", "weight": 0.9}]
}
],
"contradictions": ["与旧版值班表冲突"],
"questions": ["小王是否还负责测试环境?"],
"confidence": 0.8
}
输出示例(写入 syntheses/小王运维手册.md):
---
pageType: synthesis
id: synthesis.小王运维手册
title: 小王运维手册
sourceIds:
- source.小王周报
- source.oncall-log
claims:
- id: claim.小王.k8s
text: 小王是生产环境 K8s 集群的一线运维负责人
status: supported
confidence: 0.92
evidence:
- sourceId: source.小王周报
lines: 3-7
weight: 0.9
contradictions:
- 与旧版值班表冲突
questions:
- 小王是否还负责测试环境?
confidence: 0.8
updatedAt: "2026-04-26T10:00:00.000Z"
---
# 小王运维手册
## Summary
<!-- openclaw:wiki:generated:start -->
小王负责生产环境 Kubernetes 集群的日常运维,包括滚动升级和故障排查。
<!-- openclaw:wiki:generated:end -->
## Notes
<!-- openclaw:human:start -->
<!-- openclaw:human:end -->
页面结构:YAML frontmatter 存结构化数据(claims/contradictions/questions/confidence/sourceIds),正文分 Summary(编译器管理)和 Notes(人类可编辑)两个 managed block。
来源2: 跨插件资源同步(memory-core → memory-wiki)
触发:几乎每次 wiki 操作前先跑
输入:memory-core 的整理产物(MEMORY.md、history/date.md)
处理:按 mtime + size 做增量跳过 → 写入 sources/ 页面 → 判断是否需要编译
输出:sources/*.md 页面 + 可能触发全量编译
同步是纯文件搬运,不对内容做任何提取或加工。原始内容整体放入 ## Content 代码块,frontmatter 记录元信息。
同步示例:
输入:memory-core 的 MEMORY.md(mtime 或 size 变化)
产出:写入 sources/bridge-workspace-a1b2c3d4-memory-ef567890.md:
---
pageType: source
id: source.bridge.workspace-a1b2c3d4.memory-ef567890
title: "Memory Bridge (main): memory / MEMORY"
sourceType: memory-bridge
sourcePath: /absolute/path/to/workspace/MEMORY.md
bridgeRelativePath: MEMORY.md
bridgeWorkspaceDir: /absolute/path/to/workspace
bridgeAgentIds:
- main
updatedAt: "2026-04-26T12:00:00.000Z"
---
# Memory Bridge (main): memory / MEMORY
## Bridge Source
- Workspace: `/absolute/path/to/workspace`
- Relative path: `MEMORY.md`
- Updated: 2026-04-26T12:00:00.000Z
## Content
```markdown
<MEMORY.md 的原始内容>
增量跳过逻辑:读取 `.openclaw-wiki/source-sync.json`,比对 mtime + size + renderFingerprint,三项都没变则跳过写入。
来源3: wiki.ingest(本地文件导入)
触发:CLI / gateway 调用
输入:本地文件路径 + 可选 title
处理:读取文件 → 断言 UTF-8 文本 → slug 化标题 → 写入 sources/ 页面 → 触发全量编译
输出:sources/*.md 页面
纯文件搬运,不对内容做任何提取或 LLM 调用。产出格式与 bridge 类似,但 frontmatter 用 sourceType: local-file,正文用 ## Source 块记录文件元信息 + ## Content 块放原始内容。
全量编译 compileMemoryWikiVault(全程零 LLM)
触发:每次写入后自动触发 / CLI 命令 / gateway 调用
输入:所有 wiki 页面
处理:全量读所有页面 → 计算每个页面的 Related 块 → 矛盾检测 + freshness 评估 → 生成 5 个 Dashboard → 生成 agent-digest.json + claims.jsonl → 生成索引页
输出:页面 ## Related 块 + 5 个 Dashboard 页面 + agent-digest.json + claims.jsonl + index.md + 分组 index.md
编译示例(3 个页面):
输入页面:
| 页面 | kind | sourceIds | claims |
|---|---|---|---|
sources/小王周报.md | source | — | — |
syntheses/小王运维手册.md | synthesis | source.小王周报 | claim.小王.k8s: "小王是 K8s 一线运维" (supported, 0.92) |
syntheses/运维排班.md | synthesis | source.小王周报 | claim.小王.k8s: "小王已不负责 K8s,转交小李" (contested, 0.6) |
编译产出 1 — Related 块(注入各页面):
syntheses/小王运维手册.md 获得:
## Related
<!-- openclaw:wiki:related:start -->
### Sources
- [小王周报](sources/小王周报.md)
### Related Pages
- [运维排班](syntheses/运维排班.md)
<!-- openclaw:wiki:related:end -->
sources/小王周报.md 获得:
## Related
<!-- openclaw:wiki:related:start -->
### Referenced By
- [小王运维手册](syntheses/小王运维手册.md)
- [运维排班](syntheses/运维排班.md)
<!-- openclaw:wiki:related:end -->
Related 块计算逻辑:Sources = 当前页 sourceIds 指向的页面(Map 查找);Referenced By = sourceIds 包含当前页 id 的页面 + [[wikilink]] 指向当前页的页面;Related Pages = 共享 sourceIds 但不在前两者中的页面。全量遍历,非增量。
编译产出 2 — 矛盾检测(按 claim ID 聚类):
同一 claim ID claim.小王.k8s 出现在 2 个页面,且文本不同 + 状态不同 → 形成矛盾聚类:
key: "claim.小王.k8s"
entries: [运维排班(contested, stale), 小王运维手册(supported, fresh)]
矛盾检测全部依赖 LLM 填写的 claim id 和 contradictions 字段:claim 级矛盾按 claim.id 分组(同一 id 在 ≥2 页且 text/status 不同),页面级矛盾按 contradictions 文本归一化分组。编译器只做被动聚类,不调 LLM。
编译产出 3 — freshness 评估(按 updatedAt 天数):
| 页面 | updatedAt | 天数 | level |
|---|---|---|---|
| 小王运维手册 | 2026-04-26 | 0 | fresh |
| 运维排班 | 2026-01-05 | 111 | stale (≥90) |
编译产出 4 — Dashboard 页面:
reports/contradictions.md:
# Contradictions
- Competing claim clusters: 1
### Claim Clusters
- `claim.小王.k8s`: 运维排班 -> contested, stale | 小王运维手册 -> supported, fresh
reports/stale-pages.md:
# Stale Pages
- [运维排班](syntheses/运维排班.md): stale (2026-01-05)
编译产出 5 — agent-digest.json(元数据摘要):
.openclaw-wiki/cache/agent-digest.json:
{
"generatedAt": "2026-04-26T10:30:00.000Z",
"stats": {
"totalPages": 3,
"sources": 1,
"syntheses": 2,
"entities": 0,
"concepts": 0,
"totalClaims": 2,
"contradictionClusters": 1,
"openQuestions": 1
},
"pages": [
{
"path": "syntheses/小王运维手册.md",
"title": "小王运维手册",
"kind": "synthesis",
"id": "synthesis.小王运维手册",
"updatedAt": "2026-04-26T08:00:00.000Z",
"freshness": "fresh",
"claimCount": 1,
"questionCount": 1,
"contradictionCount": 1,
"sourceIds": ["source.小王周报.2026w17"],
"topClaims": [
{
"id": "claim.小王.k8s",
"text": "小王是 K8s 一线运维负责人",
"status": "supported",
"confidence": 0.92
}
]
},
{
"path": "syntheses/运维排班.md",
"title": "运维排班",
"kind": "synthesis",
"id": "synthesis.运维排班",
"updatedAt": "2026-01-05T14:00:00.000Z",
"freshness": "stale",
"claimCount": 1,
"questionCount": 0,
"contradictionCount": 1,
"sourceIds": ["source.小王周报.2026w17"],
"topClaims": [
{
"id": "claim.小王.k8s",
"text": "小王主要负责测试环境维护",
"status": "contested",
"confidence": 0.65
}
]
},
{
"path": "sources/小王周报.md",
"title": "小王周报",
"kind": "source",
"id": "source.小王周报.2026w17",
"updatedAt": "2026-04-22T18:00:00.000Z",
"freshness": "aging",
"claimCount": 0,
"questionCount": 0,
"contradictionCount": 0,
"sourceIds": [],
"topClaims": []
}
],
"claimHealth": {
"contradictionClusters": [
{
"key": "claim.小王.k8s",
"type": "claim-id",
"entries": [
{
"path": "syntheses/运维排班.md",
"status": "contested",
"freshness": "stale"
},
{
"path": "syntheses/小王运维手册.md",
"status": "supported",
"freshness": "fresh"
}
]
}
]
}
}
读路径(全程零 LLM)
wiki_search(wiki 内搜索)
触发:LLM 调用 wiki_search 工具 / CLI / gateway
输入:query 字符串 + corpus(wiki/memory/all)
处理:先读 agent-digest.json 做元数据预筛选 → 对候选页面全文本搜索评分
输出:排序结果列表
评分规则(纯数值,不调 LLM):
| 匹配位置 | 加分 |
|---|---|
| title 精确匹配 | +50 |
| title 包含 | +20 |
| claim text 包含 | +25 |
| sourceId 包含 | +12 |
| 正文出现(每次) | +1(上限 10) |
| claim confidence | +0~10 |
| freshness | fresh +8 / aging +4 / stale -2 |
搜索结果示例:
{
"corpus": "wiki",
"path": "syntheses/小王运维手册.md",
"title": "小王运维手册",
"kind": "synthesis",
"score": 53,
"snippet": "小王是 K8s 一线运维负责人",
"id": "synthesis.小王运维手册",
"updatedAt": "2026-04-26T08:00:00.000Z"
}
wiki_lint(结构检查)
触发:CLI / gateway / tool 调用
输入:所有 wiki 页面
处理:编译先跑一遍 → 然后逐页检查 16 种问题
输出:reports/lint.md
16 种检查全部是纯字段过滤,不调 LLM:
| 类别 | 代码 | 严重度 | 检测什么 |
|---|---|---|---|
| 结构 | missing-id / duplicate-id / missing-page-type / page-type-mismatch / missing-title | error | frontmatter 完整性和一致性 |
| 溯源 | missing-source-ids / missing-import-provenance / claim-missing-evidence | warning | 页面和 claim 的来源可追溯性 |
| 链接 | broken-wikilink | warning | [[wikilink]] 目标不存在 |
| 矛盾 | contradiction-present / claim-conflict | warning | 页面级矛盾声明 + claim ID 聚类冲突 |
| 问题 | open-question | warning | 页面有未解决问题 |
| 质量 | low-confidence / claim-low-confidence / stale-page / stale-claim | warning | 低置信度 + 过时(stale ≥ 90天) |
产出示例(reports/lint.md):
# Lint Report
- Errors: 0
- Warnings: 3
### Contradictions
- `syntheses/小王运维手册.md`: Claim cluster `claim.小王.k8s` has competing variants across 2 pages.
### Quality Follow-Up
- `syntheses/运维排班.md`: Claim `claim.小王.k8s` is missing structured evidence.
- `syntheses/运维排班.md`: Page freshness is stale (2026-01-05).
Prompt Section(agent digest 注入 LLM 上下文)
触发:每次 LLM 调用前构建 prompt 时(需 includeCompiledDigestPrompt=true,默认关闭)
输入:.openclaw-wiki/cache/agent-digest.json
处理:按 claim/question/contradiction 数量对页面排序 → 取 top 4 页面 → 每页取 top 2 claims → 拼接摘要
输出:注入 LLM system prompt 的 wiki 概要
LLM 看到的内容:
## Compiled Wiki
Use the wiki when the answer depends on accumulated project knowledge.
Workflow: wiki_search first, then wiki_get for the exact page.
## Compiled Wiki Snapshot
Compiled wiki currently tracks 2 claims across 1 high-signal pages.
Contradiction clusters: 1.
- 小王运维手册: synthesis, 1 claims, 1 open questions, 1 contradiction notes
- 小王是 K8s 一线运维负责人 (status supported, confidence 0.92, freshness fresh)
- 小王是否还负责测试环境? (open question)
本质:把编译产物的摘要注入 LLM 上下文,让 LLM 知道 wiki 里有什么,决定是否调 wiki_search/wiki_get 获取详情。
关键取舍:
id 的一致性(无 id 的 claim 无法跨页关联),而 claim id 由 LLM 填写,一致性无保证定位: 桌面应用(Tauri + React),实现 Karpathy LLM Wiki 模式——LLM 增量构建并维护持久化 wiki,而非每次查询重新推导。
架构: 原始素材(不可变)→ wiki(LLM 生成的页面)→ schema(配置 wiki 结构)
输入源: 两种方式并存:
wiki/queries/ 页面flowchart TB
nw_in[资源\n文件/网页/Deep Research] --> nw_ingest[知识提取\n两步 LLM 链式推理]
nw_ingest --> nw_page[页面\n含 wikilink 正文]
nw_chat[用户对话\n与 LLM 聊天] --> nw_save[Save to Wiki\n手动点击保存]
nw_save --> nw_query_page[wiki/queries/\n单条回复页面]
nw_query_page --> nw_page
nw_page --> nw_enrich[enrichWithWikilinks\nLLM 补充链接]
nw_page --> nw_graph[按需图构建\n聊天/图视图触发]
nw_enrich --> nw_graph
nw_graph --> nw_relevance[4 信号相关性\n在线计算不持久化]
nw_page --> nw_lint[Lint 检查\n孤立 / 死链 / 矛盾]
style nw_ingest fill:#fcf,stroke:#333
style nw_graph fill:#ccf,stroke:#333
style nw_save fill:#ffc,stroke:#333
1. 知识提取(两步 LLM)
第一步分析:读取源内容 + wiki/index.md + wiki/purpose.md,输出结构化分析(关键实体/关键概念/核心论点/矛盾/建议)
第二步生成:把第一步分析作为上下文(不解析,直接传文本),输出的是纯文本,格式是多个块依次拼接:
---FILE: wiki/path/to/page.md--- 开头、---END FILE--- 结尾的块,中间是完整的 Markdown 内容(含 frontmatter)---REVIEW: type | Title--- 开头、---END REVIEW--- 结尾的块,标注需要人工确认的项生成的页面通常包括:sources/(源摘要)+ entities/(实体页)+ concepts/(概念页)+ index.md(索引)+ log.md(日志)+ overview.md(概览)
SHA256 缓存去重,不变的素材不重复处理
具体例子:
假设导入一份 acme-k8s-migration.pdf(Acme 公司的 Kubernetes 迁移报告):
第一步分析输出:
## 关键实体
- Alice Smith (人物,核心,技术负责人)
- Acme Corp (组织,核心,迁移实施方)
- Kubernetes (产品,边缘,已有页面可能存在)
## 关键概念
- 微服务架构:将应用拆分为独立服务的方法,本报告核心
- CI/CD 流水线:自动化构建测试部署流程
## 核心论点
- Acme Corp 从单体应用迁移到微服务,使用 Kubernetes
- 部署时间减少 70%
- 证据:报告中提供的迁移前后部署指标对比
## 矛盾
- 无外部矛盾
- 内部张力:部署变快但运维复杂度上升
## 建议
- 创建 Alice Smith 和 Acme Corp 的实体页
- 创建微服务架构概念页(如果不存在)
- 更新 overview 加入这个迁移案例
第二步生成输出:
---FILE: wiki/sources/acme-k8s-migration.md---
---
type: source
title: "Source: Acme K8s Migration Report"
created: 2026-04-28
updated: 2026-04-28
tags: [migration, kubernetes]
sources: ["acme-k8s-migration.pdf"]
---
# Source: Acme K8s Migration Report
## 摘要
Acme Corp 成功从单体应用迁移到基于 Kubernetes 的微服务架构,由 Alice Smith 主导。
## 关键发现
- 部署时间减少 70%
- 运维复杂度有所上升
---END FILE---
---FILE: wiki/entities/alice-smith.md---
---
type: entity
title: "Alice Smith"
created: 2026-04-28
updated: 2026-04-28
tags: [engineer, migration-lead]
related: [[acme-corp]]
sources: ["acme-k8s-migration.pdf"]
---
# Alice Smith
## 角色
Acme Corp 技术负责人,主导微服务迁移。
## 背景
推动 Acme Corp 从单体迁移到 Kubernetes 的核心人物。
[[acme-corp]]
---END FILE---
---FILE: wiki/entities/acme-corp.md---
---
type: entity
title: "Acme Corp"
created: 2026-04-28
updated: 2026-04-28
tags: [company, technology]
related: [[alice-smith]]
sources: ["acme-k8s-migration.pdf"]
---
# Acme Corp
## 简介
一家最近完成微服务迁移的科技公司。
## 关键行动
- 2026: 从单体迁移到 Kubernetes 微服务
[[alice-smith]]
---END FILE---
---FILE: wiki/concepts/microservices-architecture.md---
---
type: concept
title: "微服务架构"
created: 2026-04-28
updated: 2026-04-28
tags: [architecture, cloud]
related: [[kubernetes]]
sources: ["acme-k8s-migration.pdf"]
---
# 微服务架构
## 定义
将应用开发为一组小型独立服务,通过 API 通信。
## 权衡
- 部署更快
- 运维复杂度上升
---END FILE---
---FILE: wiki/index.md---
---
type: overview
title: "Wiki Index"
created: 2026-04-01
updated: 2026-04-28
---
# Wiki Index
## 实体
- [[alice-smith]]
- [[acme-corp]]
## 概念
- [[microservices-architecture]]
---END FILE---
---REVIEW: duplicate | Kubernetes 可能已存在---
分析提到 Kubernetes,但不确定是否已有页面,建议检查后再决定是否创建。
OPTIONS: Create Page | Skip
SEARCH: kubernetes 容器编排 | 微服务 kubernetes 部署
---END REVIEW---
2. enrichWithWikilinks(链接补充)
[[wikilink]]3. 按需图构建
[[wikilink]] → 构建图结构 → 缓存在内存(模块级变量)cachedGraph.dataVersion === 当前 dataVersion 则直接用缓存,否则重新构建calculateRelevance 函数在线实时计算(不缓存)4 信号相关性:
| 信号 | 权重 | 来源 |
|---|---|---|
| 直接链接 | 3.0 | [[wikilink]] 语法 |
| 源文件重叠 | 4.0 | frontmatter sources: [] 共享 |
| Adamic-Adar | 1.5 | 共同邻居,按 1/log(度) 加权 |
| 类型亲和 | 1.0 | 同类型页面加分 |
4. 检索
| 步骤 | 说明 |
|---|---|
| 分词搜索 | 本地文件扫描,分数计算:文件名精确匹配(+200)> 标题包含短语(+50)> 正文包含短语(每次+20,最多10次)> 标题token匹配(每个+5)> 正文token匹配(每个+1) |
| 向量搜索 | 可选(LanceDB),返回 top10 语义相似结果 |
| RRF 融合 | `fused(p) = 1/(60 + token_rank) + 1/(60 + vector_rank) |
| 去重排序 | 按 RRF 分数降序,同分按路径字母顺序,取前 20 条 |
5. Lint 检查
无定时整理:所有知识产出依赖知识提取时一次性完成,没有后续的矛盾发现、过时更新或跨页面整合
关键取舍:
[[wikilink]]),slug 变更时链接失效定位: 开源 CLI 工具,将原始文档编译成结构化、相互链接的 wiki 风格知识库。受 Andrej Karpathy "LLM Wiki" 想法启发——LLM 自动生成摘要、概念页和交叉引用,让知识随时间积累,而非每次查询重新推导(传统 RAG 的做法)。
输入源: 文件导入(PDF、docx、pptx、md 等),或文件系统监控(watchdog 自动处理 raw/ 目录新增文件)。
架构: 文档 → 转换 → 编译(多步 LLM 管线)→ wiki 页面(summaries + concepts + index)
flowchart TB
okb_file[文件\nPDF/docx/pptx/md等] --> okb_hash[Hash去重\nSHA-256\n纯代码]
okb_hash -->|已知| okb_skip[跳过]
okb_hash -->|新文件| okb_convert[文档转换\nmarkitdown/pymupdf\n纯代码]
okb_convert -->|短文档| okb_md[wiki/sources/.md\n全文Markdown]
okb_convert -->|长PDF\n≥threshold页| okb_pi[PageIndex索引\nLLM在服务端]
okb_pi --> okb_tree[层级树结构\n+逐页内容]
okb_pi --> okb_pi_summary[摘要页\n纯代码渲染]
okb_md --> okb_step1[Step 1: 生成摘要\nLLM\nsystem+doc=上下文A]
okb_pi_summary --> okb_step1_long[Step 1: 生成概述\nLLM\nsystem+doc=上下文A]
okb_step1 --> okb_step2
okb_step1_long --> okb_step2
okb_step2[Step 2: 概念规划\nLLM\n复用上下文A] --> okb_plan[create/update/related\n三类动作]
okb_plan --> okb_step3[Step 3: 概念生成\nLLM并发\n复用上下文A]
okb_step3 --> okb_step3b[Step 3b: related链接\n纯代码追加]
okb_step3b --> okb_step3c[Step 3c: 双向回链\n纯代码追加]
okb_step3c --> okb_step4[Step 4: 更新index.md\n纯代码]
style okb_step1 fill:#fcf,stroke:#333
style okb_step1_long fill:#fcf,stroke:#333
style okb_step2 fill:#fcf,stroke:#333
style okb_step3 fill:#fcf,stroke:#333
style okb_pi fill:#fcf,stroke:#333
| 文件类型 | 转换方式 | 输出 |
|---|---|---|
.md | 直接读取 + 复制相对路径图片并改写链接 | wiki/sources/{name}.md |
.pdf(短,< threshold 页) | pymupdf dict-mode 逐页遍历 text/image block → Markdown + 内联图片 | wiki/sources/{name}.md + wiki/sources/images/{name}/*.png |
.pdf(长,≥ threshold 页) | 仅标记 is_long_doc=True,不转换,交给 PageIndex | 返回标记 |
| 其他(docx/pptx/xlsx/html/txt/csv) | markitdown 库转换 → 解码 base64 图片保存磁盘并改写链接 | wiki/sources/{name}.md + wiki/sources/images/{name}/*.png |
额外操作:原始文件复制到 raw/ 目录存档,SHA-256 哈希注册到 hashes.json。
| 子步骤 | 输入 | 计算 | LLM | 输出文件 |
|---|---|---|---|---|
| 索引 | PDF 文件 | PageIndexClient.collection().add(pdf) — 上传 PDF 到 PageIndex 服务,服务端用 LLM 解析文档结构 | 是(PageIndex 内部) | doc_id, doc_description, structure(层级树) |
| 获取页面内容 | doc_id, page_count | Cloud 模式:OCR 后的 Markdown;失败回退本地 pymupdf | Cloud 模式用 OCR | wiki/sources/{name}.json(per-page 内容数组) |
| 渲染摘要 | tree 结构 | render_summary_md() — 递归遍历树节点,渲染为 Markdown 层级标题 + summary | 否 | wiki/summaries/{name}.md |
PageIndex 使用无向量(vectorless)的推理式检索——通过层级树索引实现长文档的结构化访问,不依赖 embedding。
编译管线的核心设计是上下文 A 复用:Step 1 构造 system_msg(AGENTS.md schema + 语言指令)+ doc_msg(文档内容/PageIndex 摘要),作为 prompt 缓存的前缀;Step 2-3 复用同一前缀,让 LLM 服务端命中缓存,减少重复计算。
Step 1: 生成摘要/概述
| 项 | 短文档 | 长文档 |
|---|---|---|
| Prompt | _SUMMARY_USER — 文档全文 + "写摘要页" | _LONG_DOC_SUMMARY_USER — PageIndex 摘要 + "写概述" |
| LLM 调用 | 1 次同步 | 1 次同步 |
| 输出格式 | JSON {"brief": "...", "content": "..."} | 纯 Markdown |
| 写入文件 | wiki/summaries/{name}.md(frontmatter: doc_type=short) | 摘要已在 PageIndex 步骤写好,此步输出作为后续输入 |
Step 2: 概念规划
| 项 | 说明 |
|---|---|
| 输入 | 上下文 A + 上一步 summary + 已有概念页的 briefs |
| LLM 调用 | 1 次同步 |
| 输出 | JSON plan: {"create": [...], "update": [...], "related": [...]} |
三种动作:
已有概念页以紧凑格式呈现给 LLM:- {slug}: {brief}(brief 从 frontmatter 读取,缺省时截取正文前 150 字符)。
Step 3: 概念页生成/更新
| 项 | 说明 |
|---|---|
| LLM 调用 | N 次并发异步(N = create 数 + update 数,默认 concurrency=5) |
| create Prompt | _CONCEPT_PAGE_USER — title + doc_name,生成新概念页 |
| update Prompt | _CONCEPT_UPDATE_USER — title + doc_name + 已有概念页全文,LLM 被要求"全文重写融入新信息,不要追加" |
| 输出格式 | JSON {"brief": "...", "content": "..."} |
写入逻辑:
sources 和 briefStep 3b/3c: 关联链接 + 双向回链(纯代码,零 LLM)
| 操作 | 做什么 |
|---|---|
_add_related_link() | 在 related 概念页末尾追加 See also: [[summaries/{doc}]] + frontmatter 追加 source |
_backlink_summary() | 在摘要页追加 ## Related Concepts 章节,列出所有 [[concepts/{slug}]] |
_backlink_concepts() | 在每个概念页追加 ## Related Documents 章节,列出 [[summaries/{doc}]] |
目的:确保双向链接闭环——摘要链接概念,概念也链接摘要。
Step 4: 更新索引(纯代码,零 LLM)
在 wiki/index.md 的 ## Documents 下插入文档条目,## Concepts 下插入或更新概念条目。条目格式:- [[link]] (type) — brief text。
| 步骤 | LLM 调用次数 | 缓存利用 | 备注 |
|---|---|---|---|
| Hash 去重 | 0 | — | 纯代码 |
| 文档转换 | 0 | — | 纯代码 |
| PageIndex 索引(仅长PDF) | 1+(PageIndex 内部) | — | 对 OpenKB 透明 |
| Step 1: 生成摘要/概述 | 1 | system+doc 构成缓存上下文 A | 短文档用全文,长文档用 PageIndex 摘要 |
| Step 2: 概念规划 | 1 | 复用上下文 A(缓存命中) | |
| Step 3: 概念页生成 | N(create+update 数) | 复用上下文 A(缓存命中) | 并发执行,默认 concurrency=5 |
| Step 3b/3c: 关联+回链 | 0 | — | 纯代码 |
| Step 4: 更新索引 | 0 | — | 纯代码 |
典型短文档:2 + N 次 LLM 调用(1 摘要 + 1 规划 + N 概念页)。典型长 PDF:2 + N 次加 PageIndex 内部调用。
链接载体: 正文内 [[wikilink]] 语法(如 [[concepts/attention]]、[[summaries/attention-is-all-you-need]])。
链接类型: 无类型系统,所有链接都是无类型的 wikilink,无法区分"属于""导致""矛盾"等关系语义。
链接权重: 无权重,所有链接等价。
双向链接机制: 通过代码级回链保证——编译管线的 Step 3b/3c 在写入后立即补全反向链接。但只在编译时执行,如果后续手动编辑页面删除了链接,反向链接不会自动清理。
链接精度: 页面级([[concepts/slug]]),不支持行号级定位。
read_file、get_page_content、get_image)导航已编译 wiki 回答问题| 层级 | 检查内容 | 计算方式 |
|---|---|---|
| 结构性 lint | 断链、孤儿页、raw 文件缺 wiki 条目、index.md 不同步 | 纯代码(正则匹配 wikilink) |
| 语义 lint | 矛盾、遗漏、过时、冗余、概念覆盖 | LLM agent(OpenAI Agents SDK) |
无定时整理。 所有知识产出在文件导入时一次性完成(多步 LLM 编译管线),没有后续的矛盾发现、过时更新或跨页面整合机制。随着页面增多,概念页可能过时但不自动刷新。
[[wikilink]]),slug 变更时链接失效,无 stale-link reconciliation1. PageIdMap 消除死链(vs GBrain auto-link + check-backlinks / OpenKB 编译时回链)
GBrain 在 put_page 时通过 auto-link post-hook 自动提取实体引用并创建链接,还通过 check-backlinks 命令检查并修复回链,用 pg_trgm 模糊 slug matching 缓解读取端问题。但 auto-link 基于正则提取,无法验证目标页面是否存在(可能产生悬空链接)。OpenKB 在编译时通过代码级 _backlink_summary / _backlink_concepts 保证双向链接,但仅在编译时刻执行,手动编辑或 slug 变更后链接可能断裂,且无 stale-link reconciliation。OV 的 PageIdMap 从结构上杜绝死链——page_id 只分配给上下文中确认存在的文件,链接不可能指向不存在的页面。
2. 延迟渲染替代写入时链接(vs GBrain 正文 Markdown 链接 / OpenKB [[wikilink]] 正文链接)
GBrain 的 auto-link post-hook 有 stale-link reconciliation 功能,在内容修改时自动移除失效链接。但正文中的 Markdown 链接(如 [张三](../people/zhangsan.md))不会随 slug 改名自动更新,仍是断裂风险点。OpenKB 同样把链接写在正文中([[wikilink]]),编译时生成,无 reconciliation 机制,slug 变更直接导致断链。OV 的 content 保持纯净,链接存在 links 元数据中,检索时按需渲染 match_text。target_uri 变化只改元数据,content 不动。
3. LinkType 枚举 + weight 权重(vs GBrain 自由文本 + 类型推断 / OpenClaw 无权重 / OpenKB 无类型无权重)
GBrain 的 links 表的 link_type 列是自由文本,但 auto-link post-hook 能通过类型推断级联自动推断标准类型(founded → invested_in → advises → works_at)。不过 LLM 调 add_link 时仍可填入任意值(同义歧义如 knows vs familiar_with)。OpenClaw 没有权重。OpenKB 的 [[wikilink]] 完全没有类型和权重,所有链接等价,无法区分"属于""导致""矛盾"等语义。OV 用枚举约束关系类型,用 weight 表达关联强度,支持更精细的检索排序。
4. 不引入 Claims 层(from OpenClaw 启发)
OpenClaw 的 Claims 三层链接 + 矛盾检测 + freshness 评估是一套完整的知识可信度管理,但 OV 不引入独立 claims 层。原因:原始 md 已包含事实记录,links 体系已覆盖 claim 的核心能力——矛盾(CONTRADICTS)、演变(EVOLVED_FROM)、可信度(weight)、证据溯源(t_uri + t_field + t_line_ranges)。独立 claim 层只是 links 的冗余子集。
5. 链接元数据 vs 正文内链接(from nashsu_llm_wiki / OpenKB 启发)
nashsu_llm_wiki 和 OpenKB 都把链接写在正文中([[wikilink]]),slug 变更时链接失效,且无法携带类型和权重。GBrain 的结构化链接存储在独立数据库表中(不在正文中),auto-link post-hook 通过 stale-link reconciliation 自动修复内容变更引起的失效链接,但正文中的 Markdown 交叉引用仍有 slug 变更问题。OV 的链接存在元数据中,正文保持纯净,与第 2 点延迟渲染策略一致。
6. 外部 Bot T+1 触发主题整合 vs Dream Cycle 维护管道(from GBrain + nashsu_llm_wiki + OpenKB 启发) nashsu_llm_wiki 和 OpenKB 都没有定时整理,全靠导入/编译时一次性产出。GBrain 的 Dream Cycle 是纯维护管道(lint → backlinks → sync → extract → embed → orphans),不涉及 LLM,没有记忆整合或实体升级功能。随着页面增多,一次性提取或纯维护都难以发现跨页面的矛盾、过时和遗漏。OpenKB 的语义 lint 能发现矛盾和过时,但需人工触发,无自动整理。OV 的整理由外部 Bot T+1 触发,从已有记忆中发现问题,调用 ExtractLoop 生成报告,保持知识库活力。
7. 现有 merge_op 映射(vs GBrain 页面内容覆写 / OpenClaw Dreaming) OV 已有的 merge_op 体系与竞品的记忆整理模式天然对应:
upsert(PATCH)≈ GBrain compiled_truth(LLM 主动覆写式重编译)add_only(SUM)≈ GBrain timeline / OpenClaw history/date.md(追加式)8. PPR 搜索增强(vs GBrain backlink boost / nashsu_llm_wiki 4 信号图扩展 / OpenKB LLM agent 导航)
GBrain 已实现 backlink boost——搜索排序时被更多页面链接的实体排名更高(BrainBench v1: P@5 +5.4pts, Recall@5 +11.5pts),但这是简单排序加分,不做图传播,无法发现种子文件多跳之外的关联。OV 用 PPR 算法实现 query 相关的图增强检索,从搜索种子出发沿 links 做带权随机游走,天然支持多种子桥接发现,详见 3.5 节。nashsu_llm_wiki 用 4 信号加权做图扩展,但权重硬编码且无类型区分;OV 的 PPR 按 (link_type, links/backlinks) 配置表驱动,灵活可调。OpenKB 不做图增强,query/chat 完全依赖 LLM agent 自主决策导航 wiki 页面,召回能力受 agent 推理能力限制。
9. ExtractLoop 统一提取 vs 多步编译管线(from OpenKB 启发)
OpenKB 的编译管线是精心设计的多步 LLM 调用链:摘要 → 概念规划 → 并发概念生成 → 代码级回链 → 索引更新,每步有明确的输入/输出契约,且通过 prompt 缓存复用上下文降低成本。但每次新文档导入都独立走完整管线,概念页的"update"路径依赖 LLM 全文重写(非增量),随着页面增多成本线性增长。OV 的 ExtractLoop 在单次 LLM 调用中统一输出记忆操作 + links,由 merge_op 体系处理增量合并(upsert PATCH / add_only SUM),更轻量且天然支持增量更新。
定位: AI agent 的持久化记忆系统,在对话中实时写入记忆,外部 Bot T+1 触发整理发现主题生成研究报告。
输入源: 对话消息 + 资源(默认进 wiki / 默认不进 wiki)。
数据写入(3 种输入场景):
| 场景 | 触发 | 说明 |
|---|---|---|
| (1) 对话消息 | session.commit | 对话进记忆,互链 |
| (2) 邮件会议纪要等默认进 wiki 的 resource | add-resource | 资源进记忆,互链 |
| (3) 默认不进 wiki 的 resource | add-resource | 资源不进记忆,但记忆可链接到资源 |
三种场景统一走 ExtractLoop,LLM 按 schema 输出记忆操作 + links
链接机制: 链接存在文件元数据 JSON(VikingFS)中,content 保持纯净。6 种 LinkType 枚举约束,weight 表达关联强度,t_line_ranges 行号级精度。一条链接写入 from 端 links + to 端 backlinks,两侧记录完全相同。PageIdMap 从结构上杜绝死链。
T+1整理: 主题整合。从已有记忆中发现问题(新记忆无关联 report / CONTRADICTS 链接 / report_candidate),逐主题调用 ExtractLoop(DreamContextProvider)生成 report。暴露 CLI 供 Bot 通过 Cron 触发。report_candidate 来源:session 中提到的研究主题 / CLI 提交研究主题 / 基于搜索结果分布。
整理产物: 问题驱动的研究报告(report memory_type)。
flowchart TB
ov_s1["(1) 对话消息 session.commit"]
ov_s2["(2) 邮件会议纪要等默认进wiki的 resource"]
ov_s3["(3) 默认不进wiki的 resource"]
ov_s1 --> ov_extract["ExtractLoop\n(SessionContextProvider)"]
ov_s2 --> ov_extract
ov_s3 --> ov_extract
ov_extract <--> ov_memory[entity/event/preference等记忆\n links + backlinks]
ov_extract --> |session中提到的研究主题|ov_rc
ov_rc[report_candidate\n候选主题] --> ov_dream["ExtractLoop\n(DreamContextProvider) (per report)"]
ov_memory --> ov_dream
ov_bot[暴露CLI供Bot通过Cron触发] --> ov_dream
ov_bot --> |CLI提交研究主题| ov_rc
ov_search[search请求] -->|基于search结果分布| ov_rc
ov_dream --> ov_report[问题驱动的研究报告]
style ov_s1 fill:#ffe6cc,stroke:#333
style ov_s2 fill:#ffe6cc,stroke:#333
style ov_s3 fill:#ffe6cc,stroke:#333
style ov_extract fill:#9f9,stroke:#333
style ov_dream fill:#9f9,stroke:#333
style ov_memory fill:#9cf,stroke:#333
style ov_rc fill:#9cf,stroke:#333
style ov_report fill:#9cf,stroke:#333
style ov_bot fill:#ffc,stroke:#333
关键路径:对话消息/资源 → ExtractLoop(LLM 统一输出记忆 + links)→ merge_op 合并写入 → 向量化/摘要异步入队 → report_candidate → Bot Cron 触发 T+1 整理 → PPR 在线图增强
1. 在线写入(3 种输入场景统一走 ExtractLoop)
| 场景 | 触发 | 说明 |
|---|---|---|
| (1) 对话消息 | session.commit | 记忆间互链 |
| (2) 邮件会议纪要等默认进 wiki 的 resource | add-resource | 资源进记忆,互链 |
| (3) 默认不进 wiki 的 resource | add-resource | 资源不进记忆,但记忆可链接到资源 |
2. MergeOp 字段合并
3. 向量化计算
4. Semantic Processor(摘要生成)
5. T+1 触发整理
6. PPR 图增强检索
链接系统分为两层:LLM 输出层(使用 page_id 引用)和存储层(使用 URI)。
LLM 在 structured output 中统一输出链接,不散在每个记忆字段内:
class LinkType(str, Enum):
RELATED_TO = "related_to"
BELONGS_TO = "belongs_to"
CAUSED_BY = "caused_by"
DERIVED_FROM = "derived_from"
CONTRADICTS = "contradicts"
EVOLVED_FROM = "evolved_from"
class WikiLink(BaseModel):
f: int # page_id A
t: int # page_id B
t_field: str # B 的字段名(to 端精确定位)
t_line_ranges: Optional[str] # B 的行号范围:"3-5"(to 端精确定位)
link_type: LinkType # 关系类型(枚举)
weight: float = 1.0 # 关联权重 0~1
match_text: Optional[str] # A 中需链接化的文本片段(检索时用于渲染)
description: str = "" # 链接描述:为什么建立此关联
链接不对称:from 侧是锚点(match_text),to 侧是展开信息(t_field + t_line_ranges)。存储时,一条链接写入两端文件的独立列表:from 文件的 links(正链,需渲染)+ to 文件的 backlinks(反链,不渲染,只用于遍历)。两侧记录内容完全相同,不翻转 link_type。
LLM 输出示例:
{
"preferences": [
{"page_id": 100, "topic": "Python code style", "content": "User dislikes type hints..."},
{"page_id": 101, "topic": "Communication style", "content": "User prefers direct..."}
],
"events": [
{"page_id": 102, "event_name": "Code review", "content": "Caroline reviewed Python code..."}
],
"links": [
{"f": 100, "t": 3, "t_field": "content", "t_line_ranges": "3-5",
"link_type": "belongs_to", "weight": 0.9, "match_text": "User",
"description": "该偏好属于 Caroline"},
{"f": 102, "t": 100, "t_field": "content", "t_line_ranges": "1-2",
"link_type": "related_to", "weight": 0.7, "match_text": "Python code",
"description": "事件中讨论的代码风格与该偏好相关"}
]
}
page_id 分配规则:
page_id 字段写入文件后,page_id 全部转换为 URI:
class StoredWikiLink(BaseModel):
f_uri: str # 源文件 URI
t_uri: str # 目标文件 URI
t_field: str # 目标字段名
t_line_ranges: Optional[str] # 目标行号范围
link_type: LinkType # 关系类型(枚举)
weight: float # 权重
match_text: Optional[str] # 源中需链接化的文本
description: str # 链接描述
URI ↔ page_id 双向映射,ExtractLoop 生命周期内有效:
class PageIdMap:
"""URI ↔ page_id 双向映射,ExtractLoop 生命周期内有效"""
def get_or_assign(self, uri: str) -> int:
"""URI → page_id。首次分配,后续返回同一 id"""
def get_uri(self, page_id: int) -> Optional[str]:
"""page_id → URI。从 id 还原 uri"""
def next_new_page_id(self) -> int:
"""分配新建页面的 page_id(从 100 开始自增)"""
page_id 范围隔离:
content 保持纯净,不插入 Markdown 链接。链接存储在 .relations.json 目录级 sidecar 文件中,复用并扩展现有 VikingFS relation 基础设施。
| 维度 | .relations.json | MEMORY_FIELDS |
|---|---|---|
| 向下兼容 | 兼容老格式(读取时自动适配),无需数据迁移 | 不兼容,需迁移老 .relations.json |
| PPR 遍历 | 按目录读一个文件拿到全量 links,I/O 小 | 逐文件读 MEMORY_FIELDS 解析 links,I/O 大 |
| 写入独立性 | 写 link 不修改记忆文件,不触发内容重写和重新向量化 | 写 link 需改两端文件的 MEMORY_FIELDS |
| 现有基础设施 | 复用 link/unlink/加密/ovpack 导入导出/HTTP API | 需新建或大幅改造 serialize/deserialize |
| 改动范围 | 中(viking_fs 扩展字段 + 上层适配) | 大(content.py + memory_updater + viking_fs + 迁移) |
链接存储在 from_uri 所在目录的 .relations.json 中。现有 RelationEntry 扩展为 StoredLink,新增 from_uri/to_uri/direction/link_type/weight/t_field/t_line_ranges/match_text/description 字段:
[
{
"id": "link_1",
"from_uri": "viking://user/caroline/memories/preferences/Python_code_style.md",
"to_uri": "viking://user/caroline/memories/profile.md",
"direction": "links",
"link_type": "belongs_to",
"weight": 0.9,
"t_field": "content",
"t_line_ranges": "3-5",
"match_text": "User",
"description": "该偏好属于 Caroline",
"created_at": "2026-04-27T10:00:00.000Z"
},
{
"id": "link_2",
"from_uri": "viking://user/caroline/memories/events/2026/04/27/code_review.md",
"to_uri": "viking://user/caroline/memories/preferences/Python_code_style.md",
"direction": "backlinks",
"link_type": "related_to",
"weight": 0.7,
"t_field": "content",
"t_line_ranges": "1-2",
"match_text": "Python code style",
"description": "事件中讨论的代码风格与该偏好相关",
"created_at": "2026-04-27T10:00:00.000Z"
}
]
direction: "links" = 当前文件引用别人(正链,渲染时替换 match_text);"backlinks" = 别人引用当前文件(反链,不渲染,只用于 PPR 遍历和整理主题发现).relations.json:from 端写 direction="links",to 端写 direction="backlinks"StoredLink 记录内容完全相同(link_type 不翻转)同一个 .relations.json 文件内新老格式共存,读取时按字段判断:
uris 字段 → 老格式 RelationEntry,每个 uri 展开为一条 StoredLink,新字段走默认值(direction="links", link_type="related_to", weight=1.0),reason → descriptionto_uri 字段 → 新格式 StoredLink,完整解析写入只产新格式,老 entry 随文件自然更新逐步迁移。
3.1 节的 StoredWikiLink 是 LLM 输出后 page_id→URI 转换后的逻辑模型,字段与 StoredLink 一致。写入 .relations.json 时额外携带 id/from_uri/direction/created_at。
get_file_links(uri, direction) — 获取某文件的所有 links/backlinks,在文件所在目录的 .relations.json 中按 from_uri + direction 过滤relations(uri) — 返回目录级关联列表(向后兼容,返回值扩展 link_type/weight 等字段)content 原文不修改,链接渲染延迟到检索阶段,根据 match_text 在 content 中做运行时替换:
存储: "User dislikes type hints, prefers concise comments."
渲染: "[User](viking://.../profile.md) dislikes type hints, prefers concise comments."
不同场景的渲染策略:
| 场景 | 是否替换 match_text | 原因 |
|---|---|---|
| LLM prefetch 读入上下文 | 不替换 | LLM 更新时处理链接增加复杂度,PPR 已做实体关联召回 |
| 向量化嵌入 | 不替换 | 纯文本语义更干净 |
| search 返回摘要 | 替换(仅 links) | 用户可见的链接提升可读性和导航 |
| 外部 Bot T+1 整理读取全量 | 不替换 | 原文做去重/合并更准确 |
| 展示页查看 | 替换(仅 links) | 用户可见的链接提升可读性 |
渲染规则:
好处:
.relations.json 中,不参与向量化,更新链接不触发重新 embeddinglinks 和 backlinks 的合并逻辑在 utils/links_merge.py 中独立实现,供 link() 调用。两个方向使用相同的合并规则。
合并规则:
from_uri + to_uri + t_field + t_line_ranges 组合去重(同一对文件同一字段不同行号范围视为不同链接)link_type 和 description 以最新写入为准文件 PATCH 更新后,t_line_ranges 可能偏移。修正流程嵌入 apply_operations() 的写入后阶段,实时完成:
修正步骤:
文件写入完成后,对该文件的所有 links(links + backlinks)执行修正:
1. 读取旧文件内容(_apply_upsert 前缓存的 old_memory_file_content)
2. 对每条链接的 t_line_ranges,提取原始段落文本
- 解析 "3-5" → 读取旧文件第3~5行
3. 在新文件内容中定位该段落的新行号
- 先用字符串精确查找(str.find())
- 找不到 → 调 LLM 语义匹配重新定位
- 还是找不到 → 删除该链接(从两端 .relations.json 同时清除)
4. 双向一致性:修正/删除操作同时作用于两端 .relations.json(从 links 删除 = 从对端 backlinks 删除)
查找规则:
t_line_ranges 中的每个连续段落(如 "3-5,8-10" 有两个段落),独立查找str.find(),匹配第一次出现.relations.json 删除该链接class LinkType(str, Enum):
RELATED_TO = "related_to" # 一般关联
BELONGS_TO = "belongs_to" # 归属关系
CAUSED_BY = "caused_by" # 因果关系
DERIVED_FROM = "derived_from" # 派生关系(从已有记忆推导出的合成产物)
CONTRADICTS = "contradicts" # 矛盾关系
EVOLVED_FROM = "evolved_from" # 演变关系(知识/观点随时间迭代)
link_type 始终从 from 视角定义(如 A CAUSED_BY B = A 被 B 导致)backlinks 时,当前文件是 to 端,通过 (link_type, links/backlinks) 组合解读反链语义(如当前文件是 to 端的 CAUSED_BY = 当前文件是原因)按系统行为影响划分,不是按语义:
信号型(触发整理主题发现的独立代码分支):
| link_type | links 语义 | backlinks 语义 | 整理行为 |
|---|---|---|---|
| CONTRADICTS | 我与 to 矛盾 | from 与我矛盾 | 冲突信号 → 生成冲突报告 |
| DERIVED_FROM | 我从 to 派生 | from 从我派生 | 源更新 → 检查源记忆是否更新,触发重研 |
| EVOLVED_FROM | 我从 to 演进 | from 从我演进 | 版本过时 → 旧版本被替代,触发更新 |
结构型(影响 PPR 遍历策略,算法统一,不需要独立代码分支):
| link_type | links 语义 | backlinks 语义 |
|---|---|---|
| BELONGS_TO | 我属于 to(追上下文) | from 属于我(追细节) |
| CAUSED_BY | 我由 to 导致(追根因) | 我导致了 from(找影响范围) |
兜底型(无特殊行为):RELATED_TO,1 跳即止。
PPR:统一配置表驱动,按 (link_type, links/backlinks) 组合查询权重、是否继续、最大深度。不需要为每种类型写不同的遍历逻辑。
Prefetch:消费 PPR 结果排序 + CONTRADICTS 强制包含(确保矛盾信息不遗漏)。
在线检索:1 跳扩展,仅对 CONTRADICTS / EVOLVED_FROM 强制跟随(毫秒级 budget,不做多跳)。
整理主题发现:3 种信号型各有独立逻辑:
| (link_type, links/backlinks) | 传播权重 | 是否继续 | 最大深度 | 说明 |
|---|---|---|---|---|
| (CONTRADICTS, links) | 0.8 | 否 | 1 | 直接矛盾,必须跟随 |
| (CONTRADICTS, backlinks) | 0.8 | 否 | 1 | 对称 |
| (BELONGS_TO, links) | 0.7 | 是 | 3 | 追上下文,传递 |
| (BELONGS_TO, backlinks) | 0.7 | 是 | 3 | 追细节,传递 |
| (CAUSED_BY, links) | 0.5 | 是 | 2 | 追根因,链式衰减 |
| (CAUSED_BY, backlinks) | 0.3 | 否 | 1 | 找影响范围,不扩散 |
| (DERIVED_FROM, links) | 0.2 | 否 | 1 | 溯源,低权重 |
| (DERIVED_FROM, backlinks) | 0.6 | 否 | 1 | 合成产物,中权重 |
| (EVOLVED_FROM, links) | 0.3 | 否 | 1 | 新→旧,一般不需要 |
| (EVOLVED_FROM, backlinks) | 0.9 | 是 | 5 | 旧→新,找最新版本 |
| (RELATED_TO, links) | 0.4 | 否 | 1 | 1跳即止 |
| (RELATED_TO, backlinks) | 0.4 | 否 | 1 | 1跳即止 |
class MemoryTypeSchema(BaseModel):
# ... 现有字段不变 ...
# 新增链接字段
link_enabled: bool = Field(True, description="Whether linking is active for this type")
link_enabled: true 时,该记忆类型参与链接现有 YAML 文件无需改动(默认启用)。如需禁用链接能力:
memory_type: some_type
link_enabled: false
links 不作为 MemoryField 注入到每个记忆类型的 schema 中,而是作为 StructuredMemoryOperations 的独立顶层字段 links: List[WikiLink]。同时在每个记忆类型的 flat data model 中注入 page_id: int 字段(从 100 开始自增)。
Prefetch 阶段:
ls/search/read 已有文件 → PageIdMap 分配 page_id (1~99)
已有页面的 links 读入 LLM 上下文,避免重复建链
↓
ExtractLoop:
LLM 上下文中用 [page:N] 引用已有文件
LLM 输出记忆操作(每项带 page_id 从 100 起)+ 统一 links
↓
resolve_operations():
f page_id → PageIdMap.get_uri() 或 ResolvedOperations[idx].uri → f_uri
t page_id → 同上 → t_uri
t_field + t_line_ranges → 从目标文件计算实际行号范围和字符数
将链接分发到 .relations.json(写入 from 端和 to 端各自目录):
- from 文件所在目录 → link(direction="links")
- to 文件所在目录 → link(direction="backlinks")
↓
MemoryUpdater.apply_operations():
├─ _apply_upsert(): 写入记忆文件(content 纯净,links/backlinks 在 .relations.json 中)
│ - from 文件所在目录的 .relations.json 包含以该文件为 from 的链接
│ - to 文件所在目录的 .relations.json 包含以该文件为 to 的反向链接
├─ t_line_ranges 行号修正(3.6 节,精确匹配 + LLM 语义匹配)
└─ 链接双向一致性保证
链接场景:
| 场景 | from | to | 示例 |
|---|---|---|---|
| 新记忆 → 已有页面 | page_id (100+) | page_id (1~99) | 新偏好 → 已有 profile |
| 新记忆 → 新记忆 | page_id (100+) | page_id (100+) | 新 event → 新 preference |
| 已有页面 → 新记忆 | page_id (1~99) | page_id (100+) | 已有 profile → 新偏好 |
所有链接天然双向,一条 WikiLink 同时写入 from 端的 links 和 to 端的 backlinks,两侧记录内容完全相同。
死链不可能存在:page_id 只在 ExtractLoop 上下文中产生,对应文件一定存在(已有文件已读入,新建文件即将写入)。
一趟写入策略:
resolve_operations() 阶段将链接分发:from 端链接写入 from 文件所在目录的 .relations.json(direction="links"),to 端反向链接写入 to 文件所在目录的 .relations.json(direction="backlinks")viking_fs.link() 内部通过 utils/links_merge.py 与已有链接合并整理是通用框架:从已有记忆中发现问题(主题),基于已注册的 memory_type schema 生成报告。用户通过 YAML 配置 dream_task,不写死产出什么。
框架流程:
外部 Bot T+1 触发(如次日由外部 Bot 调用)
↓
Step 1: 主题发现
输入:当天新增/修改的记忆文件 + 已有的 report 列表
输出:新主题列表(question)+ 需更新的已有 report URI 列表
↓
Step 2: 逐主题生成
对每个主题:
整理 ContextProvider 读取该主题相关文件 → ExtractLoop 调 LLM 按 schema 输出 → MemoryUpdater 写入
主题发现的输入:
| 数据 | 说明 | 获取方式 |
|---|---|---|
| 当天新增/修改的记忆文件 | 增量,不是全量 | 按时间筛选 VikingFS 文件 |
| 当天新文件的 links | 新文件关联了谁 | deserialize_metadata() 提取 |
| 已有 report 列表 | 避免重复 + 决定哪些需更新 | 按 memory_type=report 搜索 |
| report_candidate 列表 | 用户主动提出的主题 | 按 memory_type=report_candidate 搜索 |
主题发现的逻辑:
report_candidate: 用户在对话中主动提出"帮我研究 X"时,实时阶段提取为 report_candidate 写入。外部 Bot T+1 整理消费后生成对应 report 时,该 candidate 标记为已处理。
memory_type: report_candidate
fields:
- name: question
field_type: string
merge_op: patch
- name: user_requirement
field_type: string
merge_op: patch
- name: created_at
field_type: string
merge_op: patch
- name: status
field_type: string
merge_op: patch
复用实时阶段组件:
| 外部 Bot T+1 整理 | 实时阶段 |
|---|---|
| 整理 ContextProvider(按主题选文件) | SessionContextProvider(按对话 prefetch) |
| ExtractLoop(调 LLM 按 schema 输出) | ExtractLoop(同样) |
| MemoryUpdater.apply_operations(写入) | MemoryUpdater.apply_operations(同样) |
核心区别只在 ContextProvider 的输入选择策略不同。
整理 ContextProvider 流程(Step 2 逐主题生成时):
1. 按主题读取相关文件
- memory_type 锚点:找指定类型(entity/preferences/events 等)的所有文件,沿 links 入边收集 1-hop 邻居
- conflict:检测有 CONTRADICTS 链接的文件对
2. 读取每组文件的内容
- VikingFS.read_file() 读每个 URI 的内容 + get_file_links() 读 links
- 从 links 过滤组内链接
3. 读已有同类 report
- 按 memory_type=report 搜索已有报告
- 相关 report 读入 LLM 上下文,避免重复产出
4. 组装 LLM 输入
- 文件正文(plain_content)
- 组内 links(关系结构,特别是 CONTRADICTS / EVOLVED_FROM)
- 已有相关 report
- output memory_type 的 schema 定义
5. LLM 产出 0~N 个 report(无值得研究的问题则跳过)
配置示例:
dream_tasks:
- input:
source: entity
scope: 1-hop-neighbors
output:
memory_type: report
- input:
source: conflict
output:
memory_type: report
report schema 定义:
memory_type: report
fields:
- name: question
field_type: string
merge_op: patch
- name: answer
field_type: string
merge_op: patch
- name: source_uris
field_type: list
merge_op: sum
- name: conflicts
field_type: list
merge_op: sum
- name: created_at
field_type: string
merge_op: patch
content_template: |
# {{question}}
> 产出时间: {{created_at}}
{{answer}}
## 依据
{% for uri in source_uris %}
- [{{uri|filename}}]({{uri|relative_path}})
{% endfor %}
{% if conflicts %}
## 矛盾
{% for c in conflicts %}
- {{c}}
{% endfor %}
{% endif %}
实时阶段已保证: 死链不存在(PageIdMap)、双向链接强制分发,外部 Bot T+1 整理无需做全局健康检查。
基于 Personalized PageRank (PPR) 算法,利用 links 数据增强检索。PPR 从种子文件出发沿 links 做带权随机游走,分数随传播衰减,实现 query 相关的图增强检索。
算法核心:
Phase 1: 种子获取
向量搜索 + 关键词搜索 → 融合排序 → top-K 文件作为 seed set
每个 seed 有 base_score(搜索排序分数)
Phase 2: PPR 传播
以 seed set 为 preference nodes,沿 links 和 backlinks 做带权随机游走
- 每步传播分数 × damping_factor (默认 0.85)
- 传播策略按 (link_type, links/backlinks) 查 3.2.7.4 配置表决定权重、是否继续、最大深度
- link_weight 低于阈值(如 0.3)的边跳过,控制扩展量
Phase 3: 分数合并
对每个文件:
- 在 seed 中: final_score = α × base_score + (1-α) × ppr_score
- 不在 seed 中: final_score = ppr_score
- ppr_score 低于阈值(如 0.05)的补充文件丢弃
Phase 4: 返回
合并 seed + 补充文件,按 final_score 降序返回
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
| α | 0.7 | 搜索分数权重,PPR 定位为补充而非主导 |
| damping_factor | 0.85 | PPR 衰减因子,每跳分数 × 0.85 |
| depth | 1 | 传播深度,1 跳已足够(2 跳仅搜索质量差时启用) |
| min_link_weight | 0.3 | 传播时跳过低权重边 |
| min_ppr_score | 0.05 | 补充文件的最低分数阈值 |
计算成本: 纯数值计算,不需要 LLM 或 embed。假设 top-K=10,每文件平均 5 条 links,一跳扩展最多 50 个文件。瓶颈仅在文件 I/O(读 .relations.json 中的 links),计算本身毫秒级。
搜索 API 的后处理层,补充召回 + 重排序。
场景: 用户主动搜索记忆(如搜"Caroline 的代码风格"),返回结果可能遗漏关联文件。
流程: 向量+关键词搜索返回 seed → PPR 传播 → 补充召回向量未命中但图上强关联的文件 → 重排序返回。
举例:
seed = {code_review.md(0.8), Python_style.md(0.7)}
PPR 传播 (damping=0.85, 按 3.2.7.4 配置表):
code_review.md →[belongs_to, links, w=0.7]→ caroline.md
Python_style.md →[evolved_from, links, w=0.3]→ old_style.md
结果:
caroline.md: 0.7×0.85 = 0.60 (补充召回,向量未命中)
old_style.md: 0.3×0.85 = 0.26 (低于 min_ppr_score,丢弃)
返回 [code_review.md, Python_style.md, caroline.md]
caroline.md 是向量未命中但 PPR 补充召回的高关联文件
特点: 单次短 query,seed 通常 1~3 个,PPR 传播量小,补充召回为主。
ExtractLoop 的 prefetch 阶段,将 PPR 高分文件主动读入 LLM 上下文。
场景: session.commit 时 prefetch 已有记忆。query 是完整对话,向量搜索可能返回多个不相关的种子文件。
多种子桥接: prefetch 的 query 是完整对话,向量搜索可能返回多个不同主题的种子。PPR 在多种子同时传播时,桥接文件会获得多个种子的分数叠加,自然浮出——这是单种子场景无法发现的价值。
对话涉及 "Caroline" 和 "Go 语言兴趣"
向量搜索 seed:
seed1: entities/caroline.md (命中"Caroline")
seed2: preferences/go_interest.md (命中"Go语言")
PPR 传播(多种子叠加,按 3.2.7.4 配置表):
caroline.md →[belongs_to, links, w=0.7]→ Python_style.md: 0.7 × 0.85 = 0.60
go_interest.md →[evolved_from, links, w=0.3]→ Python_style.md: 0.3 × 0.85 = 0.26
Python_style.md 的 ppr_score = 0.60 + 0.26 = 0.86 ← 桥接文件,多种子叠加
Python_style.md 连接了两个种子,向量搜索未命中(query 没提 Python),
但 PPR 多种子传播使其自动浮出
特点: 完整对话 query,seed 数量多(可能 5~10 个),多种子叠加是核心价值。PPR 高分的桥接文件读入 LLM 上下文,避免重复建链和遗漏关联。
外部 Bot T+1 整理阶段,PPR 用于整理 ContextProvider 的输入选择——从记忆图中定位枢纽文件,优先处理高链接密度的区域。
场景: 整理 ContextProvider 按 memory_type 策略选取文件群时,全量遍历成本高。PPR 可以快速定位"枢纽区域"——哪些文件的入边邻居最多、权重最高。
流程: 以某 memory_type 的所有文件为均匀种子 → PPR 传播 → 分数高的文件是图中的枢纽(多条链接汇聚)→ 对高分区文件及其入边邻居生成 report。
特点: 均匀种子(所有目标类型文件等权),PPR 分数反映图的拓扑结构——链接越多、权重越高的文件分数越高,天然对应"值得研究的知识集群"。source 可以是任意已注册的 memory_type(entity、preferences、events 等),不限于 entity。
| 模块 | 改动 |
|---|---|
dataclass.py | MemoryTypeSchema 新增 link_enabled;新增 WikiLink, StoredWikiLink, LinkType 模型 |
viking_fs.py | RelationEntry → StoredLink(扩展字段);link() 新签名;unlink() 扩展;新增 get_file_links();_read_relation_table() 兼容老格式 |
schema_model_generator.py | create_flat_data_model() 中 link_enabled 时自动注入 page_id 字段 |
memory_updater.py | apply_operations() 中链接分发(from→links, to→backlinks 写入 .relations.json)+ 行号修正 |
memory_type_registry.py | _parse_memory_type() 解析 link_enabled 字段;支持 dream_tasks 配置 |
tools.py | 搜索 API 新增 PPR 后处理层(6.1.1);prefetch 扩展 PPR(6.1.2) |
session/compressor_v2.py | _create_relations() 使用新 link() 签名 |
session/session.py | _run_memory_extraction() 使用新 link() 签名 |
server/routers/relations.py | LinkRequest 扩展 direction/link_type/weight 等字段 |
openviking_cli/client/base.py | link() 方法签名扩展 |
| YAML templates | 新增 report + report_candidate memory_type 定义 + dream_tasks 配置;现有模板默认 link_enabled=true |
| 新增模块 | memory/dream_context_provider.py — 整理上下文提供者;utils/links_merge.py — links/backlinks 合并逻辑;retrieve/ppr.py — PPR 算法 |
extract_loop.py — LLM 自然输出 links 字段,不需要改 ReAct 循环逻辑session_extract_context_provider.py — prefetch 基础逻辑不变,PPR 扩展作为后处理层叠加memory_isolation_handler.py — 隔离逻辑不变,links 的 URI 遵循隔离规则| 维度 | GBrain | OpenClaw | nashsu_llm_wiki | OpenViking(目标) |
|---|---|---|---|---|
| 输入源 | 对话 + Git 资源 | 对话 | 资源 | 对话 + 资源(待建) |
| 页面写入 | LLM 直接写入 | 攒批提取(memory-core)+ 工具调用写入(memory-wiki) | 攒批提取(两步 LLM) | hook 攒批提取(session.commit)+ 资源目录提取(add-resource) |
| 链接存储 | 数据库表(整数 ID 引用) | 页面 sourceIds + 编译计算 | 正文 [[wikilink]] | 文件元数据 JSON(VikingFS) |
| 链接权重 | 无 | 无 | 无(4 信号加权替代) | 有(weight 字段) |
| 链接精度 | 文件级 | 文件级 | 文件级 | 行号级(target_ranges) |
| 链接类型 | 有(auto-link 推断标准类型 + LLM 填写自由文本) | 无(隐式 sourceIds) | 无(无类型,[[wikilink]] 平等) | 有(枚举约束) |
| 自动链接 | 已实现(put_page auto-link post-hook + 类型推断级联) | 编译器全量计算(零 LLM) | 正文 [[wikilink]] 解析 | LLM 统一输出 |
| 双向回链 | auto-link 自动建链 + check-backlinks 审计 | 编译时计算 | 无反向链接 | 自动生成反向链接 |
| 定时整理 | Dream Cycle 6阶段维护管道(cron + skills) | Dreaming 三阶段 | 无 | 外部 Bot T+1 触发主题整合 |
| 整理产物 | 维护产物(lint/backlinks/sync/extract/embed/orphans) | MEMORY.md + 重编译关联 | 无 | 问题驱动的研究报告(外部 Bot T+1 触发) |
| 搜索 | 向量+关键词+RRF+多查询扩展+backlink boost | 混合搜索 + 多样性重排 | 分词+向量 → RRF 融合 → 图扩展 | 向量+关键词 + PPR 图增强 |
| 链接参与检索 | Backlink boost(简单排序加分,非图传播) | 编译产物参与搜索 | 4 信号图扩展参与排序 | PPR 图增强,链接直接参与检索排序 |
| 知识可信度 | 无 | Claims + 矛盾检测 + 新鲜度评估 | Lint 检查(结构+语义) | links 已覆盖矛盾/演变/权重/溯源,不引入独立 claims 层 |
OpenViking 的差异化:
核心差异:
| GBrain | OpenClaw | nashsu_llm_wiki | OpenViking | |
|---|---|---|---|---|
| 在线写入 | put_page + auto-link post-hook(零 LLM) + LLM 调 add_link | 攒批提取 + 工具调用写入,编译器全量计算关系 | 攒批提取(两步 LLM),链接写正文 | hook 攒批提取(session.commit)+ 资源目录提取(add-resource),LLM 统一输出记忆 + links |
| 离线整理 | Dream Cycle(cron + skills)6阶段维护管道 | Dreaming 三阶段提升 + 关键词频率标记主题 | 无 | 外部 Bot T+1 触发主题整合,从已有记忆生成 report |
| 自动建链 | put_page auto-link post-hook(零 LLM) + extract links 批量回填 | 编译器全量计算 | 正文 [[wikilink]] 解析 | 不自动建链,靠 LLM 输出 + 外部 Bot T+1 整理补充 |
| 链接参与检索 | Backlink boost(简单排序加分,非图传播) | 编译产物参与搜索 | 4 信号图扩展参与排序 | PPR 图增强,链接直接参与检索排序 |