很多开发者都不太喜欢写文档
如果你曾在一个大公司当过开发人员,你就会知道编程只是日常职责中很小的一部分。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 插件系统就像 web 开发世界中的许多出色工具一样,ZenStack 采用了插件架构。系统的核心是 ZModel 模式,围绕 ZModel 实现的各种功能插件。下面我们来创建一个插件,用于生成 ZModel 的 Markdown 文档,以便其他人可以轻松使用。
为了简洁起见,我们将重点介绍核心内容。请参考ZenStack 文档以获取插件开发细节。
一个插件,仅仅是一个包含两个部分的 Node.js 模块:
- 名为
name
的命名导出,指定用于日志记录和错误报告的插件的名称。 - 一个包含插件逻辑的默认函数导出。
它看起来是这样的:
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,无论你选择哪个供应商。
这是我们用来让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的密钥
点击全屏 点击退出全屏
共同学习,写下你的评论
评论加载中...
作者其他优质文章