第十一章:高级主题(Advanced Topics)#
本章覆盖 GBrain 中较为深入或特殊的主题,适合有一定基础的读者。不讲安装和 CLI 用法,直接深入内部设计。
11.1 Cathedral II 两遍检索深度解析#
11.1.1 什么是两遍检索#
Cathedral II 是 GBrain v0.20.0 引入的结构化检索机制,核心思想是:先用快速但粗糙的方式找到候选 Chunk,再在候选 Chunk 内做精确匹配。这个两遍设计专门解决传统 RAG 的「中间丢失」问题——当答案跨越多个 Chunk 时,单遍检索只能返回「最相关」的单个 Chunk,导致中间上下文丢失。
flowchart TD
A["用户查询"] --> B["Pass 1: 向量/关键词搜索<br/>快速找到 Anchor Chunks"]
B --> C["expandAnchors() 扩展结构邻居"]
C --> D{"walkDepth > 0?"}
D -->|是| E["遍历 code_edges_chunk<br/>顺着调用边扩展"]
D -->|否| F["直接返回 anchors"]
E --> G["遍历 code_edges_symbol<br/>通过符合名称扩展"]
G --> H["Pass 2: hydrateChunks()<br/>补全元数据"]
H --> I["返回带调用图上下文的 SearchResult"]
11.1.2 第一遍:锚点搜索#
第一遍使用标准的混合搜索(Hybrid Search)找到初始候选集。这一步牺牲精度换速度:
向量搜索:在高维向量空间中找语义相似的 Chunk(cosine similarity)
关键词搜索:用
pg_trgm做三字母组匹配,处理拼写错误和部分匹配RRF 融合:两种结果用 Reciprocal Rank Fusion 合并排序
// search/two-pass.ts 核心接口
export interface TwoPassOpts {
/** 1 或 2 — 最大 2,跳数控制爆炸半径 */
walkDepth?: number;
/** 限定符号名匹配,附加到锚点集 */
nearSymbol?: string;
/** 限定来源 ID,跨来源搜索 */
sourceId?: string;
}
11.1.3 第二遍:结构邻居扩展#
第一遍的结果叫 Anchor Chunks。第二遍通过代码边(code edges)扩展这些锚点:
直接边(
code_edges_chunk):Chunk 之间的直接调用关系,to_chunk_id非空符号边(
code_edges_symbol):通过函数/变量名关联,to_symbol_qualified非空
每扩展一跳,score 乘以衰减系数 1/(1+hop):
// 跳数衰减示意
hop=0 (锚点自身): score * 1/(1+0) = score * 1.0
hop=1 (直接邻居): score * 1/(1+1) = score * 0.5
hop=2 (邻居的邻居): score * 1/(1+2) = score * 0.33
扩展有严格上限:深度最多 2 跳,每跳最多 50 个邻居。这是为了防止高扇出代码(如大型开源库)产生爆炸性检索。
11.1.4 解决「中间丢失」问题#
传统 RAG 的「中间丢失」问题:
文档 D = [Chunk A] [Chunk B] [Chunk C] [Chunk D] [Chunk E]
用户问: "B 和 D 之间的关系"
单遍检索: 返回最相关的单个 Chunk,比如 C
结果: 缺少 B→C→D 的传递路径
Cathedral II 的解法:
Pass 1: 找到锚点 Chunk C(命中关键词 "relationship")
Pass 2: 扩展 C 的结构邻居,发现 B 和 D 都与 C 有调用边
结果: 返回 [B, C, D] — 完整的传递路径
图感知还带来了文档关系理解:通过链接分析,A 页面的链接指向哪些 Chunk,帮助理解文档间的引用关系。
11.1.5 实现文件#
核心实现:search/two-pass.ts
函数 |
作用 |
|---|---|
|
从锚点出发,沿边扩展 N 跳,返回 |
|
根据 chunk_id 批量补全 |
11.2 后台作业系统(Minions)#
11.2.1 为什么要 Minions#
有些任务天然很慢,不应该阻塞主流程:
大页面向量化(Embedding):可能涉及数万个 token
Git sync:网络请求 + diff 计算
数据库迁移:大表 ALTER 操作
传统方案是引入 Redis 或 RabbitMQ 等外部队列。GBrain 的 Minions 选择了更轻量的路:用 PGLite 本身存储任务状态,不依赖额外的基础设施。
11.2.2 架构概览#
flowchart LR
subgraph 主流程
A["主进程<br/>BrainEngine"] --> B["MinionQueue<br/>任务入队"]
end
subgraph Worker进程
C["MinionWorker<br/>抢占式拉取"] --> D["Handler<br/>任务处理"]
D --> E["结果回写<br/>PGLite"]
end
B -->|"poll 'waiting' 任务<br/>SETFORUPDATE SKIP LOCKED"| C
E --> B
style A fill:#e1f5ff
style C fill:#fff3e0
style E fill:#e8f5e9
11.2.3 队列实现:不用外部依赖#
Minions 的任务队列本质是一张 PostgreSQL 表:
CREATE TABLE minions_jobs (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL, -- 任务类型: 'embedding', 'git-sync', 'migrate'
queue TEXT NOT NULL DEFAULT 'default',
status TEXT NOT NULL, -- waiting/active/completed/failed/delayed/dead
priority INTEGER DEFAULT 0,
data JSONB NOT NULL, -- 任务负载
attempts_made INTEGER DEFAULT 0,
max_attempts INTEGER DEFAULT 3,
backoff_type TEXT DEFAULT 'exponential',
backoff_delay INTEGER DEFAULT 1000,
-- 等等...
);
Worker 抢占式拉取任务,用 SELECT ... FOR UPDATE SKIP LOCKED 确保多 Worker 不会抢到同一个任务:
SELECT * FROM minions_jobs
WHERE status = 'waiting' AND queue = $1
ORDER BY priority DESC, id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED;
11.2.4 心跳机制#
Worker 定期更新任务状态为 active,同时更新 attempts_started 时间戳。如果主进程发现某个 active 任务的 attempts_started 超过阈值(默认 60 秒),就认为该 Worker 已崩溃,将任务重新放回 waiting 队列等待重试。
// 60 秒无心跳,任务重新入队
const STALLED_INTERVAL_MS = 60_000;
11.2.5 主要任务类型#
任务类型 |
Handler |
说明 |
|---|---|---|
大页面向量化 |
|
超过单次 embedding 上限的大页面,入队后台处理 |
Git sync |
|
定时从远程仓库拉取更新 |
迁移任务 |
|
|
11.3 Resolver 系统#
11.3.1 Resolver 是什么#
Resolver = 从外部数据源获取信息的模块。它是 GBrain 与外部世界交互的桥梁。
当你问「@alice 今天发了什么推文」,Resolver 负责:
解析输入(
@alice→ X handle)调用 X API 获取数据
格式化结果返回
11.3.2 统一接口设计#
所有 Resolver 实现统一的 resolve(input): Promise<Output> 接口:
// resolvers/interface.ts
export interface Resolver<I, O> {
readonly id: string; // 唯一标识,如 "x_handle_to_tweet"
readonly cost: ResolverCost; // 'free' | 'rate-limited' | 'paid'
readonly backend: string; // "x-api-v2", "brain-local", "head-check"
// 上下文感知:能读取 brain 自身数据
available(ctx: ResolverContext): Promise<boolean>;
resolve(req: ResolverRequest<I>): Promise<ResolverResult<O>>;
}
export interface ResolverResult<O> {
value: O;
confidence: number; // 0.0-1.0,LLM 回的有可能是推断的
source: string; // 来源标识
fetchedAt: Date;
costEstimate?: number; // 美元成本估算
}
关键设计原则:confidence 机制。LLM 提取的信息 confidence < 1.0,直接 API 响应的 confidence = 1.0。调用方用这个字段决定是否要人工确认。
11.3.3 调用链#
flowchart TD
A["LLM / Agent"] --> B["ResolverRegistry.resolve()"]
B --> C{"available()?"}
C -->|否| D["ResolverError: unavailable"]
C -->|是| E["resolver.resolve()"]
E --> F["返回 ResolverResult"]
subgraph 内置 Resolver
G["GitHub Resolver"]
H["X (Twitter) Resolver"]
I["Notion Resolver"]
J["Brain-local Resolver"]
end
E --> G
E --> H
E --> I
E --> J
11.3.4 内置 Resolver#
Resolver |
输入 |
输出 |
费用 |
|---|---|---|---|
|
X handle |
用户最新推文 |
API 费用 |
|
issue URL |
issue 内容 + 评论 |
API 费用 |
|
slug |
brain 页面内容 |
免费 |
|
URL |
HTTP HEAD 检查结果 |
免费 |
11.3.5 自定义 Resolver#
自定义 Resolver 需要:
实现
Resolver<I, O>接口在
resolvers/registry.ts中注册
// 示例:自定义 URL 检查 Resolver
const myResolver: Resolver<string, boolean> = {
id: 'url_reachable',
cost: 'free',
backend: 'head-check',
available: async (ctx) => !!ctx.config['networkEnabled'],
resolve: async (req) => ({
value: await checkUrl(req.input),
confidence: 1.0,
source: 'head-check',
fetchedAt: new Date(),
}),
};
11.4 迁移框架#
11.4.1 为什么需要迁移框架#
GBrain 使用 PGLite(嵌入式 PostgreSQL)或完整 Postgres + pgvector。随着版本迭代,数据库 schema 会发生变化。迁移框架确保 schema 演进时数据不丢失、升级平滑。
典型场景:
新增表或索引
修改字段类型
重建既有数据(如
slugify_existing_pages)
11.4.2 迁移设计#
// core/migrate.ts
interface Migration {
version: number; // 版本号,必须单调递增
name: string; // 人类可读名称
sql: string; // 迁移 SQL
sqlFor?: { // 引擎特定 SQL
postgres?: string; // Postgres 的版本(如 CONCURRENTLY)
pglite?: string; // PGLite 的版本
};
transaction?: boolean; // 是否在事务中执行(默认 true)
handler?: (engine: BrainEngine) => Promise<void>; // TS 级数据转换
}
11.4.3 执行顺序#
迁移按版本号顺序执行,每个迁移都在事务中运行:
-- 伪代码
BEGIN;
UPDATE schema_versions SET version = $new_version WHERE version = $old_version;
$sql
COMMIT;
如果 SQL 失败,版本号不变,下次启动时重试。
11.4.4 迁移命名约定#
Version 1: 基线(schema.sql 创建所有表,IF NOT EXISTS)
Version 2: slugify_existing_pages — 重命名既有页面 slug
Version 3: unique_chunk_index — 去重 + 添加唯一索引
Version 4: access_tokens_and_mcp_log — 新增访问令牌表
...
规则:从不修改已发布的迁移,只在末尾追加新迁移。每个迁移必须幂等(重复执行结果相同)。
11.5 安全机制#
11.5.1 remote 字段的信任边界#
remote 字段是 GBrain 安全模型的核心,它区分了两类调用者:
调用来源 |
|
含义 |
|---|---|---|
|
|
受信任的本地用户 |
|
|
不受信任的外部 Agent |
信任边界规则:
remote = false:允许文件系统全权访问,允许执行 shell 命令remote = true:启用严格的文件路径限制( confinement),SSRF 防护激活
11.5.2 SQL Injection 防护#
GBrain 使用 参数化查询(Parameterized Query)防止 SQL 注入:
// ✅ 正确:参数绑定
await engine.executeRaw(
`SELECT id FROM content_chunks WHERE symbol_name_qualified = $1`,
[userInput] // 参数绑定
);
// ❌ 错误:字符串拼接(绝对禁止)
await engine.executeRaw(
`SELECT id FROM content_chunks WHERE symbol_name_qualified = '${userInput}'`
);
所有 engine.executeRaw() 调用都使用 $1, $2 参数占位符,由底层数据库驱动处理转义。
11.5.3 外部内容导入时的 Sanitization#
当从外部(URL、API 响应)导入内容到 brain 时:
HTML 净化:使用
marked解析 Markdown 时,禁用原始 HTML路径穿越防护:文件路径必须位于
root目录下,symlink 会被拒绝Frontmatter 验证:不合法的 frontmatter 字段被丢弃
// import-file.ts 中的净化逻辑示例
const sanitized = {
...frontmatter,
// 移除危险字段
_raw: undefined,
__proto__: undefined,
constructor: undefined,
};
11.6 MCP 服务端#
11.6.1 什么是 MCP#
MCP(Model Context Protocol) 是一种标准协议,让 AI Agent 可以调用外部工具。GBrain 作为 MCP Provider,把自身操作暴露为 MCP tools,供 Claude Desktop、Cursor 等 AI 工具调用。
11.6.2 服务端实现#
// mcp/server.ts
export async function startMcpServer(engine: BrainEngine) {
const server = new Server(
{ name: 'gbrain', version: VERSION },
{ capabilities: { tools: {} } }
);
// 1. 暴露工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: buildToolDefs(operations), // 从 operations.ts 导出工具定义
}));
// 2. 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: params } = request.params;
const ctx: OperationContext = {
engine,
remote: true, // MCP 调用一律视为 untrusted
// ...
};
const result = await op.handler(ctx, params);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});
await server.connect(new StdioServerTransport());
}
11.6.3 工具定义#
tool-defs.ts 把 operations.ts 中的 Operation 映射为 MCP tool:
// mcp/tool-defs.ts
export function buildToolDefs(ops: Operation[]): McpToolDef[] {
return ops.map(op => ({
name: op.name,
description: op.description,
inputSchema: {
type: 'object',
properties: Object.fromEntries(
Object.entries(op.params).map(([k, v]) => [k, { type: v.type }])
),
required: Object.entries(op.params)
.filter(([, v]) => v.required)
.map(([k]) => k),
},
}));
}
11.6.4 和直接 CLI 调用的区别#
维度 |
CLI 调用 |
MCP 调用 |
|---|---|---|
|
|
|
文件限制 |
允许访问 home 目录 |
限制在 |
Shell 执行 |
允许 |
受限 |
用途 |
人类用户直接操作 |
AI Agent 自动化调用 |
本章小结#
本章深入介绍了 GBrain 的几个高级主题:
Cathedral II 通过两遍检索解决了传统 RAG 的「中间丢失」问题,利用代码结构边扩展检索范围
Minions 用 PGLite 本身做队列存储,实现了零外部依赖的后台作业系统
Resolver 提供统一接口连接外部数据源,confidence 机制确保结果可靠性
迁移框架 用版本号 + 幂等 SQL 保证 schema 演进安全
安全机制 通过
remote字段区分信任边界,参数化查询防注入MCP 把 GBrain 操作暴露为标准协议工具,供 AI Agent 调用
这些机制共同支撑了 GBrain 作为「个人知识大脑」的可靠性和可扩展性。
GBrain v0.22.5 · 源码路径:/home/liyifan/gbrain/src/