创建日期:2026-09-09 | 最近更新:2026-09-09 这是一篇设计架构文:承接篇3:把 Agent 的时序看穿的结论,把「智能体 = 消息序列的编排器」推进一步,回答「长期任务里上下文膨胀怎么办」。文中的数据结构/流程为规格草案,尚未在
ioi/mini-agent落地实现;所有机制都从会计里借,都有可验收的标准。
结算式长期 Agent:把会计带进上下文设计
一句话:长期 Agent 的难点不是“让模型记住”,而是“让越滚越大的上下文有界、可信、可审计”。 会计早就把「如何在长期记录里不丢真、可追查、能结转」这套纪律固化成了流程。本文把它翻译成 Agent 架构:原始凭证 → 登账 → 结账前对账 → 结转期初余额,得到一种「外部只增账本 + 滚动有界窗口」的长期 Agent 形态。
0. 为什么需要它(问题定位)
篇3 得出过一个精确说法:Agent 的记忆 = 每次把历史再发一遍,上下文窗口是一张越来越长的“黑板”。于是任何想做长期任务(跨多天、几十个子任务、产物要可验证可复用)的 Agent,都会撞上同一个墙:
任务推进 → 上下文变长
→ token 成本线性涨
→ 模型对“埋在深处的关键事实”注意力稀释
→ 上下文触顶,被迫截断,早期结论丢失
传统的两个逃避姿势都有硬伤:
- 整段截断:丢关键事实,任务“失忆”;
- 无差别摘要:摘要由模型生成 = 又引入一份“可能”,还丢了原始证据,无法复核。
这篇的答案是把「结算/记账」的纪律引入:不是删上下文,而是把上下文“结算”掉——已验证的结论转成余额结转,原始凭证留在外部账本可随时对账。
1. 三层账本:凭证 → 账 → 表
会计的底层是三个分离的层次,Agent 照搬:
| 层 | 会计 | Agent 对应 | 放哪 | 特性 |
|---|---|---|---|---|
| 原始凭证 | 发票/回单 | 宿主工具的真实产物:写盘的文件、查询返回、测试/编译结果 | 外部只增存储(文件/DB) | 不可改、可审计 |
| 账(Ledger) | 明细账 | 事实表:每条已验证事实 + 指向凭证的引用 | 外部结构文件 | 追加式,可版本化 |
| 表(Statement) | 总账/报表 | 上下文余额:本期要用的结论摘要 + 指针 | 模型上下文 | 每期有界、可结转 |
核心纪律:只有宿主能“登账”。 模型输出的任何话都只是“待审凭证”(claim);被宿主用确定性手段验证(文件真实存在、read_file 读回一致、测试通过)后才升级为“已入账事实”(fact)。这正是篇3「truth 只能由宿主盖章」的落地。
2. 整体架构
一个 Agent 实例的推进方式:要么在窗口内小步前进(普通多轮问答),要么把当前子任务“做账做平”后结账、开新窗口。上下文里永远只放两样:期初余额(已验证事实的摘要 + 指针)+ 最近若干轮的滚动工作窗口。
3. 数据规格(草案)
给关键实体定个形,方便之后敲实现(字段都是最小集):
// 凭证库:宿主工具产出的原件,只增不改,带哈希防篡改可对账
Artifact = { id, at, kind: 'file'|'query'|'test'|'tool_result',
ref: 'workspace/report.md', // 实际位置
hash: 'sha256:…', // 对账用:可重算比对
contentMeta: { size, lines } }
// 事实表:每一条入账事实,必须带 source 指针 → 可审计
Fact = { id, at, kind: 'verified'|'claim',
statement: '上海人口 2487 万(2026 快照)',
source: ['artifact:report.md#L12'], // ★ 必须指向凭证,可空=未入账
supersededBy: null, // 红字冲销:不物理删,追加标记
status: 'current'|'superseded' }
// 期/余额:一次结账的产物,就是下一期的“期初余额”
Period = { id, closedAt, windowFrom, windowTo,
openingBalanceRef: 'period:3', // 上一期结转而来
balance: [ Fact.id, … ], // 本期结转的事实(摘要+指针)
lossy: true } // 明示:这是有损摘要,证据在凭证库
三个不可违反的不变量(Invariant),对应 §5 的验收:
- 可审计:每条
balance的 fact 都能沿source一路追到凭证库里的原件; - 只增不回写:凭证与事实都追加式写,纠正 = 新增
supersededBy,不删旧记录; - 有界:任何一个时刻,模型上下文里的余额 + 工作窗口大小都受预算约束,与总任务时长无关。
4. 生命周期:做账四步
以「跨三天的数据分析任务」为例,把一天的“账期”走一遍:
翻译成宿主该提供的最小接口(规格,非实现):
// settle API —— 规格草案
const ledger = await openLedger('workspace/ledger'); // 账期上下文
const art = await vault.store({ ref: 'avg.json', content }); // 1 留凭证
const ok = await verify(art, expected); // 2 宿主验证(读回/测试)
if (ok) await ledger.post({ statement, source: [art.id] }); // 3 登账
await ledger.reconcile(); // 4 对账:逐条复核凭证
const period = await ledger.close(); // 5 结账 → 生成期初余额
const ctx = period.opening(ctxBudget); // 6 开新窗口,只带余额+指针
5. 验收标准(写代码前先定“怎样算赢”)
| # | 验收项 | 判定方式 |
|---|---|---|
| 1 | 有界性 | 连续结算 N 个子任务后,ctx 的 token 数 ≤ 预算(与 N 无关);对话原稿不再整体进窗口 |
| 2 | 可推进 | 只给「期初余额 + 指针」开新任务,模型能续上(通过 read 工具读凭证取细节) |
| 3 | 可审计 | 随机抽一条 balance 的 fact,沿 source 追到凭证并重算比对一致 |
| 4 | 抗漂移 | 故意篡改凭证库某原件 → 下一轮 reconcile 必须报对不上 |
| 5 | 容错/冲销 | 一条事实被新结果推翻 → 走 supersededBy,旧记录仍在、可回滚 |
没有 3/4,这套就退化成“高级摘要”——对账和审计,才是会计带来的、普通记忆方案给不了的东西。
6. 已知边界与折衷(别神话它)
- 语义事实没有“借贷必相等”:会计有数值恒等式自动爆错账;文本事实没有天然勾稽,所以对账不能省、且要挑对象——关键事实强对账(读回+比对),次要事实弱对账(指针即可),否则对账本身会吃掉成本;
- 摘要是有损的:
lossy: true写在明处,凡要“精确”的都回到凭证,别指望摘要保真; - 结转时机 = 里程碑而非时间:一个子任务被验证完成才是天然账期边界(呼应篇3「以落盘/结果为准」);硬按时间结转容易在任务做到一半时把现场打碎;
- 多 Agent / 并行共享账本:追加式 + 指针天然友好;写同一账本需乐观锁或“先落凭证再登账”的顺序约定;
- 余额放 system 还是开头:期初余额属高优先级事实,放 system / 窗口开头,降低模型“背对结论”的概率。
7. 结语:它是不是对的,取决于对账
回到篇3那个坐标——模型是无状态单步函数,Agent 是宿主编排的问答序列。这篇的贡献只有一句:当这个序列长到装不下时,别裁、别盲压,而是像会计一样“结账”——已由宿主验证的事实登账、结转成有界的期初余额,原始凭证留在外部账本里随时可对账。智能体的长期记忆,不该是一段越来越长的自言自语,而是一套能复核、能结转、越滚越精的账。
动手(把这套做成真的)
- 在
mini-agent里先做最小闭环:runtime加ledger.json,让 complex 每次write_file后自动登一条 fact + 读回对账; - 实现
reconcile:随机挑一条 fact,调read_file重读凭证核对哈希/数值; - 用验收表 1/3/4 跑:结算 5 个子任务后量窗口 token、抽 3 条审计、篡改一个文件看是否报对不上。
自测
- 为什么「摘要」必须有指针指向凭证?没有会退化成什么?
- 「结账前对账」解决的是哪一类漂移?会计里叫什么?
- 上下文里永远放哪两样东西?为什么它是「有界」的?
supersededBy为什么不物理删除旧事实?- 这套设计里,模型的“记忆”到底存不存在?如果不存在,那“长期”是谁在长期?
关联:篇3 时序模型与本质、篇1 简单 Agent、篇2 复杂 Agent;配套实验库 ioi/mini-agent。