【懂点AI】基于AI实现代码评审的思考及可用方案

420 阅读9分钟

AI提效之代码评审

代码评审的好处

提高代码质量、促进知识共享、增强团队协作、降低项目风险

代码评审的痛点

标准、效率、质量、反馈

AI存在的问题

输出不稳定

解决方案:

调整temperature参数

简化不必要的提示描述

规定输出格式

细分任务职责

调整Prompt各部分的顺序

传统检索会出现将示例问题作为评审问题输出的情形

解决方案:

减少规范或示例文档内的示例代码内容,通过问题描述+解决描述的方式指引

表面话过多“综上所述,上述代码逻辑XXX,未查出XXXX。同时XXXX的调整有助于解决XX问题”

解决方案:

提示“不需要解释或总结”

按 {指定} 格式输出

输出不必要建议或非建议空话,“参数X未进行判空”“函数X用于XXX,逻辑和注释是一致的”

解决方案:

提示“不要强调小问题和吹毛求疵”

提示“不要强调次要问题和挑剔的细节”

输出代码示例过长

解决方案:

提示“代码长度要保持简短只给包含核心逻辑代码片段,中间可使用"..."省略”

没有推理逻辑

解决方案:

提示“请在充分思考之后”

提示“请在充分理解代码逻辑之后”

未特殊训练的大模型不能识别内部知识

解决方案:

模型微调

智能体

Token数量受限

解决方法:

代码分段输入,例如:指导我给你输入【end】才xxx

只评审关键代码,例如:只评审代码变更部分

LLM回答质量随着Token数的增多逐渐降低

技术方案设计

核心流程

1、由开发人员提交评审请求

2、系统接收任务进行评审

3、人工复审评审质量

4、对评审结果进行归档

单Agent模式

使用一个Agent实现代码评审

Agents工作流

使用多个Agent实现代码评审评审中的多个流程

例:

Agent1:代码逻辑确认

Agent2:代码风格审查

Agent3:代码问题检查

Agent4:...

实现方案

评审代码块:指定代码块

{
    "input":代码块
}

分析:评审代码块,代码块内容较少,现有大模型的token完全可以支持

实现:

1、组装好prompt 和 代码

2、使用大模型进行评审

评审代码块:diff

{
    "input":代码块
}

分析:评审内容是基于SVN或Git提交的代码diff内容,内容较少,现有大模型的token完全可以支持

实现:

1、获取到要评审的的代码的SVN或Git的diff

2、组装好prompt 和 代码

3、使用大模型进行评审

评审代码文件:xxx.java

{
    "input":代码文件
}

若大模型支持上传附件:

分析:一个完整的代码文件中内容较多,现有大模型的token可能无法完全支持,若大模型支持上传附件,则只需要将代码文件作为附件上传即可

实现:

1、获取到要评审的的代码作为附件上传

2、组装prompt

3、使用大模型进行评审

若大模型不支持上传附件:

分析:一个完整的代码文件中内容较多,现有大模型的token可能无法完全支持,若大模型支持不上传附件,则需要将代码内容拆分为多段,在多次会话中逐步上传给大模型(若代码文件较小,一次会话即可)

实现:

1、获取到要评审的的代码

2、根据大模型token限制,将代码文件切分为若干段

3、进行多轮对话,将代码逐步发送给大模型

4、使用大模型进行评审

评审代码包:xxx.jar

{
    "input":代码包
}

分析:一个完整的代码包中内容较多,现有大模型的token可能无法完全支持,只能依赖于大模型要支持上传附件,只需要将代码包作为附件上传即可

实现:

1、获取到要评审的的代码包作为附件上传

2、组装prompt

3、使用大模型进行评审

Prompt设计

你是一位资深编程专家,拥有深厚的编程基础和广泛的技术栈知识。你的专长在于识别代码中的低效模式、安全隐患、以及可维护性问题,并能提出针对性的优化策略。
你擅长以易于理解的方式解释复杂的概念,确保即使是初学者也能跟随你的指导进行有效改进。
在提供优化建议时,你注重平衡性能、可读性、安全性、逻辑错误、异常处理、边界条件,以及可维护性方面的考量,同时尊重原始代码的设计意图。
你总是以鼓励和建设性的方式提出反馈,致力于提升团队的整体编程水平,详尽指导编程实践,雕琢每一行代码至臻完善。
用户会将仓库代码分支修改代码给你,以git diff 字符串的形式提供,你需要根据变化的代码,帮忙review本段代码。
然后你review内容的返回内容必须严格遵守下面我给你的格式,包括标题内容。

模板中的变量内容解释:
变量1是给review打分,分数区间为0~100分。
变量2 是code review发现的问题点,包括:可能的性能瓶颈、逻辑缺陷、潜在问题、安全风险、命名规范、注释、以及代码结构、异常情况、边界条件、资源的分配与释放等等
变量3是具体的优化修改建议。
变量4是你给出的修改后的代码。
变量5是代码中的优点。
变量6是代码的逻辑和目的,识别其在特定上下文中的作用和限制


必须要求:
1. 以精炼的语言、严厉的语气指出存在的问题。
2. 你的反馈内容必须使用严谨的markdown格式
3. 不要携带变量内容解释信息。
4. 有清晰的标题结构

返回格式严格如下:
项目名称: 代码评审
代码评分:{变量1}
代码逻辑与目的:{变量6}
代码优点:{变量5}
问题点:{变量2}
修改建议:{变量3}
修改后的代码:{变量4}

代码如下:

代码样例

基于AI实现SVN中diff的代码评审

基于我们公司内自由的模型实现代码评审

主要思路:

1、确定要评审的代码

2、查询出当前代码在指定时间范围的commoit记录,这个时间可以是一天、三天、一周等

3、对指定时间范围内提交的代码获取diff记录

4、将diff发送给大模型评审

5、解析流式输出内容

package com.wind.bizserver.chat;


import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.wind.bizserver.utils.OkHttpUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.*;

import javax.xml.crypto.Data;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;

@Slf4j
public class SVNLogExample {

    //调用模型的PKEY
    private static final String PKEY = "xxx";
    //模型的调用地址
    private static final String AIGC_URL = "http://xxx/windAlice";
    //模型的应用名称标记
    private static final String APP_NAME = "xxx";
    //stream流式返回标记
    private static final String STREAM_RETURN_START_FLAG = "data:";
    //stream流式返回结束标记
    private static final String STREAM_RETURN_END_FLAG = "data:[DONE]";
    
    private static final String userName = "your userName"; //svn账号
    private static final String password = "your password"; //svn密码


    public static void main(String[] args) throws SVNException, IOException {

        //要评审的代码
        String urlString = "http://.../wind.ent.web/src/main/java/com/wind/ent/web/controller/chat/AIChatController.java";


        String tempDir = System.getProperty("java.io.tmpdir"); //临时文件


        SVNURL svnurl = SVNURL.parseURIEncoded(urlString);

        DefaultSVNOptions options = SVNWCUtil.createDefaultOptions(true);

        SVNRepository repos;
        ISVNAuthenticationManager authManager;

        log.info("开始加载");
        authManager = SVNWCUtil.createDefaultAuthenticationManager(new File(tempDir + "/auth"), userName, password.toCharArray());
        options.setDiffCommand("-x -w");
        repos = SVNRepositoryFactory.create(svnurl);
        repos.setAuthenticationManager(authManager);
        log.info("init completed");

        // 创建DiffClient & 设置差异回调处理器
        SVNDiffClient diffClient = new SVNDiffClient(repos.getAuthenticationManager(), new DefaultSVNOptions());
        diffClient.setDiffGenerator(new DefaultSVNDiffGenerator());

        //查询当前文件[queryStartRevision,queryEndRevision]之间的变更记录
        long queryStartRevision = 0;
        long queryEndRevision = repos.getLatestRevision();

        //查询出要评审的代码文件的所有的变更记录
        Collection<SVNLogEntry> logEntries = repos.log(new String[]{""}, null, queryStartRevision, queryEndRevision, true, true);
        SVNLogEntry[] svnLogEntries = logEntries.toArray(new SVNLogEntry[0]);
        SVNLogEntry[] svnLogEntriesList = Arrays.copyOf(svnLogEntries, svnLogEntries.length);

        //设置要查询的变更记录的时间范围
        Date start = Date.from(LocalDateTime.now().minusDays(3).atZone(ZoneId.systemDefault()).toInstant());
        Date end = new Date();

        //根据时间范围对变更记录进行过滤,如果在实际范围内在则获取代码的diff内容
        long indexStartVersion = 0;
        long indexEndVersionIndex;

        List<String> diffList = new ArrayList<>();

        for (SVNLogEntry logEntry : svnLogEntriesList) {
            long revision = logEntry.getRevision();
            Date date = logEntry.getDate();
            indexEndVersionIndex = revision;
            boolean s = date.compareTo(start) >= 0;
            boolean e = date.compareTo(end) <= 0;
            if (s && e) {
                String diff = getDiffByVersion(diffClient, svnurl, indexStartVersion, indexEndVersionIndex);
                //logSVNLogEntryInfo(indexStartVersion, indexEndVersionIndex, logEntry.getAuthor(), logEntry.getMessage(), diff);
                diffList.add(diff);
            }
            indexStartVersion = revision;
        }

        //获取prompt
        String codeReviewPrompt = FileUtil.readUtf8String("D:\kygeng\SYN\Src\GEL\wind.ent.BIZServer\dev\src\src\test\resources\prompt\02_codeReview_V2.txt");

        //AI查询
        for (String diff : diffList) {

            //组装prompt
            String content = codeReviewPrompt.replace("{myCode}", diff).replace("{author}", "kygeng").replace("codeMessage", "AICodeReview测试");

            //记录结果
            JSONObject modelInputJSON = getModelInput(content, UUID.randomUUID().toString());
            String modelInput = modelInputJSON.toJSONString();

            //流式输出
            okhttp3.RequestBody body = okhttp3.RequestBody.create(okhttp3.MediaType.parse("application/json;charset=utf-8"), modelInput);
            OkHttpClient client = new OkHttpClient();

            log.info("AIGC接口地址:{}", AIGC_URL);

            StringBuilder result = new StringBuilder();

            //通过HTTP的方式调用模型
            Request request = new Request.Builder().url(AIGC_URL).post(body).build();
            try (okhttp3.Response response = client.newCall(request).execute()) {

                if (!response.isSuccessful()) {
                    throw new IOException("Unexpected code " + response);
                }
                okhttp3.ResponseBody responseBody = response.body();
                if (responseBody == null) {
                    throw new IOException("Empty response body");
                }

                InputStream inputStream = responseBody.byteStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));

                String line;
                System.out.println("====用户问句流式输出中=============");
                while ((line = reader.readLine()) != null) {
                    String returnContent = getStreamReturnContent(line);
                    if (!StringUtils.isEmpty(returnContent)) {
                        System.out.print(returnContent);
                    }
                    if (!StringUtils.isEmpty(returnContent)) {
                        result.append(returnContent);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //stream流式数据完成,将答案插入数据库
                //System.out.println("===获取到用户问句答案,查询结果:{}====" + result);
            }

            //break;
        }
    }

    /**
     * 获取 stream 流式返回中的内容
     *
     * @param string
     * @return
     */
    public static String getStreamReturnContent(String string) {
        if (!StringUtils.isEmpty(string) && string.startsWith(STREAM_RETURN_START_FLAG)) {
            //流式响应结束符:data:[DONE]
            if (string.equals(STREAM_RETURN_END_FLAG)) {
                log.info("流式响应结束了!");
            }
            //流式数据正常返回
            else {
                StringBuilder result = new StringBuilder();
                String substring = string.substring(STREAM_RETURN_START_FLAG.length(), string.length());
                try {
                    JSONObject object = JSONObject.parseObject(substring);
                    JSONArray choices = object.getJSONArray("choices");
                    for (Object o : choices) {
                        JSONObject obj = (JSONObject) o;
                        JSONObject delta = obj.getJSONObject("delta");
                        String content = delta.getString("content");
                        result.append(content);
                    }
                    return result.toString();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }


    /**
     * 组装模型的输入条件
     *
     * @param string
     * @return
     */
    public static JSONObject getModelInput(String content, String requestId) {

        JSONObject user = new JSONObject();
        user.put("role", "user");
        user.put("content", content);

        List<JSONObject> message = new ArrayList<>();
        message.add(user);

        JSONObject body = new JSONObject();
        body.put("model", "AliceBase");
        body.put("stream", true);
        body.put("messages", message);

        JSONObject input = new JSONObject();
        input.put("PKey", PKEY);
        input.put("source", "wind.ent.web");

        input.put("requestId", requestId);
        input.put("body", body);

        return input;
    }


    /**
     * 打印变更记录
     *
     * @param startVersionIndex
     * @param endVersionIndex
     * @param author
     * @param message
     * @param diff
     */
    public static void logSVNLogEntryInfo(long startVersionIndex, long endVersionIndex, String author, String message, String diff) {
        System.out.println("当前版本:" + endVersionIndex + ",上一个版本:" + startVersionIndex);
        System.out.println("修改人:" + author);
        System.out.println("变更内容:" + message);
        System.out.println("SVN变更记录========");
        System.out.println(diff);
    }

    /**
     * 通过版本号获取Diff
     *
     * @param diffClient
     * @param svnurl
     * @param startVersionIndex
     * @param endVersionIndex
     * @return
     * @throws SVNException
     */
    public static String getDiffByVersion(SVNDiffClient diffClient, SVNURL svnurl, long startVersionIndex, long endVersionIndex) throws SVNException {
        OutputStream outputStream = new ByteArrayOutputStream();
        diffClient.doDiff(svnurl, SVNRevision.HEAD, SVNRevision.create(startVersionIndex), SVNRevision.create(endVersionIndex), SVNDepth.EMPTY, false, outputStream);
        return outputStream.toString();
    }


}