发布于 2026-01-06 6 阅读
0

代码即文档:免费使用 Vercel AI SDK 和 ZenStack 实现自动化

代码即文档:免费使用 Vercel AI SDK 和 ZenStack 实现自动化

很少有开发人员喜欢编写文档。

如果你曾在大型公司担任过开发人员,你就会知道,编写代码只是日常工作中很小的一部分。谷歌的一位全栈软件工程师 Ray Farias 曾估计,谷歌的开发人员每天大约编写 100 到 150 行代码。虽然这个估计值可能因团队而异,但数量级与我作为微软开发人员的观察结果相符。

那么时间都去哪儿了呢?相当一部分时间都花在了会议、代码审查、计划会议和文档编写等活动上。在所有这些任务中,文档编写是我最不喜欢的——我猜很多其他团队成员也有同感。

主要原因是我们觉得它没什么价值。每个迭代周期开始之前,我们都要先写设计文档,互相审核之后,大部分文档就再也没改过。我数不清有多少次在文档里发现奇怪的地方,结果作者告诉我文档过时了。😂 为什么我们不更新文档呢?因为我们的老板觉得更新文档不如修复bug或添加新功能重要。

文档应该作为代码的高级抽象,以帮助理解。当文档与代码不同步时,它就失去了意义。然而,保持文档与代码同步需要付出努力——而这恰恰是很少有人乐意做的。

鲍勃大叔(罗伯特·C·马丁)有一句关于编写整洁代码的名言:

好的代码本身就是注释。

我认为如果能将这一原则推广到文档编写领域,那就太好了:

好的代码本身就是最好的文档

使用人工智能生成文档

当前人工智能应用的趋势遵循一条简单的原则:如果人类不喜欢做某件事,那就让人工智能来做。文档编写似乎完美地契合了这一范畴,尤其是在如今越来越多的代码已经由人工智能生成的情况下。

时机真是太好了,GitHub 刚刚宣布Copilot 的所有功能都免费了。您可以尝试让它免费生成您项目的文档。不过,结果可能不如您预期。是因为您的提示不够好吗?也许是,但背后还有更重要的原因:

LLM 处理命令式代码的能力不如处理声明式文本的能力。

命令式代码通常涉及复杂的控制流、状态管理和错综复杂的依赖关系。这种过程式特性要求对代码背后的意图有更深入的理解,而语言学习模型(LLM)很难准确推断出这种意图。此外,代码量越大,结果就越有可能不准确且信息量越少。

在 Web 应用程序的文档中,你最想看到什么?很可能是作为整个应用程序基础的数据模型。数据模型可以用声明式的方式定义吗?当然可以!Prisma ORM在这方面做得非常出色,它允许开发人员使用直观的数据建模语言来定义应用程序模型。

ZenStack工具包基于 Prisma 构建,通过添加额外功能增强了数据模式。通过直接在数据模型中定义访问策略和验证规则,它成为应用程序后端唯一的数据源。

我所说的“单一数据源”,不仅包含后端所需的所有信息,它实际上就是你的整个后端。ZenStack 会自动为你生成 API 和相应的前端钩子。定义好访问策略后,就可以直接从前端安全地调用这些 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)
}
Enter fullscreen mode Exit fullscreen mode

我们可以轻松地创建一个工具,利用人工智能技术根据此模式生成文档。您无需再手动编写和维护文档——只需将生成流程集成到 CI/CD 流水线中,即可彻底解决文档不同步的问题。以下是根据此模式生成的文档示例:

zenstack-doc

我将一步步指导您如何创建这个工具。

ZenStack插件系统

与许多优秀的 Web 开发工具一样,ZenStack 也采用了基于插件的架构。该系统的核心是 ZModel 模式,各种功能都围绕它以插件的形式实现。让我们创建一个插件,为 ZModel 生成 Markdown 文档,以便其他人能够轻松地使用它。

为简洁起见,我们将重点介绍核心部分。完整的插件开发细节请参阅ZenStack 文档。

插件其实就是一个Node.js模块,它由两部分组成:

  1. 一个命名导出 name ,用于指定用于日志记录和错误报告的插件名称。
  2. 包含插件逻辑的默认函数导出。

它的样子是这样的:

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) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

model是 ZModel AST。它是解析和链接 ZModel 模式的结果对象模型,ZModel 模式是一个树状结构,包含模式中的所有信息。

我们可以使用ZModelCodeGeneratorZenStack SDK 提供的功能从 AST 中获取 ZModel 内容。

import { ZModelCodeGenerator } from '@zenstackhq/sdk';
const zModelGenerator = new ZModelCodeGenerator();
const zmodel = zModelGenerator.generate(model);
Enter fullscreen mode Exit fullscreen mode

现在我们已经有了食材,就让人工智能来烹饪吧。

使用 Vercel AI SDK 生成文档

最初,我计划使用 OpenAI 来完成这项工作。但很快我意识到,这会将那些无法使用 OpenAI 付费服务的开发者排除在外。感谢埃隆·马斯克,你可以从 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');
    }
    ...
Enter fullscreen mode Exit fullscreen mode

其余实现部分都使用相同的统一 API,无论你选择哪个提供商。

vercel-ai-sdk

以下是我们用来让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}
    \`\`\`
    `;
Enter fullscreen mode Exit fullscreen mode

生成结构化数据

在处理 API 时,我们更倾向于使用 JSON 数据而非纯文本。虽然许多 LLM 都能够生成 JSON,但它们各自的实现方式有所不同。例如,OpenAI 提供 JSON 模式,而 Claude 则要求在提示符中指定 JSON 格式。好消息是,Vercel SDK 也使用 Zod schema 统一了不同模型提供商的这一功能。

对于上述提示,我们期望收到的相应响应数据结构如下。

    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()),
            })
        ),
    });
Enter fullscreen mode Exit fullscreen mode

然后调用generateObjectAPI 让 AI 执行其任务:

const { object } = await generateObject({
        model
        schema
        prompt
    });
Enter fullscreen mode Exit fullscreen mode

返回的类型可以让你以类型安全的方式进行操作:

const object: {
    overview: {
        description: string;
        functionality: string;
    };
    models: {
        name: string;
        access_control_policies: string[];
    }[];
}
Enter fullscreen mode Exit fullscreen mode

生成美人鱼ERD图

我们再为每个模型生成ER图。这部分比较简单易行,所以我认为用代码实现更可靠高效。当然,你也可以借助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');
    }
}
Enter fullscreen mode Exit fullscreen mode

把一切都缝合起来

最后,我们将所有生成的组件组合在一起,得到最终文档:

 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');
Enter fullscreen mode Exit fullscreen mode

现成的

当然,您不必自己实现。它已经作为 NPM 包发布,您可以直接安装:

npm i -D zenstack-markdown
Enter fullscreen mode Exit fullscreen mode

将插件添加到您的 ZModel 架构文件中

plugin zenstackmd {
    provider = 'zenstack-markdown'
}
Enter fullscreen mode Exit fullscreen mode

别忘了把你可用的所有 AI API 密钥都添加到 .env 文件中。否则,你可能会遇到一些意想不到的结果。😉

OPENAI_API_KEY=xxxx
XAI_API_KEY=xxxxx
ANTHROPIC_API_KEY=xxxx
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/zenstack/code-as-doc-automate-by-vercel-ai-sdk-and-zenstack-for-free-1ch4