为了账号安全,请及时绑定邮箱和手机立即绑定

用AI自动生成文档,开发更轻松:Vercel AI SDK 和 ZenStack 的免费解决方案

标签:
人工智能 API

很多开发者都不太喜欢写文档

如果你曾在一个大公司当过开发人员,你就会知道编程只是日常职责中很小的一部分。Google 的全栈软件工程师 Ray Farias 曾估计,Google 的开发人员每天大约编写 100-150 行代码。虽然这个估算在不同团队间可能有所出入,但作为微软的开发人员,我观察到的情况大致相符。

所以时间都花到哪里去了?很大一部分花在了会议、代码审阅、计划会议和写文档上。在所有这些事情中,写文档是我最不喜欢的就是——感觉跟我一样的队友应该不少。

主要原因是我们觉得它没有太多价值。每次冲刺开始前,我们都被要求编写设计文档,在互相审阅之后,大多数文档几乎从不更新。我数不清多少次发现文档中的内容已过时,却发现作者告诉我已过时。😂为什么我们不更新这些文档呢?因为我们的老板觉得修复bug和添加新功能比更新文档更重要。

文档应当作为代码的高层次抽象,帮助我们更好地理解代码。当文档和代码脱节时,它的作用就消失了。然而,保持文档和代码同步需要花费不少力气——这并不是大多数人喜欢做的事情。

鲍勃·马丁(Bob Martin)关于干净代码的名言:

好的代码自己会说话

我觉得这个原则也能用在文档上会很好。

好代码自己就是最好的说明

使用AI来生成文档

有一个简单的原则当前AI应用的趋势是:如果人类不喜欢做某事,就让AI来处理。文档似乎完全符合这种情况,特别是现在很多代码都是由AI生成的。

时机真是再好不过了,GitHub 刚刚宣布 [Copilot 功能] 免费了。你可以试试免费让 Copilot 为你生成项目的文档。不过,结果可能不会像你期望的那么好。这是否因为你的提示不够好?也许有这个原因,但其实还有一个更根本的原因:

大型语言模型(LLM)们处理命令式代码的效果不如处理声明性文本好这一点,更符合口语习惯。

命令式代码常常涉及复杂的控制流程、状态管理和繁杂的依赖关系。这种过程性特性要求我们对代码背后的意图有更深入的理解,这对于大型语言模型准确推断来说颇具挑战。此外,代码量越大,结果就越可能不准确且缺乏信息量。

你在网页应用程序的文档中首先想看什么?大多数情况下,你会希望先看到整个应用程序的基础——数据模型。数据模型能否用声明式的方式来定义?当然可以!Prisma ORM 已经在这方面做得非常好,允许开发者用一种直觉性强的数据建模语言来定义应用模型。

ZenStack(https://zenstack.dev)工具包基于Prisma构建,增强了模式,添加了更多功能。通过在数据模型中直接定义访问策略和验证规则,它成为应用程序后端的唯一事实来源

当我提到“唯一数据源”时,它不仅包含所有后端信息,实际上就是整个后端。ZenStack 自动为你生成 API 和前端钩子。一旦制定了访问策略,就可以直接从前端安全调用这些 API,无需在数据库层启用行级安全设置(RLS)。换句话说,你几乎不需要编写任何后端代码。

下面是一个超级简单的博客帖子示例:

    数据源 db {
        提供商 = 'postgresql'
        url = env('DATABASE_URL')
    }

    生成器 js {
        提供商 = 'prisma-client-js'
    }

    插件 hooks {
        提供商 = '@zenstackhq/tanstack-query'
        输出目录 = 'lib/hooks'
        目标框架 = 'react'
    }

    角色 Role {
        USER
        ADMIN
    }

    实体 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() && published )
        @@allow('read', auth().role == 'ADMIN')
    }

    实体 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-doc

我会一步步教你如何制作这个工具。

ZenStack 插件系统

就像 web 开发世界中的许多出色工具一样,ZenStack 采用了插件架构。系统的核心是 ZModel 模式,围绕 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';

    // "ZenStack MarkDown" 是 ZenStack 的 Markdown 插件名称
    export const name = 'ZenStack MarkDown';

    export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) {
        ...
    }

切换到全屏 退出全屏

model 是 ZModel 的抽象语法树(AST)。它是对 ZModel 架构进行解析并链接后生成的结果对象模型,包含架构中所有的信息,具有树形结构。

可以使用ZenStack SDK提供的ZModelCodeGenerator从AST中获取ZModel内容。

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

切换到全屏模式,退出全屏

现在我们有了这些材料,让AI来给我们做菜。

试试用 Vercel AI SDK 来生成文档

最初,我打算使用OpenAI来完成任务。但很快,我意识到这会排除那些无法使用付费OpenAI服务的开发者。多亏了Elon Musk,可以从Grok (https://x.ai/) 免费获取API密钥。

然而,我需要为每个模型提供商编写单独的代码。这就是Vercel AI SDK特别出色的地方。它提供了一个标准接口来与各种LLM提供商交互,让我们可以编写适用于多种AI模型的代码。不论是使用OpenAI、Anthropic的Claude还是其他提供商,实现方式都保持一致,简单易用。

它提供了一个统一的LanguageModel类型,让你可以指定任何你想要使用的LLM模型。只需检查一下环境就能知道哪个模型是可用的。

        let model: 语言模型;

        if (process.env.OPENAI_API_KEY) { // 如果环境变量中有 OPENAI_API_KEY
            model = openai('gpt-4-turbo');
        } else if (process.env.XAI_API_KEY) { // 或者如果有 XAI_API_KEY
            model = xai('grok-beta');
        }
        ...
        // 该代码片段用于根据环境变量选择合适的语言模型

全屏模式。退出全屏。

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

vercel-ai-sdk

这是我们用来让AI生成文档的内容的提示,

      const prompt = `
        您是ZenStack 开源工具包方面的专家。您需要根据提供的ZModel模式文件生成技术设计文档,帮助开发人员理解应用程序的结构和行为。该文档应包括以下部分:

1. 概述
            a. 对此应用的总体描述的简短段落
            b. 功能描述

2. 模型列表。每个模型包括以下两项信息:
            a. 模型名称
            b. 每个模型包含的权限策略列表及其详细说明
        以下是ZModel模式文件的内容:
        \`\`\`zmodel
        ${zmodel}
        \`\`\`
        `;

点击全屏,点击退出全屏

生成结构化的数据

在使用API时,我们更倾向于使用JSON数据的数据而不是纯文本。虽然许多大型语言模型能生成JSON,但每个模型都有自己处理JSON的方式。例如,OpenAI提供了JSON模式的支持,而对于Claude,则需要在提示中指定JSON格式。好消息是,而Vercel SDK则通过Zod模式统一了各模型提供商对JSON数据的处理能力。

对于上面的提示,我们期望收到的是回复的数据格式。

        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;
            功能说明: string;
        };
        models: {
            名称: string;
            access_control_policies: string[];
        }[];
    }

全屏,退出全屏

生成 Mermaid 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) {
                    // 多对多
                    relation = '}o--o{';
                } else if (currentType.array && !oppositeType.array) {
                    // 一对多
                    relation = '||--o{';
                } else if (!currentType.array && oppositeType.array) {
                    // 多对一
                    relation = '}o--||';
                } else {
                    // 一对一
                    relation = currentType.optional ? '||--o|' : '|o--||';
                }

                return [`"${dataModel.name}"`, relation, `"${oppositeField.$container.name}": ${x.name}`].join(' ');
            })
            .join('\n');

        return ['```\n\nmermaid', 'erDiagram', {% raw %}`"${dataModel.name}" {\n${fields}\n}`{% endraw %}, relations, '\n```'].join('\n');
    }
}

点击全屏。退出全屏。

把一切都缝好

最后,我们把所有生成的组件拼在一起,得到我们最终的文档(doc)。

     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 = [
            `# 技术设计文档`,
            '> 由 [`ZenStack-markdown`](https://github.com/jiashengguo/zenstack-markdown) 生成',
            `${object.overview.description}`,
            `## 功能模块`,
            `${object.overview.functionality}`,
            '## 模型列表:',
            dataModels.map((x) => `- [${x.name}](https://dev.to#${x.name} )`).join('\n'),
            modelChapter,
        ].join('\n\n');

全屏模式 退出全屏

现成的货品

当然,你不必自己实现它。它已经是一个你可以安装的NPM包了。

npm i -D zenstack-markdown  # 安装Zenstack Markdown插件,使用这条命令。

进入全屏 / 退出全屏

将插件添加到您的 ZModel 模型文件中。

插件 zenstackmd {
    提供者 = 'zenstack-markdown'
}

点击全屏 退出全屏

别忘了把你可以使用的任何AI API密钥放到.env里,不然可能会有些惊喜哦😉

    OPENAI_API_KEY=xxxx
    XAI_API_KEY=xxxxx
    ANTHROPIC_API_KEY=xxxx
    # 这些是用于调用不同API的密钥

点击全屏 点击退出全屏

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消