关于 factstore
factstore 是基于 Postgres 的事实存储,面向需要从公司系统和文档中记录可追溯信息的 AI Agent、开发者及团队。属性需预先注册,写入事实时关联证据,并保留不可变历史,以查询特定日期已知的信息。
主要功能
- 01注册并限定类型的属性
- 02关联证据的事实事务
- 03支持指定时点读取的不可变历史
- 04MCP 服务与 Python SDK
- 05用于目录编制和文档摄取的技能
适用场景
- 01将发票和邮件摄取为事实
- 02为业务系统导出数据编制目录
- 03按日期查询事实及其历史
MCP 相关
MCP 服务通过 stdio 将 factstore 操作作为工具提供,并从环境中读取凭据。MCP 客户端可通过它访问事实;只有提供单独凭据时才会开放删除工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx factstoreFACTSTORE_DSN密钥The credential `factstore actor STORE NAME` prints: a Postgres connection string for one actor of one store.FACTSTORE_EXCISE_DSN密钥An excision credential (`factstore actor --excise`). Only then is the excise tool listed.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
factstore
一个构建在 Postgres 上、供 AI Agent 使用的事实存储。Agent 将公司系统和文档中的内容记录为事实:针对已注册属性的一个值,记录在一笔事务中,并标注记录者和来源文档。内容不会被覆盖,因此存储可以说明自己在任意日期知道什么。Agent 通过 MCP 服务使用它,脚本则通过 Python SDK 使用它。
它还提供技能,即 Agent 遵循的操作流程:一个从公司的系统导出数据中建立索引,另一个将文档和邮件读取为事实。设计文档说明了这样构建的原因,评测则说明了测试内容。采用 MIT 许可证。
安装
你需要 Postgres 16 或更高版本、Python 3.12 或更高版本,以及用于运行 uvx 的 uv。
1. Postgres。 使用你自己的实例,或启动一个实例。factstore init 需要超级用户权限。
docker run -d --name factstore-pg -e POSTGRES_PASSWORD=postgres -p 5432:5432 --shm-size=1g postgres:17
2. 创建存储,并为 Agent 准备凭据。
pip install factstore
export FACTSTORE_ADMIN_DSN=postgresql://postgres:postgres@localhost:5432/postgres
factstore init acme # a store, with factstore-core
factstore install acme factstore-skills # the vocabulary the skills use
factstore actor acme "ingest agent" # prints the actor's ID and its credential
3. 连接 Agent。 在 Claude Code 中,插件会提供 MCP 服务和技能,并提示你输入凭据:
/plugin marketplace add Victor-EU/factstore
/plugin install factstore@factstore
其他 MCP 客户端则在其环境变量中设置凭据并运行服务:
{"mcpServers": {"factstore": {"command": "uvx", "args": ["factstore", "mcp"],
"env": {"FACTSTORE_DSN": "<credential>"}}}}
factstore skills DIR 会将技能复制到 Agent 的技能目录,例如 .claude/skills。目录和导入技能也会通过 SDK 批量写入数据,因此 Agent 的 shell 环境需要运行 pip install factstore。
4. 提出请求。 “将 ./invoices 中的发票读入存储”会使用 factstore-ingest。“为我们的 Shopify 和 QuickBooks 导出数据建立索引”会使用 factstore-catalogue。“存储中有哪些类型的事物?”会使用 factstore-ontology。
此版本包含的内容
factstore install STORE NAME 会按名称安装软件包及其依赖项。
| 软件包 | 内容 |
|---|---|
factstore-core | 证据、文档、置信度、货币、合并和领域时间。由 init 安装。 |
factstore-skills | 目录、知识本体和导入技能,以及知识本体技能用于命名形状的词汇表。 |
factstore-ecom-ops | 在中国生产并在线销售的品牌的供应侧内容:供应商、SKU、采购订单、检验、货运和海关申报。其技能会读取供应商的 PDF、聊天记录和运输邮件。 |
factstore-ecom-index | Shopify、Amazon、3PL 和 QuickBooks 所有内容的 ID 和关联键,供目录技能使用。 |
个人数据。 服务的规则会将人名、地址和电话号码排除在存储之外:这些信息会保留在文档中。若企业希望某个属性能够存储这些信息,则需通过 core/personal 允许。
开发
git clone https://github.com/Victor-EU/factstore && cd factstore
docker compose up -d # Postgres 17 on port 54329
cd factstore
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
每项测试都会创建一个存储,并在测试结束后将其删除。从源码检出目录运行时,factstore install 也可以接受软件包目录作为参数,例如 ../packages/ecom-ops。packages/介绍了软件包格式和各个软件包。新版本的软件包会注册其新增内容。它还可以将某个属性设为多值或身份属性,这是内核允许的两种变更。factstore-skills/存放各项技能。
import factstore
store = factstore.connect("<credential from above>")
store.register_attribute({"ident": "shopify/order_id", "type": "string", "cardinality": "one",
"unique": "identity", "doc": "Shopify's ID for an order."})
store.transact([{"e": ["shopify/order_id", "1234"], "a": "shopify/order_id", "v": "1234"}])
store.query('select e, v from "shopify/order_id"').rows
设置 FACTSTORE_DSN 后,也可以在模块级别进行相同调用:factstore.transact([...])、factstore.query(...)。
MCP 服务
factstore mcp 或 factstore-mcp 会通过 stdio 将调用作为工具提供。凭据来自其环境变量(FACTSTORE_DSN),绝不会来自工具调用。只有当 FACTSTORE_EXCISE_DSN 中也设置了删除凭据时,才会列出 excise。tools.py 中的工具描述就是模型读取的文档。
目录结构
| 文件 | 内容 |
|---|---|
schema.sql | 表、角色,以及用于在 Postgres 中强制执行不变量的触发器 |
kernel.py | 写入锁、模式加载、分配,以及将变更应用到日志和派生表 |
store.py | 调用:transact、query、stats、register_attribute、search_attributes、excise |
read.py | 安全执行一次读取:只读、回滚、单条语句、超时、行数上限 |
stats.py | 属性用法、签名和引用连接情况 |
server.py | MCP 服务 |
admin.py、cli.py | 创建存储、参与者和凭据;安装软件包 |
packages.py | 软件包清单、某个版本携带的软件包,以及安装顺序 |
tools.py | MCP 工具描述和输入架构:面向模型的文档 |
fs.py | 内核自身的 fs/ 属性 |
tests/reference.py | 参考读取器:对日志进行朴素折叠,用作测试判定依据 |
如何强制执行不变量
这些不变量由 Postgres 强制执行,而不只是由 Python 代码实现:
- 不可变性。 写入者对
fact表拥有INSERT和SELECT权限,除此之外没有其他权限。触发器会拒绝UPDATE、TRUNCATE,以及任何不在fs_excise()中执行的DELETE,即使操作者是所有者也不例外。 - 由内核标明写入者。 凭据是 Postgres 登录。触发器会根据
session_user设置每个事务的参与者,并从时钟获取事务时间,覆盖客户端发送的任何值。只有内核自身的函数可以写入fs/actor、fs/at和fs/excised_*事实。 - 已注册且类型正确的属性。 每个事实都必须指定一个已注册属性(由外键强制执行),并且触发器会检查其值是否符合该属性的类型。另一个触发器会阻止属性向错误方向变更(更改类型、从多值改为单值、移除唯一性)。
- 提交顺序。 每次写入都会为每个存储获取一个咨询锁,并在锁内分配事务 ID。触发器会再次获取同一个锁,并拒绝任何不晚于最新 ID 的 ID。
读取
query 接受一条针对视图的 SQL 语句(设计中的开放问题 1,由探索性验证决定):
- 每个属性都是一个以属性命名的视图:模式
current中的"po/status"(e, v, tx),以及涵盖整个日志的history."po/status"(e, v, tx, op)。 - 注册时会通过触发器创建视图。
- 执行某个时间点的读取时,会设置
factstore.as_of,所有视图都会遵循该设置。
由于 SQL 由模型编写,read.py 会通过单独的连接运行它:
- 在始终会回滚的
READ ONLY事务中运行,从而撤销语句所做的任何更改,包括SET ROLE; - 使用预备语句,因此 Postgres 会拒绝第二条语句;
- 设置 10 s 超时和 1,000 行上限。
咨询锁不会随回滚而释放,因此每次读取后都会释放锁;查询绝不会持有写入锁。
stats 会按签名对实体分组,即每个实体所携带属性的精确集合,并统计签名之间的引用数量。这种共现形式正是本体技能所需的:一种事物通常对应一个签名,或几个仅在可选属性上有所差异的签名。
构建过程中的决策
- 在写入时强制执行基数为 1。 断言新值时,会将旧值的显式撤回写入日志。无论某个属性当时的基数是多少,当前状态都是对断言和撤回进行简单折叠得到的。
- 丢弃冗余写入。 断言实体已有的值,或撤回实体没有的值,都会报告为未更改,且不会记入日志。不产生任何更改的调用不会写入事务,因此重新运行数据导入不会改动日志。
- 查找仅通过断言创建实体。 查找没有结果时,如果调用断言了某些内容,就会创建该实体。如果查找仅用于撤回,则调用会报错。
- 字符串相等性使用哈希索引。 字符串值上的 btree 会索引其前 200 个字符,因此长值不会导致插入失败。相等性判断通过针对整个值的哈希索引完成,任何视图上的
v = 'x'都可以使用该索引。 - 内核测试在裸存储上运行。
init会安装 factstore-core,但内核测试会传入core=False:内核不认识任何名称,因此其测试不应依赖 core 中的名称。这些包各自有自己的测试。 - 近似匹配阈值。 这些阈值是基于样本属性进行的初步校准,设定在
store.py顶部;tests/test_register.py记录了用于验证的案例。M5 中的“已注册属性”指标用于调整这些阈值。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。