← Back to Blog

当 AI 开始编写不再简单的软件

当 AI 开始编写不再简单的软件

如今,借助 AI 的软件开发往往遵循一个简单直接的流程:

  1. 撰写任务描述(prompt)。

  2. 让 AI 给出分析和建议的解决方案。

  3. 准备 skill、agent 和 subagent。

  4. 开始实现。

  5. 让 AI 编写代码、测试并修复发现的问题。

乍一看,这似乎是理想的开发工作流。AI 自主运行,在各个 agent 之间分配任务,编写测试,并逐步完成所要求的功能。

对于较小的项目,这种方法可以非常有效。

如果你要构建一个简单的电商商店、一个五页的公司网站,或一个基础的管理系统,AI 往往可以完成其中相当大的一部分工作,而且效果很好。

为什么?

因为数以百万计的开发者已经构建过类似的软件。

想象两家网店。一家卖玩具,另一家卖苹果。

从顾客的角度看,这是两门不同的生意。但从软件架构的角度看,它们的运作方式非常相似:

  • 按类别组织的商品。

  • 定价和库存管理。

  • 购物车。

  • 订单处理。

  • 支付方式。

  • 客户管理。

变化的主要是数据、具体的业务规则和某些功能。核心的架构原则基本保持不变。

AI 能识别这些模式,因为它在训练中见过类似的结构。

当你不再构建常规应用,而开始开发大型企业级软件时,问题就出现了。

当系统包含 30 万到 50 万行代码时会发生什么?

想象一个企业级应用,包含数十万行代码、数十个模块、数百个依赖项和复杂的业务规则。

这样的系统可能已经演进了好几年。在此期间,架构决策、例外情况、历史依赖和业务规则不断累积。

并非所有这些都有文档记录。

一个方法可能影响其他十个模块。修改一个参数,就可能扰乱开票、用户权限或订单处理。

现在设想把一个 AI agent 引入这样的环境。

你交给它一个任务:

"修改支付流程以支持一种新增的支付方式。保持向后兼容并测试所有内容。"

agent 制定计划,在 subagent 之间分配工作,实施修改,运行测试并返回结果。

一切看起来都可能是正确的。

但是,测试通过并不一定意味着软件保留了所有既有的业务规则。

这就引出了六个月前彻底改变我对 AI agent 看法的那个问题。

改变我对 agent 系统看法的实验

六个月前,我做了一个实验,它让我选择了另一条道路,而不是单纯地构建更多的 agent 和 subagent。

在那之前,我一直抱有许多开发者共同的假设:

我把任务在各个专业 agent 之间分配得越好,引入的验证机制越多,结果就会越可靠。

但在实验中,我遇到了一个根本性的问题:

如果 agent 根本不知道某条业务规则的存在,它怎么可能正确地实现这条规则?

而另一个 agent 如果同样缺乏关于系统应有行为的完整信息,又怎么能可靠地验证结果?

我开始研究上下文是如何在 agent 之间传递的。

我发现,信息传递和被解读的方式,可能是整个流程中最薄弱的环节之一。

agent 和 subagent 实际上是如何工作的?

在实践中,AI agent 之间的通信有两种常见方式。

1. 层级模型——由主 agent 协调 subagent

这种架构通常被称为 Hub-and-Spoke(中心辐射)或 Star Topology(星型拓扑)。

原理很直接:

  1. 用户把任务提交给主 agent。

  2. 主 agent 分析任务并将其拆分为更小的部分。

  3. 它创建或激活 subagent A 并给出指令。

  4. subagent A 完成工作并返回结果。

  5. 主 agent 评估结果,并可能把进一步的工作委派给 subagent B。

  6. 所有部分完成后,主 agent 合并结果。

subagent A 不一定知道 subagent B 在做什么。通信和信息交换由主 agent 协调。

优点: 集中控制,状态跟踪更容易,职责更清晰,agent 之间失控通信的风险更低。

缺点: 主 agent 成为解读任务指令和结果的中心节点。如果它在委派工作时遗漏了重要信息,subagent 就可能在不完整的上下文中工作。

2. 点对点——agent 之间相互通信

第二种方式涉及 agent 之间的直接或持续通信,例如通过共享的对话线程或消息系统(*Message Bus*)。

设想如下场景:

  • agent A 编写代码并发布 CodeWritten 事件。

  • 负责测试的 agent B 收到该事件并启动测试。

  • agent C 同时准备文档。

  • 各 agent 持续交换结果并对变化做出反应。

优点: 任务并行执行,信息交换快速,对单一中心 agent 的依赖降低。

缺点: 协调更复杂,存在使用过时信息的风险,可能出现相互冲突的修改,追查错误根因也更困难。

这两种方式都可能非常有用。

但单靠任何一种都无法解决根本问题。

一个 agent 可能拥有定义完美的任务,但如果不理解系统真实的业务逻辑,它可能给出技术上正确、业务上却错误的方案。

agent 工作流中的错误是如何产生的?

AI 不是把指令直接翻译成源代码的确定性编译器。语言模型会解读可用信息,并基于该上下文生成解决方案。

当信息缺失时,它可能做出错误的假设。当可用信息不完整或相互矛盾时,它可能选择不恰当的方法。

让我们来看一个较大任务的典型工作流。

步骤 1 - 解读用户 prompt

主 agent 收到任务,并尝试理解所要求的变更。

在这个阶段,它可能忽略某个依赖,误解某个约束,或者假设了从未被明确说明的内容。

步骤 2 - 为 subagent 创建指令

主 agent 创建更具体的任务描述。它从原始 prompt 中选取它认为相关的信息。

但是,如果它不了解某些业务规则,就无法正确地传达这些规则。

步骤 3 - 实现

subagent 收到指令,分析代码库的一部分,并选择实现策略。

它可能产出语法正确、质量很高的代码,却无意中改变了本应保持不变的行为。

步骤 4 - 评估结果

主 agent 收到 subagent 的输出,对其进行评估并判断任务是否完成。

但它可能仍然基于与之前相同的不完整假设在工作。

步骤 5 - 测试和修复错误

随后是单元测试、集成测试、E2E 测试、静态分析和文档审查。

这些机制显著提高了软件的可靠性,但它们也有局限。

测试验证的是具体的预期行为。如果某条重要的业务规则不在测试规格中,实现就可能通过所有测试,却违反了该规则。

而且当 AI 修复一个问题时,可能会无意中影响系统的另一部分。

这就形成了一个熟悉的循环:

实现 → 测试 → 修复 → 回归 → 更多测试 → 又一次修复。

因此,单纯增加 agent 的数量或更频繁地重复测试,并不会自动保证得到正确的结果。

为什么错误会在 agent 链中累积?

让我们考虑一个简化的数学模型。

假设有三个连续的步骤,每个步骤被正确执行的假设概率为 95%。

如果这些步骤相互独立,并且三个都必须成功,则概率为:

0.95 × 0.95 × 0.95 = 0.8574

也就是说,三个步骤全部成功的概率约为 85.7%。

如果有十个这样的独立步骤,结果将是:

0.95¹⁰ ≈ 59.9%

这是一个说明性的计算,而不是对 AI agent 成功率的实测。在真实系统中,错误并不总是相互独立的,验证机制可以发现错误,各个步骤也可以纠正之前的失误。

不过,这个例子说明了一个重要的原理:

单个 agent 的可靠性并不会自动保证整条链的可靠性。

当不完整的指令在每个阶段被反复解读时,就会出现更多做出错误决策的机会。

这就是为什么我认为,单纯增加更多的 agent、测试和验证机制是不够的。

我们需要改变 AI 获取系统本身信息的方式。

关键区别:测试验证结果,业务逻辑界定边界

传统的 AI 辅助开发方式是被动反应式的。

你提交一个需求,AI 实现一个方案,然后用测试来判断是否有东西被破坏。

这当然比完全没有测试要好。

但仍然存在一个根本性的弱点:

测试无法可靠地验证那些无人知晓、也不在测试规格内的业务规则。

因此我认为,更好的做法是在实现开始之前,就向 AI 提供关于系统实际行为的信息。

不只是 需要改变什么,还包括:

  • 哪些业务规则适用于受影响的代码。

  • 还有哪些其他模块依赖这项功能。

  • 哪些输入和输出必须保持兼容。

  • 哪些场景必须保持不变。

  • 存在哪些例外和历史约束。

  • 实现的哪些部分 AI 可以修改,哪些必须保留。

这大大减少了 AI 必须自行推断的行为量。

同一任务的两种方法

方法 A - Prompt + 测试

AI 收到任务,根据可用的上下文选择一种实现,产出方案并运行测试。

如果测试不完整,对既有业务规则的违反可能无法被发现。

方法 B - Prompt + 业务逻辑 + 测试

AI 收到任务的同时,还收到相关的规则、依赖、契约和约束。

因此,在实现开始之前,它就知道哪些行为必须保留。

这样,测试不仅仅是发现错误,还会验证具体的业务契约。

区别不在于我们不再测试。

区别在于,我们试图在错误被引入之前就加以预防。

当 AI 开始编写不再简单的软件 — 截图 1

实际示例:添加一种新的支付方式

假设我们有一个名为 CheckoutService.ts 的文件,负责电商应用中的支付。

我们想添加银行卡支付作为一种额外的支付方式。

方案 A:常规 prompt

任务:

> 在 CheckoutService.ts 中添加银行卡支付。保留现有功能并运行所有测试。

AI 修改代码,添加新的支付分支,并更新测试。

所有测试通过。

任务被标记为完成。

然而,系统中存在一条重要的业务规则:

公司账户必须以银行转账作为默认支付方式。

这条规则是几年前引入的。实现它的开发者已经不在公司工作了。该规则没有清晰的文档,也没有专门的回归测试。

AI 把银行卡支付设为所有客户的默认方式。

从实现的角度看,新功能运行正确。

从业务逻辑的角度看,系统已经被破坏。

而测试没有发现这个问题。

方案 B:在 AITM 中使用带业务逻辑的结构化任务

现在设想 AI 收到同样的请求,但这次任务中包含相关的业务契约、相关变量、指向历史任务的链接,以及对允许修改范围的精确界定。

任务可能是这样的:

READ CLAUDE.md first.

TASK:

Add card payment as an additional payment method

in CheckoutService.ts.

Contract:

%CHECKOUT-019 — "For company accounts
(companyId != null), bank transfer must remain
the default payment method. Card payment may
be offered only as an additional option."

Related variables:

BL.CHECKOUT.PAYMENTMETHOD_DEFAULT
BL.CHECKOUT.PAYMENTMETHOD_OPTIONS

Related task:

#<link to the task that originally implemented bank-transfer-as-default checkout>

In scope:

- Add a new "card" option to the available payment methods in CheckoutService.ts.

- Expose it as a selectable option in the existing payment method selection flow.

Out of scope:

- Changing the default payment method for any account type.

- Any UI/checkout-flow redesign beyond adding the new option.

- New payment provider integration logic beyond the method registration itself.

Must not break:

- Existing default payment selection logic (company accounts → bank transfer stays default).

- Company account checkout behavior end-to-end.

- Existing bank transfer processing path.

- Compatibility with the current payment API contract.

File path:

src/Services/CheckoutService.ts
Confirm the exact path via contractsForFile before editing.

Verify:

- Company account checkout still selects bank transfer by default
  (companyId != null).

- Card payment appears as a selectable, non-default option.

- Existing bank transfer processing is unchanged.

- Run unit and integration tests covering CheckoutService.

关于其在 AITM 中如何运作的更多信息:https://aitm.cz/Docs/KnowledgeSharing

为什么这种格式很重要?

乍一看,它可能只是一个更详细的 prompt。

但真正的区别在于,这个任务与可以检索、并能对照实际项目进行验证的信息相连接。

1. 契约 %CHECKOUT-019

标识符 %CHECKOUT-019 不只是一条文字备注。

它是一个示例性的契约标识符,agent 可以通过 contractsForFile 或 contractsForVariable 等 GraphQL 操作检索到它。

agent 不必只依赖 prompt 中写下的规则,而是可以检索其结构化表示,并对照当前代码库进行验证。

2. 业务逻辑变量 _BL.

诸如 _BL.CHECKOUT.PAYMENT_METHOD_DEFAULT 和 _BL.CHECKOUT.PAYMENT_METHOD_OPTIONS 之类的标识符,代表以 AITM 通信语言表达的业务逻辑。

它们有助于把具体需求与决定系统行为的规则和变量联系起来。

3. 对相关任务的引用

Related task 部分引用了实现该业务规则的原始任务。

这为 AI 提供了额外的上下文,不仅让它知道系统当前如何运行,还知道当初为何会做出某个特定决定。

4. 清晰界定的变更边界

In scope、Out of scope 和 Must not break 三个部分定义了三个独立的类别:

  • AI 必须实现什么。

  • AI 不得实现什么。

  • AI 必须保留哪些既有行为。

AI 收到的不是一个可以随意解读或扩展的开放式任务,而是一个精确定义的实现范围。

5. 编写代码之前先验证

文件路径不会被自动假定为正确。

agent 被指示先使用 contractsForFile,确认实际路径,并找出相关联的契约。

然后才应开始修改代码。

6. 通过 TPV 进行 prompt 验证

AITM 方法的一个重要组成部分是 TPV prompt 验证器。

它在任务提交执行之前检查必备的任务章节。

例如,它会验证是否存在范围定义、限制条件、契约、文件路径和验证要求。

这把一部分错误预防从测试已完成的代码,前移到了准备任务本身。

注意: 本示例中的契约标识符和业务逻辑变量仅为示意性的占位符。在真实项目中,优化器会通过 GraphQL 从具体项目中检索它们。不应在未经验证的情况下猜测或生成它们。

这种方法带来了什么变化?

在传统方式中,agent 先解读一个开放式任务,并自行决定哪些文件、依赖和业务规则重要。

而在 AITM 中,任务与具体的契约、业务逻辑变量、历史任务以及清晰界定的变更边界相连接。

因此,agent 可以基于可验证的信息做出决策,而不是主要依赖假设。

这就是我认为的根本区别所在。

业务逻辑不再仅仅是文档的一部分。它成为实现控制流程本身的一部分。

这就是 AITM 背后的原理

当我们开始设计 AITM(AI Task Manager)时,我们并不想构建又一个只是启动 agent、分发任务、事后检查结果的系统。

我们关注的是另一个问题:

如果 AI 能在做出任何修改之前,直接从现有源代码中获得相关的业务逻辑,会怎样?

不只是某人一年前写下的文档。

不只是存放在 skill 中的通用指令。

也不只是 agent 碰巧选中的几个文件。

而是描述受影响的系统部分实际如何运行、受哪些契约约束、以及有哪些其他组件依赖它的结构化信息。

因此,AITM 的核心原则是:用从代码库相关部分识别出的业务逻辑来丰富工作 prompt。

agent 获得的不仅是关于它必须做什么的信息,还有关于它必须保留什么的信息。

这在大型软件系统中尤其重要,因为单个 prompt 可能无法包含每一个相关的依赖或约束。

业务逻辑:AI 的护栏

把业务逻辑想象成道路两侧的护栏。

没有护栏,AI 可以走许多不同的路径。有些可能通向正确的方案,而另一些则可能违反系统中隐藏的规则。

通过提供相关的契约和约束,我们大大缩小了出现不良决策的空间。

AI 仍然可以独立选择实现策略、算法和代码结构。

但它必须遵守已知的业务规则。

随后的测试会验证这些规则是否真的得到了保留。

这代表着理念上的根本转变。

我们不只是试图修复 AI 产生的错误。我们是在试图降低这些错误一开始就被引入的概率。

这是否意味着我们不再需要 agent 和测试?

当然不是。

agent 适用于并行执行、专业化活动和大型任务的自动化。

单元测试、集成测试和 E2E 测试仍然是软件质量保证的重要组成部分。

但 agent 编排和自动化测试无法取代对业务逻辑的了解。

如果 agent 不理解系统必须保留什么,即使是组织得再完美的 subagent 团队也解决不了根本问题。

而如果测试没有覆盖相关场景,那么所有测试都可能通过,而应用从业务角度看仍然行为不正确。

正确的做法不是在业务逻辑和测试之间二选一,而是将两者结合。

首先,识别系统的实际行为。

然后,把这些行为表达为清晰的规则和契约。

接着,把这些规则连同任务一起提供给 AI。

只有这时,才进入实现、验证和测试。

这种方法可以减少不必要的修复、回归和重复的实现周期。

结论:关键不在于 agent 的数量,而在于信息的质量。

对于简单的应用,即使指令相对简短,AI 也能交付出色的结果。

但对于拥有五十万行代码的企业级软件,仅仅假设 agent 会正确理解一切、发现每一个依赖,然后完美地测试自己的工作,是不够的。

系统越大,理解并保护其真实的业务行为就越重要。

你可以让十个 agent 完美协作。但如果它们都不知道某条关键的业务规则,它们就可能共同产出一个经过完美测试、却从根本上错误的方案。

这就是我们在 AITM 中采用不同方法的原因。

首先是上下文、规则和契约。

然后是实现。

最后是验证和测试。

因为目标不是让 AI 在最短时间内写出尽可能多的代码。

目标是让 AI 精确地修改需要修改的地方,而不破坏已经正常运行的部分。

www.aitm.cz