---
title: 知识库 API
description: Gem 通过 ctx.knowledge 读写用户知识库:概念、权限字符串、完整 API、写入规则与卸载行为。
section: dev
order: 45
updated: 2026-11-01
---
# 知识库 API
用户的长期知识存放在知识库里:关于用户和他认识的人的**事实**,以及日记、留存等**文档**。Gem 通过 `ctx.knowledge` 访问它,能访问多少完全取决于用户在 gem 的页面上打开了哪些权限——装好时一条都没有。
几条先要知道的边界:
- **写进知识库的内容归用户,不归 gem。** 卸载 gem 时,它的私有存储会被彻底删除;它写进知识库的记录默认保留,用户可以在卸载时选择一起删除。
- **你拿不到内部 id。** 所有实体、事实、文档、知识库都以不透明的 `ref` 出现,不同 gem 拿到的 ref 各不相同。
- **邮件不在知识库里。** 读邮件请申请 `email` 能力(`ctx.email`);知识库只保存从邮件中提炼出的背景信息。
- **一个组织一套数据。** 个人和各个组织的数据在物理上分开存放,gem 只能看到它所在的那一份,不存在跨组织访问。
## 概念
| 概念 | 说明 |
| --- | --- |
| 实体(entity) | 一个人、组织、地点……以及用户本人(`self`)。实体有 `kind`(`person`、`org`、`place` 等)、显示名和标识(邮箱、电话、域名、网址、社交账号)。 |
| 事实(fact) | 「主语 · 谓词 · 值」,例如 `self · pref.food.dislike · 香菜`。值可以是任意 JSON;也可以指向另一个实体(例如同事关系)。事实会过期(被新值替代或被忘记),但不会被物理删除。 |
| 谓词(predicate) | 事实的类型,点号分隔,至少两级,例如 `location.home.city`。第一级固定为:`identity`、`contact`、`location`、`relation`、`pref`、`schedule`、`work`、`health`、`finance`、`x`(其他)。往下不固定:应用内置了一批常用谓词(见[核心谓词](#核心谓词)),用户和 gem 都可以在任何一级下面再加。 |
| 知识库(collection) | 文档的容器,也是授权的单位。内置 `diary`(日记,每天一篇)和 `keep`(留存);用户和 gem 也可以新建。知识库可以嵌套(文件夹里再建子文件夹,`diary` 除外);一篇文档只属于一个知识库。 |
| 文档(document) | 标题 + 正文(Markdown)+ 可选的 `meta`。写入后会自动切块、建立全文和语义索引,并识别其中提到的联系人。 |
## 在清单里申请
在 Gem Studio 的草稿 `manifest.json` 里,知识库权限是 gem 级字段,每个工具默认都能用(某个工具不需要时,在它的 `permissions` 里不列 `knowledge`)。也可以在 Gem Studio 概览的「知识库」一栏里逐条添加,写错的会直接列出来,和安装时的检查一样:
```json
{
"name": "recipes",
"description": "保存用户打过分的菜谱",
"kind": "tool",
"entry": "tools/rate.js",
"knowledge": {
"permissions": ["facts.read:self:pref.food.*", "facts.write:self:pref.food.*", "collections.create"],
"predicates": [
{ "predicate": "pref.food.rated", "cardinality": "many", "description": "你打过高分的菜,用来推荐相似的菜。例如:红烧肉", "aliases": ["高分菜"] }
],
"reason": "记住你喜欢的菜,并把菜谱存进你的知识库"
}
}
```
打包后的 `notochord.json` 里,同样的内容写在 `permissions.knowledge`,需要 `ctx.knowledge` 的工具在自己的 `permissions` 里写 `"knowledge": true`(Studio 生成清单时会自动完成):
```json
{
"permissions": { "knowledge": { "permissions": ["facts.read:self:location.home.*"] } },
"tools": [{ "name": "home", "entry": "tools/home.mjs", "permissions": { "knowledge": true }, "...": "..." }]
}
```
- `permissions`:权限字符串列表,见下一节。
- `predicates`:[核心谓词](#核心谓词)都不合适时,这个 gem 新加的谓词,安装时写进全局词表。它**不是权限**,只是声明词汇。`cardinality` 为 `one`(同一时间只有一个值,例如居住城市)或 `many`(可以有多个值,例如喜欢的菜)。
- `description` **必填**,最多 200 个字符,是**写给用户看的**:是什么、gem 为什么需要它、举个例子。gem 的页面上会把它显示在用户的值旁边,用户在那里补填或修改。
- `reason`:可选,显示在安装界面上。
## 权限字符串
格式统一为 `数据类型.动作:范围`。gem 的页面(「设置 › 知识库」)把每一条翻译成一句话列给用户,**全部默认关闭**,用户逐条打开;打开或关掉都在下一次调用时立即生效。没打开的权限,调用会被拒绝。
| 权限 | 安装界面显示为 | 说明 |
| --- | --- | --- |
| `facts.read:<主语>:<前缀>` | 读取你的居住地 | 主语是 `self` 或实体 kind(如 `person`)。前缀写 `a.b.*`(该前缀下所有谓词)或完整谓词 `a.b.c`。**前缀至少两级**,`self:*`、`self:location.*` 都会被拒绝。 |
| `facts.write:<主语>:<前缀>` | 记录你的饮食偏好 | 必须被某条 `facts.read` 覆盖。 |
| `entities.resolve:` | 根据邮箱等查询联系人的名字 | 单点查询:拿已知的邮箱或名字换回 ref 和显示名,不能列出全部。 |
| `entities.read:` | 查看你的联系人列表 | 列出该 kind 的全部实体及其标识。 |
| `entities.write:` | 新增和编辑联系人 | 新建实体、添加标识。包含 `entities.resolve:`。 |
| `collections.read:<目标>` / `collections.write:<目标>` | 读取你的日记 | 目标是 `diary`、`keep` 或 `user-selected`(用户装好后在 gem 的页面上勾选具体知识库)。对一个知识库的授权包含它里面的所有子文件夹,包括以后新建的。 |
| `collections.create` | 创建自己的知识库 | 新建的知识库自动对创建者可读可写,它里面的子文件夹也是。要建在某个知识库里面(`parent`),还需要对那个知识库有写权限。 |
| `changes.manage` | 查看和撤销所有应用写入的知识 | 可以查看、撤销、修改其他写入者的记录(仍限于上面授权的范围)。 |
| `origin.override` | 代表其他应用写入数据 | 写入时可以自己指定 `origin` 和 `source`,给「整理类」gem 用。 |
申请尽量窄:只读天气所需的城市,就只申请 `facts.read:self:location.home.*`。
两种失败方式要区分清楚:
- **查询类方法**(`profile.get`、`facts.lookup`、`search`、`collections.list`、`changes.list`)遇到没授权的部分**不报错**,只是不返回。你无法据此判断某条数据是否存在。
- **针对单个对象的方法**(写入、按 ref 读取、`entities.*`、`documents.list` 指定的知识库、创建)没有权限时**直接抛错**,错误信息里包含 `lacks knowledge permission <缺少的权限>`。
## API 参考
所有方法都是异步的。`collection` 参数可以是内置名 `'diary'`、`'keep'`,也可以是 `collections.list()` / `collections.create()` 返回的 ref。
### 读取事实
```js
await ctx.knowledge.profile.get('location.home')
// → [{ predicate: 'location.home.city', value: 'Utrecht' }]
```
常驻画像:用户本人被置顶(pinned)的事实,再与你的 `facts.read` 权限取交集。前缀可省略。
```js
await ctx.knowledge.facts.lookup('self', 'pref.food') // 或 lookup(entityRef, 'relation')
// → [{ ref, predicate, value, object: entityRef | null, validFrom }]
```
主语当前有效的事实。需要对应主语 kind 的 `facts.read`。**不会返回**来源、写入者、置信度,也不会返回系统推测(inferred)的事实。
### 写入事实
```js
await ctx.knowledge.facts.put({
subject: 'self', // 默认 'self';也可以是实体 ref
predicateHint: 'pref.food.rated',// 最好写完整谓词;也接受别名或一句描述
description: '用户打过高分的菜', // 可选,新谓词时有用
value: '番茄牛腩',
object: entityRef, // 可选:值指向另一个实体
sourceRef: 'record/123', // 可选:指回你自己存储里的记录
runId: 'nightly-2026-09-29' // 可选:同一批写入共用,可整批撤销
})
// → { status: 'applied' | 'duplicate' | 'conflict', ref, predicate }
```
- **先解析谓词,再检查权限。** `predicateHint` 会先按「完整谓词 → 别名 → 描述」归一到词表里的谓词,再用归一后的结果检查 `facts.write`。含糊的 hint 可能落到你没有权限的前缀下而被拒绝;完全匹配不上时会落到 `x.`,同样需要权限。所以请直接使用完整谓词名。
- `duplicate`:同一主语、谓词、值已经存在。
- `conflict`:`cardinality: one` 的谓词已经有**用户亲口说的值**,你的值不会写入,原值不变;用户会在「查看改动」里看到这次冲突。
- `cardinality: one` 且原值不是用户说的:旧值自动过期,新值生效。
- `origin`、`source` 默认由系统填写(`gem:<你的 id>` / `extracted`),自己指定需要 `origin.override`。
```js
await ctx.knowledge.facts.retract(factRef) // → { status: 'retracted' | 'already_retracted' }
```
让事实过期(不物理删除)。只能撤回自己写的;撤回别人写的需要 `changes.manage`。
### 实体
```js
await ctx.knowledge.entities.resolve({ email: 'zhangsan@acme.nl' }) // kind 默认 'person'
await ctx.knowledge.entities.resolve({ name: 'Acme' }, { kind: 'org' })
await ctx.knowledge.entities.resolve({ email, name }, { create: true }) // 找不到就新建,需要 entities.write
// → { ref, kind, displayName, created } | null
```
可用的标识:`email`、`phone`、`domain`、`url`、`handle`(写成 `平台:账号`,例如 `github:zhangsan`),以及 `name`。
```js
await ctx.knowledge.entities.get(ref) // → { ref, kind, displayName, identifiers },需要 entities.read
await ctx.knowledge.entities.list('person', { query }) // 需要 entities.read
await ctx.knowledge.entities.addIdentifier(ref, 'phone', '+31 6 1234 5678') // → { status: 'added' | 'exists' },需要 entities.write
```
实体只能由用户(在「知识库」页里)或助手删除、合并,gem 做不到。如果你把实体 ref 存在自己的存储里,要预料到它可能失效:
- **被合并**:旧 ref 仍然可用,会自动指向保留下来的那个实体。
- **被删除**:再用这个 ref 调用会抛出「找不到」错误(错误信息含 `no entity`)。这时请丢弃这个 ref,必要时用 `entities.resolve` 重新查找。
### 检索与文档
```js
await ctx.knowledge.search('番茄牛腩', { collections: ['diary'], timeRange: { from: '2026-09-22', to: '2026-09-22' }, limit: 10 })
// → [{ document: ref, title, collection: ref, text, locator, meta }]
```
全文 + 语义混合检索,只在你可读的知识库里查。`collections` 里写的知识库连同它里面的所有子文件夹一起查。`timeRange` 接受毫秒或 ISO 日期(只写日期表示整天),对日记按 `meta.date` 过滤。`query` 可以为空,此时按时间倒序列出。`entities: [entityRef]` 只返回提到这些人的段落。
```js
await ctx.knowledge.documents.upsert({
collection: 'diary',
key: '2026-09-29', // 传 key:同一个 key 重复写入会覆盖,不会产生重复文档
title: '2026-09-29',
body: '# 今天\n\n……',
meta: { date: '2026-09-29' }
})
// → { status: 'created' | 'updated' | 'unchanged', ref }
```
- 传 `key` 得到确定的文档,适合「每天一篇」「每条记录一篇」这类幂等写入;内容没变时返回 `unchanged`,不会重复建索引。
- 传 `ref` 更新已有文档;两者都不传则新建。
- `text` + `sourceUri` 用于导入的外部内容:只建索引,不保存正文。`sourceUri` 是原文所在的网址,用户可以点开,所以只能是 `http(s)://` 网址,而且主机要在你的 [`permissions.network.hosts`](/zh/dev/permissions.md) 里(没有申请联网的 gem 不能写 `sourceUri`);其他地址(`file:` 等)会被拒绝。
- 覆盖或删除别人写的文档需要 `changes.manage`。
```js
await ctx.knowledge.documents.get(ref, { range: { from: 0, to: 3 } }) // 全文,或按块取片段
await ctx.knowledge.documents.getByKey('diary', '2026-09-29') // → 文档 | null
await ctx.knowledge.documents.list('diary', { timeRange: { from: '2026-09-01' } })
await ctx.knowledge.documents.delete(ref)
```
### 知识库
```js
await ctx.knowledge.collections.list() // → [{ ref, name, builtin: 'diary' | 'keep' | null, kind, canWrite, parent }],只列你能读或写的
await ctx.knowledge.collections.create('菜谱') // → { ref, name, parent: null },需要 collections.create
await ctx.knowledge.collections.create('川菜', { parent: ref }) // 建在 ref 这个知识库里面,还需要对它有写权限
```
- 知识库可以嵌套。`list()` 返回的是平铺的列表,`parent` 是它所在知识库的 ref;顶层的是 `null`。你看不到的上级(只被授权了其中一个子文件夹)也是 `null`。
- `parent` 可以是 `'keep'` 或一个知识库 ref;`'diary'` 里不能建。最多 5 层(顶层算第 1 层),再往下建会报错。
- `documents.list(collection)` 只列直接放在这个知识库里的文档,不包含子文件夹里的;要连子文件夹一起找用 `search`。
- **用户可以移动知识库和文档**(在应用的知识库页面里,gem 不能移动)。你能读写什么跟着位置走:移进你有权限的知识库里的内容,你从此就能读到;移出去的就读不到了。移动不改变 ref。用 `key` 写的文档(日记、你按 key 管理的文档)用户移动不了,所以 `getByKey` 和按 key 写入总能在原来的知识库里找到它。
### 变更与撤销
```js
await ctx.knowledge.changes.list({ since: '2026-09-28', status: 'conflict' })
// → [{ ref, runId, actor, op, summary, status, proposed, createdAt }]
await ctx.knowledge.changes.revert({ runId: 'nightly-2026-09-29' }) // 或 revert(changeRef)
```
没有 `changes.manage` 时只能看到、撤销自己的变更。每次写入都会记录变更;不传 `runId` 时,写入归到当前这次运行的 `runId`,也可以自己传一个,把一批写入归在一起。
## 写入规则
- **每个 gem 每天最多 500 次写入**(按本地日期计算),写入的文字每天合计最多 5000 万字符(有 `collections.bulk` 的是 2 亿)。这是系统保护,用户不能调整。超出后写入会抛出含 `quota` 的错误。
- **单次写入的上限**:一篇文档的正文最多 500 万字符,标题最多 1000 字符,`meta` 和一条事实的值序列化成 JSON 后各最多 64 KB。实体的显示名、标识的值和知识库的名称也和标题一样,最多 1000 字符。
- **所有写入都可撤销**,用户可以在「查看改动」里按单条或按批次撤销。
- **不要把知识库当你的数据库。** 运行状态、缓存、草稿放在 gem 自己的数据里([`ctx.data`](/zh/dev/storage.md));只有对用户长期有价值、离开你的 gem 也有意义的内容才写进知识库。
## 卸载与数据归属
用户卸载 gem 时:
1. 确认对话框显示你贡献了多少事实、文档和知识库,用户选择「保留」(默认)或「一起删除」。
2. 你的私有存储、授权记录被删除。
3. 保留:知识原样留着,来源标记仍指向你。删除:你写入的文档被删除,事实过期,你创建的知识库连同其中的一切一起删除(包括用户后来放进去或移进去的),这一步也可以由用户撤销。
4. 你声明的谓词留在词表里,因为已有事实可能还在引用它们。
重新安装时,你拿不到以前的私有数据;但只要有读权限,就能通过 `ctx.knowledge` 读到之前保留下来的内容。
## 核心谓词
常用谓词已经内置。请优先使用它们——用户可能已经填过,gem 页面上也会用应用里的名字显示——都不合适时才在清单里声明新的。下面是全部内置谓词:
| 谓词 | 基数 | 含义 |
| --- | --- | --- |
| `identity.name` / `identity.nickname` / `identity.birthday` / `identity.language` | one / many / one / many | 姓名、称呼、生日、会说的语言 |
| `contact.email` / `contact.phone` / `contact.address` | many | 联系方式 |
| `location.home.city` / `location.home.address` / `location.work.city` / `location.timezone` | one | 居住城市、住址、工作城市、时区 |
| `relation.family` / `relation.spouse` / `relation.friend` / `relation.colleague` / `relation.member_of` | many(spouse 为 one) | 人际关系,值通常用 `object` 指向另一个实体 |
| `work.company` / `work.title` / `work.project` | one / one / many | 公司、职位、项目 |
| `pref.food.like` / `pref.food.dislike` / `pref.food.diet` / `pref.food.cuisine` | many | 饮食偏好 |
| `pref.communication.style` / `pref.communication.language` | one | 沟通风格、回复语言 |
| `schedule.routine` / `schedule.event` | many | 作息、日程 |
| `health.allergy` / `health.condition` | many | 过敏、健康状况 |
| `finance.currency` | one | 常用货币 |
## 用户看到什么
gem 的页面「设置 › 知识库」里,每条权限都有一个开关,默认关闭。下面列出你的 `facts.read|write:self:…` 覆盖到的、关于用户本人的事实,以及用户现在的值:
- 内置谓词用应用里的名字显示(例如「居住城市」);
- 你新加的谓词显示你写的 `description`,标着「这个 gem 新加的」;
- 用户可以直接修改、补填、删除。这样写下的值算用户亲口说的:`cardinality: one` 的谓词上,你的写入不会覆盖它(`conflict`)。
关于联系人的事实(`facts.read:person:…`)是一人一份,在 Vault 的联系人页里改,不在这里列出。
## 示例
只读的天气 gem:
```js
// 权限:facts.read:self:location.home.*
export default async function (input, ctx) {
const [home] = await ctx.knowledge.profile.get('location.home')
if (!home) return { error: '不知道你住在哪里' }
return { city: home.value }
}
```
读写的菜谱 gem:
```js
// 权限:facts.read:self:pref.food.*、facts.write:self:pref.food.*、collections.create
export default async function (input, ctx) {
const cols = await ctx.knowledge.collections.list()
const col = cols.find((c) => c.name === '菜谱') ?? (await ctx.knowledge.collections.create('菜谱'))
const fact = await ctx.knowledge.facts.put({ predicateHint: 'pref.food.rated', value: input.dish, sourceRef: `record/${input.id}` })
const doc = await ctx.knowledge.documents.upsert({ collection: col.ref, key: input.id, title: input.dish, body: input.recipe })
return { fact: fact.status, doc: doc.status }
}
```
## 还不支持
- 知识库的变化不会通知 gem(这一版不发布知识库事件,见[事件与触发](/zh/dev/events.md#可用的事件))。需要感知变化请用定时触发去读,`documents.list` 每项带 `updatedAt`。
- 读取其他 gem 的私有存储:不会开放。gem 之间要共享的信息,写进知识库。