如何保持 AI 生成代码的模块化

发布: (2026年1月16日 GMT+8 17:34)
13 min read
原文: Dev.to

Source: Dev.to

嗨,我叫 Paul,是一名专门为初创公司工作的高级软件工程师。

我把编程语言当作自助餐,无论使用哪种语言,我最看重的就是 modularization。为此,我们的 OpenAI 和 Claude 助手并不是很有帮助。它们是基于大量老旧项目训练的,这些项目里所有代码都塞进巨大的 index.js 文件。无监督的 vibe coding 往往会让每个文件产生 +1000 行代码,并且出现大量重复。

为什么这是个问题?
在初创公司,代码的寿命很短。当大量使用 vibe coding 时,AI 总能找到办法,重构的负担似乎永远不需要。但现实并非如此。需求变化迅速,新增组件或行为必须快速且无风险。AI 在评估一次改动的影响方面非常差劲。如果我给模型添加一个字段,它会对整个系统产生什么后果?我是否引入了安全风险?我是否破坏了某些功能?

当然,这就需要单元测试,但在我看来,更大的需求是 modularization。让代码保持隔离,不依赖外部,针对每个函数的单一使用场景进行编写、测试,并放在单个文件中。这种做法为我省下了大量麻烦和重构工作。虽然会让开发稍显冗长——有点像脑筋急转弯——但它让代码更稳健,也更有趣。

这篇文章的主题正是:如何强制你的 AI 工具生成模块化代码。为此我们需要严密限制模型的自由,剔除所有随意性,抹杀一切创造力。但我在 Vybe.build 工作,保持 AI 在一条短小、设计良好的“链子”上正是我们的风格。

我的技术栈

为了本文的阐述,我会使用一个我非常熟悉的技术栈,这几乎是我每次构建小型 SaaS 或 “SaaS 替代” 项目时都会选用的。它并不新奇,但与模块化思维以及 AI 辅助开发高度兼容。

  • Next.js 与 App Router
  • TypeScript,无处不在
  • Vercel 用于部署(无服务器、零摩擦)
  • Cursor 作为我的 AI 驱动编辑器(相同的思路同样适用于 Claude Code、Copilot 等)

这套技术栈之所以重要,是因为它迫使你以边界的方式思考:服务器 vs. 客户端、路由 vs. 逻辑、数据 vs. UI。边界正是 AI 往往会忽视的,除非你让它们不可逾越。

你可以在此找到一个入门项目

特定的文件组织

这是我使用的结构:

./
├── app/                    # Next.js App Router pages and routes
├── src/
│   ├── __template__/      # Template used to generate new modules
│   │   ├── api/           # Server‑side actions (DB writes, heavy logic, etc.)
│   │   ├── components/    # React components specific to the module
│   │   ├── hooks/         # React hooks
│   │   └── types/         # TypeScript types (shared client/server)
│   ├── auth/              # Authentication module
│   ├── db/                # Database module
│   └── ui/                # Shared UI library
│       ├── components/
│       ├── hooks/
│       └── globals.css
├── prisma/                # Database schema and migrations
└── public/                # Static assets

src 下的每个文件夹都是一个 模块。一个模块拥有自己的逻辑、钩子、组件和类型。不要“只这一次”去触及其他模块的内部。如果有共享的内容,就放到 ui 或专门的共享模块中。

__template__ 文件夹非常重要。它是每个新模块的蓝图。当出现新功能时,我不会直接开始写代码;我会先从该模板生成一个模块。这可以消除大量的决策工作,无论是对我本人还是对 AI 都如此。

我喜欢 api / components / hooks / types 的划分,因为它与 Next.js 的双环境特性配合得很好。服务器端代码和客户端代码被明确分离,AI 也不容易把它们混在一起。最终,模块的具体组成方式由你自行决定。

Next.js 结构

我非常严格的一条规则是:app/ 目录只包含非常特定的逻辑:

  • 属性注入、页面的服务器端数据获取
  • 访问保护、身份验证、路由的返回值

Next.js 对路由和执行上下文有强硬的约定,这没问题。应用路由器定义页面和 API 路由——仅此而已。

任何与业务逻辑、数据访问、转换,或甚至稍微可复用的行为相关的代码,都放在 src/ 中的模块里。

示例页面

import { getUsers } from '@/src/user/api';
import { User } from '@/src/user/types';

export default async function UserPage() {
  let users: User[] = [];
  let error: string | null = null;

  try {
    users = await getUsers();
  } catch (err) {
    error = err instanceof Error ? err.message : 'An error occurred';
  }

  if (error) {
    return Error: {error};
  }

  return <div>{/* render users */}</div>;
}

获取数据和处理路由层面的错误属于 Next.js 的职责。其余所有内容都属于 user 模块。

示例 API 路由

import { NextResponse } from 'next/server';
import { getUsers } from '@/src/user/api/getUsers';
import { auth } from '@/src/auth';

export const GET = withAuth(async (request: Request) => {
  const session = await auth.api.getSession();
  if (!session) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const users = await getUsers();
  return NextResponse.json(users);
});

身份验证检查和 HTTP 相关的处理留在路由中。实际业务逻辑则放在模块里。

这种分离看似枯燥、重复,却极其有效。它还能为 AI 提供一个明确且可强制遵守的边界,这正是我们保持代码库模块化、可维护所需要的。

路由将事物粘合在一起,模块完成工作

现在使用 AI

一旦这种结构建立起来,AI 就会变得既极其强大又极其危险。如果任其自行,它总是会以最懒散的方式试图提供帮助:它会在页面中编写所有代码,结果你会无端进行多次 API 调用,到处重写逻辑,等等。

目标很简单: 消除选择。如果只有一种有效的做事方式,AI 最终会遵循它。

使用 EJS 模板生成模块

第一步是让创建新模块变得轻而易举、机械化。

我使用一个小脚本(Node + EJS)从模板文件夹生成模块。从外部看,它的使用方式如下:

npm run create-module user

它会生成以下文件结构:

src/user/
├── api/
│   ├── getUser.ts
│   ├── getUsers.ts
│   ├── createUser.ts
│   └── updateUser.ts
├── components/
│   └── UserCard.tsx
├── hooks/
│   ├── useUser.ts
│   └── useUsers.ts
└── types/
    └── User.ts

这里没有任何魔法。API 文件是简洁、明确的函数。hooks 是小型包装器,通常围绕 React Query。组件默认是“哑”组件。类型在服务器和客户端之间共享。

这有两个作用:

  • 为人类消除摩擦。
  • 为 AI 提供非常强的先验。

额外的好处:

  • 没有巨大的服务文件。
  • 没有 “utils.ts” 这类倾倒场。

当我让 AI “添加用户功能” 时,它不会自行发明结构,而是遵循已有的结构。如果它没有,我会重新生成或修复模块,而不是让熵逐渐渗入。

自定义 ESLint 规则作为护栏

模板处理 happy path。Linters 处理作弊。

一个具体示例:API 路由

在 Next.js 中,编写下面的代码非常容易:

export const POST = async (req: Request) => {
  // do stuff
};

但在我的项目里,每个路由都必须经过一个注入上下文(session、user、permissions 等)的包装器:

export const POST = withAuth(async ({ user, req }) => {
  // do stuff
});

于是我添加了一个 自定义 ESLint 规则 来强制执行此约束。如果一个路由导出 POSTGETPUT 等,而没有正确包装,lint 就会报错。

有趣的是 AI 对此的反应。
当 AI 生成路由时忘记了包装器,lint 错误会立刻出现。AI 看到错误后,理解了模式,并通过添加缺失的包装器来修复代码。你不需要再次解释安全性或争论;只需让工具链完成教学即可。

这对许多场景都有效:

  • 强制文件边界
  • 防止跨模块导入
  • 强制命名约定
  • 阻止 “聪明” 的捷径

清晰指令

这看似显而易见,但值得重复。无论你是在使用 Cursor 规则、Claude Code 指令,还是在其他地方使用系统提示,思路都是一样的:尽早写下你的约束,并随时间进行演进。

我并不追求穷尽。我从一个简短的列表开始:

  • 代码可以存放的地点
  • 模块的结构方式
  • 禁止的内容(单体文件、交叉导入、在路由中写业务逻辑,等等)

每当 AI 以不好的方式让我惊讶时,我不会只修复一次;我会添加一条规则。

我通常会保留一个大型指令块,解释模块结构、app/ 相关的规则,以及如何在扩展已有模块时避免创建新模式。我不会在这里粘贴完整内容,但具体细节不如养成习惯重要:把 AI 指令当作代码来对待。给它们版本号。不断改进。假设如果不维护,它们会逐渐失效。

结果

那些工具已经存在很久了,我大多一直忽视它们,直到最近才开始关注。只有当 AI 完成大部分打字工作时,它们对生产力的影响才真正让我恍然大悟。

我并不是在倡导模块化代码或“氛围编码”。你应该使用适合自己的方式。我只是分享我自己的做法,对我而言效果相当不错。

希望能给你一些灵感。

感谢阅读!

Back to Blog

相关文章

阅读更多 »

Rapg:基于 TUI 的密钥管理器

我们都有这种经历。你加入一个新项目,首先听到的就是:“在 Slack 的置顶消息里查找 .env 文件”。或者你有多个 .env …

技术是赋能者,而非救世主

为什么思考的清晰度比你使用的工具更重要。Technology 常被视为一种魔法开关——只要打开,它就能让一切改善。新的 software,...

踏入 agentic coding

使用 Copilot Agent 的经验 我主要使用 GitHub Copilot 进行 inline edits 和 PR reviews,让我的大脑完成大部分思考。最近我决定 t...