Better - AI代码审查工具

代码审查对于保持高标准和强化编码项目中的最佳实践一直至关重要。这篇文章不是关于开发人员应该如何审查代码,而是关于将其中的一部分委托给人工智能。

正如迈克尔·林奇在他的文章“如何像人类一样进行代码审查”中提到的那样,我们应该让计算机处理代码审查中无聊的部分。虽然迈克尔强调了格式化工具,但我想更进一步,让人工智能来解决这个问题。我的意思是,为什么不利用行业中的人工智能热潮呢?

现在我并不是说应该使用人工智能来代替格式化工具和 linters。相反,它应该被用在那之上,以捕捉人类可能错过的琐碎内容。这就是为什么我决定创建一个 github Action,该操作使用人工智能对拉取请求差异进行代码审查并生成建议。让我带你了解一下。

注意:

1、获取 diff

为了与 github API 交互,我使用了 octokit,它是一种 SDK 或客户端库,用于以惯用的方式与 github API 交互。

为了获取引发的拉取请求的 diff,你需要传递带有值 application/vnd.github.diff 的 Accept 标头以及所需的参数:

async function getPullRequestDetails(octokit, { mode }) {
    let AcceptFormat = "application/vnd.github.raw+json";

    if (mode === "diff") AcceptFormat = "application/vnd.github.diff";
    if (mode === "json") AcceptFormat = "application/vnd.github.raw+json";

    return await octokit.rest.pulls.get({
        owner: github.context.repo.owner,
        repo: github.context.repo.repo,
        pull_number: github.context.payload.pull_request.number,
        headers: {
            accept: AcceptFormat,
        },
    });
}

一旦我得到 diff,就会解析它并删除不需要的更改,然后在下面显示的模式中返回它:

/** using zod */
schema = z.object({
    path: z.string(),
    position: z.number(),
    line: z.number(),
    change: z.object({
        type: z.string(),
        add: z.boolean(),
        ln: z.number(),
        content: z.string(),
        relativePosition: z.number(),
    }),
    previously: z.string().optional(),
    suggestions: z.string().optional(),
})

2、忽略文件

忽略文件非常简单。用户输入列表需要一个以分号分隔的 glob 模式字符串。然后对其进行解析,与默认的忽略文件列表连接并删除重复项。

**/*.md; **/*.env; **/*.lock;

const filesToIgnoreList = [
    ...new Set(
        filesToIgnore
            .split(";")
            .map(file => file.trim())
            .filter(file => file !== "")
            .concat(FILES_IGNORED_BY_DEFAULT)
    ),
];

然后使用忽略的文件列表删除引用这些忽略文件的差异更改。 这样会为您提供仅包含所需更改的原始有效负载。

3、生成建议

在解析差异后获得原始有效负载后,我会将其传递给平台 API。 这是 OpenAI API 的实现:

async function useOpenAI({ rawComments, openAI, rules, modelName, pullRequestContext }) {
    const result = await openAI.beta.chat.completions.parse({
        model: getModelName(modelName, "openai"),
        messages: [
            {
                role: "system",
                content: COMMON_SYSTEM_PROMPT,
            },
            {
                role: "user",
                content: getUserPrompt(rules, rawComments, pullRequestContext),
            },
        ],
        response_format: zodResponseFormat(diffPayloadSchema, "json_diff_response"),
    });

    const { message } = result.choices[0];

    if (message.refusal) {
        throw new Error(`the model refused to generate suggestions - ${message.refusal}`);
    }

    return message.parsed;
}

你可能会注意到 API 实现中使用了响应格式。这是许多 LLM 平台提供的功能,它允许你告诉模型以特定的模式/格式生成响应。它在这种情况下特别有用,因为我不希望模型产生幻觉并生成拉取请求中不正确的文件或位置的建议,或者向响应负载添加新属性。

系统提示是为了给模型提供更多关于它应该如何进行代码审查以及需要注意哪些事项的背景信息。你可以在此处查看系统提示。用户提示包含实际的差异、规则和拉取请求的上下文。它是启动代码审查的依据。

这个 github 操作支持 OpenAI 和 Anthropic 模型。以下是它实现 Anthropic API 的方式:

async function useAnthropic({ rawComments, anthropic, rules, modelName, pullRequestContext }) {
    const { definitions } = zodToJsonSchema(diffPayloadSchema, "diffPayloadSchema");
    const result = await anthropic.messages.create({
        max_tokens: 8192,
        model: getModelName(modelName, "anthropic"),
        system: COMMON_SYSTEM_PROMPT,
        tools: [
            {
                name: "structuredOutput",
                description: "Structured Output",
                input_schema: definitions["diffPayloadSchema"],
            },
        ],
        tool_choice: {
            type: "tool",
            name: "structuredOutput",
        },
        messages: [
            {
                role: "user",
                content: getUserPrompt(rules, rawComments, pullRequestContext),
            },
        ],
    });

    let parsed = null;
    for (const block of result.content) {
        if (block.type === "tool_use") {
            parsed = block.input;
            break;
        }
    }

    return parsed;
}

4、添加评论

最后,在检索到建议后,我会对其进行清理,并将其传递给 GitHub API,以将评论作为评论的一部分添加。

我选择以下方式添加评论,因为通过创建新评论,你可以一次性添加所有评论,而不是一次添加一条评论。逐个添加评论也可能触发速率限制,因为添加评论会触发通知,而你不想向用户发送垃圾邮件通知。

function filterPositionsNotPresentInRawPayload(rawComments, comments) {
    return comments.filter(comment =>
        rawComments.some(rawComment => rawComment.path === comment.path && rawComment.line === comment.line)
    );
}

async function addReviewComments(suggestions, octokit, rawComments, modelName) {
    const { info } = log({ withTimestamp: true }); // eslint-disable-line no-use-before-define
    const comments = filterPositionsNotPresentInRawPayload(rawComments, extractComments().comments(suggestions));

    try {
        await octokit.rest.pulls.createReview({
            owner: github.context.repo.owner,
            repo: github.context.repo.repo,
            pull_number: github.context.payload.pull_request.number,
            body: `Code Review by ${modelName}`,
            event: "COMMENT",
            comments,
        });
    } catch (error) {
        info(`Failed to add review comments: ${JSON.stringify(comments, null, 2)}`);
        throw error;
    }
}

5、结束语

我希望保持 GitHub 操作的开放性并对集成开放,这就是为什么你可以使用你选择的任何模型(请参阅支持的模型列表),或者你可以在支持的基础模型之上微调和构建自己的自定义模型并将其与此 GitHub 操作一起使用。

如果你遇到任何令牌问题或速率限制,可能需要通过参考相应平台的文档来升级模型限制。


原文链接:Better - An AI Powered Code Reviewer

汇智网翻译整理,转载请标明出处