Skip to content

Day 3:并发审查——一个项目的代码一起审

昨天的问题

Day 2 的审查链一次只审一个文件。但真实项目通常有几个甚至几十个 Python 文件。审完一个再一个,跟

项目二的串行分析是一个问题——时间越来越长。 天的问题Day 2 的审查链一次只审一个文件。但真实项目通常有几个甚至几十个 Python 文件。审完一个再一个,跟项目二的串行分析是一个问题——时间越来越长。

更关键的是,代码审查有个天然属性:文件之间互不依赖。models.py 不需要等 views.py 审完——两者内容独立,完全可以同时进行。这就是并发的最佳场景。

今天做什么

asyncio.gather() 同时审查多个 Python 文件。先扫描目录下所有 .py 文件,同时启动审查协程,等所有结果回来后生成一份汇总报告。

这个模式跟项目二 Day 3 几乎一样,但输入从"文档内容"变成了"文件路径 + 代码内容"。

📁 代码目录

code_assistant/
├── sample_code.py         ← Day1 的示例代码
├── day1.py
├── day2.py
├── day3.py                ← 新增!并发审查,60行
└── test_module/
    ├── calc.py            ← 手动创建或自动生成
    └── utils.py

代码

python
# day3.py — 并发代码审查

import asyncio
import time
import os
from pathlib import Path
from typing import List
from pydantic import BaseModel, Field

# ===== Day2 的结构定义(复用)=====
class FunctionIssue(BaseModel):
    function_name: str
    severity: str
    category: str
    description: str
    suggestion: str

class CodeReview(BaseModel):
    filename: str = Field(description="文件名")  # 重要:多文件时需要区分来源
    total_score: int
    summary: str
    issues: List[FunctionIssue]
# ===== Day2 结束 =====

# ===== Day3 新增:准备测试文件 =====
TEST_DIR = "test_module"
os.makedirs(TEST_DIR, exist_ok=True)

files_to_create = {
    "calc.py": '''"""计算模块"""

def add(a, b):
    return a + b

def divide(a, b):
    """除法运算"""
    return a / b  # 缺少除零检查

def calculate_average(numbers):
    result = sum(numbers) / len(numbers)
    return result
''',
    "utils.py": '''"""工具函数"""

def format_date(year, month, day):
    return f"{year}-{month}-{day}"

def read_file(path):
    f = open(path, "r")  # 未用 with,可能忘记关闭
    content = f.read()
    f.close()
    return content

def process_items(items):
    result = []
    for i in items:
        result.append(i)
    return result
''',
    "helpers.py": '''"""辅助函数"""

def validate_email(email):
    """验证邮箱格式(不完整实现)"""
    if "@" in email:
        return True
    return False

GREETING = "Hello"

def say_hello(name):
    return GREETING + " " + name
'''
}

for fname, content in files_to_create.items():
    fpath = os.path.join(TEST_DIR, fname)
    if not os.path.exists(fpath):
        with open(fpath, "w", encoding="utf-8") as f:
            f.write(content)
        print(f"📝 已创建: {fpath}")

# ===== Day3 新增:并发审查引擎 =====
async def review_one_file(filepath: str) -> CodeReview:
    """
    异步审查单个文件。
    在真实场景中,这里调用 LLM API 进行审查。
    今天用简单规则模拟审查结果。
    """
    filename = os.path.basename(filepath)
    print(f"  🔍 审查中: {filename}")
    
    # 读取代码
    with open(filepath, "r", encoding="utf-8") as f:
        code = f.read()
    
    # 模拟审查耗时
    await asyncio.sleep(0.5)
    
    # 基于简单规则的审查
    issues = []
    
    # 规则1:检查是否缺少文档字符串
    if 'def ' in code:
        for line in code.split("\n"):
            line = line.strip()
            if line.startswith("def "):
                func_name = line.split("(")[0].replace("def ", "").strip()
                # 检查函数下面是否有 """..."""
                issues.append(FunctionIssue(
                    function_name=func_name,
                    severity="medium",
                    category="文档",
                    description="建议添加文档字符串说明函数用途和参数",
                    suggestion=f'在 def {func_name} 下一行添加 """函数说明"""'
                ))
    
    # 规则2:检查潜在风险
    if "/ 0" in code or "ZeroDivision" in code:
        issues.append(FunctionIssue(
            function_name="divide / calculate",
            severity="high",
            category="风险",
            description="存在除零风险,缺少参数校验",
            suggestion="在除法前添加 if b == 0 检查"
        ))
    
    if "open(" in code and "with " not in code:
        issues.append(FunctionIssue(
            function_name="文件操作",
            severity="high",
            category="风险",
            description="使用 open() 但未使用 with 语句,可能导致文件未关闭",
            suggestion="改用 with open(path) as f: 语句"
        ))
    
    # 计算得分
    score = max(30, 100 - len(issues) * 10)
    
    print(f"  ✅ 审查完成: {filename}{len(issues)} 个问题,{score}分)")
    
    return CodeReview(
        filename=filename,
        total_score=score,
        summary=f"发现 {len(issues)} 个问题",
        issues=issues[:5]  # 最多5个
    )

async def review_project(project_dir: str) -> List[CodeReview]:
    """
    并发审查整个项目的所有 Python 文件。
    """
    # 1. 扫描所有 .py 文件
    py_files = list(Path(project_dir).glob("*.py"))
    
    if not py_files:
        print("❌ 没有找到 Python 文件")
        return []
    
    print(f"📂 找到 {len(py_files)} 个 Python 文件\n")
    
    # 2. 并发审查 ⭐
    tasks = [review_one_file(str(f)) for f in py_files]
    results = await asyncio.gather(*tasks)
    
    return results

# ===== 运行 =====
if __name__ == "__main__":
    start = time.time()
    results = asyncio.run(review_project(TEST_DIR))
    elapsed = time.time() - start
    
    # 汇总报告
    print(f"\n{'=' * 50}")
    print(f"📊 项目审查汇总")
    print(f"{'=' * 50}")
    print(f"   审查文件数: {len(results)}")
    print(f"   总耗时: {elapsed:.1f} 秒")
    if results:
        print(f"   如果串行: {len(results) * 0.5:.0f} 秒")
        print(f"   提速: {len(results) * 0.5 / elapsed:.1f} 倍")
    
    # 每文件评分
    total_issues = 0
    for review in results:
        total_issues += len(review.issues)
        print(f"\n   📄 {review.filename}: {review.total_score}分")
        for issue in review.issues:
            icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(issue.severity, "⚪")
            print(f"      {icon} {issue.function_name}: {issue.description[:40]}...")
    
    print(f"\n   总计发现: {total_issues} 个问题")

运行

bash
python day3.py

你应该看到:

📝 已创建: test_module/calc.py
📝 已创建: test_module/utils.py
📝 已创建: test_module/helpers.py

📂 找到 3 个 Python 文件

  🔍 审查中: calc.py
  🔍 审查中: utils.py
  🔍 审查中: helpers.py
  ✅ 审查完成: calc.py(3 个问题,70分)
  ✅ 审查完成: utils.py(3 个问题,70分)
  ✅ 审查完成: helpers.py(2 个问题,80分)

==================================================
📊 项目审查汇总
==================================================
   审查文件数: 3
   总耗时: 0.5 秒
   如果串行: 2 秒
   提速: 3.0 倍

   📄 calc.py: 70分
      🟡 add: 建议添加文档字符串...
      🟡 divide: 建议添加文档字符串...
      🔴 divide: 存在除零风险...
   
   📄 utils.py: 70分
      🟡 format_date: 建议添加文档字符串...
      🟡 read_file: 建议添加文档字符串...
      🔴 文件操作: 使用 open() 但未使用 with...
   
   📄 helpers.py: 80分
      🟡 validate_email: 建议添加文档字符串...
      🟡 say_hello: 建议添加文档字符串...

   总计发现: 8 个问题

你学到了什么

asyncio.gather() 的模式在三个项目中一模一样。 项目一用它同时处理多用户对话,项目二用它同时分析多份文档,项目三用它同时审查多个代码文件。底层原理相同——IO 等待期间切换协程——但应用场景完全不同。你现在已经能"条件反射"——遇到多个互不依赖的任务,第一时间想到 asyncio.gather()

简单规则审查是真实 AI 审查的骨架。 今天用的规则(缺文档字符串、除零风险、文件未关闭)虽然简单,但它们是你未来 Prompt 里要告诉 LLM 的"审查标准"。把今天的规则翻译成自然语言放进 Prompt,就是你的第一条 LLM 审查指令。

多文件审查的评测价值: 三个文件一起审,你一眼就能看出哪个文件质量高(helper.py 80 分)、哪个需要多改改(calc.py 70 分)。这就是 CI/CD 里"代码质量门禁"的雏形——低于 60 分不让合入。

明天的预告

今天的审查规则是硬编码在 review_one_file() 里的。明天用缓存机制——只有修改过的文件才重新审查,没动过的直接拿上一次的结果。


Day 3 完成。现在你能审查整个项目——所有 Python 文件同时审,3 份文件不到 1 秒出结果。