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();
}
}