Skip to content

面向 AI 的架构原则(开放式探讨)

核心问题

结合前五章的案例和分析,能否提炼出一些通用的架构原则,指导 AI 时代的代码设计?

以下五个原则不是"标准答案",而是一个讨论的起点。

面向 AI 的七大架构原则


原则一:层次精简优于多级叠加

问题

传统的三层架构(Controller → Service → Mapper)本身是合理的,但实践中容易在"标准三层"之上不断叠加新层——ApplicationService、Command、RequestConverter 等。

以 Growth AIOS 的重构经历为例(参考第 04 章),一个请求链路涉及:

Controller → RequestConverter → ApplicationService → Command → DomainService → Repository

ApplicationService 几乎没有真正的业务逻辑——只是在调用 DomainService 之前做参数转换、之后做 VO 转换。Command 和 Request 字段几乎一样。RequestConverter 只做字段级映射。

对 AI 来说,这些中间层带来的成本远大于价值: 每多一层就多一次文件跳转,改一个字段需要同步修改 4-6 处,而 AI 的大部分 token 消耗在了无意义的转发逻辑上。

原则

标准三层不要动,多余的中间层要砍掉。

Controller → Service → Mapper 跨三个工程、每层职责清晰、可独立测试,这是正确的架构设计。这套模式在 C# 项目中被验证为最佳实践——Components 工作区中所有组件服务都遵从这个结构。同样适用于 Java 项目,wukong 就是按这个模式组织的。

需要精简的是那些"没有实际业务价值、纯转发"的中间层。

推荐(三层三工程,每层独立测试):
  wukong-web/
    BillController.java        ← Web 层(接收请求、校验、返回)
  wukong-business/
    BillService.java           ← 业务层(业务编排、事务管理)
  wukong-domain/
    BillMapper.java            ← 数据层(数据访问)

不推荐(叠加无价值中间层):
  Controller → RequestConverter → ApplicationService → Command → DomainService

适用边界

跨工程的接口契约仍然重要。当中间层有真实的业务逻辑(如跨多个 DomainService 的事务协调、复杂的参数构造逻辑)时,保留是合理的。但纯转发的中间层应该果断去掉。

判断标准:如果一层只做"转发"而不做"转换",那它大概率是多余的。


原则二:聚合对象优于碎片分散

问题

传统 Java 项目中,一个业务实体对应多个碎片化对象——VO、DTO、BO、PO、DO、Form、Query、Command(参考第 02 章)。

一个简单的 CRUD 涉及的对象体系:
  SysUserEntity.java          ← Entity(ORM 映射)
  SysUserVO.java              ← VO(视图展示)
  SysUserDTO.java             ← DTO(数据传输)
  SysUserBO.java              ← BO(业务对象)
  CreateUserCommand.java      ← Command(写操作)
  UserListQuery.java          ← Query(查询条件)
  UserVOConverter.java        ← 转换器

"分散"不是指三层三个工程分散,而是指对象体系碎片化。 对 AI 来说,这意味着:

  1. AI 不知道该用什么——每次生成时,它需要猜测入参用 Form 还是 Command 还是 Request
  2. 转换逻辑是 Bug 高发区——VO↔DTO↔BO↔Entity 的每一层转换,AI 都要精确匹配字段
  3. Token 的严重浪费——大部分上下文被中间对象的 import/注解/getter-setter 占用

原则

三层三工程的架构是正确的,不需要合并。需要精简的是同一层内的碎片化对象体系

核心思路:Entity + Request + VO 三种对象覆盖 90% 场景,中间传递统一用 Entity 或 Tuple。

推荐(三层三工程,每层只保留必要对象):
  wukong-web/
    CreateUserRequest.java     ← Request(入参,继承 BaseRequest)
    UserVO.java                ← VO(出参)
  wukong-business/
    BUserService.java          ← Service(方法参数扁平,无多余包装)
    SysUserEntity.java         ← Entity(业务层和 Mapper 共用)
  wukong-domain/
    SysUserMapper.java         ← Mapper

不推荐(碎片化对象体系):
  web/BillVO.java
  web/BillForm.java
  business/BillDTO.java
  business/BillBO.java
  domain/BillPO.java
  domain/BillDO.java
  domain/BillQuery.java
  business/BillService.java
  business/BillServiceImpl.java

适用边界

外部 RPC 接口仍然需要独立的 DTO 定义。组件化服务的接口需要标准入参出参(参考第 05 章)。跨表组合查询可以用泛型 Tuple 代替碎片化 DTO——不需要为每个组合场景创建新类。

核心思想:给 AI 一套统一的规则,它就懂得怎么干活。不给出规则,它就随机发挥。


原则三:自包含优于继承

问题

传统架构喜欢建基类——BaseController、AbstractService、BaseEntity。AI 在理解代码时,需要跟踪多层父类才能理解完整行为。

csharp
// 不推荐:AI 需要看 BaseController → BaseApiController → 当前 Controller
public class BillController : BaseController
{
    // save 方法在哪里定义的?在父类还是子类?
}

原则

AI 在理解代码时,自包含的文件比依赖多条继承链的文件容易得多。

不推荐:BaseController → AbstractService → 多级基类
推荐:  一个文件包含完整逻辑,通过显式调用而非隐式继承

适用边界

复用代码应该通过组合而非继承实现。如果跨模块需要共享功能,用静态方法或单独的服务类,而不是通过基类注入。


原则四:数据透明优于封装隐藏

问题

领域驱动设计(DDD)是过去十年企业软件最流行的架构思想。它的核心是"封装数据库细节"——Repository 隐藏 SQL、领域对象隐藏表结构、聚合根隐藏关联关系。但对 AI 来说,这些封装恰恰是理解代码的障碍。

// 领域驱动设计——AI 需要理解多个抽象层才能追踪数据流向
Service → Repository → Domain Entity → 聚合根 → Value Object → ORM 映射 → 数据库

// MyBatis——AI 直接看到数据流动路径
Service → Mapper → SQL → 数据库

程序的本质是"数据 + 算法",LLM 最擅长理解的就是数据的流动路径。 每多一层封装,AI 就需要做一次"逆向推导"才能搞清楚数据从哪里来、到哪里去。

原则

直接暴露表结构和查询操作,比层层封装后让 AI 逆向推导要高效得多。

MyBatis 是这种原则的最佳实践——基于原生 SQL,可预测、可测试、可编写、AI 可优化。虽然代码量比 Hibernate/JPA 大,但对 AI 来说,代码量大不是问题,隐式规则多才是问题

推荐(数据透明,AI 一目了然):
  // Service 中直接调用 Mapper
  SysUserEntity user = sysUserMapper.selectById(userId);
  List<SysOrderEntity> orders = sysOrderMapper.selectList(query);

  // SQL 是透明的,AI 直接看到筛选和返回的字段
  // SELECT * FROM sys_order WHERE user_id = #{userId}

不推荐(封装隐藏,AI 需要逆向推导):
  // Repository 隐藏了 SQL
  UserDomain user = userRepository.findById(userId);
  // Domain 对象隐藏了表结构
  // AI 需要追踪:findById → JPA 自动生成 → 缓存 → 懒加载 → 级联
  // 隐式行为太多,AI 无法"看到"完整的数据流

适用边界

不是所有封装都是有害的。当代码作为公共库暴露给外部系统,或者团队规模较大需要严格语义契约时,封装仍然是必要的。但内部业务代码中,直接暴露数据源和操作,对 AI 更友好

判断标准:如果封装是为了"隐藏细节"而不是"复用逻辑",那对 AI 来说就是多余的。


原则五:可执行优于可声明

问题

Java 的注解机制是声明式编程的典型代表——@Transactional@Cacheable@Async@Retryable……一行注解就能开启事务、启用缓存、切换异步。但对这些注解背后的行为,AI 是"看不见"的

java
// 声明式——AI 能看到的是什么
@Transactional
public void settleBill(Long billId) {
    Bill bill = billMapper.selectById(billId);
    bill.setStatus(Status.SETTLED);
    billMapper.updateById(bill);
}

AI 看到的只是三行代码。它"看不到"的是:

  • 这个方法什么时候开启事务?
  • 什么时候提交?
  • 什么异常触发回滚?
  • 事务传播行为是什么?
  • 当前线程是否已有事务?

这些行为完全由 Spring 的 AOP 代理在运行时决定,对 AI 来说是一个黑盒。 AI 无法通过代码追踪来理解事务边界——它只能"猜"注解的语义。

这和前面章节讨论的问题一样:AI 的理解方式是字段溯源,不是语义解析。 一个无法通过代码路径追踪的行为,对 AI 就是不透明的。

原则

能用编程式代码明确表达的行为,就不要用声明式注解隐藏起来。

以事务处理为例,使用 TransactionTemplate 替代 @Transactional

java
// 可执行式——AI 能看到完整的事务边界
@Autowired
private TransactionTemplate txTemplate;

public void settleBill(Long billId) {
    txTemplate.execute(status -> {
        // ① 锁定——悲观锁,防止并发
        Bill bill = billMapper.selectByIdForUpdate(billId);

        // ② 判断——业务校验
        if (bill.getStatus() != Status.PENDING) {
            throw new BusinessException("当前状态不允许结算");
        }

        // ③ 更新——失败时自动回滚
        bill.setStatus(Status.SETTLED);
        billMapper.updateById(bill);
        return null;
    });
}

AI 现在可以追踪完整的数据流:

  • txTemplate.execute() 明确标记了事务边界
  • selectByIdForUpdate 显式声明了锁操作
  • throw BusinessException 是 AI 知道的回滚触发条件
  • 从锁定 → 判断 → 更新的路径,每一步都可见

注解 vs 编程式的对比

维度@Transactional(声明式)TransactionTemplate(编程式)
事务边界AOP 隐式代理,AI 不可见代码显式包裹,AI 可见
回滚条件默认 RuntimeException,隐含异常抛出路径,代码可见
锁操作需要额外注解或 SQLselectByIdForUpdate 显式声明
行为验证必须运行测试代码审查即可确认
AI 理解成本高(需推断 AOP 行为)低(代码即行为)

标准化事务模板 Skill

锁定 → 判断 → 更新,失败抛异常自动回滚——这个模式可以标准化为一个 Skill,既能检查现有代码是否符合规范,也能生成合规的代码。

// Skill 示例:audit-transaction
// 检查规则:
//   1. 所有写操作必须包裹在 TransactionTemplate 中
//   2. 禁止使用 @Transactional 注解(除测试代码)
//   3. 写操作必须包含:锁 → 判断 → 更新 三步
// 生成规则:
//   1. 注入 TransactionTemplate
//   2. 包裹 execute 回调
//   3. 显式声明锁(selectByIdForUpdate)
//   4. 校验通过后执行更新

适用边界

声明式注解并非全无价值。在以下场景可以保留:

  • 框架/公共库设计:注解作为 SPI 扩展点(如 @EventListener
  • 纯查询缓存@Cacheable 不影响数据一致性
  • 测试 Mock:测试代码中的注解不影响生产逻辑

所有影响数据一致性的行为(事务、锁、并发控制)都应该用编程式代码明确表达,而不是靠注解隐式完成。

判断标准:如果一行注解隐藏了不可见的副作用,那它对 AI 就是不友好的。


原则六:CLI 可测优于 IDE 依赖

问题

传统的测试框架把"在 IDE 中运行"作为默认方式——测试类用 [Test] 注解标记、通过 IDE 的测试运行器点击执行、结果在 IDE 面板中展示。但对 AI 来说,IDE 的测试运行器是不可操作的——AI 无法点击"Run"按钮,也无法读取测试结果面板。

更隐蔽的问题是:传统测试框架把测试和 IDE 绑定在一起——启动配置、调试端口、环境变量都藏在 IDE 的配置文件中,脱离 IDE 就无法运行。

原则

测试框架首先应该是一个 CLI 应用,其次才是一个测试框架。

Components.Test 的设计充分体现了这一原则。它的入口是一个标准的 CLI 程序,所有行为通过命令行参数控制(参考第 07 章):

bash
# 运行所有测试
Components.Test

# 运行指定模块测试
Components.Test -n           # 节点测试
Components.Test -w           # 工作流测试
Components.Test -l           # LLM 测试
Components.Test -c           # CLI 测试

# 运行指定场景
Components.Test -n TextProcessor
Components.Test -w SimpleWorkflow001

每个测试用例是一个数据文件(input.json + output.json),而不是一个需要编译运行的测试类:

json
// input.json —— 测试输入(AI 可以生成)
{
  "testType": "chat",
  "message": "你好,请简单介绍一下你自己"
}

// output.json —— 测试输出(AI 可以验证)
{
  "Success": true,
  "Content": "你好!我是...",
  "DurationMs": 7382
}

这意味着:

  • AI 可以直接读取测试用例——input/output 是 JSON,AI 天然理解
  • AI 可以生成测试用例——根据接口定义生成对应的 input.json
  • AI 可以执行测试——通过 CLI 命令运行,不需要 IDE
  • AI 可以验证结果——output.json 包含完整的执行指标

架构特点

Components.Test 采用模块化 CLI 架构:

Components.Test/          ← CLI 入口(Program.cs)
├── Modules/              ← 按模块组织的测试逻辑
│   ├── WorkflowTestModule
│   ├── LLMTestModule
│   └── CLITestModule
├── TestCases/            ← JSON 数据驱动的测试用例
│   ├── AiAgent/BasicChat/
│   │   ├── input.json    ← 输入
│   │   ├── output.json   ← 输出(自动生成)
│   │   └── execute_log.txt  ← 执行日志(自动生成)
├── Mock/                 ← Mock 服务
├── Configuration/
│   └── CommandLineParser ← CLI 参数解析
└── Documents/            ← 测试文档即规范

没有 IDE 依赖——不依赖 Visual Studio 的 Test Explorer、不依赖 IntelliJ 的测试运行器、不依赖任何 GUI 工具。测试即命令行,AI 可以直接操作。

适用边界

对于需要复杂 UI 交互验证的场景(如 Playwright 的 E2E 测试),浏览器仍然是必要的。但所有非 UI 的测试——Service 层测试、集成测试、组件测试——都应该设计为 CLI 驱动的形式,让 AI 可以直接操作。

判断标准:如果测试不能通过一条 CLI 命令执行,那它就还没有被设计好。


原则七:模块标准化优于自由组织

问题

没有标准化之前,每个开发者都有自己的模块组织偏好:

// 开发者 A 的风格
UserModule/
├── services/
│   ├── UserService.java
│   └── UserServiceImpl.java
├── models/
│   └── User.java
└── utils/
    └── UserHelper.java

// 开发者 B 的风格
user/
├── UserController.java
├── UserService.java
├── UserDao.java
└── User.java

AI 在理解不同模块时需要反复适应不同的目录结构、命名风格、访问修饰符。自由组织对人是"灵活",对 AI 是"噪音"。

原则

模块应该有统一的标准结构,而且这个标准应该由 AI 来执行。

Components 项目中的 38 个模块全部遵循同一套模板,由 module-architecture-check.md 规则保证一致性:

标准模块结构:
XxxModule.cs              ← 模块入口,public static,Register/Start/Stop
Interfaces/               ← 接口层,public
  I{Name}Service.cs
  Definition/             ← 入参出参定义,public
    {Name}Request.cs
    {Name}Result.cs
Implementations/          ← 实现层,internal
  {Name}ServiceImpl.cs
LogFormatHelper.cs        ← 日志,internal
LoggerConfig.cs           ← 日志配置,internal

38 个模块全部遵从这个结构:

Components/
├── AiAgent/          ← AiAgentModule.cs + Interfaces/ + Implementations/
├── CLI/              ← CLIModule.cs + Interfaces/ + Implementations/
├── LLM/              ← LLMModule.cs + Interfaces/ + Implementations/
├── MCP/              ← McpModule.cs + Interfaces/ + Implementations/
├── Plugin/           ← PluginModule.cs + Interfaces/ + Implementations/
├── Trigger/          ← TriggerModule.cs + Interfaces/ + Implementations/
├── VarVault/         ← VarVaultModule.cs + Interfaces/ + Implementations/
├── Video/            ← VideoModule.cs + Interfaces/ + Implementations/
├── Workflow.Node/    ← WorkflowNodeModule.cs + Interfaces/ + Implementations/
└── ...               ← 全部 38 个模块同一套标准

AI 是标准化的关键

这套标准化在 AI 时代之前很难推行——因为开发者会觉得"约束太多"、"写起来麻烦"。但 AI 的介入彻底改变了这一点:

  • AI 生成模块:给定模块名,AI 自动生成符合标准的所有文件(Module、Interface、Impl、Logger),且永远一致
  • AI 检查合规module-architecture-check.md 作为 Rule 被 AI 自动遵循环,人不需要再审查
  • AI 适应成本为零:AI 看到任意一个新模块,都能通过目录结构瞬间确定它的功能边界
yaml
# module-architecture-check.md —— AI 自动执行
# 三标准类
- [ ] {ModuleName}Module.cs 存在
- [ ] LogFormatHelper.cs 存在
- [ ] LoggerConfig.cs 存在

# 目录结构
- [ ] Interfaces/ 目录存在
- [ ] Interfaces/Definition/ 目录存在
- [ ] Implementations/ 目录存在

# 访问修饰符
- [ ] Interfaces/ 下为 public
- [ ] Implementations/ 下为 internal

跨生态对比

不同语言生态的模块标准化能力差异很大:

生态模块管理方式AI 友好度
C#.sln + 项目引用,原生支持多项目模块化✅ 简单直接
Java/Maven多 Module POM 继承,配置复杂、缺少统一约定❌ 较弱
Node/RushRush 管理多包仓库,但需要外部工具链支持⚠️ 可用

C# 的解决方案最简单——一个 .sln 包含多个项目,项目之间通过项目引用通信,每个项目就是标准化的模块。

可插拔性

标准化模块的另一个价值是可插拔

csharp
// Program.cs —— 按需插拔模块
if (config["MemoryProvider"] == "OpenViking")
    services.RegisterOpenViking(config);  // 插上高级记忆模块
else
    services.RegisterAgentMemory(config); // 插上基础记忆模块

每个模块只有一个入口——XxxModule.Register(),外部不需要知道模块内部的任何细节。新增模块就是新增一个目录 + 一个入口注册方法,不影响任何现有代码。

适用边界

标准化框架不能代替领域设计。对于纯业务逻辑(如一个 CRUD 的 Service),仍然需要按业务场景组织代码。

判断标准:如果同一个项目中不同模块的目录结构、命名风格、访问修饰符不一致,那标准化就还没有到位。


知识篇:七个原则的优先级和适用场景

优先级

在开始一个新模块时,建议按以下优先级应用这些原则:

  1. 层次精简(最重要的)——决定了层次结构的合理性
  2. 聚合对象——决定了对象体系的简洁性
  3. 自包含优于继承——决定了复用方式
  4. 数据透明优于封装隐藏——决定了数据访问方式
  5. 可执行优于可声明——决定了验证方式
  6. CLI 可测优于 IDE 依赖——决定了测试方式
  7. 模块标准化优于自由组织——决定了模块组织方式

不适用的场景

这些原则主要适用于 业务逻辑层模块内部。在以下场景需要谨慎:

  • 公共库/框架设计:API 的接口契约仍然需要
  • 跨团队协作:模块边界的接口声明仍然需要
  • 安全敏感模块:额外的间接层可能是安全需求

这不是"放弃架构"

有人在读到这里时可能会想:"你是不是在说不要分层、不要架构了?"

不是的。

这些原则不是抛弃传统架构,而是重新审视传统架构中的哪些部分是为"人"设计的,哪些是对 AI 也友好的。

  • 模块边界要保留——AI 需要知道"这件事去哪找"
  • 接口契约要保留——AI 需要知道"输入输出是什么"
  • 命名规范要保留——AI 需要从名字推断意图
  • 但碎片化的 VO/BO/DTO 可以重新考虑——AI 不需要这些中间层来帮助理解

本章小结

原则核心思想解决的问题适用边界
层次精简砍掉无价值的中间层AI 中间层需要多次跳转三层内
聚合对象减少碎片化对象类型AI 猜测对象类型成本高同层内
自包含优于继承完整逻辑在一个文件AI 追踪父类成本高业务逻辑
数据透明优于封装隐藏直接暴露表结构和 SQLAI 需要逆向推导封装的数据内部业务
可执行优于可声明编程式代码代替声明式注解AI 看不见 AOP 注解的隐式行为影响数据一致性的逻辑
CLI 可测优于 IDE 依赖测试框架本身是 CLI 应用AI 无法操作 IDE 测试运行器非 UI 测试
模块标准化优于自由组织统一模块结构由 AI 执行AI 需适应不同开发者的组织习惯组件/框架层

关联:下一章从反面视角切入,分析中小企业最常用的 Java + SpringBoot + Vue 技术栈在 AI 时代面临的问题。