Skip to content

Day 2:LCEL 审查链——让 Agent "评价"你的代码

昨天的问题

Day 1 的 Agent 只是"统计"——几个类、几个函数、谁缺注释。但它不知道代码写得好不好add(self, a, b): return a + b 这个函数没有类型注解、没有参数校验——Agent 看到了缺文档字符串,但没看出更深层的问题。 天的问题Day 1 的 Agent 只是"统计"——几个类、几个函数、谁缺注释。但它不知道代码写得好不好add(self, a, b): return a + b 这个函数没有类型注解、没有参数校验——Agent 看到了缺文档字符串,但没看出更深层的问题。

今天 Agent 要像一个真正的 Code Reviewer——不只是"看到",而是"评价"。

今天做什么

构建一条代码审查链

跟项目二的逻辑一样:定义输出格式 → 写审查 Prompt → MockLLM 模拟 → Pydantic 解析。不同的是——Prompt 里不是"提取信息",而是"按标准审查"。

我们定义 4 个审查维度:命名规范函数职责文档注释潜在风险

📁 代码目录

code_assistant/
├── sample_code.py       ← Day1 的示例代码
├── day1.py
└── day2.py              ← 新增!审查链,80行

代码

python
# day2.py — LCEL 代码审查链

from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List

# ===== Day1 的代码加载(复用)=====
import os

SAMPLE = "sample_code.py"
if not os.path.exists(SAMPLE):
    with open(SAMPLE, "w", encoding="utf-8") as f:
        f.write('''class Calculator:
    """简单计算器"""
    def add(self, a, b):
        return a + b
    def subtract(self, a, b):
        return a - b
    def multiply(self, a, b):
        return a * b
    def divide(self, a, b):
        """除法运算(缺少参数校验)"""
        return a / b

def greet(name):
    """生成问候语"""
    return f"Hello, {name}!"

def process_data(data):
    result = []
    for item in data:
        if item > 0:
            result.append(item * 2)
    return result
''')

with open(SAMPLE, "r", encoding="utf-8") as f:
    source_code = f.read()

print(f"📂 加载代码: {SAMPLE} ({len(source_code)} 字符)\n")
# ===== Day1 结束 =====

# ===== Day2 新增:审查报告结构 =====
class FunctionIssue(BaseModel):
    """单个函数的问题"""
    function_name: str = Field(description="函数名")
    severity: str = Field(description="严重程度:high / medium / low")
    category: str = Field(description="问题类别:命名/职责/文档/风险")
    description: str = Field(description="问题的详细描述")
    suggestion: str = Field(description="修改建议")

class CodeReview(BaseModel):
    """完整的代码审查报告"""
    total_score: int = Field(description="总分(满分100)")
    naming_score: int = Field(description="命名规范得分(满分25)")
    responsibility_score: int = Field(description="函数职责得分(满分25)")
    documentation_score: int = Field(description="文档注释得分(满分25)")
    safety_score: int = Field(description="安全与健壮性得分(满分25)")
    summary: str = Field(description="一句话总评")
    issues: List[FunctionIssue] = Field(description="发现的问题列表")

parser = PydanticOutputParser(pydantic_object=CodeReview)

# ===== Prompt 模板 =====
REVIEW_PROMPT = """你是一位资深 Python 代码审查员。请按照以下四个维度审查代码:

### 审

查维度
1. **命名规范(25分)**:变量名、函数名、类名是否符合 Python 命名约定
2. **函数职责(25分)**:每个函数是否只做一件事,是否过长或过于复杂
3. **文档注释(25分)**:函数是否有文档字符串,注释是否清晰有用
4. **安全与健壮性(25分)**:是否有输入校验、异常处理、边界条件检查

### 输

出要求
- 总分 100 分,四个维度各 25 分
- 对每个函数给出问题描述和修改建议
- 严重程度:high(必须改)、medium(建议改)、low(锦上添花)

{format_instructions}

### 待

审查代码
```python
{source_code}
```
"""

prompt = ChatPromptTemplate.from_template(REVIEW_PROMPT)

# ===== MockLLM —— 返回预设的审查结果 =====
class MockLLM:
    """模拟 LLM 审查代码"""
    
    def invoke(self, messages):
        import json
        
        # 模拟审查结果 —— 基于 sample_code.py 的已知问题
        review = CodeReview(
            total_score=62,
            naming_score=22,
            responsibility_score=18,
            documentation_score=10,
            safety_score=12,
            summary="代码结构清晰但缺少参数校验和文档字符串,健壮性需要加强",
            issues=[
                FunctionIssue(
                    function_name="divide",
                    severity="high",
                    category="风险",
                    description="没有检查除数是否为零,传入 divide(x, 0) 会抛出 ZeroDivisionError",
                    suggestion="添加 if b == 0 检查,返回有意义的结果或抛出更友好的异常"
                ),
                FunctionIssue(
                    function_name="process_data",
                    severity="high",
                    category="风险",
                    description="没有检查 data 是否为 None 或非列表类型",
                    suggestion="添加参数校验:if not isinstance(data, list): raise TypeError(...)"
                ),
                FunctionIssue(
                    function_name="add",
                    severity="medium",
                    category="文档",
                    description="缺少文档字符串,不知道该函数的用途和参数类型",
                    suggestion="添加 docstring 说明参数类型和返回值"
                ),
                FunctionIssue(
                    function_name="subtract",
                    severity="medium",
                    category="文档",
                    description="缺少文档字符串",
                    suggestion="添加 docstring"
                ),
                FunctionIssue(
                    function_name="multiply",
                    severity="medium",
                    category="文档",
                    description="缺少文档字符串",
                    suggestion="添加 docstring"
                ),
                FunctionIssue(
                    function_name="process_data",
                    severity="low",
                    category="命名",
                    description="函数名 'process_data' 有点模糊——它在做什么处理?",
                    suggestion="建议改为更具体的名字,如 filter_and_double_positives"
                ),
            ]
        )
        return review.model_dump_json()

llm = MockLLM()

# ===== 组装 LCEL 链 =====
chain = prompt | llm | parser

# ===== 运行审查 =====
print("=" * 50)
print("🔍 开始代码审查...")
print("=" * 50)

result = chain.invoke({
    "source_code": source_code,
    "format_instructions": parser.get_format_instructions()
})

# 输出审查报告
print(f"\n📊 审查报告")
print(f"{'=' * 50}")
print(f"   总分: {result.total_score}/100")
print(f"   命名规范: {result.naming_score}/25")
print(f"   函数职责: {result.responsibility_score}/25")
print(f"   文档注释: {result.documentation_score}/25")
print(f"   安全健壮: {result.safety_score}/25")
print(f"   总评: {result.summary}")

print(f"\n🔍 发现 {len(result.issues)} 个问题:")
for issue in result.issues:
    icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(issue.severity, "⚪")
    print(f"   {icon} [{issue.severity}] {issue.function_name}")
    print(f"      类别: {issue.category}")
    print(f"      问题: {issue.description}")
    print(f"      建议: {issue.suggestion}")
    print()

运行

bash
python day2.py

你应该看到:

📂 加载代码: sample_code.py (419 字符)

==================================================
🔍 开始代码审查...
==================================================

📊 审查报告
==================================================
   总分: 62/100
   命名规范: 22/25
   函数职责: 18/25
   文档注释: 10/25
   安全健壮: 12/25
   总评: 代码结构清晰但缺少参数校验和文档字符串,健壮性需要加强

🔍 发现 6 个问题:
   🔴 [high] divide
      类别: 风险
      问题: 没有检查除数是否为零...
      建议: 添加 if b == 0 检查...

   🔴 [high] process_data
      类别: 风险
      问题: 没有检查 data 是否为 None 或非列表类型
      建议: 添加参数校验...

   🟡 [medium] add
      类别: 文档
      问题: 缺少文档字符串...
      建议: 添加 docstring 说明参数类型和返回值

   ...

你学到了什么

代码审查的本质是"照镜子"。 Agent 没有在"创造"标准——你在 Prompt 里定义了 4 个维度(命名、职责、文档、安全),Agent 只是拿着这把尺子去量代码。每个维度 25 分,总分 100 分。你今天写的这个 Prompt 框架,用在真实 LLM 上效果会更好——因为真实 LLM 能理解代码语义,而 MockLLM 返回的是你预设好的审查结论。

跟项目二 Day 2 的对比: 项目二是"从文本中提取",项目三是"对代码做评价"。输入格式变了(Python 代码 vs 合同文本),Prompt 重点变了(审查标准 vs 提取规则),但 LCEL 管道完全一样——prompt | llm | parser同样的骨架,换一个 Prompt,就是一个全新的 Agent。

MockLLM 的价值再次体现: 不用 LLM API,你就能验证整个审查流程——评分出来了,问题列出来了,严重程度用 emoji 标出来了。明天换一个真实代码文件跑,MockLLM 不会给你新的结果——但你会看到流程已经在生产环境中能跑的样子。

明天的预告

今天 Agent 一次审查一个文件。明天用 asyncio 并发审查——一个项目的所有 Python 文件同时审,节省时间。


Day 2 完成。Agent 现在是代码审查员了——四个维度逐项打分,每个问题有描述有建议有严重程度。