代码审查对于维持高标准和强化编码项目中的最佳实践一直至关重要。这篇文章不是关于开发人员应如何审查代码,而是关于将部分工作委托给 AI。
正如 Michael Lynch 在他的文章《如何像人类一样进行代码审查》中提到的那样,我们应该让计算机来处理代码审查中那些无聊的部分。虽然 Michael 强调了格式化工具,但我想更进一步,让人工智能来解决这个问题。我的意思是,为什么不利用这个行业的人工智能热潮呢?
现在我并不是说应该用 AI 来代替格式化工具和代码检查器。相反,AI 应该被用在格式化工具和代码检查器之上,用来捕捉人类可能忽略的琐碎内容。这就是为什么我决定创建一个GitHub 操作,该操作可以对拉取请求差异进行代码审查并使用 AI 生成建议。让我带你了解一下。
🚨注意:
- 此 GitHub 操作现已在GitHub 市场上线。
- 这是一个 javascript 动作 - 了解有关创建 javascript github 动作的更多信息。
为了与 github API 交互,我使用了octokit
,它是一种 SDK 或客户端库,用于以惯用的方式与 github API 交互。
为了获得提出的拉取请求的差异,您需要传递值为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, }, }); }
如果您对 github 操作完全不熟悉,这里有Victoria Lo 撰写的 github 操作 101 系列,是一个很好的开始。
一旦我得到差异,我就会解析它并删除不需要的更改,然后按照下面显示的模式返回它:
/** 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(), })
忽略文件非常简单。用户输入列表需要以分号分隔的 glob 模式字符串。然后对其进行解析,将其与默认的忽略文件列表连接起来并删除重复项。
**/*.md; **/*.env; **/*.lock; const filesToIgnoreList = [ ...new Set( filesToIgnore .split(";") .map(file => file.trim()) .filter(file => file !== "") .concat(FILES_IGNORED_BY_DEFAULT) ), ];
然后使用忽略的文件列表删除引用这些忽略文件的差异更改。这样你就可以获得仅包含所需更改的原始有效负载。
解析差异后,一旦获得原始有效负载,我就会将其传递给平台 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.com/murtuzaalisurti/better 。用户提示包含实际差异、规则和拉取请求的背景信息。这就是启动代码审查的原因。
此 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; }
最后,在检索到建议后,我会对其进行清理并将其传递给 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; } }
我希望保持 GitHub 操作的开放性并对集成开放,这就是为什么您可以使用您选择的任何模型(参见支持的模型列表) ,或者您可以在支持的基础模型之上微调和构建您自己的自定义模型并将其与此 GitHub 操作一起使用。
如果您遇到任何令牌问题或速率限制,您可能需要参考相应平台的文档来升级您的模型限制。
那么,你还在等什么?如果你在 github 上有存储库,请立即尝试该操作 - 它位于github 操作市场上。