Les revues de code ont toujours été cruciales pour maintenir des normes élevées et renforcer les meilleures pratiques dans un projet de codage. Cet article ne porte pas sur la manière dont les développeurs doivent réviser le code, mais plutôt sur la délégation d'une partie de celui-ci à l'IA.
Comme le mentionne Michael Lynch dans son article « Comment faire des revues de code comme un humain » , nous devrions laisser les ordinateurs s’occuper des parties ennuyeuses de la revue de code. Bien que Michael mette l’accent sur un outil de formatage, j’aimerais aller plus loin et laisser l’intelligence artificielle s’en charger. Je veux dire, pourquoi ne pas profiter de l’essor de l’IA dans l’industrie ?
Je ne dis pas que l'IA doit être utilisée à la place des outils de formatage et des linters. Au lieu de cela, elle doit être utilisée en plus de cela, pour détecter des éléments triviaux qui pourraient être manqués par un humain. C'est pourquoi j'ai décidé de créer une action GitHub qui examine le code d'une demande d'extraction et génère des suggestions à l'aide de l'IA. Laissez-moi vous expliquer comment procéder.
🚨 REMARQUE :
- Cette action GitHub est désormais disponible sur la place de marché GitHub .
- C'est une action JavaScript - apprenez-en plus sur la création d'actions JavaScript sur GitHub .
Pour interagir avec l'API github, j'ai utilisé octokit
, qui est une sorte de SDK ou de bibliothèque cliente permettant d'interagir avec l'API github de manière idiomatique.
Pour obtenir la différence de la demande d'extraction soulevée, vous devez passer l'en-tête Accept
avec la valeur application/vnd.github.diff
ainsi que les paramètres requis.
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, }, }); }
Si vous n'êtes pas du tout familier avec les actions GitHub, voici une série d'actions GitHub 101 par Victoria Lo et c'est un bon début.
Une fois que j'obtiens le diff, je l'analyse et supprime les modifications indésirables, puis je le renvoie dans un schéma illustré ci-dessous :
/** 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(), })
Ignorer des fichiers est assez simple. La liste d'entrée utilisateur nécessite une chaîne de motifs glob séparés par des points-virgules. Elle est ensuite analysée, concaténée avec la liste par défaut des fichiers ignorés et dédupliquée.
**/*.md; **/*.env; **/*.lock; const filesToIgnoreList = [ ...new Set( filesToIgnore .split(";") .map(file => file.trim()) .filter(file => file !== "") .concat(FILES_IGNORED_BY_DEFAULT) ), ];
La liste des fichiers ignorés est ensuite utilisée pour supprimer les modifications différentielles qui font référence à ces fichiers ignorés. Cela vous donne une charge utile brute contenant uniquement les modifications souhaitées.
Une fois que j'obtiens la charge utile brute après avoir analysé la différence, je la transmets à l'API de la plateforme. Voici une implémentation de l'API OpenAI.
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; }
Vous remarquerez peut-être l'utilisation du format de réponse dans l'implémentation de l'API. Il s'agit d'une fonctionnalité fournie par de nombreuses plateformes LLM, qui vous permet d'indiquer au modèle de générer la réponse dans un schéma/format spécifique. C'est particulièrement utile dans ce cas, car je ne veux pas que le modèle hallucine et génère des suggestions pour des fichiers ou des positions incorrects dans la demande d'extraction, ou ajoute de nouvelles propriétés à la charge utile de la réponse.
L'invite système est là pour donner au modèle plus de contexte sur la manière dont il doit effectuer la révision du code et sur les éléments à garder à l'esprit. Vous pouvez consulter l'invite système ici github.com/murtuzaalisurti/better . L'invite utilisateur contient la différence réelle, les règles et le contexte de la demande d'extraction. C'est ce qui déclenche la révision du code.
Cette action GitHub prend en charge les modèles OpenAI et Anthropic. Voici comment elle implémente l'API Anthropic :
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; }
Enfin, après avoir récupéré les suggestions, je les nettoie et les transmets à l'API GitHub pour ajouter des commentaires dans le cadre de la révision.
J'ai choisi la méthode ci-dessous pour ajouter des commentaires car en créant un nouvel avis, vous pouvez ajouter tous les commentaires en une seule fois au lieu d'ajouter un seul commentaire à la fois. L'ajout de commentaires un par un peut également déclencher une limitation du débit, car l'ajout de commentaires déclenche des notifications et vous ne voulez pas spammer les utilisateurs avec des notifications.
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; } }
Je voulais garder l'action GitHub ouverte et ouverte aux intégrations et c'est pourquoi vous pouvez utiliser n'importe quel modèle de votre choix (voir la liste des modèles pris en charge ) , ou vous pouvez affiner et créer votre propre modèle personnalisé sur les modèles de base pris en charge et l'utiliser avec cette action GitHub.
Si vous rencontrez des problèmes de jeton ou de limitation de débit , vous souhaiterez peut-être mettre à niveau les limites de votre modèle en vous référant à la documentation de la plate-forme respective.
Alors, qu'attendez-vous ? Si vous avez un dépôt sur github, essayez l'action maintenant - elle est sur la place de marché des actions github .