site logo

Marico's space

构建 AI Agent 账本:OpenLedger 的 15 个 MCP 工具、不可变事务,以及为何审计日志与写入操作共用同一 DB 事务

Others 2026-09-11 20:56:21 9

最近折腾了一下给 AI Agent 跑财务流水的问题,踩了几个坑,这篇把 OpenLedger 这个项目说清楚。

Attri 把 OpenLedger 开源了——一个本地优先的复式记账系统,上面套了一层 MCP(模型上下文协议)服务器。Python 3.11+、SQLite、aiosqlite。通过 MCP 暴露 15 个工具。事务不可变。纠错靠红字冲销。所有的变更操作都把审计日志行和变更本身写在同一个 DB(数据库)事务里。Apache 2.0 协议。

下面会讲:架构设计、代码里强制执行的四个不变式、MCP 工具集、完整的上手演示,以及我们主动做出的权衡(v1 单币种、不用 ORM、没有管理后台)。

项目地址:https://github.com/Attri-Inc/open-ledger

为什么会有这个项目

如果你试过让 AI Agent 跑财务流程,就知道问题在哪了。Agent 跑得挺顺,速度也上去了。然后某天审计人员过来问:"证明一下 Agent 到底干了什么?"

大多数记账系统的审计日志和写入操作不在同一个系统里。Datadog、Splunk、某张没人看的 Postgres 日志表。写入成功了但日志写失败了——或者反过来——这个缝隙,真正的审计人员五分钟就能发现。

OpenLedger 的整个设计围绕一个保证:不可能出现变更成功但没有对应审计日志行的情况,因为两者都在同一个 SQL 事务里。要么两个都提交,要么两个都回滚。没有第三种可能。

架构一览


┌─────────────────────────────────────┐
│ SQLite ledger │
│ accounts · transactions · │
│ entry_lines · audit_log · settings │
└──────────────────┬──────────────────┘ │ ▼ MCP server (stdio / SSE :8791) 15 tools — reads + safe writes │ ▼ Claude Desktop, Claude Code, agent frameworks

分层结构:

src/
├── domain/ # pure constants + typed errors (no I/O)
├── infrastructure/ # DB connection, Unit of Work, id/clock helpers
├── repositories/ # protocols.py — narrow Reader/Writer contracts
│ # sqlite.py — the only code that writes SQL
├── services/ # accounts · ledger · reports · audit · query
├── serialization.py # response/error envelope helpers
├── container.py # composition root
└── mcp_server.py # thin MCP transport adapter
run_mcp.py # stdio entry point

四个不变式(靠代码强制执行,不是靠文档声明)

复式记账,原子操作,在写入路径上

每笔交易至少有两条分录。借方合计 == 贷方合计。在单个 DB 事务内强制执行。如果余额校验失败,整个插入回滚。不存在"先写入,回头再校验"这种模式。

整数最小单位(分)。不用浮点数。

所有金额都是整数。25000 就是 250.00 元。500 就是 5.00 元。如果你调试过通用账本里的浮点舍入问题,就知道这为什么重要。Decimal 类型原则上没问题;但整数最小单位在实际中是无争议的选择。

不可变性。纠错靠红字冲销。

交易和分录永不更新或删除。如果一笔交易错了,就发一条红字冲销交易来反向冲掉。reverse_transaction(txn_id) 就是这个工具。交易表和分录表上不存在 UPDATE 和 DELETE 操作。

审计日志和变更写在同一个 DB 事务里

async with unit_of_work: await ledger.post_transaction(...) # mutation
 await audit.append(...) # audit-log row
 await unit_of_work.commit() # both or neither

要么两个写入都提交,要么都回滚。审计日志不会落后于实际状态。变更也不可能绕过审计轨迹。这就是 SOX(萨班斯-奥克斯利法案)级别的保证。

MCP 工具集(15 个工具)

• 账户:list_accounts、get_account、get_balance、get_account_ledger、create_account

• 日记账:get_transaction、search_transactions、post_transaction、transfer_funds、reverse_transaction

• 报表:get_trial_balance、get_profit_loss、get_balance_sheet

• 审计:get_audit_log

• 逃生舱:run_query(仅限 SQL SELECT)——给那些我们没预料到的问题准备的

上手演示:从克隆到第一条查询

git clone https://github.com/Attri-Inc/open-ledger
cd open-ledger
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python scripts/seed.py # bootstrap fresh dev DB

接入 Claude Desktop / Code:

python run_mcp.py # stdio transport (default)
# for Claude Code:
claude mcp add openledger -s user -- \ /absolute/path/to/open-ledger/.venv/bin/python \ /absolute/path/to/open-ledger/run_mcp.py

连上之后,直接用自然语言问 Claude:

• "我们现在有多少现金?"

• "给我看一下一月的损益表。"

• "账平了吗?"

• "记一笔今天 250 元的现金销售收入。"

• "把 500 元从钱包 A 转到钱包 B。"

• "最近有哪些被冲销了,为什么?"

值得说清楚的设计决策(以及为什么这么选)

为什么不用 ORM?

考虑过 SQLAlchemy,后来放弃了。两条理由:(1)那些不变式(借等于贷、原子化审计日志写入)是在单个 SQL 事务里强制执行的,不用 ORM 隐式的 session 语义更容易理清逻辑。(2)将来把 SQLite 换成 Postgres 应该只是改一个 repository 文件的事,而不是一次完整的 ORM 迁移。

为什么用 SQLite,而不是第一天就上 Postgres?

SQLite 是嵌入式、零配置、部署量最大的数据库。对单租户、单节点的账本场景——这是大多数财务团队自建部署的形态——SQLite 不是妥协,是正确的选择。repository 层设计成换 Postgres 只需要动两个文件。

为什么选 Apache 2.0,而不是 MIT 或 AGPL?

• AGPL 会吓到企业法务。商业嵌入会有法律风险。

• MIT 没问题,但在专利授权上较弱。

• Apache 2.0 获得企业法务认可,同时避开 AGPL 的著佐权陷阱。

为什么选 MCP,尽管规范还很年轻?

MCP 还早。Anthropic 发起的。确实有风险。但我们还是押注了,因为这是第一个认真尝试制定标准的方案——让 Agent 能和它不拥有的系统对话。如果 18 个月后 MCP 死了,账本和业务规则还活着;MCP 服务器只是个约 100 行的适配器。

v1 里没有的功能(故意的)

• 多币种。当前只支持单币种。

• Postgres 后端。SQLite 是第一天;Postgres 在路线图上。

• 管理后台。所有操作走 MCP 或 SQL。

• 文档摄取管道。发票解析 / OCR / 收据提取自己接。

• 税务 / GAAP / IFRS 模块。不是核心功能,叠在上面就好。

上手试试

四条命令起一个跑起来的账本。两条命令接 Claude。十秒钟问出第一条"我们有多少现金?"。

项目地址:https://github.com/Attri-Inc/open-ledger

如果你在上面搭了有意思的东西,欢迎分享。发现 bug,麻烦提 issue。如果觉得哪个不变式不对,也欢迎在 issue 里拍砖——我们宁愿现在吵,也不愿意等十支团队基于一个烂设计搭完再说。