AI 代码自动评审组件开发
业务功能设计
本项目的目的是实现一个动态代码评审SDK,动态代码评审需要什么?
- git diff:评审两次提交的差异记录
- openai:通过LLM去分析评审代码变化记录,根据设置好的提示词(prompt),告诉LLM需要注意哪些点,如何评审以及从哪种视角出发去评审,检测是否有风险等等,再把结果返回
- 通知:评审结果需要通知,简单的可以通过公众号测试平台,对于企业订阅号可以根据具体的消息模板设置
大致流程如下: 
那么问题就来了,SDK创建好了,由谁来调用这个组件实现这一套流程呢
- 直接在业务工程中引入这个SDK包,哪个工程需要自动评审的功能引入该
SDK即可,但是每个工程都需要引入,代码侵入性高; GitHub ActionsCI&CD, 将Github Action看成一台云服务器,它可通过拉取镜像系统文件来执行我们的脚本文件,这里的文件就是上面的workflow文件;- webhook 钩子,提供一个配置接口,去调用服务实现流程,需要云服务器部署服务。
前两种都存在一定不足,所以作者使用了第三种,那么SDK是如何与Github Action关联的呢,流程如下:
需要使用代码自动评审的用户通过CURL下载SDK到本地,以jar包的方式执行调用SDK的Main函数。
关于什么是CURL?,C表示的是客户端client,其是一款开源的命令行工具与库,几乎覆盖所有常见的网络协议(HTTP、HTTPS、FTP等等),起作用如下:
- 1)API接口测试,可以通过命令行发送GET/POST/DELETE/PUT请求;
- 2)下载或上传文件,这里我们使用的就是该功能;
- 3)提交表单数据;
- 4)自动化脚本;
- 5)检测网络问题;
检出提交记录:连接git,检出当前所有的分支或所有代码的变更记录.
评审记录:把评审记录写道自建的log日志仓库中,并把url地址返回给通知.
整个功能流程实现中最关键的在于prompt的设计和llm的调用
功能扩展: 自己写一个issue需求说明,让llm来完成功能实现,并创建分支,进行编码写好mock测试,与需求说明对比提交,再合并代码。
项目背景知识
1、CI&CD
CI&CD是持续集成(Continuous Integration)、持续交付(Continuous Delivery)的简称,旨在通过自动化的流程与工具,提高软件开发的效率、质量和交付速度。
1.1 CI
CI意为持续集成,其操作就是频繁的将代码提交到主干;持续集成的目的是为了快速迭代,同时保证代码的质量;使用git多方协作,提交代码分支以及分支合并的过程就是持续集成的思想所在。
持续集成的过程为:在协同开发中,每个人都有自己负责的模块,也有可能一个人负责多个模块。所有的代码由版本控制工具集成在共享存储库中,开发人员由自己的需求创建分支,开发完成后需要提交分支代码并且将代码合并到主分支,这时就需要解决代码冲突的问题,可以通过code review即代码评审的方式,一旦提交请求合并到主分支。自动化构建共聚就会根据流程自动编译构建安装应用,并执行单元测试框架的自动化测试来校验提交的修改。
CI中使用的关键组件有:
- 版本控制系统,如·
Git; - 自动化构建工具,如
jenkins,每次提交代码之后自动构建的过程; - 单元测试框架,如
junit框架,目的是为了确保基础功能在集成之后仍然有效;
1.2 CD
CD意为持续交付,可以看作持续集成的下一阶段,其操作是不断将软件的新版本,交给软件项目质量检查团,以供评审;持续交付的目的是无论软件迭代多少个版本,都可以随时交付;
此时持续交付的代码已经在主分支上了,随时可以通过自动部署工具部署在服务器上。并且监控不同环境(开发、测试、生产)的项目状态。
CD中使用的关键组件有:
- 自动部署工具,如常用的
Docker、Docker-Compose、K8s等等; - 环境配置工具,如
Terraform,确保不同环境(开发、测试、生产)的一致性; - 持续监控及反馈,目的是在项目部署之后监控项目的状态,如普罗米修斯(
Prometheus)、Grafana;
与之相关的还有持续部署(Continuous Deployment)的概念,就是在通过评审之后,项目完成自动化的部署,持续部署可以看作持续交付的下一个阶段;持续部署的目的是,项目在任何时刻都是可以部署的,可以立刻投入生产阶段;
2、Github Action
采用CI/CD可以通过自动化流程和工具帮你构建应用、测试应用、部署应用,将你的应用交给流程工具来管理,实现自动触发、验证、部署等等,从而减少人力成本,提高交付质量与效率;这在敏捷开发与DevOps中扮演者重要的角色;
Github Action就是Github开发的一款实现持续集成交付的自动化流程工具组件,用户可以通过yaml配置文件来构建执行CI/CD流水线,并可以在触发不同事件(如代码的提交、推送、合并)时执行这些自动流程;而这种事件触发机制就是代码仓库的Hook;可以将github action看成一台免费的服务器,可以在上面执行自动化流程命令;
官方文档:https://docs.github.com/zh/actions/get-started/quickstart
Github Action中的几个核心概念为:
- WorkFlow(工作流):workflow是Github Action任务执行的基本单位,
- job(任务):一个workflow由一个或多个jobs构成,含义是一次持续集成的运行;
- step(步骤):一个job由多个step组成,一步步完成;
- action(动作):每个step可以依次执行一个或多个命令;
2.1 工作流文件编写
工作流文件是yaml格式,文件的大致内容可能为:
- 指定工作流名称,在Action的任务中以该名展示;
- on来指定工作流触发条件(Hook钩子触发机制),例如在代码push到master分支时执行;
- job编写工作流程,是工作流文件的核心,一个工作流文件可以执行多个job,一个job下有至少一个step;
# 1、工作流名称
name:
# 2、工作流触发条件
on:
# 3、具体工作任务
job1:
step1:
step2:
job2:
step1:
step2:
注意:工作流文件需要放在指定目录下,才能被github识别然后自动执行,目录结构为:/.github/workflows/
SDK工程搭建
项目的基本结构如下:
项目包含创建两个模块
SDK:功能实现模块,github action调用该模块实现code reviewtest:项目中的功能测试模块,引入 SDK 与常用依赖,提供最小化的入口与测试骨架,方便在不影响主模块的情况下验证 SDK 或框架配置。
注意:SDK模块最终会被打包成jar包,该模块所需要的Maven以来也需要同时打包,为了保证环境的一致性,避免使用时还需要去重新加载依赖项目,这里需要配置需要打包的依赖如下
<!-- 如果你用了三方jar,不想一个个确定的引入,可以去掉 configuration 以及以内的配置,这样会全部打包 -->
<configuration>
<artifactSet>
<includes>
<include>com.google.guava:guava:jar:</include>
<include>com.alibaba.fastjson2:fastjson2:jar:</include>
<include>org.slf4j:slf4j-api:jar:</include>
<include>org.slf4j:slf4j-simple:jar:</include>
<include>com.auth0:java-jwt:jar:</include>
<include>com.fasterxml.jackson.core:jackson-core:</include>
<include>com.fasterxml.jackson.core:jackson-databind:</include>
<include>com.fasterxml.jackson.core:jackson-annotations:</include>
<include>org.eclipse.jgit:org.eclipse.jgit:</include>
</includes>
</artifactSet>
</configuration>
Maven构建与脚本执行
为了测试模拟其他工程使用该SDK的场景,需要创建一个新的工作流文件(workflow),执行流程如下 
对应的workflow文件为:
# 1、工作流文件名称
name: Build and Run Openai-Code-Review By Main Maven Jar
# 2、触发规则
on:
push:
branches:
- '*'
pull_request:
branches:
- '*'
# 3、构建任务, 其中检出代码与配置JDK环境是通用的
jobs:
build:
runs-on: ubuntu-latest # 使用ubuntu最新版本虚拟机
steps:
- name: Checkout repository # 检出代码
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Setup JDK 17 # 配置JDK环境
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '17'
- name: Build with Maven # 编译构建Maven
run: mvn clean install
- name: Copy Openai-Code-Review-SDK JAR # 复制JAR包
run: mvn dependency:copy -Dartifact=com.lz:openai-code-review-sdk:1.0 -DoutputDirectory=./libs
- name: Run Code Review # 运行SDK的jar包
run: java -jar ./libs/openai-code-review-sdk-1.0.jar
steps中的五个步骤就对应上图中的五个流程。
一个 .yml 文件对应一个 workflow,也就是一次持续集成。一个 GitHub 仓库可以包含多个 workflow,只要是在 .github/workflow 目录下的 .yml 文件都会被 GitHub 执行。
扩展:可以参考市场中共享的actions去包装完善、创新自己的action,并且可以将我们的共享actions给其他人使用。
具体检出diff的代码为:
public class OpenAiCodeReview {
public static void main(String[] args) throws Exception{
System.out.println("开始CI/CD");
// 1.检出代码diff,
ProcessBuilder processBuilder = new ProcessBuilder("git", "diff", "HEAD~1", "HEAD");
processBuilder.directory(new File("."));
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader((process.getInputStream())));
String line;
StringBuilder diffCode = new StringBuilder();
while((line = reader.readLine()) != null) {
diffCode.append(line).append('\n');
}
int exitCode = process.waitFor();
System.out.println("Exited with code:" + exitCode);
System.out.println("评审代码: " + diffCode);
}
}
PricessBuilder是Java中用来创建与管理操作系统的类,它允许你在Java程序中启动外部程序(如命令行、脚本或者其他可执行文件),并与其进行交互(例如传递输入、捕获输出或错误流)。相比传统的Runtime.exec(),ProcessBuilder提供了更灵活且安全的进程控制方式;
提交代码到github上,就可以观察到github Action的执行流程,获取到最近两次提交变更代码的字符串。AI代码评审的本质就是将这种字符串传递给AI大模型,让大模型替我们进行整理、归纳、总结。 
大模型对接-ChatGlm
流程分析
本节主要任务是对接ChatGlm大模型,代码评审的本质就是将用户提供的code diff交给大模型做理解分析,最后返回评审结果;
智谱官方文档:https://www.bigmodel.cn/dev/api/normal-model/glm-4
对接时,作者并没有直接引入SDK包,而是通过Java原生API与原生网络请求来搭建了一个简单的组件项目,不使用如Lombok、OkHttp等外部组件,如果引入过多的依赖与SDK,会使得本来轻量的组件显得臃肿,不符合我们开发的初衷;
等开发完成OpenAiSDK组件后,再引入SDK对接,直接一步到位对接其他的大模型。
关于接口对接鉴权,智谱提供了两种鉴权方式:
- ApiKey鉴权
- Token鉴权
出于安全考虑,这里采用了Token鉴权方式,使用JWT签名.
- ApiKey鉴权,通过一个静态的字符串来表示请求方的身份,无状态且长期有效,但是安全性低;
- Token鉴权,Token 是一个 动态的凭证(如 JWT、OAuth2 Token),通常在用户登录后由服务端动态生成,包含身份信息、有效期和签名,用于临时授权。安全性高,可进行加密、刷新等操作;
代码实现
首先创建一个工具类BearerTokenUtils, 通过对ApiKey密钥结构拆分,然后通过JWT签名的方式生成token,
public class BearerTokenUtils {
// 过期时间:默认30分钟
private static final long expireMillis = 30*60*1000L;
// 缓存服务
public static Cache<String,String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(expireMillis - (60*1000L), TimeUnit.MILLISECONDS)
.build();
public static String getToken(String apiKeySecret){
String[] split = apiKeySecret.split("\\.");
return getToken(split[0],split[1]);
}
public static String getToken(String apiKey,String apiSecret){
//缓存Token
String token = cache.getIfPresent(apiKey);
if(null!=token) return token;
//创建Token
Algorithm algorithm = Algorithm.HMAC256(apiSecret.getBytes(StandardCharsets.UTF_8));
Map<String, Object> payload = new HashMap<>();
payload.put("api_key",apiKey);
payload.put("exp",System.currentTimeMillis()+expireMillis);
payload.put("timestamp", Calendar.getInstance().getTimeInMillis());
Map<String,Object> headerClaims = new HashMap<>();
headerClaims.put("alg","HS256");
headerClaims.put("sign_type","SIGN");
token = JWT.create().withPayload(payload).withHeader(headerClaims).sign(algorithm);
cache.put(apiKey, token);
return token;
}
}
接着根据模型对接文档创建模型请求与响应对象,只保留我们需要的关键属性,十分简洁;
- 请求对象
public class ChatCompletionRequest {
private String model = Model.GLM_4_5_Air.getCode();
private List<Prompt> messages;
public static class Prompt{
private String role;
private String content;
public Prompt(){
}
public Prompt(String role,String content){
this.role = role;
this.content = content;
}
... getter and setter
}
... getter and setter
}
- 响应对象
public class ChatCompletionSyncResponse {
private List<Choice> choices;
public static class Choice{
private Message message;
public Message getMessage() {
return message;
}
public void setMessage(Message message) {
this.message = message;
}
}
public static class Message{
private String role;
private String content;
public String getRole() {
return role;
}
public String getContent() {
return content;
}
public void setRole(String role) {
this.role = role;
}
public void setContent(String content) {
this.content = content;
}
}
public List<Choice> getChoices() {
return choices;
}
public void setChoices(List<Choice> choices) {
this.choices = choices;
}
}
然后就是通过Java原生网络Api发送Http请求到ChatGlm,然后通过流读取评审结果;
private static String codeReview(String diffCode)throws Exception{
String apiKey = System.getenv("GLM_KEY");
//String apiKey = "a96e4f3b1ac040859ab86fdfc7bcdede.QpOgtBdp83eRWQMd";
String token = BearerTokenUtils.getToken(apiKey);
URL url = new URL("https://open.bigmodel.cn/api/paas/v4/chat/completions");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", "Bearer " + token);
connection.setRequestProperty("Content-Type","application/json");
connection.setRequestProperty("User-Agent","Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
connection.setDoOutput(true);
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest();
chatCompletionRequest.setModel(Model.GLM_4_5_Air.getCode());
chatCompletionRequest.setMessages(new ArrayList<ChatCompletionRequest.Prompt>(){
private static final long serialVersionUID = -7988151926241837899L;
{
add(new ChatCompletionRequest.Prompt("user","你是一个高级编程架构师,精通各类场景方案、架构设计和编程语言请,请您根据git diff记录,对代码做出评审。代码如下: " ));
add(new ChatCompletionRequest.Prompt("user",diffCode));
}
});
try(OutputStream os = connection.getOutputStream()){
byte[] input = JSON.toJSONString(chatCompletionRequest).getBytes(StandardCharsets.UTF_8);
os.write(input);
}
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder context = new StringBuilder();
while ((inputLine=in.readLine())!=null){
context.append(inputLine);
}
in.close();;
connection.disconnect();
System.out.println("评审结果:"+context.toString());
ChatCompletionSyncResponse response = JSON.parseObject(context.toString(), ChatCompletionSyncResponse.class);
return response.getChoices().get(0).getMessage().getContent();
}
代码中使用System.getenv()从环境变量中获取密钥信息,相较之硬编码到程序里更安全一些。 当通过Github的workflow执行时,密钥的信息可以在Github的Environment secret中维护(工程仓库 Settings → Secrets and variables → Actions),需要更改下workflow脚本
jobs:
build:
runs-on: ubuntu-latest
environment: GLM_KEY # 这里是环境名
...
...
- name: Run Code Review
env:
GLM_KEY: ${{ secrets.GLM_KEY }}
run: java -jar ./libs/openai-code-review-sdk-1.0.jar
整个流程即为:修改测试模块中的代码,并提交到github,通过ProcessBuilder拿到code diff之后,调用大模型评审,返回并输出评审结果;
评审日志处理
流程分析
虽然接入大模型得到了评审结果,但其评审记录只能在Github Action中看到,每一次工作流文件执行都会创建一个新的Runner环境,之前的评审记录就丢失了;
本节的任务就是保存评审记录,将评审记录保存在Github日志仓库,这就涉及到git的操作,并且需要在Java代码中操作git;需要使用到jgit组件,用于在Java中操作git;
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.6.1.202309021850-r</version>
</dependency>
日志处理流程图如下: 
代码实现
在操作git时需要做身份校验,会使用到github的token,在Settings里创建 

然后将其配置在Repository secret中,方便SDK通过工作流读取,并将其加入workflow的环境变量中。
- name: Run Code Review
env:
GLM_KEY: ${{ secrets.GLM_KEY }}
GITHUB_TOKEN: ${{ secrets.CODE_TOKEN }}
那么日志存储的代码为:
/**
* 日志文件存储
* @param token
* @param log
* @return
* @throws Exception
*/
private static String writeLog(String token, String log) throws Exception{
Git git = Git.cloneRepository()
.setURI("https://github.com/orbisz/openai-code-review-log.git")
.setDirectory(new File("repo"))
.setCredentialsProvider(new UsernamePasswordCredentialsProvider(token, ""))
.call();
String dateFolderName = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
File dateFolder = new File("repo/" + dateFolderName);
if (!dateFolder.exists()){
dateFolder.mkdirs();
}
String fileName = generateRandomString(12) + ".md";
File newFile = new File(dateFolder, fileName);
try(FileWriter writer = new FileWriter(newFile)){
writer.write(log);
}
git.add().addFilepattern(dateFolderName+"/"+fileName).call();
git.commit().setMessage("Add new file via GitHub Actions").call();
git.push().setCredentialsProvider(new UsernamePasswordCredentialsProvider(token, "")).call();
System.out.println("Changes have been pushed to the repository.");
return "https://github.com/orbisz/openai-code-review-log/blob/master/"+dateFolderName+"/"+fileName;
}
/**
* 生成指定长度的随机字符串
* @param length
* @return
*/
private static String generateRandomString(int length) {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = (int) (Math.random() * characters.length());
sb.append(characters.charAt(index));
}
return sb.toString();
}
上述日志文件的命名是一个12位随机字符串,没有区分度,考虑拼接提交者信息、提交仓库、提交分支、提交日期等信息;
通过执行git相关命令
git log命令的作用是查看提交历史版本,而git branch则是可以操作分支,查询文档后,于是产生以下两条命令:
git log -1 --pretty=%cn # 查看分支的提交者
git branch --show-current # 查看当前的分支
那么修改后的工作流文件为:
- name: Get Commit Author # 获取提交人的信息
run: echo "COMMIT_AUTHOR=$(git log -1 --pretty=%cn)" >> $GITHUB_ENV
- name: Get Commit Branch # 获取提交的分支名
run: echo "COMMIT_BRANCH=$(git branch --show-current)" >> $GITHUB_ENV
- name: Copy openai-code-review-sdk JAR
run: mvn dependency:copy -Dartifact=orbiszx.project:openai-code-review-sdk:1.0 -DoutputDirectory=./libs
- name: Run Code Review
env:
GLM_KEY: ${{ secrets.GLM_KEY }}
GITHUB_TOKEN: ${{ secrets.CODE_TOKEN }}
COMMIT_AUTHOR: ${{env.COMMIT_AUTHOR}}
COMMIT_BRANCH: ${{env.COMMIT_BRANCH}}
REPOSITORY: ${{ github.repository }} # 提交仓库名
修改后的业务代码为:这里使用提交者 + 提交仓库名 + 提交分支 + 4位随机字符串的方式命名文件。
String commitAuthor = System.getenv("COMMIT_AUTHOR");
String commitBranch = System.getenv("COMMIT_BRANCH");
String repo = System.getenv("REPOSITORY");
int idx = repo.indexOf('/');
String repository = (idx >= 0 && idx < repo.length() - 1)
? repo.substring(idx + 1)
: repo;
String fileName = commitAuthor + "-" + repository + "-" + commitBranch + "-" + generateRandomString(4) + ".md";
微信模板消息对接
流程分析
本节的任务就是对接微信Api发送模板消息,该节的流程如下: 
代码实现
代码中所需要的微信公众号appid、模板消息id都可以在微信公众平台获取。微信公众平台:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
首先对接微信公众号测试平台,需要得到access_token,是后面调用接口的唯一凭证;这里作者提供了一个工具类用来获得access_token
Access Token是公众号调用微信平台API时必须的全局唯一凭证,有效期为2小时,需定期刷新。
public class WXAccessTokenUtils {
private static final String APPID = "";
private static final String SECRET = "";
private static final String GRANT_TYPE = "client_credential";
private static final String URL_TEMPLATE = "https://api.weixin.qq.com/cgi-bin/token?grant_type=%s&appid=%s&secret=%s";
public static String getAccessToken(){
try{
String urlString = String.format(URL_TEMPLATE, GRANT_TYPE, APPID, SECRET);
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
System.out.println("Response Code:"+responseCode);
if(responseCode == HttpURLConnection.HTTP_OK){
BufferedReader in = new BufferedReader((new InputStreamReader(connection.getInputStream())));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine())!=null){
response.append(inputLine);
}
in.close();
System.out.println("Response:"+response.toString());
Token token = JSON.parseObject(response.toString(),Token.class);
return token.getAccess_token();
}else{
System.out.println("GET request failed");
return null;
}
}catch (Exception e){
e.printStackTrace();
return null;
}
}
public static class Token{
private String access_token;
private Integer expires_in;
public String getAccess_token() {
return access_token;
}
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
public Integer getExpires_in() {
return expires_in;
}
public void setExpires_in(Integer expires_in) {
this.expires_in = expires_in;
}
}
}
然后创建了实体Message,用于承接模板消息,其中每一个param都需要对应模板消息中的DATA数据
public class Message {
private String touser = "";
private String template_id = "";
private String url = "";
private Map<String, Map<String, String>> data = new HashMap<>();
public void put(String key, String value) {
data.put(key, new HashMap<>() {
private static final long serialVersionUID = 7092338402387318563L;
{
put("value", value);
}
});
}
... setter and getter
}
发送模板消息的代码为:
/**
* 微信公众号消息通知
* @param logUrl
*/
private static void pushMessage(String logUrl){
String accessToken = WXAccessTokenUtils.getAccessToken();
System.out.println(accessToken);
String repo = System.getenv("REPOSITORY");
int idx = repo.indexOf('/');
String repository = (idx >= 0 && idx < repo.length() - 1)
? repo.substring(idx + 1)
: repo;
Message message = new Message();
message.put("project",repository);
message.put("author",System.getenv("COMMIT_AUTHOR"));
message.put("branch",System.getenv("COMMIT_BRANCH"));
message.put("review",logUrl);
message.setUrl(logUrl);
String url = String.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s",accessToken);
sendPostRequest(url,JSON.toJSONString(message));
}
/**
* 发送POST请求
* @param urlString
* @param jsonBody
*/
private static void sendPostRequest(String urlString, String jsonBody) {
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; utf-8");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
try (Scanner scanner = new Scanner(conn.getInputStream(), StandardCharsets.UTF_8.name())) {
String response = scanner.useDelimiter("\\A").next();
System.out.println(response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
优化:最终SDK是要给其它工程使用的,微信公众号appid、模板消息id、github仓库地址等参数,最终是需要从外界传入的,初步设想可以从环境变量中读取,将各个参数放置在Github的Repository secret中,在workflow文件中设置为环境变量,从而实现透传。
代码工程结构重构
结合开发过程中遇到的问题,我们需要重构工程,一是让功能结构更加合理,后续增加优化功能方便。二是让数据能由调用 SDK者传入;数据传入的方式就是在仓库中配置Action Secret,之后在工作流文件中读取即可;
代码实现
1. 环境配置
需要传入的数据可以定义在Github的Repository secret中,在workflow文件中设置为环境变量,还有一些根据每次提交获取的信息,如仓库名、提交分支名、提交者、提交信息等,则通过github action上下文以及git log命令获取,那么修改后的工作流文件为:
- name: Copy openai-code-review-sdk JAR
run: mvn dependency:copy -Dartifact=orbiszx.project:openai-code-review-sdk:1.0 -DoutputDirectory=./libs
- name: Get repository name
id: repo-name
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Get branch name
id: branch-name
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Get commit author
id: commit-author
run: echo "COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an <%ae>')" >> $GITHUB_ENV
- name: Get commit message
id: commit-message
run: echo "COMMIT_MESSAGE=$(git log -1 --pretty=format:'%s')" >> $GITHUB_ENV
- name: Print repository, branch name, commit author, and commit message
run: |
echo "Repository name is ${{ env.REPO_NAME }}"
echo "Branch name is ${{ env.BRANCH_NAME }}"
echo "Commit author is ${{ env.COMMIT_AUTHOR }}"
echo "Commit message is ${{ env.COMMIT_MESSAGE }}"
- name: Run Code Review
env:
# Github 配置;GITHUB_REVIEW_LOG_URI「https://github.com/orbisz/openai-code-review-log」、GITHUB_TOKEN「https://github.com/settings/tokens」
GITHUB_REVIEW_LOG_URI: ${{ secrets.CODE_REVIEW_LOG_URI }}
GITHUB_TOKEN: ${{ secrets.CODE_TOKEN }}
COMMIT_PROJECT: ${{ env.REPO_NAME }}
COMMIT_BRANCH: ${{ env.BRANCH_NAME }}
COMMIT_AUTHOR: ${{ env.COMMIT_AUTHOR }}
COMMIT_MESSAGE: ${{ env.COMMIT_MESSAGE }}
# 微信配置 「https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index」
WEIXIN_APPID: ${{ secrets.WEIXIN_APPID }}
WEIXIN_SECRET: ${{ secrets.WEIXIN_SECRET }}
WEIXIN_TOUSER: ${{ secrets.WEIXIN_TOUSER }}
WEIXIN_TEMPLATE_ID: ${{ secrets.WEIXIN_TEMPLATE_ID }}
# OpenAi - ChatGLM 配置「https://open.bigmodel.cn/api/paas/v4/chat/completions」、「https://open.bigmodel.cn/usercenter/apikeys」
CHATGLM_APIHOST: ${{ secrets.CHATGLM_APIHOST }}
CHATGLM_APIKEYSECRET: ${{ secrets.CHATGLM_APIKEYSECRET }}
run: java -jar ./libs/openai-code-review-sdk-1.0.jar
2. 基础设施层
该层主要承接了以下内容:
- 1、git相关操作
获取代码差异:这里放弃了之前使用git diff HEAD HEAD~1来获取代码差异的方式,而是通过先获取当前提交的commit-id即哈希值,然后该哈希值来获取代码差异;
/**
* 获取diffCode
*
* @return diffCode 代码差异
* @throws Exception
*/
public String diff() throws IOException, InterruptedException {
// 1.git log: 得到git上一次提交的hash, 使用git diff HEAD HEAD~1 HEAD的方式有时不可靠, %H指的是提交完整Hash值
ProcessBuilder logProcessBuilder = new ProcessBuilder("git", "log", "-1", "--pretty=format:%H");
logProcessBuilder.directory(new File("."));
Process logProcess = logProcessBuilder.start();
BufferedReader logReader = new BufferedReader(new InputStreamReader(logProcess.getInputStream()));
String latestCommitLogHash = logReader.readLine(); // 每次提交都会生成一个hash值, 用来标识这次提交
logReader.close();
logProcess.waitFor();
// 2.git diff: 获取代码diff
ProcessBuilder diffProcessBuilder = new ProcessBuilder("git", "diff", latestCommitLogHash + "^", latestCommitLogHash);
diffProcessBuilder.directory(new File("."));
Process diffProcess = diffProcessBuilder.start();
BufferedReader diffReader = new BufferedReader(new InputStreamReader(diffProcess.getInputStream()));
StringBuilder diffCode = new StringBuilder();
String line;
while((line = diffReader.readLine()) != null) {
diffCode.append(line).append("\n");
}
diffReader.close();
// 3.等待git diff执行完成
int waitCode = diffProcess.waitFor();
if (waitCode != 0) {
logger.info("failed to execute git diff and get code diff, waitCode:{}", waitCode);
}
// 4.返回diffCode
return diffCode.toString();
}
这里有一个git命令
git log -1 --pretty=format:%H
这个命令是Git版本控制系统中用来获取最近一次提交的哈希值的命令。
- git log: 这是Git命令行工具中的一个基本命令,用于显示提交历史记录。
- -1: 这是一个参数,用于指定要显示的提交历史记录的条目数量。在这里,-1 意味着只显示最近的一次提交。
- --pretty=format:: 这是一个选项,用于控制提交信息的格式。format: 后面跟着的是一个格式化字符串。
- %H: 这是一个格式化占位符,表示提交的哈希值。
执行这个命令后,将得到一个唯一的字符串,这个字符串表示Git仓库中最近一次提交的表示。
第二个命令
git diff e4759c3dfb2f3fd299d9b31a9e0a605f0b1b6d95^ e4759c3dfb2f3fd299d9b31a9e0a605f0b1b6d95
这个命令是用来比较两个提交之间的差异的Git命令。
- git diff: 这是Git的一个命令,用于显示两个或多个提交、分支或文件之间的差异。
- e4759c3dfb2f3fd299d9b31a9e0a605f0b1b6d95: 这是第一个提交的哈希值,表示你想要查看其差异的提交。
- ^: 在Git中,^ 符号用于引用当前分支的最近一次提交。如果前面有数字,它会引用该分支的第N个最近提交。在这个命令中,^ 指的是最新提交。
- e4759c3dfb2f3fd299d9b31a9e0a605f0b1b6d95: 这是第二个提交的哈希值,也是你想要查看其差异的提交。
提交评审日志到日志仓库:这里需要返回日志URI给后续发送模板消息;
/**
* 提交日志并推送到远程仓库
*
* @param reviewLog 评审日志
* @return logURL 评审结果路径
* @Expection GitAPIException
*/
public String commitAndPush(String reviewLog) throws Exception {
// 1.克隆日志仓库
Git git = Git.cloneRepository()
.setURI(githubCommitLogUri)
.setDirectory(new File("repo"))
.setCredentialsProvider(new UsernamePasswordCredentialsProvider(githubToken, ""))
.call();
// 2.创建日志文件夹
String dateFolderName = new SimpleDateFormat("yyyy-mm-dd").format(new Date());
File dateFolder = new File("repo/" + dateFolderName);
if (!dateFolder.exists()) { // 如果不存在则创建
dateFolder.mkdirs();
}
// 3.创建日志文件&写入日志
String fileName = project + "-" + branch + "-" + author + System.currentTimeMillis() + "-" + RandomUtil.randomString(4) + ".md";
File file = new File(dateFolder, fileName);
try (FileWriter writer = new FileWriter(file)) {
writer.write(reviewLog);
}
// 4.将文件添加到暂存区并提交
git.add().addFilepattern(dateFolderName + "/" + fileName).call();
git.commit().setMessage("Add new file via Github Actions").call();
git.push().setCredentialsProvider(new UsernamePasswordCredentialsProvider(githubToken, "")).call();
logger.info("openai-code-review push and commit done!, fileName:{}", fileName);
// 5.返回日志路径方便后续模板消息使用
return githubCommitLogUri + "/blob/main/" + dateFolderName + "/" + fileName;
}
- 2、AI代码评审
接口定义,后面需要对接其它大模型,只需要新建一个类实现该接口即可(扩展开放),而不需要修改代码(修改关闭),满足开闭原则;
/**
* @Author: Lz
* Date: 2025/3/9 22:50
* Module function: openai接口, 定义规范方法, 可以接入不同的大模型
*/
public interface IOpenAi {
ChatCompletionSyncResponseDTO completion(ChatCompletionRequestDTO requestDTO) throws Exception;
}
接口实现
/**
* openai实例-ChatGlm
*
* @param requestDTO
* @return
* @throws Exception
*/
@Override
public ChatCompletionSyncResponseDTO completion(ChatCompletionRequestDTO requestDTO) throws Exception {
String token = BearerTokenUtils.getToken(apiSecretKey);
// 1.创建HTTP连接对象
URL url = new URL(apiHost);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", token);
connection.setRequestProperty("Content-type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
connection.setDoOutput(true);
// 2.读取响应数据
try (OutputStream os = connection.getOutputStream()) {
byte[] input = JSON.toJSONString(requestDTO).getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// 3.处理响应结果
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
reader.close();
// 4.返回响应结果
return JSON.parseObject(content.toString(), ChatCompletionSyncResponseDTO.class);
}
- 3、微信公众号对接
发送模板消息:
/**
* 发送微信模板消息
*
* @param logUrl 日志评审日子, 不是发送微信模板消息的URL!!!
* @param data 模板消息数据
* @throws Exception
*/
public void sendTemplateMessage(String logUrl, Map<String, Map<String, String>> data) throws Exception {
// 1.获取access_token
String accessToken = WXAccessTokenUtils.getAccessToken(appId, appSecret);
URL url = new URL("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");;
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
// 2.创建模板消息对象
TemplateMessageDTO templateMessageDTO = new TemplateMessageDTO();
templateMessageDTO.setUrl(logUrl);
templateMessageDTO.setData(data);
// 3.发送模板消息, 即将消息以字节形式写入输入流, 然后发送到服务器(微信)
// 这里使用try-with-resources语法, 确保流关闭
try (OutputStream os = connection.getOutputStream()) {
byte[] input = JSON.toJSONString(templateMessageDTO).getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// 4.响应结果解析, 即通过Scanner从输入流中读取数据
try (Scanner scanner = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8.name())) {
String response = scanner.useDelimiter("\\A").next(); // 确保读出所有响应数据
logger.info("openai-code-review weixin TemplateMessage response:{}", response);
}
}
3. 领域层
本层使用了模板设计模式,将代码评审的过程拆分成多个步骤,在抽象类中定义了代码评审流程的骨架,而具体的功能由其子类实现;相比较MVC中Service层一直都是万年不变的接口 + 实现类,抽象类的优势是什么:
- 固化流程:使用了模板设计模式,在抽象类中定义了代码评审的固定流程,而子类就只需要关注具体步骤的实现,而不需要关注整体流程;
- 复用公共逻辑:如依赖注入、异常处理、日志处理等等;
- 扩展性高:后续如果需要实现其它git平台的代码评审,直接创建子类实现抽象类中的方法即可,而不需要关注具体流程,同时这也符合开闭原则;
- 但是并不是所有场景都适合,简单、规模较小或对扩展性要求不高的场景,就可以考虑直接使用接口+实现类的模式,避免不必要的分层;
那么具体的代码实现为:
- 接口
/**
* @Author: Lz
* Date: 2025/3/9 16:36
* Module function: 评审服务接口
*/
public interface IOpenAiCodeReviewService {
void exec();
}
- 抽象类
其中的exec就是一个模板方法,定义了代码评审流程的骨架,并且在这里统一做了异常处理;
/**
* @Author: Lz
* Date: 2025/3/9 16:36
* Module function: 代码评审服务抽象类-用于定义模板, 就是流程, 具体实现由子类去做
*/
public abstract class AbstractOpenAiCodeReviewService implements IOpenAiCodeReviewService{
private final Logger logger = (Logger)LoggerFactory.getLogger(AbstractOpenAiCodeReviewService.class);
protected final GitCommand gitCommand;
protected final IOpenAi openAi;
protected final WeiXin weiXin;
public AbstractOpenAiCodeReviewService(GitCommand gitCommand, IOpenAi openAi, WeiXin weiXin) {
this.gitCommand = gitCommand;
this.openAi = openAi;
this.weiXin = weiXin;
}
/**
* 定义执行流程
*/
@Override
public void exec() {
try {
// 1.获得提交代码: 返回diff_code
String diffCode = getDiffCode();
// 2.代码评审记录: 返回评审日志
String log = codeReview(diffCode);
// 3.评审记录保存: 返回评审日志URL
String logUrl = recordCodeReview(log);
// 4.发送消息通知: 模板消息, 评审日志
pushMessage(logUrl);
} catch(Exception e) {
logger.info("openai-code-review error: " + e);
}
}
protected abstract String getDiffCode() throws Exception;
protected abstract String codeReview(String diffCode) throws Exception;
protected abstract String recordCodeReview(String log) throws Exception;
protected abstract void pushMessage(String logUrl) throws Exception;
- 实现类
public class OpenAiCodeReviewService extends AbstractOpenAiCodeReviewService {
public OpenAiCodeReviewService(GitCommand gitCommand, IOpenAi openAi, WeiXin weiXin) {
super(gitCommand, openAi, weiXin);
}
/**
* 获取diffCode
*
* @return diffCode
* @throws Exception
*/
@Override
protected String getDiffCode() throws Exception {
return gitCommand.diff();
}
/**
* 代码评审
*
* @param diffCode
* @return 代码评审结果 log
* @throws Exception
*/
@Override
protected String codeReview(String diffCode) throws Exception {
// 构造Request请求对象
ChatCompletionRequestDTO requestDTO = new ChatCompletionRequestDTO();
requestDTO.setModel(Model.GLM_4_FLASH.getCode());
requestDTO.setMessages(new ArrayList<>() {
private static final long serialVersionUID = -7988151926241837899L;
{
add(new ChatCompletionRequestDTO.Prompt("user", "你是一个高级编程架构师,精通各类场景方案、架构设计和编程语言请,请您根据git diff记录,对代码做出评审。代码如下:"));
add(new ChatCompletionRequestDTO.Prompt("user", diffCode));
}
});
ChatCompletionSyncResponseDTO completion = openAi.completion(requestDTO);
ChatCompletionSyncResponseDTO.Message message = completion.getChoices().get(0).getMessage();
return message.getContent();
}
/**
* 代码评审日志写入
*
* @param log
* @return logUrl
* @throws Exception
*/
@Override
protected String recordCodeReview(String log) throws Exception {
return gitCommand.commitAndPush(log);
}
/**
* 发送微信模板消息
*
* @param logUrl
*/
@Override
protected void pushMessage(String logUrl) throws Exception {
// 1.构造请求数据Data
HashMap<String, Map<String, String>> data = new HashMap<>();
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.PROJECT_NAME, gitCommand.getProject());
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.BRANCH_NAME, gitCommand.getBranch());
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.COMMIT_AUTHOR, gitCommand.getAuthor());
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.COMMIT_MESSAGE, gitCommand.getMessage());
// 2.调用服务
weiXin.sendTemplateMessage(logUrl, data);
}
}
4. 程序入口
流程方法由此调用,所有的环境变量也都由此传入,代码如下:
public class OpenAiCodeReview {
private static Logger logger = LoggerFactory.getLogger(OpenAiCodeReview.class);
/**
* 程序入口
*
* @param args
*/
public static void main(String[] args) {
// 1.GitCommand
GitCommand gitCommand = new GitCommand(
getEnv("GITHUB_REVIEW_LOG_URI"),
getEnv("GITHUB_TOKEN"),
getEnv("COMMIT_AUTHOR"),
getEnv("COMMIT_PROJECT"),
getEnv("COMMIT_BRANCH"),
getEnv("COMMIT_MESSAGE")
);
// 2.ChatGlm
ChatGlm chatGlm = new ChatGlm(
getEnv("CHATGLM_API_HOST"),
getEnv("CHATGLM_API_SECRET_KEY")
);
// 3.WeiXin
WeiXin weiXin = new WeiXin(
getEnv("WEIXIN_APPID"),
getEnv("WEIXIN_APP_SECRET"),
getEnv("WEIXIN_TOUSER"),
getEnv("WEIXIN_TEMPLATE_ID")
);
OpenAiCodeReviewService service = new OpenAiCodeReviewService(gitCommand, chatGlm, weiXin);
service.exec();
logger.info("openai-code-review done!");
}
/**
* 读取环境变量
*
* @param key
* @return
*/
private static String getEnv(String key) {
String env = System.getenv(key);
if (env == null || env.isEmpty()) {
throw new RuntimeException("key is null");
}
return env;
}
}
微信模板消息发送成功,点击模板消息跳转评审记录;
发布部署使用
本节的任务就是让别人可以使用我们开发的SDK,思路是将jar放在别人能下载的地方,然后在工作流文件中下载该jar包并执行,这里和作者一样选择放在日志仓库的release中; 
将Maven构建的过程改成下载Jar包,这里使用curl或者wget命令下载均可(但是我使用wget不知道为什么一直报错);
# 1、Maven构建
- name: Build with Maven # 编译构建Maven
run: mvn clean install
- name: Copy Openai-Code-Review-SDK JAR # 复制JAR包
run: mvn dependency:copy -Dartifact=com.lz:openai-code-review-sdk:1.0 -DoutputDirectory=./libs
# 2、Jar包下载
# 创建libs目录
- name: Create libs directory
run: mkdir -p ./libs
# 下载SDK-jar包, 注意下载仓库需要为public
- name: Download openai-code-review-sdk jar
run: curl -L -o ./libs/openai-code-review-sdk-1.0.jar https://github.com/Deanccccc/openai-code-review-log/releases/download/v1.0/openai-code-review-sdk-1.0.jar
然后测试可以实现同样的功能,在之后我们开发自己的项目过程中也可以引入该SDK;
用户提交代码时触发评审,调用基于RAG优化的大模型分析代码差异并生成评审建议,评审结果通过微信公众号推送,评审日志持久化至独立仓库。该系统通过自动化评审辅助人工评审,显著提高了评审的效率与准确性;
功能优化
- 基于模板设计模式抽象代码评审流程,通过构建Github Action工作流,定义代码检出、AI评审、日志存储及消息触达的自动化链路;
- 对接DeepSeek API分析代码质量并提供评审建议,实现AI驱动的代码评审流程;
- 对接RAG知识库,用户上传的文本经过解析、拆分、打标以及向量化后存入向量数据库,通过相似性检索优化LLM提示词生成,有效提高DeepSeek模型评审的上下文感知能力与精准度;
- 对接微信公众号平台,将代码评审结果通过模板消息及时触达开发人员,提升反馈效率;
- 该代码评审工具服务上面的抽奖系统设计,显著提高工程代码质量与可靠性;
扩展与优化
企业级应用
开源 LLMs + 领域知识库 + 私有化部署
- 这里 LLMs 指多个大模型组合使用;大模型再强大也必须结合内部的知识库才能发挥作用;
- 私有化部署好处是打消各行各业对数据安全的担忧
- 最终的产品形态需要具体场景具体分析!
扩展对接的大模型接口,甚至可以使用开源模型进行私有化部署.隔离外网访问,确保代码 CR 过程仅在内网环境下完成。
通知对接多个平台,例如飞书、钉钉等
优化提示词,增加提示词模板等等
统一环境配置参数的封装
让 LLM 通过文件代码分析当前代码涉及的知识点,用于后续知识库相似度匹配;对接知识库,使用国产文本相似度计算模型,并私有化部署公司内网,然后存储在向量数据库中。
如果使用的模型(如 LLaMA 2)对中文 Prompt 支持较差,需要在设计 Prompt 时采用『输入英文』『输出中文』的方式
大模型基座只包含互联网上的公开数据,对公司内部的框架知识和使用文档并不了解。有了知识库,通过三个过程找到相关度最高的内容:
- Text Embeddings(文本向量化)
- Vector Stores(向量存储)
- Similarity Search(相似性搜索)
