为什么 Agent 在 Demo 里正常,上生产就出问题
Demo 与生产环境的差别,通常被理解成规模差别——数据量更大、并发更高、场景更复杂。按这个理解,压力测试就该能覆盖。
但有一类问题压力测试测不出来,因为它们的成因不是规模,是时间尺度和失败路径。演示只走成功路径,且全过程通常不超过几分钟;生产环境要在几个月里反复走失败路径。这两条差异会让一些在 Demo 中完全正确的设计,在生产中系统性地出错。
以下四类失效各自给出脱离具体技术栈的判据。
Demo 环境和生产环境的差别不在规模
一次演示的完整生命周期大约是几分钟,操作者只有一个人,走的是事先验证过的路径。生产环境里,同一份资产会被多人在几个月内反复修改,其中相当一部分操作会失败、会被中断、会在错误的顺序上执行。
这个差异导致的直接后果是:演示环境几乎不产生并发冲突,也几乎不产生中间状态。而并发冲突和中间状态恰恰是生产系统中大部分复杂度的来源。
下面四条各自对应这个差异的一个侧面。
乐观锁为什么会在人工审批环节失效
乐观锁的前提假设是冲突很少发生:读取时记下版本号,写入时校验版本号未变,变了就重试。这个假设成立的条件是临界区足够短——通常是一次函数调用,微秒到毫秒量级。
当工作流中加入人工审批,临界区的定义发生了变化。它不再是一次函数调用,而是:系统生成变更提案 → 人审阅 → 人确认 → 应用变更。中间那段是人类的思考时间,几分钟到几小时。
跨越六个数量级之后,「冲突很少发生」不再是一个合理假设。更麻烦的是失败处理:乐观锁失败的标准应对是重试,而这里的重试意味着让人重新审阅一遍,审阅结果还可能不一样。
正确的做法通常是退回粗粒度的悲观锁——一个全局写槽,同一时刻只允许一个变更流程处于进行中,其余请求直接被拒绝并告知原因。这牺牲了并发度,换来的是确定性:任何时刻系统里只有一份待应用的变更,不存在需要合并的分支。
对多数需要人工审批的场景,这个交换是划算的。这类流程的天然吞吐量本来就受限于人的审阅速度,并发度不是瓶颈。
判据:临界区里是否包含人类决策时间。包含就用悲观锁,不要用乐观锁加重试。
「原子操作」承诺到哪里为止
当一次操作需要同时修改两个不共享事务边界的资源——一个文件系统和一个数据库、两个独立的服务、一份配置和一份索引——真正的原子性做不到。分布式事务能缓解,但引入的复杂度往往超过它解决的问题。
实践中常见的处理方式是:按顺序修改,大概率两个都成功,然后在文档里写「该操作是原子的」。这个描述在演示中永远不会被证伪,因为演示不会在两次修改之间崩溃。
生产环境会。而且一旦崩溃在中间,系统进入了一个文档里没有描述过的状态,没有人知道该怎么恢复。
诚实的做法是明确写出边界:不承诺两个资源的联合强原子性;每一步操作前写操作日志;崩溃后按日志判断停在哪一步,执行对应的补偿动作。这份文档比「该操作是原子的」长得多,但它描述的是真实行为。
这里的收益不只是可靠性。一个说不清失败状态的系统,每次故障都要重新推理一遍;而一个写明了补偿路径的系统,故障处理是查表。
判据:能否说清楚「崩溃在第 N 步会留下什么状态、怎么恢复」。说不清就不要声称原子。
为什么开源 Agent 工作流不能直接用于持续维护
许多开源的 Agent 编排框架,其隐含设计假设是一次性生成:使用者提出需求,Agent 产出一套结果,流程结束。这个假设支撑了大量简化,其中一些会以硬性限制的形式出现在接口上:只允许新增不允许修改、不支持删除、状态不可回退、生成的资产没有版本概念。
在一次性场景下这些限制是合理的。演示不需要修改已生成的内容,初次接入也不需要。
生产环境需要的恰恰相反。同一份资产会被反复修订:口径改了要改、字段废弃了要删、上游结构变了要跟着调整。此时「只增不改」不是一个可以绕过的限制,而是直接卡死正常业务——每次修订都变成新增一份,几轮之后没有人知道哪一份是有效的。
这类限制不容易在选型阶段发现,因为框架的示例代码通常只演示创建路径。
判据:看框架的示例和测试里,有没有「修改一份已存在的资产」和「删除一份资产」的完整路径。只有创建路径的框架,是为演示设计的。
数据仓库没有主键约束时,语义层怎么办
多数分析型数据仓库不强制主键与外键约束。有些完全不支持声明,有些允许声明但不做执行时校验——约束在那里更像一条注释,而不是一个保证。
而建模层需要知道主键。去重、扇出判断、聚合正确性都依赖它:不知道哪一列唯一,就无法判断一次连接会不会让度量值翻倍。
这里有两种常见的错误处理。一种是从建表语句里猜——按列名里有没有 id、按数据分布抽样看唯一性。猜对的时候没人注意,猜错的时候产生的是静默错误:查询正常返回,数字是错的。另一种是用空值表示「没有主键」,这就把「这张表确实没有主键」和「不知道这张表有没有主键」混成了同一个状态,上层无法区分。
正确的做法是显式区分三种状态:已声明且经过校验、已声明但未校验、未声明。对第三种明确返回「不支持」,把决定权交给上层——由建模者补充声明,或者接受该表上的某些操作不可用。
明确说「不知道」比猜一个值更有用,因为前者可以被处理,后者只能被发现。
判据:系统能否区分「这张表没有主键」与「不知道这张表有没有主键」。不能区分,就一定会在某个时刻产生静默错误。
反方:这些是特定技术栈的问题吗
对上述四条,最直接的反驳是:它们看起来是特定选型带来的问题。换一个支持约束的数据库,主键问题不存在;换一个设计更完整的编排框架,修改路径的问题不存在。这些不是通用规律,是选型失误。
这个反驳部分成立。第三条和第四条确实可以通过选型缓解。
但它没有回应更根本的一点:这四类问题都不会在演示中暴露。选型阶段能看到的材料——文档、示例、演示——全部运行在演示的时间尺度上,只走成功路径。一个框架是否支持修改路径,一个仓库是否真的校验约束,一次操作在中途崩溃会留下什么,这些都不会出现在选型材料里,因为提供材料的一方通常也没有在那个尺度上跑过。
所以真正可迁移的不是「避开这四个坑」,而是选型时问的问题要换一类:不问「它能做什么」,问「它失败时留下什么」。
四个判据
整理成可以在设计评审上直接问出口的形式:
临界区里有没有人。 有人就意味着临界区是分钟量级,乐观锁的前提不成立。
崩溃在中间会留下什么。 答不上来,就不要在文档里写「原子」。
示例里有没有修改和删除。 只有创建路径的框架,是为演示设计的。
分不分得清「没有」和「不知道」。 分不清,静默错误只是时间问题。
这四个问题的共同点是,它们问的都不是功能,而是失败时的行为。演示回答不了这类问题——这正是它们值得在演示之外单独问一遍的原因。