每个系统一旦投入生产并开始实际运行,就会遇到这样一个问题:究竟发生了什么,按什么顺序发生的,依据的是什么数据——你能证明吗?

人工智能现在正把这个问题放大,而且声音越来越大。想象一下,随着越来越多使用工具的客服人员出现,这种情况发生的可能性也越来越大:一个客服人员——不是真人,而是一个拥有工具访问权限的LLM(高级语言学习专家)——取消了订阅,退款,然后发送了三封后续邮件。第二天,客户却说:我从未取消过订阅。现在,请回答上面的问题。

在大多数代码库中,诚实的答案是:你可以看到数据库的当前状态(订阅已取消),但看不到导致这种情况发生的路径。几行日志信息就会被下一次重构覆盖。没有可靠的记录可以确定是哪个参与者代表哪个客户执行了操作。而撤销操作意味着需要手动编写修正代码,并祈祷能够捕获所有副作用。

这不是模型的问题。GPT并没有“出错”。问题出在更深层次上。

人工智能部分现在反而是最简单的部分了

两年前,模型是难点。如今,你只需十分钟就能搭建一个代理,它能调用工具、制定计划并执行操作。演示看起来棒极了——而这恰恰是陷阱所在。演示无法带来任何实际影响。一旦系统真正投入生产环境,那些再好的演示也无法解决的问题就会暴露出来:

  • 状态:做出决定时的情况如何?CRUD 表只知道现在的情况
  • 历史:哪些步骤导致了最终结果?没有记录——就消失了。
  • 归属:谁或什么实施了该行为,并得到了什么授权?
  • 可逆性:一个错误的行为——如何才能干净利落地挽回?
  • 信任:事后有人能悄悄篡改记录吗?

这实际上并不是人工智能问题

对我来说,比所有关于代理的炒作更重要的是:这些都不是新鲜事,也并非人工智能独有。夜间修改记录的 webhook;批量作业;管理员在压力下误按按钮;第二个服务通过消息队列写入数据——所有这些都会引发同样的五个问题。状态、历史记录、归因、可逆性和信任,这些都是优秀软件的必备属性,毋庸置疑。

人工智能只做了一件事:它让你再也没有借口了。只要操作者只有人类,每分钟点击一次,你还能勉强应付——查阅日志,在不确定时靠猜测。但一个每秒执行上百次操作的自主代理,就不允许你再勉强应付了。它只是让原本就存在的漏洞变得无法忽视。

从一开始,我的设计理念就是:打造的不是一个人工智能框架,而是一个优秀软件的基础。在其上运行智能体是一种选择——而且是目前非常流行的选择——但这并非重点。

这样的基础建立在什么之上?

没有人需要遵循某种特定模式来发布功能——我绝不会这么说。但是,一旦系统认真地管理状态,无论谁来操作,以下三个决定就不再是纸上谈兵了:

1. 使用记录系统而非快照——CQRS + 事件溯源。
覆盖字段意味着丢弃过去。将每次更改都设为不可变事件,历史记录就会内置于系统中,而不是附加上去。您可以重放数据流并准确重建发生的一切——无论是代理、作业还是人触发的。“撤销”变成了领域级补偿事件,而不是恐慌事件UPDATE

2. 将不稳定因素置于边缘的结构——清晰架构 + 垂直切片。
每个集成点都处于不稳定状态——第三方 API、支付提供商,当然还有每周都在变化的提示和模型的 AI 代码。如果让这些不稳定因素渗透到领域核心,就会腐蚀它。一个清晰的核心位于中间,不稳定因素位于外层——每个功能都作为一个垂直切片(命令 → 处理程序 → 事件 → 投影)。这样,添加新功能无需拆解五层架构,也不会破坏现有功能。

3. 信任是一种属性,而非一种希望——审计、防篡改、加密、验证、授权。
任何被允许更改状态的行为者都需要系统强制执行的防护措施。每个操作都作为一条经过验证、授权和审计的命令运行——审计记录会将触发操作的人与操作涉及的数据分开记录,因为代理代表客户操作时,这两者是不同的身份。记录本身是防篡改的(哈希链——事后修改一行数据会显示出来)。个人数据始终以主体为单位进行加密并可擦除——因为“人工智能做的”或“这只是日常工作”都不能成为规避 GDPR 的通行证。

只有将这三者结合起来,才能对“发生了什么——你能证明吗?”这个问题给出审计人员会相信的答案。这适用于你的代理人,也适用于其他所有情况。

我必须诚实面对(界限)的地方

所以,这并不是一份灵丹妙药清单:基础架构并不能保证你的软件正确无误。它只是让软件的功能可验证、可重现和可控——这两者是截然不同的。

  • 它记录的是发生了什么,而不是模型做出决定的原因。人工智能黑箱始终是黑箱;你记录的是输入、操作和结果——而不是其背后的因果关系。
  • 它并不能阻止愚蠢的行为发生。它只是让这种行为变得可见,并可以通过补偿来处理——但已经发出的邮件,无法撤回。
  • 它用“干净利落地管理密钥和权限”取代了“到处删除数据”。说实话:这算是一个更好的问题——但仍然是个问题。

建造的是建筑,而不是管道。

但问题在于:自己构建这套基础架构——事件存储、命令管道、发件箱、预测、审计、加密以及所有相关的连接——在你发布任何功能之前就需要耗费数月时间。因此,大多数团队会跳过这一步,直接发布 CRUD 功能,然后在之后全力应对“你能证明它是否有效”的问题——此时无需人工智能。

我不想为每个项目都重复支付那几个月的费用。所以我一次性构建好了基础架构,并将其从我们自己的产品中剥离出来:Stratara,一个基于 .NET 10 的技术栈,它恰好提供了所需的功能——CQRS、事件溯源、中介器、发件箱、Saga、投影、身份验证,以及防篡改流和租户绑定加密,这些功能通过 22 个 NuGet 包以同步版本的方式提供,并且可以按需选择。其背后的理念并非“AI 平台”——而是构建应用程序的架构,而不是底层的底层细节。代理程序能够安全地运行在其上,这只是基础架构正确带来的一个意外收获。

速度快到你真的会一直开着它

整个前提是某个角色每秒执行一百个动作——因此基础架构必须跟上,不能屈服于自身的审计保证。而这里存在一个无人承认的失败模式:这些保证是真实存在的,响应速度很慢,所以一旦负载测试出现问题,就会有人悄悄地将其关闭。审计抽样率降至十分之一。投影重建不再实时运行,而是改为每晚定时任务。原本应该确保系统可验证性的东西,反而成了你为了达到 p99 要求而禁用的东西。这根本不是什么基础架构——这只是一个等待被关闭的功能开关。

因此,这些高效路径不使用反射。重放数据流意味着Apply为每个事件调用一个方法,而这种简单的MethodInfo.Invoke逐事件调用方式正是导致人们偷工减料的根本原因。取而代之的是,每个 apply 方法、投影处理程序和构造函数都会被编译一次,生成一个强类型委托Expression.Lambda(...).Compile()并进行缓存。首次编译之后,就可以直接调用,无需查找。在这台机器上,编译后的属性写入速度比等效的反射快约 13 倍——而且由于它是逐事件执行的,因此这种速度差距会随着数据流长度的增加而线性增长。

最终的回报体现在关键时刻,也就是完整的回放中:

事件重现 时间 已分配
10,000 0.11毫秒 64 B
100,000 1.13毫秒 64 B
1,000,000 11.6毫秒 64 B

百万个事件在约 12 毫秒内处理完毕——而且,我最喜欢的是,无论数据流有多长,每个事件都保持64 字节的恒定大小。重放整个历史记录几乎不会给垃圾回收器留下任何可追踪的线索。你为审计员保留的审计跟踪数据结构与你为应用程序在个位数毫秒内重放的数据结构相同。你无法在可验证性和速度之间做出选择;要么两者兼得,要么两者都不具备,而在这里,两者兼得。

(使用 BenchmarkDotNet 在一台无风扇 MacBook Air M4 上进行测试。请将数值视为比率,而非服务器的绝对值——有真正气流的冷却设备才能改变绝对值,而非数值形状。基准测试项目已包含在代码库中,dotnet run -c Release可以复现此结果。)

通过添加方框来扩展规模,而不是通过重写代码。

单核处理器的速度是基本要求。更难保证的性能——也是单事件溯源通常无法实现的——是当每秒数百个动作同时从众多参与者那里涌来时会发生什么。

教科书式的陷阱是全局锁。为了保持聚合事件的有序性,最简单的实现方式是将每次写入都序列化,这样一来,无论你购买了多少个核心,吞吐量上限都只能达到一个核心。Stratara 采用了不同的方法,值得深入了解,因为它关乎扩展性:

命令不会阻塞调用者。默认的写入路径是“即发即弃”:命令会发送到消息总线并202 Accepted立即返回,同时由进程外的工作线程处理。请求线程永远不会等待业务逻辑,流量高峰会在总线中缓冲,而不是阻塞 Web 层。

工作节点竞相处理任务。总线(RabbitMQ 或 Azure 服务总线)会将每条消息传递给空闲的工作节点。增加副本(更多 Pod、更多节点)后,它们会自动分担负载。无需选举领导者,也无需手动重新分配分区。横向扩展只是部署清单中的一个数字。

顺序在并行处理中得以保留——通过存储桶而非锁来实现。这正是其巧妙之处。每个聚合 ID 都会被确定性地哈希到4096 个存储桶中的一个:同一个 ID 总是落入同一个存储桶。存储桶内的写入操作通过单写锁进行串行化,因此同一聚合的事件保持严格有序——但不同的存储桶可以完全并行运行。这样既保证了聚合内部的一致性,又实现了跨聚合的并发性,而无需任何全局协调。

4096 特意是 2 的幂(使用低成本的位掩码而非取模运算),并且每个持久化行——事件、快照、命令日志、发件箱——都带有其存储桶 ID 并以此建立索引。因此,存储桶轴不仅是一个锁,它还是一个分区键,可以用于对数据库进行分片。

读取模型通过订阅而非轮询来保持数据同步。预测模型不会定时向表询问“是否有新数据?”。它们订阅总线,写入路径会在提交的瞬间发布事件包。读取模型通过一个节拍而非轮询间隔来滞后于写入操作——这样,在流量低谷时,您就不会浪费时间执行“是否有工作?”之类的查询。

综上所述,扩展不再是架构项目,而是运维项目。命令工作进程、投影工作进程和 Saga 工作进程都作为相互竞争的消费者进行扩展;唯一特意例外的是防篡改哈希工作进程,它被设计成单实例运行——因为它只向单个链追加数据,你肯定不希望两个写入器争抢它的资源。

公交车停下时,没有任何东西在排队等候。

每秒一百次的前提还有一点要求:快速路径不能是唯一路径,否则经纪人的失误会导致命令丢失。

因此,调度器首先尝试通过总线发布——直接、快速发布。只有当总线不可达时,命令才会进入持久化发件箱表,并在OutboxWorker总线恢复后重新发布。正常情况下,热路径上根本不会进行发件箱往返;持久化网络仅在故障时才会启用。

让我明确一点,因为营销人员通常会在这里夸大其词:保证的是“至少一次”,而不是“恰好一次”。一条命令可能会到达两次——例如,发布过程中崩溃后重试——因此,处理程序被设计成幂等的,而关联 ID 则能确保重复消息的可检测性。“不会有消息悄悄丢失”,没错。“每条消息都神奇地恰好出现一次”,不,任何向你兜售后者却不使用幂等处理程序的人都是在忽悠你。

为什么这是地板

这些技术并非首创——无反射分发、分桶单写锁、推送投影、出站回退机制等等,这些技术都早于我出现,而且你可以将它们中的任何一项应用到你自己的仅追加存储中。真正耗时数月的是将它们全部干净利落地连接起来,实现版本同步,并确保它们在高负载下也能稳定运行。我不想在这部分上花两倍的钱。如果你正在使用 .NET,并且希望你的下一个系统(无论是否基于代理)从一开始就建立在生产级的基础之上,那么这就是我的选择。

仓库中提供了可运行的、无依赖项的示例,文档位于 docs.stratara.tech。源代码以 FSL-1.1-MIT 许可发布(非 OSI 批准的开源软件;每次发布两年后转为普通 MIT 许可)。

阅读是一回事,动手实践又是另一回事。下载代码库,运行示例,在其基础上实现你自己的第一个操作——然后提出你对如何改进基础架构的想法。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐