코드 검토는 항상 코딩 프로젝트에서 높은 기준을 유지하고 모범 사례를 강화하는 데 중요했습니다. 이 글은 개발자가 코드를 어떻게 검토해야 하는지에 대한 글이 아니라, 코드의 일부를 AI에 위임하는 것에 대한 글입니다.
마이클 린치가 그의 게시물 "인간처럼 코드 리뷰를 하는 법" 에서 언급했듯이, 우리는 컴퓨터가 코드 리뷰의 지루한 부분을 처리하도록 해야 합니다. 마이클이 서식 도구를 강조하는 반면, 저는 한 걸음 더 나아가 인공 지능이 알아내도록 하고 싶습니다. 제 말은, 왜 업계의 AI 붐을 이용하지 않겠습니까?
이제 저는 AI를 포맷팅 도구와 린터 대신 사용해야 한다고 말하는 것이 아닙니다. 대신, 인간이 놓칠 수 있는 사소한 것들을 포착하기 위해 그 위에 사용해야 합니다. 그래서 저는 풀 리퀘스트 diff를 코드 검토하고 AI를 사용하여 제안을 생성하는 github 액션을 만들기로 했습니다. 제가 설명해 드리겠습니다.
🚨 참고사항:
- 이 GitHub 액션은 이제 GitHub 마켓플레이스 에서 사용할 수 있습니다.
- 자바스크립트 액션입니다. 자바스크립트 Github 액션을 만드는 방법 에 대해 자세히 알아보세요.
Github API와 상호작용하기 위해 octokit
사용했는데, 이는 관용적인 방식으로 Github API와 상호작용하는 데 필요한 SDK 또는 클라이언트 라이브러리 의 일종입니다.
풀 리퀘스트의 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, }, }); }
Github Action에 대해 전혀 모른다면 Victoria Lo가 쓴 Github Action 101 시리즈 를 읽어보면 도움이 될 것입니다.
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(), })
파일을 무시하는 것은 매우 간단합니다. 사용자 입력 목록에는 세미콜론으로 구분된 glob 패턴 문자열이 필요합니다. 그런 다음 구문 분석하여 무시된 파일의 기본 목록과 연결하고 중복을 제거합니다.
**/*.md; **/*.env; **/*.lock; const filesToIgnoreList = [ ...new Set( filesToIgnore .split(";") .map(file => file.trim()) .filter(file => file !== "") .concat(FILES_IGNORED_BY_DEFAULT) ), ];
그런 다음 무시된 파일 목록을 사용하여 무시된 파일을 참조하는 diff 변경 사항을 제거합니다. 그러면 원하는 변경 사항만 포함된 원시 페이로드가 제공됩니다.
diff를 파싱한 후 원시 페이로드를 받으면 플랫폼 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 에서 볼 수 있습니다. 사용자 프롬프트에는 실제 diff, 규칙 및 풀 요청의 맥락이 포함됩니다. 코드 검토를 시작하는 것입니다.
이 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 액션 마켓플레이스 에 있습니다.