代码即文档:免费使用 Vercel AI SDK 和 ZenStack 实现自动化
很少有开发人员喜欢编写文档
如果你曾经在一家大公司担任过开发人员,你就会知道,编码只是日常职责的一小部分。谷歌的一位全栈软件工程师 Ray Farias 曾估计,谷歌的开发人员每天编写大约 100-150 行代码。虽然这个估计值可能因团队而异,但这个数量级与我作为微软开发人员的观察结果相符。
那么时间都去哪儿了?很大一部分时间都花在了会议、代码审查、规划会议和文档任务等活动上。在所有这些任务中,文档是我最不喜欢的——我怀疑很多队友也有同样的感受。
主要原因是我们看不出它有多大的价值。我们被要求在每个冲刺开始时在编码之前编写设计文档,在彼此审查之后,大多数文档将永远保持不变。我数不清有多少次我在文档中发现一些奇怪的东西,而它的作者却告诉我它已经过时了。😂 我们为什么不更新文档?因为我们的老板认为它不如修复错误或添加新功能重要。
文档应充当代码的高级抽象,以帮助理解。当文档与代码不同步时,它就失去了其目的。但是,保持文档与代码同步需要付出努力——很少有人真正喜欢这样做。
鲍勃大叔(罗伯特·C·马丁)有一句关于整洁代码的名言:
好的代码就是自己的注释
我认为如果这个原则可以扩展到文档中就太好了:
**好的代码就是它自己最好的文档**
使用 AI 生成文档
目前人工智能应用的趋势有一个简单的规则:如果人类不喜欢做某件事,就让人工智能来处理。文档似乎完全符合这一类别,尤其是如今人工智能已经生成了越来越多的代码。
现在时机再好不过了,因为 GitHub 刚刚宣布 Copilot 功能是免费的。你可以尝试让它免费为你的项目生成文档。但是,结果可能不如你预期的那样好。是因为你提示不够好吗?也许吧,但这背后还有一个更重要的原因:
LLM 处理命令式代码的能力不如处理声明式文本的能力。
命令式代码通常涉及复杂的控制流、状态管理和错综复杂的依赖关系。这种程序性要求更深入地了解代码背后的意图,这对于 LLM 来说可能很难准确推断。此外,代码量越大,结果越有可能不准确且信息量越少。
您希望在 Web 应用程序的文档中看到的第一件事是什么?最有可能的是作为整个应用程序基础的数据模型。数据模型可以声明式定义吗?当然可以!Prisma ORM 已经做得很好了,它允许开发人员使用直观的数据建模语言定义他们的应用程序模型。
ZenStack 工具包建立在 Prisma 之上,通过附加功能增强了架构。通过直接在数据模型中定义访问策略和验证规则,它成为应用程序后端的唯一真实来源。
当我说“单一事实来源”时,它不仅包含后端的所有信息,它实际上是您的整个后端。ZenStack 会自动为您生成 API 和相应的前端挂钩。定义访问策略后,可以直接从前端安全地调用这些策略,而无需在数据库层启用行级安全性 (RLS)。或者,换句话说,您几乎不需要为后端编写任何代码。
这是一个博客文章应用程序的极其简化的示例:
datasource db { provider = 'postgresql' url = env('DATABASE_URL') } generator js { provider = 'prisma-client-js' } plugin hooks { provider = '@zenstackhq/tanstack-query' output = 'lib/hooks' target = 'react' } enum Role { USER ADMIN } model Post { id String @id @default(cuid()) title String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String @default(auth().id) @@allow('all', auth() == author) @@allow('read', auth() != null && published ) @@allow('read', auth().role == 'ADMIN') } model User { id String @id @default(cuid()) name String? email String? @unique password String @password @omit role Role @default(USER) posts Post[] @@allow('create,read', true) @@allow('update,delete', auth() == this) }
我们可以轻松创建一个使用 AI 从此模式生成文档的工具。您无需再手动编写和维护文档 - 只需将生成过程集成到 CI/CD 管道中,就不会再出现不同步的问题。以下是从模式生成的文档的示例:

我将引导您完成创建此工具的步骤。
ZenStack插件系统
与 Web 开发领域中的许多出色工具一样,ZenStack 采用基于插件的架构。系统的核心是 ZModel 架构,围绕该架构,功能以插件形式实现。让我们创建一个插件来为 ZModel 生成 markdown,以便其他人可以轻松采用它。
为简洁起见,我们将重点介绍核心部分。请参阅 ZenStack 文档了解完整的插件开发详细信息。
插件只是一个 Node.js 模块,它包含两个部分:
它看起来是这样的:
import type { PluginOptions } from '@zenstackhq/sdk'; import type { DMMF } from '@zenstackhq/sdk/prisma'; import type { Model } from '@zenstackhq/sdk/ast'; export const name = 'ZenStack MarkDown'; export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) { ... }
`model` 是 ZModel AST。它是解析和链接 ZModel schema 的结果对象模型,它是一个包含 schema 中所有信息的树结构。
我们可以使用ZenStack sdk提供的`ZModelCodeGenerator`从AST中获取ZModel内容。
import { ZModelCodeGenerator } from '@zenstackhq/sdk'; const zModelGenerator = new ZModelCodeGenerator(); const zmodel = zModelGenerator.generate(model);
现在我们有了食材,让我们让人工智能来做饭吧。
使用Vercel AI SDK生成文档
最初,我计划使用 OpenAI 来完成这项工作。但很快,我意识到这会排除那些无法使用付费 OpenAI 服务的开发人员。感谢 Elon Musk,您可以从 Grok (https://x.ai/) 获取免费的 API 密钥。
但是,我必须为每个模型提供商编写单独的代码。这就是 Vercel AI SDK 的亮点。它提供了一个标准化的接口来与各种 LLM 提供商交互,使我们能够编写适用于多种 AI 模型的代码。无论您使用的是 OpenAI、Anthropic 的 Claude 还是其他提供商,实现都保持一致。
它提供了统一的 LanguageModel 类型,允许您指定任何想要使用的 LLM 模型。只需检查环境即可确定哪个模型可用。
let model: LanguageModel; if (process.env.OPENAI_API_KEY) { model = openai('gpt-4-turbo'); } else if (process.env.XAI_API_KEY) { model = xai('grok-beta'); } ...
无论您选择哪个提供商,其余实现都使用相同的统一 API。

以下是我们用来让 AI 生成文档内容的提示:
const prompt = ` You are the expert of ZenStack open-source toolkit. You will generate a technical design document from a provided ZModel schema file that help developer understand the structure and behavior of the application. The document should include the following sections: 1. Overview a. A short paragraph for the high-level description of this app b. Functionality 2. an array of model. Each model has below two information: a. model name b. array of access policies explained by plain text here is the ZModel schema file: \`\`\`zmodel ${zmodel} \`\`\` `;
生成结构化数据
在处理 API 时,我们更喜欢使用 JSON 数据而不是纯文本。虽然许多 LLM 都能够生成 JSON,但每种方法都有自己的方法。例如,OpenAI 提供了 JSON 模式,而 Claude 要求在提示中指定 JSON 格式。好消息是,Vercel SDK 还使用 Zod 模式在模型提供商之间统一了此功能。
对于上面的提示,这里是我们期望收到的相应的响应数据结构。
const schema = z.object({ overview: z.object({ description: z.string(), functionality: z.string(), }), models: z.array( z.object({ name: z.string(), access_control_policies: z.array(z.string()), }) ), });
然后调用`generateObject` API让AI完成他的工作:
const { object } = await generateObject({ model schema prompt });
以下是允许您以类型安全的方式工作的返回类型:
const object: { overview: { description: string; functionality: string; }; models: { name: string; access_control_policies: string[]; }[]; }
生成美人鱼 ERD 图
我们还为每个模型生成 ERD 图。这部分非常简单且易于实现,所以我认为在这里编写代码更可靠、更高效。当然,你仍然可以在这里使用 AI 作为副驾驶。😄
export default class MermaidGenerator { generate(dataModel: DataModel) { const fields = dataModel.fields .filter((x) => !isRelationshipField(x)) .map((x) => { return [ x.type.type || x.type.reference?.ref?.name, x.name, isIdField(x) ? 'PK' : isForeignKeyField(x) ? 'FK' : '', x.type.optional ? '"?"' : '', ].join(' '); }) .map((x) => ` ${x}`) .join('\n'); const relations = dataModel.fields .filter((x) => isRelationshipField(x)) .map((x) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const oppositeModel = x.type.reference!.ref as DataModel; const oppositeField = oppositeModel.fields.find( (x) => x.type.reference?.ref == dataModel ) as DataModelField; const currentType = x.type; const oppositeType = oppositeField.type; let relation = ''; if (currentType.array && oppositeType.array) { //many to many relation = '}o--o{'; } else if (currentType.array && !oppositeType.array) { //one to many relation = '||--o{'; } else if (!currentType.array && oppositeType.array) { //many to one relation = '}o--||'; } else { //one to one relation = currentType.optional ? '||--o|' : '|o--||'; } return [`"${dataModel.name}"`, relation, `"${oppositeField.$container.name}": ${x.name}`].join(' '); }) .join('\n'); return ['``` mermaid', 'erDiagram', {% raw %}`"${dataModel.name}" {\n${fields}\n}`{% endraw %}, relations, ' ```'].join('\n'); } }
缝合一切
最后,我们将所有生成的组件组合在一起以获得最终的文档:
const modelChapter = dataModels .map((x) => { return [ `### ${x.name}`, mermaidGenerator.generate(x), object.models .find((model) => model.name === x.name) ?.access_control_policies.map((x) => `- ${x}`) .join('\n'), ].join('\n'); }) .join('\n'); const content = [ `# Technical Design Document`, '> Generated by [`ZenStack-markdown`](https://github.com/jiashengguo/zenstack-markdown)', `${object.overview.description}`, `## Functionality`, `${object.overview.functionality}`, '## Models:', dataModels.map((x) => `- [${x.name}](#${x.name})`).join('\n'), modelChapter, ].join('\n\n');
现成的
当然,你不必自己实现它。它已经作为 NPM 包发布供你安装:
npm i -D zenstack-markdown
将插件添加到您的 ZModel 架构文件中
plugin zenstackmd { provider = 'zenstack-markdown' }
只是不要忘记将可用的任何 AI API 密钥放入你的 .env 中。否则,你可能会得到一些令人惊讶的结果。😉
OPENAI_API_KEY=xxxx XAI_API_KEY=xxxxx ANTHROPIC_API_KEY=xxxx