第二大脑全景馆 · gbrain
Prompt 展品

ANATOMY OF A SECOND BRAIN

拆开一颗第二大脑

gbrain 把「记忆」做成了一套真实系统:用 git 仓库当大脑、信息隔夜做梦般自我整理、提问时用四路检索把往事捞回来。这座展览馆把它拆成 7 个展厅,每件展品都配 gbrain 的真实源码真实 prompt——看清一颗第二大脑究竟由什么构成。

🔗还记得上一座馆里 openclaw 那个可替换的「context engine」吗?它的本体就在这儿——见 第 6 厅。两座馆,同一个宇宙。
01

🗄️ 大脑的本体 = 一个 Git 仓库 The Brain Is a Repo

你的大脑不是数据库,是一个 git 仓库:一堆带 frontmatter 的 markdown。数据库只是为了搜索快而存在的派生缓存,丢了也能一条命令重建。

markdown 是唯一真相,DB 可丢弃

所有用户知识都以 markdown 文件存在;数据库里的 facts / takes / links / timeline 全是从 markdown 派生出来的索引。灾难恢复不需要备份——擦掉 DB,从 brain repo 重新抽取即可。多机同步就是 git push / pull

真实数据 · 一条命令重建整个大脑 system-of-recordDATA
# DB 损坏?不需要备份。markdown 就是真相,DB 是派生缓存。
gbrain rebuild --confirm-destructive   # 删掉所有派生表
gbrain sync                            # 从 brain repo 重新导入 markdown
gbrain extract all                     # 重新抽取 facts/takes/links/timeline
# 计数与崩溃前完全一致 —— 派生状态被无损重建

来自 docs/architecture/system-of-record.md。每张表分三类:FS-canonical(markdown 是真相)、derived(自动重建)、DB-only(运行时状态)。CI 有专门的 gate 防止有人偷偷绕过 markdown 直写 DB。

真实代码 · 写入是原子的 fence-write.ts:233CODE
src/core/facts/fence-write.tsGitHub ↗
// 原子写入: 先写 .tmp → 重新解析校验 → POSIX rename 覆盖
writeFileSync(tmpPath, body, "utf-8");

const tmpBody = readFileSync(tmpPath, "utf-8");
const parsed = parseFactsFence(tmpBody);
if (parsed.warnings.length > 0) {
  // 解析失败: .tmp 留在磁盘作隔离证据, 绝不写进 DB
  recordWriteFailure(target.slug, target.sourceId, parsed.warnings, filePath);
  return { inserted: 0, ids: [], fenceWriteFailed: true };
}
// rename 是 POSIX 原子操作: 文件要么是旧内容, 要么是新内容, 绝不残缺
renameSync(tmpPath, filePath);

markdown 文件永远不会写一半——POSIX rename 的原子性,就是"git 当数据库"这件事的一致性硬保证。

💡点睛:把真相放在 git 里,而不是数据库里——于是「备份」「同步」「多人协作」「版本回溯」全都免费继承了 git 几十年的工程红利。
一个"记忆"长什么样

大脑里每个实体(人、公司、概念)就是一页 markdown。frontmatter 记元数据,正文里用 fence(带 begin/end 标记的表格)装结构化的 facts 和 takes,末尾是 timeline。这一页既能被人读、能 git diff,又能被 SQL 索引。

真实数据 · 一个真实的 brain page people/alice.mdDATA
---
type: person
title: alice
slug: people/alice
tags: [yc, founder]
---

# people/alice

Met at [acme](companies/acme). They founded the company in 2017.

## Facts

<!--- gbrain:facts:begin --->
| # | claim | kind | confidence | visibility | notability | valid_from | valid_until | source | context |
|---|-------|------|------------|------------|------------|------------|-------------|--------|---------|
| 1 | Founded Acme in 2017 | fact | 1.0 | world | high | 2017-01-15 |  | linkedin | Public bio |
| 2 | Prefers async over meetings | preference | 0.85 | private | medium | 2026-04-29 |  | OH 2026-04-29 |  |
<!--- gbrain:facts:end --->

<!-- timeline -->
## Timeline

- 2017-01-15 (linkedin): Founded Acme
- 2022-05-01 (interview): Raised Series A

每个 fact 自带 confidence(可信度)、visibility(world/private)、valid_from/until(有效期)、source(出处)。私有 fact 在喂给远程 agent 前会被一道 3 层 strip 剥掉,留在本地 markdown 但不进搜索/embedding。

💡点睛:结构化数据(可信度、有效期、出处)装在一张人类可读的 markdown 表格里——既能 git merge,又能 SQL 查询,一份内容两种身份。
遗忘不是删除,是划掉

gbrain forget 不会 DELETE 掉数据库行,而是在 markdown fence 里把这条 claim 改成划掉格式(~~...~~)、把 valid_until 设成今天、在 context 里追加 forgotten: 原因。这样即使重建数据库,"被遗忘"这件事本身也被保留下来——遗忘是可审计的。

真实数据 · 遗忘前后的 fence 行 forget contractDATA
# 遗忘前:
| 1 | I will hit $10M by Q4 | fact | 1.0 | world | medium | 2026-01-01 |  | s |  |

# 执行 gbrain forget(reason: "changed my mind") 后:
| 1 | ~~I will hit $10M by Q4~~ | fact | 1.0 | world | medium | 2026-01-01 | 2026-06-01 | s | forgotten: changed my mind |
#       └─ 划掉                                                          └─ valid_until=今天  └─ 记下原因

划掉还分两种语义:~~claim~~ + "superseded by #N" 是被新行取代;~~claim~~ + "forgotten: 原因" 是主动撤回。两者都留在 markdown 里供 git blame 追溯。

💡点睛:真正的记忆系统不该有"硬删除"——gbrain 的遗忘是一条划掉的、带时间戳和原因的痕迹,而不是凭空消失。
02

📥 摄入 Capture & Ingest

一切如何进入大脑:邮件、语音、日历、一句随手记。gbrain 把入口收敛成一个前门,用内容哈希做地址,天然幂等;并在边界上分清可信不可信来源。

capture:唯一的前门

gbrain capture 取代了 put_page / commit-then-sync 等一堆混乱写法。它的 slug 由内容哈希派生(inbox/YYYY-MM-DD-<hash8>),所以同样的内容永远落在同一个地址——重复捕获天然幂等,daemon 还有 24h 去重兜底。存完立刻可被 query / search 检索。

真实代码 · capture 的契约 skills/capture/SKILL.mdCODE
skills/capture/SKILL.mdGitHub ↗
## Contract

- Input: the content to save (inline arg, --file PATH, or --stdin).
- Output: a page in the brain DB AND a markdown file on disk.
- Side effect: immediately queryable via gbrain query / search.
- Idempotency: same content => same inbox/YYYY-MM-DD-<hash8> slug.
- Trust: captures via this skill are local-CLI trust (remote: false).
  Untrusted webhook ingestion goes through POST /ingest, not this verb.

slug 由内容哈希派生而非随机生成——相同的想法永远落在同一个地址,这是幂等性的物理基础。

💡点睛:一个"想法"的地址 = 它的内容哈希。这一个设计就让"重复保存"从一个需要处理的 bug,变成了什么都不用做的 no-op。
信任边界:本地脑 vs 网络输入

每个摄入事件都带一个 untrusted_payload 布尔标记。本地 CLI / 读自己 repo 的来源标 false;webhook、URL 抓取等网络来源标 true。下游据此决定:不可信内容跳过自动建图的实体抽取、并套上 slug 白名单 gate——一个字段就把安全策略分清了。

真实代码 · IngestionEvent 的 trust 标记 ingestion/types.ts:99CODE
src/core/ingestion/types.tsGitHub ↗
/**
 * Trust tag. Set to true by sources that receive input from untrusted
 * channels (webhook, future URL fetcher sources). The downstream put_page
 * handler honors this flag: skips auto-link entity extraction and applies
 * the slug-allowlist gate. Local in-process callers (CLI capture, file
 * watcher reading the user's own brain repo) MUST leave this false.
 */
untrusted_payload?: boolean;

"信任"不是一个全局开关,而是跟着每一条数据走的标签——同一套摄入管道,本地脑和网络输入走不同的安全策略。

💡点睛:第二大脑会从外部世界(网页、webhook)吃东西,所以"这条信息可信吗"必须从入口就标清楚——prompt 注入防线从摄入这一刻就开始。
Iron Law:被提到就必须连上

摄入一条信息不只是存一页:它是个路由器——检测其中提到的每个实体(人/公司),给每个建页或更新,在所有相关实体的 timeline 都追加条目,并从每个实体页反向链接回来源。"一个没连上的提及,就是一个坏掉的大脑"(Iron Law)。

真实数据 · ingest 的产出 skills/ingest/SKILL.mdDATA
INGESTED: [title]
==================
Page: [slug]
Type: [person / company / meeting / media / concept]
Source: [source description]

Entities detected: N
- [entity] -> [created / updated] ([slug])

Back-links created: N
Timeline entries: N
Raw source: [preserved at path / uploaded to cloud]

每条写入大脑的 fact 都强制带 [Source: ...] 出处引用;每个实体提及都创建反链。遗漏反链会被当成可检查的 bug,而不是默默腐烂的死链。

💡点睛:摄入一条信息 = 强制更新整张知识图谱。Iron Law 让"图谱完整性"成为一条可被 CI 检查的硬约束,而不是靠自觉。
03

🕸️ 抽取与建图 Extract & Graph

信息进来后,gbrain 从里面抽出两种东西——facts(客观事实)和 takes(可被证伪的判断/赌注),再把它们用链接连成一张知识图谱。最妙的是:建图零 LLM 调用

Takes:可以被打脸的判断

Facts 记客观事实,Takes 记会随时间被验证对错的判断——预测、观点、赌注。每条 take 有 holder(谁持有这个信念)、weight(信念强度 0..1)、时间区间。关键辨析:holder 是"谁说的",不是"说的是谁"——这是在 10 万条 takes 上评估出的头号标注错误。

真实数据 · takes fence takes-fence.tsDATA
<!--- gbrain:takes:begin --->
| # | claim | kind | who | weight | since | source |
|---|-------|------|-----|--------|-------|--------|
| 1 | CEO of Acme | fact | world | 1.0 | 2017-01 | Crustdata |
| 2 | Strong technical founder | take | garry | 0.85 | 2026-04-29 | OH 2026-04-29 |
| 3 | ~~Will reach $50B~~ | bet | garry | 0.7 | 2026-04-29 → 2026-06 | superseded by #4 |
| 4 | Will reach $30B | bet | garry | 0.55 | 2026-06 | revised after Q2 numbers |
<!--- gbrain:takes:end --->

第 3 行被划掉 + "superseded by #4":这条赌注从 $50B 下修到了 $30B,旧判断不删,留作记录。weight 用 0.05 的步长,从措辞的把握程度推断。

💡点睛:把"判断"也当成一等数据存下来、还带上信念强度和时间——这是后面"给旧预测打分""校准你的过度自信"能成立的前提。
零 LLM 建图:每次写页面跑三个正则

知识图谱不是靠 LLM 抽关系建的。每次 put_page 都对 markdown 正文跑 extractEntityRefs,用三个正则匹配出链接,一条 SQL 批量插入。零 token、零费用,17K 页的大脑全量建图只要几秒。连接类型(works_at/invested_in/founded...)从上下文启发式推断——也不调 LLM。

真实数据 · 三种被识别的链接语法 RETRIEVAL.md:37DATA
# 每次 put_page 对 markdown 正文跑 extractEntityRefs, 匹配三种链接:

[Garry Tan](wiki/people/garry-tan)          # ① 标准 markdown 链接
[[wiki/people/garry-tan|Garry Tan]]         # ② Obsidian wikilink
> **Convention:** see [path](path).         # ③ typed-link 引用块

# 三个正则, 零 LLM token, 单条 SQL:
#   INSERT ... SELECT FROM unnest(...) JOIN pages ON CONFLICT DO NOTHING
# 图谱在每次写入时增长, 成本趋近于零

来自 docs/architecture/RETRIEVAL.md。RETRIEVAL.md 的 benchmark 显示:正是这张图,把检索 P@5 从 18 抬到 49——"图不是边缘功能,是承重墙"。

💡点睛:你写下的每个 [链接] 都在悄悄给大脑织网。把"建图"寄生在 markdown 链接这个你本来就会写的习惯上,是它能零成本运行的根本原因。
14 种类型:从 94 种混乱里收敛

生产大脑曾积累出 94 种页面类型,导致搜索过滤失效、enrichment 路由漏覆盖。后来收敛成 14 种规范类型 + 1 个兜底(note),盖不住的一律落入 note 并保留 legacy_type 供回滚。类型不是装饰,是让整条 enrichment 管道能跑起来的骨架。

真实数据 · 类型体系节选 type-taxonomy.md:33DATA
| Type    | Primitive | What it holds                  | Examples              |
|---------|-----------|--------------------------------|-----------------------|
| person  | entity    | People                         | Founders, partners    |
| company | entity    | Companies, products, orgs      | Companies, products   |
| concept | concept   | Ideas + reference pages        | Wiki concepts         |
| deal    | temporal  | Investment deals               | Term sheets           |
| media   | media     | Articles, videos, books        | Substack, YouTube     |
| note    | concept   | Catch-all (legacy_type 保留)   | Memos, anecdotes      |

完整 14 种:person / company / media / tweet / social-digest / analysis / atom / concept / source / deal / email / slack / writing / project,外加 note 兜底。

💡点睛:MECE(相互独立、完全穷尽)的类型表,是知识库不退化成"一团乱麻"的纪律——94 种类型等于没有类型。
04

🌙 隔夜做梦 The Dream Cycle

你睡着时,一个 daemon 在大脑里巡夜:把今天的对话提炼成可证伪的判断、给半年前的旧预测打分、找出反复出现的主题、标记情绪重的页面。睡前是一个 agent,醒来是更聪明的那个

propose-takes:从散文里提炼赌注

做梦的第一步:扫描今天更新过的页面,用 LLM 把藏在散文里的"可被证伪的主张"抽出来,放进提案队列等你确认。注意 prompt 怎么把把握程度量化成具体语言锚点("我赌"→0.7-0.85),让 calibration 信号从一开始就可被机器读取。

真实 prompt · 提炼可评判主张 cycle/propose-takes.ts:86PROMPT
Extract gradeable claims from the prose below.

A "gradeable claim" is a prediction, recommendation, or interpretive
judgment that could turn out wrong over time. Examples:
- "X company will hit ARR milestone by Q3" (prediction)
- "Y founder is going to struggle with execution" (judgment)
- "I bet alice wins the round" (bet)

NOT gradeable (do NOT extract these):
- Pure facts ("X was founded in 2020")
- Direct quotes from others without endorsement

For each gradeable claim, output a JSON object with:
- claim_text   (string, <=200 chars, paraphrase or near-verbatim)
- kind         ('prediction' | 'judgment' | 'bet')
- holder       ('world' | 'people/<slug>' | 'brain' — default 'brain')
- weight       (0..1 inferred from hedging language: 'I bet'=0.7-0.85,
                'I think'=0.5-0.7, 'maybe'=0.3-0.5)
- domain       (short tag — e.g. 'tactics', 'macro', 'hiring')

Output ONLY a JSON array. No prose. If no gradeable claims, return [].
从下面的散文里抽取「可评判的主张」。

「可评判的主张」是一个预测、建议或解读性判断——它会随时间被
验证对错。例如:
- "X 公司会在 Q3 前达到 ARR 里程碑"(预测)
- "Y 创始人在执行上会很吃力"(判断)
- "我赌 alice 拿下这一轮"(赌注)

不要抽取这些:
- 纯事实("X 成立于 2020")
- 转述他人原话但未表态认同

对每条可评判主张,输出一个 JSON 对象:
- claim_text  (字符串, <=200 字, 转述或近似原文)
- kind        ('prediction' | 'judgment' | 'bet')
- holder      (谁持有此判断: 'world' | 'people/<slug>' | 'brain', 默认 'brain')
- weight      (0..1, 从措辞把握度推断: "我赌"=0.7-0.85,
               "我觉得"=0.5-0.7, "也许"=0.3-0.5)
- domain      (短标签 — 如 'tactics'、'macro'、'hiring')

只输出 JSON 数组。不要散文。没有可评判主张就返回 []。

"我赌"映射到 0.7-0.85、"也许"映射到 0.3-0.5——把人类的含糊措辞翻译成可计算的信念强度,这是整个校准系统的起点。

💡点睛:这一步只写"提案",不碰正式 takes 表——做梦可以大胆假设,但要不要采纳,留给你醒来决定。
grade-takes:给半年前的自己打分

对至少 6 个月前的判断,检索证据,请一个裁判模型裁定结果:对 / 错 / 部分对 / 还无法判定。默认自动采纳(confidence ≥ 0.95 才行),还可三模型投票。这是让大脑"对自己的过度自信负责"的机制。

真实 prompt · 给预测打分 cycle/grade-takes.ts:52PROMPT
You are grading a single forecasting take. The author made this claim
on the given date. Based on the evidence provided, did the claim turn
out to be:
- correct        (the world plays out as predicted)
- incorrect      (the world clearly contradicts the prediction)
- partial        (some aspects right, some wrong; right direction wrong magnitude)
- unresolvable   (insufficient evidence; outcome still pending)

Output ONLY one JSON object with these fields:
- verdict        ('correct' | 'incorrect' | 'partial' | 'unresolvable')
- confidence     (number in [0,1]) — your confidence in this verdict.
- reasoning      (string, <=400 chars) — what evidence drove the verdict.

If the evidence is sparse or ambiguous, return verdict='unresolvable'
with confidence reflecting the lack of evidence (NOT certainty of
unresolvable).
你在给一条预测性的 take 打分。作者在给定日期做出了这个判断。
基于提供的证据,这个判断最终是:
- correct        (世界如预测般发生)
- incorrect      (世界明确与预测相反)
- partial        (部分对部分错;方向对但量级错)
- unresolvable   (证据不足;结果仍未定)

只输出一个 JSON 对象:
- verdict        ('correct' | 'incorrect' | 'partial' | 'unresolvable')
- confidence     ([0,1] 的数) — 你对这个裁决的把握。
- reasoning      (字符串, <=400 字) — 哪些证据驱动了这个裁决。

如果证据稀少或含糊,返回 verdict='unresolvable',且 confidence
反映的是「证据缺乏的程度」(而不是「你对 unresolvable 这件事的确信」)。

最后那句是整个校准设计的精髓:"unresolvable 的 confidence 反映证据缺乏程度,而非你对 unresolvable 的确信"——区分"我不知道"和"我确定无法知道"。

💡点睛:一个会给自己旧预测打分的大脑,才能告诉你"你在融资估值上总是过于乐观"——记忆系统因此长出了元认知。
voice gate:说人话,别像论文

大脑对你说的每句话(提醒、模式洞察、晨间速览)都要先过一道语气闸门:用 Haiku 判定它听起来像"朋友聊天"还是"临床/企业腔"。学术腔被打回,最多重试 2 次,再不行就用手写模板兜底。五个出口共用一套 rubric,防止语气标准分裂。

真实 prompt · 语气裁判 + rubric calibration/voice-gate.ts:94PROMPT
You are the voice gate for a personal AI brain. A surface wants to show
this candidate text to the user. Decide whether it sounds conversational
(friend talking to friend) or academic (clinical / corporate).

Output ONLY: {"verdict":"conversational"|"academic","reason":"<<=80 chars>"}.

RUBRIC (pattern_statement surface):
- Sounds like a smart friend recapping your record, not a doctor or HR.
- Uses second person ("your", "you").
- Names numbers grounded in actual takes ("2 of 3 missed"), NOT abstract
  metrics like "Brier 0.31" or "conviction-bucket 0.8-0.9".
- No preachy/clinical phrasing ("our analysis indicates", "the data shows").
- Short — under 25 words.
你是个人 AI 大脑的「语气闸门」。某个出口想把这段候选文本展示给
用户。判断它听起来像「朋友间的聊天」还是「临床/企业腔」。

只输出: {"verdict":"conversational"|"academic","reason":"<<=80 字>"}。

RUBRIC(pattern_statement 出口):
- 听起来像个懂你的朋友在回顾你的战绩,不像医生或 HR。
- 用第二人称("你的"、"你")。
- 用扎根于真实 takes 的数字("3 次错了 2 次"),而不是抽象指标
  如 "Brier 0.31" 或 "conviction-bucket 0.8-0.9"。
- 不要说教/临床腔("我们的分析表明"、"数据显示")。
- 简短 — 25 字以内。

"2 of 3 missed" beats "Brier 0.31"——整个产品的文案哲学浓缩在这一行:数字要能被你验证,不要甩内部指标名。

💡点睛:连"怎么说话"都要过一道 LLM 闸门把关——一个第二大脑要长期陪着你,语气不对,再准也没人想听。
情感权重:哪页"重",不靠 LLM

做梦时还会给每页算一个 0..1 的情感权重——纯公式,无 LLM:情感标签命中(婚礼/丧亲/孩子...最高 0.5)+ take 密度 + 平均信念 + 你自己持有的判断占比。高权重的页面在"最近怎么了"里优先冒出来,让真正要紧的事盖过活跃但浅薄的话题。

真实代码 · 情感权重公式 cycle/emotional-weight.ts:27CODE
src/core/cycle/emotional-weight.tsGitHub ↗
export const HIGH_EMOTION_TAGS: ReadonlySet<string> = new Set([
  "family", "marriage", "wedding", "loss", "death", "grief",
  "relationship", "love", "mental-health", "health", "illness",
  "birth", "children", "kids", "parents",
]);

// 四项加权求和, 上限 1.0 — 零 LLM, 零 embedding:
//   1) 情感标签命中   max 0.5
//   2) take 密度      max 0.3  (每条 0.1)
//   3) take 平均信念  max 0.1
//   4) 你自持的占比   max 0.1
const total = tagBoost + takeDensity + takeAvgWeight + userHolderRatio;
return Math.max(0, Math.min(1, total));

没有 LLM、没有 embedding——一组标签加几个线性项,就足以让"婚礼"那页在"最近在发生什么"里排到活跃但浅薄的话题前面。

💡点睛:不是所有"重要"都需要 AI 判断。最朴素的加权和,反而最透明、最可控、最便宜。
enrich --thin:把大脑已知的,合成成页 NEW · v0.42.6

新建的实体常常只是个 stub(一句话占位)。enrich --thin 把散落各处的线索——会议记录、别人的页面、deal、facts、timeline——里关于这个实体的部分合成成一页带引用的 dossier。关键:它不联网(gbrain 的 LLM 只能看大脑内部),只把你已经知道的整理到一处;若大脑知道得太少,它直接 SKIP,绝不编造。

真实代码 · thin 的阈值与哨兵 enrich/thin.tsCODE
src/core/enrich/thin.tsGitHub ↗
// gbrain enrich --thin — 把大脑内部已知的零散信息合成成一页 (不联网)
export const DEFAULT_THIN_THRESHOLD = 400;   // 正文短于 400 字符 = stub, 值得充实
export const MIN_CONTEXT_CHARS = 200;        // 检索到的大脑上下文不足这么多 → 跳过, 不编造
export const MAX_CONTEXT_CHARS = 12_000;     // 喂给模型的证据上限 (token 预算)
export const SKIP_SENTINEL = "SKIP";         // 上下文太薄时模型返回的哨兵

检索到的大脑上下文进模型前还会过 INJECTION_PATTERNS 消毒(防 prompt 注入),和检索厅的查询消毒一脉相承。

💡点睛:"grounded synthesis"的灵魂是 no-slop:宁可留个 stub、返回 SKIP,也不拿模型的想象去填一页假资料。第二大脑的可信,正是从"知道太少就承认"开始的。
05

🔍 混合检索 Hybrid Retrieval

你问一个问题,gbrain 不靠单一策略。它把四路正交信号——语义向量、关键词、source 重排、知识图谱——用 RRF 无权重投票合并,再过 cross-encoder 重排。结果:P@5 从纯向量的 18 跳到 49

一次 query 的完整流水线

四路检索不是简单拼接,而是一条精心编排的管线:先分类意图、(可选)扩展查询,四路并发后用 RRF 融合取 top30,再做图谱增强cross-encoder 重排,最后 token 预算 + 去重。整条编排开销 <1ms,延迟全花在 embedding/rerank 的网络调用上。

真实数据 · 检索流水线全貌 RETRIEVAL.md:113DATA
intent classify          # 零 LLM 正则分类: entity/temporal/event/general
       │
       ▼
expansion (if enabled)    # detail=high 时, Haiku 生成 2-3 个查询变体
       │
       ▼
hybrid search:
   ├── vector  (HNSW on pgvector)        # 语义相似
   ├── keyword (BM25 via tsvector)       # 词法精确
   ├── source-aware re-rank (CASE in SQL)# 按来源加权
   └── RRF fusion → top 30               # 无权重投票合并
       │
       ▼
graph augment (typed-edge traversal)     # 沿知识图谱边遍历
       │
       ▼
reranker (zerank-2 cross-encoder)        # top30 重排, 60% top-1 被换
       │
       ▼
autocut (score-cliff result-sizing)      # v0.42.3 在分数悬崖处截断, 不返固定 K
       │
       ▼
token-budget → deduplication → results

benchmark(BrainBench 240 页语料):纯 BM25 / 纯向量 / 混合无图,P@5 全部停在 ~18;gbrain 全栈 = 49.1。文档原话:"图不是边缘功能,是承重墙。"

💡点睛:三种"聪明"策略(向量/关键词/混合)的天花板都是 18;捅破它的是知识图谱——能回答"Bob 这季度投了谁"这种向量永远看不见的因果链。
RRF:让每个策略各投一票

向量的余弦分和 BM25 的 tf-idf 活在完全不同的量纲,没法直接比大小。RRF 的妙处:它不看分数,只看名次——每个结果在各路结果里的排名都贡献 1/(k+rank) 一票,累加。彻底绕开了"跨策略归一化"这个老大难。

真实代码 · RRF 融合 hybrid.ts:1514CODE
src/core/search/hybrid.tsGitHub ↗
export function rrfFusionWeighted(
  lists: Array<{ list: SearchResult[]; k: number }>,
  applyBoost = true,
): SearchResult[] {
  const scores = new Map<string, { result: SearchResult; score: number }>();
  for (const { list, k } of lists) {
    for (let rank = 0; rank < list.length; rank++) {
      const r = list[rank];
      const key = `${r.slug}:${r.chunk_id ?? r.chunk_text.slice(0, 50)}`;
      const rrfScore = 1 / (k + rank);   // 只看名次, 不看分数
      const existing = scores.get(key);
      if (existing) existing.score += rrfScore;   // 各路的票累加
      else scores.set(key, { result: r, score: rrfScore });
    }
  }
  // ...归一化 + 按累计票数排序
}

意图还会动态调 k 值:事件类查询给关键词更高权重(k 更小、票更重),同一个 RRF 公式实现了"意图感知"的策略倾斜。

💡点睛:RRF 用"名次"而非"分数"投票——这一个选择,让四种活在不同量纲里的检索策略可以公平地坐到同一张桌子上。
多查询扩展:一个问题问三遍

深度检索时,gbrain 用一个 Haiku 级模型把你的 query 改写成 3-4 个不同角度的变体,每个各自走完整四路检索,再 RRF 合并——捕捉同义词漏召。注意:进 LLM 前 query 先被 sanitizeQueryForPrompt 洗一遍(剥代码块、删 ignore/system/override 开头的注入模式)。

真实 prompt · 查询扩展 ai/gateway.ts:2035PROMPT
Rewrite the search query below into 3-4 different, related queries that
would help find relevant documents.
Return ONLY the JSON object. Do NOT include the original query in the result.
Each rewrite should emphasize different aspects, synonyms, or framings.

Query: ${query}
把下面的搜索 query 改写成 3-4 个不同但相关的 query,以帮助找到相关文档。
只返回 JSON 对象。不要在结果里包含原始 query。
每个改写应强调不同的方面、同义词或表述角度。

Query: ${query}

扩展的真正价值不是让一个 query 更准,而是让三个不同视角的 query 各自独立召回,再由 RRF 免权重投票合并,覆盖同义词盲区。

💡点睛:用户的搜索词只是冰山一角。与其逼一个 query 更精确,不如换三个角度各问一遍——这比任何 embedding 调参都更能救回同义词漏召。
evidence 契约:别让 agent 猜阈值

曾有个 bug:agent 看到检索分 0.64,判断"无强匹配,可以安全新建页",结果写了重复页——因为混合分根本不是校准过的概率。修法很优雅:每条结果带一个 evidence(最强信号来源)和 create_safety(exists/probable/unknown)。agent 直接读"能不能写",不再猜分数阈值。

真实代码 · evidence / create_safety 枚举 search/evidence.ts:31CODE
src/core/search/evidence.tsGitHub ↗
export type Evidence =
  | "alias_hit" | "exact_title_match" | "high_vector_match"
  | "keyword_exact" | "weak_semantic";

export type CreateSafety = "exists" | "probable" | "unknown";

export function createSafetyFor(evidence: Evidence): CreateSafety {
  switch (evidence) {
    case "alias_hit":
    case "exact_title_match":
    case "high_vector_match":
      return "exists";       // 这页已存在, 别新建
    case "keyword_exact":
      return "probable";     // 八成存在
    case "weak_semantic":
      return "unknown";      // 拿不准
  }
}

把一个"连续分数 + 阈值判断"的问题,转成一个"枚举值 API"——agent 不必理解 RRF 的量纲,只需读 create_safety

💡点睛:给 agent 的不该是一个需要它猜阈值的原始分数,而是一个它能直接照做的枚举判断——好的接口替 agent 把决策想清楚了。
autocut:该返几条,看分数悬崖 NEW · v0.42.3

老问题:固定返 top-K 噪声大——明明 1 条就够,却塞 20 条。autocut(Weaviate 式)不返固定数,而是在 reranker 分数曲线断裂处切:答案明显时返 1 条,确实有好几条时返那几条,绝不因为 K 是上限就硬凑。这修掉了"20 vs 1"的精度问题。

真实代码 · autocut 配置与决策 search/autocut.tsCODE
src/core/search/autocut.tsGitHub ↗
// autocut — 在 reranker 分数曲线断裂处切, 不返固定 top-K (v0.42.3.0)
export const DEFAULT_AUTOCUT: AutocutConfig = Object.freeze({
  enabled: true,
  jumpRatio: 0.2,   // 跌幅 ≥ 顶部分数的 20% 算一个"悬崖"
  minKeep: 1,       // 永不返回 0 条
});

export interface AutocutDecision {
  applied: boolean;
  signal: "rerank" | "none";   // rerank=切在真悬崖; none=无信号/无悬崖
  cut: number;                 // 切点(保留几条)
  kept: number; total: number;
  gapRatio: number;            // 观测到的最大归一化跌幅
}

只在 reranker 之后、且只第一页跑;reranker 因 auth/网络/超时没出分时 autocut 自动 no-op(回退 RRF 顺序),并靠 minKeep=1 永不返空。

💡点睛:为什么只信 reranker 的分数悬崖?gbrain 实测:RRF/cosine 的 rank1→rank2 落差,无论 rank-1 对(0.602)还是错(0.569)都几乎一样——是机械衰减,不是可信分界。只有 cross-encoder 的相关性分数才是真悬崖
06

🔗 Context Engine 衔接 The Bridge to OpenClaw

这就是上一座 openclaw 馆第 5 站那个可替换的「context engine」的本体。gbrain 注册成 openclaw 的插件,在每个 turn 注入实时的时间/地点/日程 + 检索到的记忆——专治 compaction 把对话压缩后丢失"现在几点、人在哪"的 time-warp。

注册成 openclaw 的 context engine

还记得 openclaw 第 5 厅讲的 plugins.slots.contextEngine 吗?默认是内置的 legacy 引擎(原样透传)。gbrain 把它换成 gbrain-context——一个插件,在 openclaw 每次组装上下文时被调用。两座馆在这里咬合。

真实代码 · 插件入口 openclaw-context-engine.ts:52CODE
src/openclaw-context-engine.tsGitHub ↗
const entry: PluginEntry = {
  id: "gbrain-context-engine",
  name: "GBrain Context Engine",
  description: "Deterministic temporal/spatial context injection on every turn",

  register(api: PluginApi) {
    api.registerContextEngine(ENGINE_ID, (ctx) =>
      createGBrainContextEngine({ workspaceDir: ctx.workspaceDir }),
    );
  },
};
export default entry;

openclaw.json 里一行 "contextEngine": "gbrain-context" 就把内置引擎换成了 gbrain——上一座馆里那个"可插拔的 context engine 槽",这里插的就是它。

💡点睛:openclaw 留了一个 context engine 插槽,gbrain 正好是插进去的那块记忆芯片——一个定接口,一个填实现,两个项目就这样合体。
每个 turn 都重建一次"此刻"

gbrain 的 assemble() 在每个 turn 被调用时,生成一段确定性的"现场上下文"(<5ms,零 LLM):当前时间、时区、地点、正在进行的会议、未来 4 小时日程、待办——再叠加检索到的记忆,作为 systemPromptAddition 注入。历史消息原样透传。

真实代码 · assemble 注入逻辑 core/context-engine.ts:571CODE
src/core/context-engine.tsGitHub ↗
async assemble({ messages, availableTools, citationsMode }) {
  // 1. 生成确定性现场上下文 (<5ms, 零 LLM)
  const liveCtx = generateLiveContext(workspaceDir);
  const contextBlock = formatContextBlock(liveCtx);

  // 2. 叠加记忆 prompt (若 memory 插件激活)
  const memoryAddition = _buildMemorySystemPromptAddition?.({ availableTools, citationsMode });

  const parts = [contextBlock];
  if (memoryAddition) parts.push(memoryAddition);

  return {
    messages,                                  // 历史原样透传
    systemPromptAddition: parts.join("\n\n"),  // ← 注入到 system prompt
  };
}

对照 openclaw 馆第 5 厅:内置 legacy 引擎的 assemble 是 pass-through;gbrain 的 assemble 多做了一件事——每轮从磁盘重建"此刻"。

💡点睛:compaction 可以放心大胆地压缩历史,因为下一个 turn 的 assemble() 总会从磁盘重建一个精确的"此刻"——这是 gbrain 与 openclaw 之间最关键的接缝。
注入的真身:一段"现场播报"

这是真正被塞进 system prompt 的文本模板。最后一句是点睛之笔:"这一块每轮都重新计算,时间/地点/活动以它为准,别信压缩摘要。" ——直接命令模型不要被压缩后的旧上下文带偏。

真实数据 · 注入的 Live Context 模板 context-engine.ts:495DATA
## Live Context (deterministic, injected by gbrain-context engine)
- **Time:** ${ctx.now} (${ctx.timezone})
- **Day:** ${ctx.dayOfWeek}
- **Location:** ${ctx.location.city} (source: ${ctx.location.source})
- **Active travel:** ${ctx.activeTravel}        # 仅旅行中
- **Right now:** ${currentEvent}                # 进行中的会议
- **Coming up:** ...                            # 未来 4 小时最多 3 个
- **Open tasks:** ...                           # 最多 5 个

> This block is computed on every turn. Trust it over compaction
> summaries for time/location/activity.

遇到正在飞行、目的地机场不在已知映射表时,引擎拒绝猜测时区(返回 UNKNOWN_TZ),渲染"时区未知"警告,而不是给一个看起来可信却错误的本地时间——这正是 time-warp bug 的根源。

💡点睛:"宁可说不知道,也不给一个可信的错误"——一颗诚实的第二大脑,知道自己什么时候不该假装知道。
07

🧩 生态:Skills · MCP · 多人 The Ecosystem

thin harness, fat skills:薄薄的 CLI 只管执行循环,真正的能力封装在 60+ 个 markdown skill 里。MCP 把同一套 operations 同时暴露给 Claude Code 和远程;而 git,让多个 agent 共享同一颗大脑。

一个 skill 就是一个 markdown 文件

能力不写在代码里,写在 markdown 里。每个 skill 有一段 YAML frontmatter 声明 triggers(触发短语)、tools(可用工具)、mutating(是否改数据)。agent 在运行时解析 frontmatter 完成路由,而不是任何硬编码的 if/else。一个 skill 同时是文档、规格、软件包和方法调用。

真实数据 · query skill 的 frontmatter skills/query/SKILL.mdDATA
---
name: query
description: |
  Answer questions using the brain's knowledge with 3-layer search,
  synthesis, and citation propagation.
triggers:
  - "what do we know about"
  - "who is"
  - "relationship between"
  - "connections"
  - "graph query"
tools:
  - search
  - query
  - get_page
  - traverse_graph
  - get_timeline
mutating: false
---

agent 每收到一条消息,就遍历所有 skills/*/SKILL.md 的 frontmatter,把消息和每个 skill 的 triggers 做匹配来路由——零硬编码。

💡点睛:把"能力"从代码里解放到 markdown 里,于是新增一个技能 = 写一个 .md 文件,而不是改 harness 一行代码——这就是 "Homebrew for Personal AI"。
MCP:一套定义,两个出口

同一套 operations 定义(单一 source of truth),由 buildToolDefs 自动生成 MCP 工具 schema,同时暴露给 stdio(Claude Code / Desktop)和 HTTP(跨机器远程)。杜绝了"多个调用路径各写一份 schema 然后漂移"的经典坑。

真实代码 · operations → MCP schema mcp/tool-defs.ts:40CODE
src/mcp/tool-defs.tsGitHub ↗
export function buildToolDefs(ops: Operation[]): McpToolDef[] {
  return ops.map(op => ({
    name: op.name,
    description: op.description,
    inputSchema: {
      type: "object" as const,
      properties: Object.fromEntries(
        Object.entries(op.params).map(([k, v]) => [k, paramDefToSchema(v)]),
      ),
      required: Object.entries(op.params)
        .filter(([, v]) => v.required).map(([k]) => k),
    },
  }));
}

MCP stdio 调用者被当作远程/不可信(remote: true),和 HTTP 走同一套 dispatch——信任策略统一,不给绕过留缝。

💡点睛:单一 source of truth 生成所有出口的 schema——这是"别重复自己"在工具定义层的体现,也是跨进程一致性的地基。
gbrain connect:给编码 agent 装上记忆 NEW · v0.42.2

Claude Code、Codex 很会写代码,却对别的健忘——忘了上次对话、说不出三次会议前的决定。gbrain connect 一条命令把大脑经 MCP 接进它们:回答前先查你的大脑,边干边把新知识写回。两条路径——已有 brain 的远程接入,或从零本地起一个。

真实文档 · 接入编码 agent docs/tutorials/connect-coding-agent.mdDATA
Path A — 已有 brain (OpenClaw / Hermes / 任意 gbrain serve 宿主):
         让笔记本上的 Claude Code / Codex 连同一个大脑 (远程, 带 token)

Path B — 从零: 2 秒起一个本地大脑, 接进你的编码 agent (本地)

前置: bun install -g github:garrytan/gbrain
两条路最终都到同一处: agent 回答前先搜你的大脑, 边干边把新知识写回。

来自 docs/tutorials/connect-coding-agent.md(v0.42.2 新增)。这是 gbrain 从"自有 agent 的记忆"走向"任何 MCP 客户端的记忆层"的一步。

💡点睛:大脑不再绑定某一个 agent——经 MCP,它成了 Claude Code、Cursor、Codex 谁都能接的一层共享记忆。
多人协作:冲突交给 git

因为大脑是 git repo、DB 只是派生缓存,多个 agent 写同一颗大脑时,合并点是 git,不是数据库锁。我的 agent 可以往你的 brain repo push 内容,你的 agent 下次 sync 就接住了。并发编辑,git 怎么处理它就怎么处理。

真实数据 · git 即同步层 system-of-record.md:24DATA
- Multi-machine sync is git. Push from one machine, pull from
  another, and the second machine's DB rebuilds on its next sync.
  No "back up the database" step.

- Cross-agent collaboration is possible. Multiple agents can write
  to the same brain because the fence is the merge point, not the DB.
  Git handles concurrent edits the way git handles concurrent edits.

"the fence is the merge point, not the DB"——合并发生在 markdown 的 fence 层,而不是数据库。这让多 agent 协作天然继承 git 的冲突解决。

💡点睛:当大脑是一个 git repo,"多人共享一颗大脑"就不需要发明任何新东西——push、pull、merge,人类协作了几十年的老办法,直接拿来给 agent 用。

🧠 逛完了 A Brain, Disassembled

你刚刚拆完了一颗第二大脑:它把记忆在 git 里、从信息里出事实与判断、隔夜做梦般自我整理、用四路检索回忆、再作为 context engine 注入给 openclaw 的每一次对话。

gbrain 最反直觉的一点:它几乎处处依赖 LLM。建图靠正则,情感权重靠加权和,意图分类靠正则,灾难恢复靠 git。LLM 只在真正需要"理解语义"的地方出现——提炼判断、给预测打分、扩展查询、把关语气。把确定性的事交给确定性的代码,把模糊的事才交给模型——这是一颗可靠的第二大脑的工程纪律。

↑ 回到顶部可重新沿展厅导览;想深挖任意一处,顺着展品里的 GitHub 链接跳进真实源码。