☰
Agent 进度合并能用 CRDT,预算扣减也能吗?
2026/10/24 23:14:48 网站建设 项目流程

两个Agent离线做事,重新连上后把各自的进度合起来,看起来是个适合CRDT的问题。若它们合并的是“已经处理了多少文件”,收敛的计数很有用。

把字段换成“还剩一次调用额度”,问题就变了。两个副本都看到还剩1,各自扣一次,重连后账本确实能一致,却可能一致地变成负数。副本收敛并不自动保住业务不变量。

一个只在本地运行的反例

下面用两个按actor分开的单调计数映射保存增量和减量。合并时,每个actor各取最大值;预算定义为初始1加总增量减总减量。这是教学用状态模型,没有实现网络同步、权限控制或生产计数库。

use std::collections::BTreeMap; #[derive(Clone, Debug, PartialEq, Eq)] struct Counter { p: BTreeMap<&'static str,u64>, n: BTreeMap<&'static str,u64> } impl Counter { fn new() -> Self { Self { p: BTreeMap::new(), n: BTreeMap::new() } } fn spend(&mut self, actor: &'static str) { *self.n.entry(actor).or_default() += 1; } fn merge(&mut self, other: &Self) { for (dst,src) in [(&mut self.p,&other.p),(&mut self.n,&other.n)] { for (&actor,&value) in src { let v=dst.entry(actor).or_default(); *v=(*v).max(value); } } } fn budget(&self) -> i128 { 1 + self.p.values().map(|&v| i128::from(v)).sum::<i128>() - self.n.values().map(|&v| i128::from(v)).sum::<i128>() } } fn main() { let mut a=Counter::new(); let mut b=Counter::new(); a.spend("A"); b.spend("B"); assert_eq!((a.budget(),b.budget()),(0,0)); let mut ab=a.clone(); ab.merge(&b); let mut ba=b.clone(); ba.merge(&a); assert_eq!(ab,ba); assert_eq!(ab.budget(),-1); let before=ab.clone(); ab.merge(&before); assert_eq!(ab,before); let mut c=Counter::new();c.spend("C"); let mut left=a.clone();left.merge(&b);left.merge(&c); let mut bc=b.clone();bc.merge(&c); let mut right=a;right.merge(&bc);assert_eq!(left,right); println!("merge laws: passed on constructed replicas"); println!("merged budget: {}",ab.budget()); }

保存为main.rs,运行rustc --edition=2024 main.rs -o counter-demo && ./counter-demo。本地macOS aarch64、rustc 1.98.0实际输出:

merge laws: passed on constructed replicas merged budget: -1

两个副本单独都得到0,合并后得到-1。这个构造例足以否定“用了可收敛计数器就不会超卖”的说法,不需要等真实API账单出现。

测试中的交换、幂等和结合断言只覆盖这几组输入,不是对任意实现的自动形式证明。这里可额外做数学推导:单个actor的合并是max,max满足交换、结合和幂等;把它逐项应用到两个映射,仍保留这些性质。

这个推导也有前提。actor需要独立写入自己的单调分量,不能多个离线写者复用同一个actor并覆盖对方的计数。计数溢出、actor身份重用、丢失持久化历史等,都需要另设协议,本例没有处理。

“最后写入”也不是替预算作决定

用时间戳选一个最后写入的值,可能让界面上只显示一个答案,但会把另一次已经发生的消费藏起来。真实外部调用不会因为本地账本选了另一个值而撤销。

如果使用LWW结构,相同时间戳的不同值还需要确定的平局规则。仅写“时间晚的赢”,不足以说明时间一样时怎么收敛。逻辑时钟、唯一actor ID和事件身份也各有用途,不能随手拿系统时间拼成正确协议。

作者提交的CRDT论文摘要讨论并发操作的可交换性、收敛及局限,发表于2009-07-06,是这里的基础概念来源,不是2026年10月新技术。本次复核读到摘要与元数据,本文的max合并与预算反例由上面的明示模型推导,不冒充论文完整算法的复现。

为不同字段选不同保证

进度、已观察事件集合等字段,可以先问它们能否接受离线合并。独占额度、仅执行一次的发布动作,则应先写出不变量:允许多少个执行者同时持有什么权利,谁有权批准下一次动作。

可选办法包括中心准入、事先分配不可重复的额度,或把拒绝过期执行者的校验放在真正消费资源的入口。这些方案各有可用性和复杂度代价;不能在短例后宣布一个万能答案。涉及复制日志的语义可参照Raft作者说明,也要记住复制同一决定与决定本身是否越权是不同问题。

2026-10-07的Copilot本地沙箱公告把工具执行边界作为独立控制项。它可以提醒我们分开考虑模型输出、工具权限和状态记录,但不为本文的预算模型提供额外正确性保证。

这次小实验没有访问模型或扣费系统。能确定的是:算对一份合并后的账,不代表此前每次消费都获准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询