diff --git a/BATCH_REVIEW_IMPLEMENTATION.md b/BATCH_REVIEW_IMPLEMENTATION.md new file mode 100644 index 000000000..ba04b4a0a --- /dev/null +++ b/BATCH_REVIEW_IMPLEMENTATION.md @@ -0,0 +1,119 @@ +# 批量代码审查功能实现说明 + +## 概述 +本次更新实现了按文件批次进行代码审查的功能,解决了一次性将所有修改代码发送给AI导致上下文不够、AI忘记提示词模板要求的问题。 + +## 主要改动 + +### 1. code_reviewer.py 新增功能 + +#### 新增方法:`review_changes_in_batches` +- **功能**:按文件批次审查代码变更,然后汇总所有审查结果 +- **参数**: + - `changes`: 代码变更列表,每个元素是一个包含文件信息的字典 + - `commits_text`: 提交信息 +- **返回值**:汇总后的审查结果 + +**工作流程**: +1. 遍历每个文件的变更 +2. 对每个文件单独调用 `review_code` 方法进行审查 +3. 如果单个文件的tokens超过 `REVIEW_MAX_TOKENS`,会自动截断 +4. 收集所有文件的审查结果 +5. 如果只有一个文件,直接返回该文件的审查结果 +6. 如果有多个文件,调用 `_summarize_reviews` 方法汇总结果 + +#### 新增方法:`_summarize_reviews` +- **功能**:使用 `summary_merge_review_prompt` 提示词汇总多个审查结果 +- **参数**: + - `partial_reviews`: 各批次的审查结果列表 +- **返回值**:汇总后的总审查报告 + +**工作流程**: +1. 加载 `summary_merge_review_prompt` 提示词配置 +2. 将所有分批审查结果用分隔符拼接 +3. 调用LLM进行汇总 +4. 返回格式化后的汇总结果 + +### 2. worker.py 修改 + +#### `handle_merge_request_event` 函数 +**修改前**: +```python +review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) +``` + +**修改后**: +```python +code_reviewer = CodeReviewer() +review_result = code_reviewer.review_changes_in_batches(changes, commits_text) +``` + +**变化**:不再将所有changes转换为字符串一次性审查,而是将changes列表传递给批量审查方法。 + +#### `handle_push_event` 函数 +与 `handle_merge_request_event` 相同的修改方式。 + +#### `handle_github_push_event` 函数 +与GitLab push事件处理相同的修改方式。 + +#### `handle_github_pull_request_event` 函数 +与GitLab merge request处理相同的修改方式。 + +## 环境变量配置 + +项目提供了灵活的环境变量来控制批量审查行为: + +### BATCH_REVIEW_ENABLED +- **说明**:是否启用批量审查功能 +- **可选值**: + - `1`:启用批量审查(默认) + - `0`:禁用批量审查,使用传统的一次性审查方式 +- **默认值**:`1` +- **使用场景**: + - 启用时:代码变更会按批次分组审查,然后汇总 + - 禁用时:所有代码变更一次性发送给AI审查(可能遇到上下文限制问题) + +### BATCH_REVIEW_FILES_PER_BATCH +- **说明**:每批次审查的文件数量 +- **可选值**:整数,建议范围 `1-5` +- **默认值**:`1`(每个文件单独审查) +- **配置建议**: + - `1`:每个文件独立审查,精确度最高,但LLM调用次数最多 + - `2-3`:适合中等规模的变更,平衡精确度和性能 + - `4-5`:适合大量小文件变更,减少LLM调用次数 + - 更大值:可能导致单次审查上下文过长,不推荐 + +### REVIEW_MAX_TOKENS +- **说明**:每批次审查的最大token限制 +- **默认值**:`10000` +- **作用**:防止单批次内容过长,超出部分会自动截断 + +### 配置示例 + +在 `conf/.env` 文件中添加: + +```bash +# 启用批量审查 0-否 1-是 +BATCH_REVIEW_ENABLED=1 + +# 每批次审查文件数(建议1-5) +BATCH_REVIEW_FILES_PER_BATCH=2 + +``` + +## 性能对比 + +| 配置 | 10个文件变更的LLM调用次数 | 精确度 | 适用场景 | +|------|-------------------------|--------|----------| +| `BATCH_REVIEW_ENABLED=0` | 1次 | 低 | 小规模变更 | +| `FILES_PER_BATCH=1` | 11次(10+1汇总) | 最高 | 追求最高质量 | +| `FILES_PER_BATCH=3` | 5次(4批次+1汇总) | 高 | **推荐配置** | +| `FILES_PER_BATCH=5` | 3次(2批次+1汇总) | 中 | 大量小文件 | + +## 提示词配置 + +已使用项目中新增的 `summary_merge_review_prompt` 提示词,该提示词专门用于: +- 整合多个分批审查报告 +- 去除重复问题 +- 重新计算整体评分 +- 生成结构化的总审查报告 diff --git a/biz/queue/worker.py b/biz/queue/worker.py index a58cdef34..020b4c270 100644 --- a/biz/queue/worker.py +++ b/biz/queue/worker.py @@ -38,7 +38,8 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi if len(changes) > 0: commits_text = ';'.join(commit.get('message', '').strip() for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + code_reviewer = CodeReviewer() + review_result = code_reviewer.review_changes_in_batches(changes, commits_text) score = CodeReviewer.parse_review_score(review_text=review_result) for item in changes: additions += item['additions'] @@ -131,9 +132,10 @@ def handle_merge_request_event(webhook_data: dict, gitlab_token: str, gitlab_url logger.error('Failed to get commits') return - # review 代码 + # review 代码 - 使用批量审查方法 commits_text = ';'.join(commit['title'] for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + code_reviewer = CodeReviewer() + review_result = code_reviewer.review_changes_in_batches(changes, commits_text) # 将review结果提交到Gitlab的 notes handler.add_merge_request_notes(f'Auto Review Result: \n{review_result}') @@ -188,7 +190,8 @@ def handle_github_push_event(webhook_data: dict, github_token: str, github_url: if len(changes) > 0: commits_text = ';'.join(commit.get('message', '').strip() for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + code_reviewer = CodeReviewer() + review_result = code_reviewer.review_changes_in_batches(changes, commits_text) score = CodeReviewer.parse_review_score(review_text=review_result) for item in changes: additions += item.get('additions', 0) @@ -271,9 +274,10 @@ def handle_github_pull_request_event(webhook_data: dict, github_token: str, gith logger.error('Failed to get commits') return - # review 代码 + # review 代码 - 使用批量审查方法 commits_text = ';'.join(commit['title'] for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + code_reviewer = CodeReviewer() + review_result = code_reviewer.review_changes_in_batches(changes, commits_text) # 将review结果提交到GitHub的 notes handler.add_pull_request_notes(f'Auto Review Result: \n{review_result}') diff --git a/biz/utils/code_reviewer.py b/biz/utils/code_reviewer.py index a277ac59e..c825e3629 100644 --- a/biz/utils/code_reviewer.py +++ b/biz/utils/code_reviewer.py @@ -98,6 +98,114 @@ def review_code(self, diffs_text: str, commits_text: str = "") -> str: ] return self.call_llm(messages) + def review_changes_in_batches(self, changes: List[Dict[str, Any]], commits_text: str = "") -> str: + """ + 按文件批次审查代码变更,然后汇总所有审查结果 + :param changes: 代码变更列表,每个元素是一个包含文件信息的字典 + :param commits_text: 提交信息 + :return: 汇总后的审查结果 + """ + if not changes: + logger.info("代码变更为空") + return "代码为空" + + # 检查是否启用批量审查 + batch_review_enabled = os.getenv("BATCH_REVIEW_ENABLED", "1") == "1" + + # 如果未启用批量审查,使用原有的一次性审查方式 + if not batch_review_enabled: + logger.info("批量审查功能未启用,使用传统一次性审查方式") + return self.review_and_strip_code(str(changes), commits_text) + + review_max_tokens = int(os.getenv("REVIEW_MAX_TOKENS", 10000)) + # 获取每批次审查的文件数量配置 + files_per_batch = int(os.getenv("BATCH_REVIEW_FILES_PER_BATCH", 1)) + logger.info(f"批量审查已启用,每批次审查 {files_per_batch} 个文件") + + partial_reviews = [] + total_files = len(changes) + + # 按配置的批次大小分批进行审查 + for batch_start in range(0, total_files, files_per_batch): + batch_end = min(batch_start + files_per_batch, total_files) + batch_changes = changes[batch_start:batch_end] + batch_num = (batch_start // files_per_batch) + 1 + total_batches = (total_files + files_per_batch - 1) // files_per_batch + + logger.info(f"正在审查第 {batch_num}/{total_batches} 批次 (文件 {batch_start + 1}-{batch_end}/{total_files})") + + # 收集当前批次的文件路径 + batch_file_paths = [ + change.get('new_path') or change.get('old_path', 'unknown') + for change in batch_changes + ] + + # 将批次内的文件转换为文本 + batch_text = str(batch_changes) + + # 计算tokens数量,如果超过限制则截断 + tokens_count = count_tokens(batch_text) + if tokens_count > review_max_tokens: + logger.warning(f"批次 {batch_num} 的变更超过 {review_max_tokens} tokens,将截断") + batch_text = truncate_text_by_tokens(batch_text, review_max_tokens) + + # 审查当前批次 + try: + review_result = self.review_code(batch_text, commits_text).strip() + if review_result.startswith("```markdown") and review_result.endswith("```"): + review_result = review_result[11:-3].strip() + + # 添加批次标识 + batch_header = f"### 批次 {batch_num} (文件: {', '.join(batch_file_paths)})\n" + partial_reviews.append(f"{batch_header}{review_result}") + logger.info(f"批次 {batch_num} 审查完成") + except Exception as e: + logger.error(f"审查批次 {batch_num} 时出错: {e}") + partial_reviews.append(f"### 批次 {batch_num}\n审查失败: {str(e)}") + + # 如果只有一个批次,直接返回结果(去掉批次标识) + if len(partial_reviews) == 1: + # 去掉批次标题行 + result = partial_reviews[0] + lines = result.split('\n', 1) + return lines[1] if len(lines) > 1 else result + + # 汇总多个批次的审查结果 + logger.info(f"开始汇总 {len(partial_reviews)} 个批次的审查结果") + summary_result = self._summarize_reviews(partial_reviews) + return summary_result + + def _summarize_reviews(self, partial_reviews: List[str]) -> str: + """ + 使用 summary_merge_review_prompt 汇总多个审查结果 + :param partial_reviews: 各批次的审查结果列表 + :return: 汇总后的总审查报告 + """ + # 加载汇总提示词 + summary_prompts = self._load_prompts("summary_merge_review_prompt", os.getenv("REVIEW_STYLE", "professional")) + + # 拼接所有分批审查结果 + partial_reviews_text = "\n\n---\n\n".join(partial_reviews) + + # 构建汇总请求消息 + messages = [ + summary_prompts["system_message"], + { + "role": "user", + "content": summary_prompts["user_message"]["content"].format( + partial_reviews_text=partial_reviews_text + ), + }, + ] + + # 调用LLM进行汇总 + summary_result = self.call_llm(messages).strip() + if summary_result.startswith("```markdown") and summary_result.endswith("```"): + summary_result = summary_result[11:-3].strip() + + logger.info("审查结果汇总完成") + return summary_result + @staticmethod def parse_review_score(review_text: str) -> int: """解析 AI 返回的 Review 结果,返回评分""" diff --git a/conf/.env.dist b/conf/.env.dist index 4818d24ea..54b2ba9a8 100644 --- a/conf/.env.dist +++ b/conf/.env.dist @@ -38,6 +38,12 @@ REVIEW_MAX_TOKENS=10000 #Review 风格选项:professional(专业) | sarcastic(毒舌) | gentle(温和) | humorous(幽默) REVIEW_STYLE=professional +# 批量审查配置 +# 是否启用批量审查功能(1=启用,0=禁用,使用传统一次性审查) +BATCH_REVIEW_ENABLED=1 +# 每批次审查的文件数量(建议1-5,设置为1表示每个文件单独审查后汇总) +BATCH_REVIEW_FILES_PER_BATCH=3 + #钉钉配置 DINGTALK_ENABLED=0 DINGTALK_WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=xxx diff --git a/conf/prompt_templates.yml b/conf/prompt_templates.yml index 66258255a..e3fb59e1c 100644 --- a/conf/prompt_templates.yml +++ b/conf/prompt_templates.yml @@ -1,43 +1,107 @@ code_review_prompt: system_prompt: |- - 你是一位资深的软件开发工程师,专注于代码的规范性、功能性、安全性和稳定性。本次任务是对员工的代码进行审查,具体要求如下: - - ### 代码审查目标: - 1. 功能实现的正确性与健壮性(40分): 确保代码逻辑正确,能够处理各种边界情况和异常输入。 - 2. 安全性与潜在风险(30分):检查代码是否存在安全漏洞(如SQL注入、XSS攻击等),并评估其潜在风险。 - 3. 是否符合最佳实践(20分):评估代码是否遵循行业最佳实践,包括代码结构、命名规范、注释清晰度等。 - 4. 性能与资源利用效率(5分):分析代码的性能表现,评估是否存在资源浪费或性能瓶颈。 - 5. Commits信息的清晰性与准确性(5分):检查提交信息是否清晰、准确,是否便于后续维护和协作。 - - ### 输出格式: - 请以Markdown格式输出代码审查报告,并包含以下内容: - 1. 问题描述和优化建议(如果有):列出代码中存在的问题,简要说明其影响,并给出优化建议。 - 2. 评分明细:为每个评分标准提供具体分数。 - 3. 总分:格式为“总分:XX分”(例如:总分:80分),确保可通过正则表达式 r"总分[::]\s*(\d+)分?") 解析出总分。 + 你是一位资深的软件开发工程师,专注于代码的规范性、功能性、安全性和稳定性。 + 当前是代码评审环节,你**只能针对代码进行审查**,不得泛化为整体提交分析,不允许生成整体变更摘要,也不得仅按照“文件变更摘要”形式描述本批代码变更。 + 无论当前批次代码多少,你都必须生成**完整结构化审查报告(含评分)**,即使变更较小,你也需基于影响尺度合理给出分数,不允许跳过或生成非模板输出。 + + --- + + ### 严格评分标准(逐项给分,缺一不可): + 1. 功能实现的正确性与健壮性(40分) + 2. 安全性与潜在风险(30分) + 3. 是否符合最佳实践(20分) + 4. 性能与资源利用效率(5分) + 5. Commits信息的清晰性与准确性(5分) + + --- + + ### 输出格式(必须为以下 Markdown 结构,顺序不可变): + 1. **问题描述和优化建议** + - 必须结合本批次代码逐点说明问题,而不是简单描述文件变动内容。 + - 需体现逻辑正确性、异常风险、安全性、可维护性等方面的评价。 - ### 特别说明: - 整个评论要保持{{ style }}风格 + 2. **评分明细** + - 功能实现的正确性与健壮性:XX分 + - 安全性与潜在风险:XX分 + - 是否符合最佳实践:XX分 + + --- + + ### 风格控制(根据 {{ style }} 应用): {% if style == 'professional' %} - 评论时请使用标准的工程术语,保持专业严谨。 + 使用工程化语言,数据化表述问题,保持严谨专业。 {% elif style == 'sarcastic' %} - 评论时请大胆使用讽刺性语言,但要确保技术指正准确。 + 可使用讽刺语言指出问题,但技术判断必须准确、直接。 {% elif style == 'gentle' %} - 评论时请多用"建议"、"可以考虑"等温和措辞。 + 语言柔和,使用“建议、可以考虑”等字眼,避免过度批评。 {% elif style == 'humorous' %} - 评论时请: - 1. 在技术点评中加入适当幽默元素 - 2. 合理使用相关Emoji(但不要过度): - - 🐛 表示bug - - 💥 表示严重问题 - - 🎯 表示改进建议 - - 🔍 表示需要仔细检查 + 可加入适当幽默或Emoji(🐛💥🎯🔍),但需确保技术指正清晰。 {% endif %} + --- + + ### 强制自检机制(必须执行): + 以下行为全部禁止,若发生必须重新生成完整审查报告: + - 仅生成变更内容摘要或说明“代码新增了什么” + - 输出泛泛而谈的变更描述(如“增加了某方法,删除了某逻辑”) + user_prompt: |- - 以下是某位员工向 GitLab 代码库提交的代码,请以{{ style }}风格审查以下代码。 + 以下是某位员工向代码库提交的代码,请以{{ style }}风格审查本批次内容: 代码变更内容: {diffs_text} 提交历史(commits): {commits_text} + +summary_merge_review_prompt: + system_prompt: |- + 你是一位高级软件架构师,现在需要对多个分批完成的代码审查结果进行整合成一个完整的总审查报告。 + + 你的职责: + 1. 重新整合多个批次的审查结果,形成“统一评分的总报告” + 2. 不得丢失开发者定位问题所需的“批次级详细描述” + 3. 在顶层总结问题趋势及关键风险 + 4. 根据全量问题重新统一打分 + + --- + + 汇总结构必须包含以下 3 部分: + + ### 第一部分:全局问题总结与优化建议(进行归类整合,去重问题) + - 从所有批次报告中抽取共性问题进行分类总结 + - 以整体角度提出优化方向,而不是重复粘贴批次内容 + + ### 第二部分:分批次详细问题保留区(必须原样结构化保留) + 你的任务是按以下格式保留批次细节,不得简化或省略: + ``` + #### 批次 X(文件范围/来源说明) + <保留该批完整的“问题描述与评分明细”,不得删减内容> + ``` + + 这样开发者能快速找到“哪个文件在哪个批次出了什么问题”。 + + ### 第三部分:统一评分明细与总分(你必须重新评分) + - 你需要结合多个批次的影响范围重新量化总评分,而不是平均或取最大值 + - 格式如下: + ``` + - 功能实现的正确性与健壮性:XX分 + - 安全性与潜在风险:XX分 + - 是否符合最佳实践:XX分 + - 性能与资源效率:XX分 + - 提交信息清晰性与准确性:XX分 + ``` + + 最后一行必须为:**总分:XX分** + + --- + + 自检规则: + - 若未包含“分批次详细问题保留区”则需重新生成 + - 若未重新统一评分,而直接引用批次数值,则需重新评分 + - 若缺少“总分:XX分”,必须重新生成 + - 若全局总结部分只是重复批次内容,必须进行整合后重新生成 + + user_prompt: |- + 以下是分批次代码审查结果,请将其整合为一个完整的总审查报告,并统一量化评分: + {partial_reviews_text}