一、前言

第八篇已经把 Code Review Agent MVP 的 V1 跑通了。

上一版完成的链路是:

Git diff
  ↓
OpenCodeReview / LLM
  ↓
review-result.json
  ↓
review-report.md

也就是说,第八篇解决的是“能不能跑起来”的问题。

但是只生成报告还不够。

真实代码审查里,开发者更关心的是:

哪些问题最严重?
哪些问题应该优先修?
哪些只是建议?

所以第九篇开始给 Agent MVP 增加一个新能力:风险分级。

本篇代码放在:

D:\agent\open-code-review-main\ocr-practice-demo\s09

对应项目目录是:

D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp

二、本篇目标

本篇要完成 5 件事:

  1. 新增 risk_tools.py 风险分级模块。
  2. 给每条 OCR comment 添加 severity
  3. 生成顶层 risk_summary
  4. 在 Markdown 报告中展示 Risk Summary。
  5. 在每条 Finding 标题中显示 [Critical][Warning][Suggestion]

升级后的流程是:

Git changes
  ↓
Run OpenCodeReview
  ↓
Parse JSON
  ↓
Classify Risk
  ↓
Generate Markdown Report

相比第八篇,多了一个步骤:

Classify Risk

三、为什么需要风险分级

第八篇的报告已经可以展示审查结果,例如:

Findings
  1. SQL Injection Vulnerability
  2. Function not exported

但它还没有告诉我这两个问题哪个更严重。

从开发者视角看,SQL 注入明显比“函数没有导出”更紧急。

所以报告最好能变成这样:

[Critical] SQL Injection Vulnerability
[Warning] Function not exported

这就是风险分级要解决的问题。

加入风险分级后,报告不只是“列出问题”,而是开始具备“处理优先级”的能力。

这也是 Agent 项目从 Demo 走向工程化的重要一步。

四、项目结构变化

第九篇的代码结构如下:

open-code-review-agent-mvp
├── agent_graph.py
├── tools
│   ├── __init__.py
│   ├── git_tools.py
│   ├── ocr_tools.py
│   ├── report_tools.py
│   └── risk_tools.py
├── README.md
├── requirements.txt
└── .gitignore

相比第八篇,新增了:

tools/risk_tools.py

同时改造了:

agent_graph.py
report_tools.py
git_tools.py
ocr_tools.py

每个文件的变化如下:

文件 本篇变化
risk_tools.py 新增风险分级逻辑
agent_graph.py 在 OCR JSON 后加入风险分级步骤
report_tools.py 展示 Risk Summary 和 severity
git_tools.py 增加 changed files 的 exclude 过滤
ocr_tools.py Windows 下优先解析 ocr.cmd

五、风险等级设计

第一版风险等级先设计成三类:

等级 含义
Critical 高风险问题,例如 SQL 注入、权限绕过、密钥泄露
Warning 中风险问题,例如错误处理、参数校验、导出缺失
Suggestion 一般建议,例如命名、可读性、重构建议

为什么第一版只做三类?

因为 MVP 阶段不需要过度复杂。

如果一开始就设计 5 到 7 个等级,会增加解释成本,也不一定更准确。

三类已经足够支撑第一版报告:

Critical:必须优先处理
Warning:建议处理
Suggestion:可以后续优化

六、为什么第一版用关键词规则

风险分级有很多做法:

  1. 再调用一次 LLM 判断风险。
  2. 使用规则库匹配。
  3. 结合静态扫描器结果。
  4. 使用人工标注数据训练分类器。

第一版我选择关键词规则。

原因是:

  • 不额外消耗 LLM token。
  • 逻辑透明,容易解释。
  • 后续可以自然升级成 RAG 规则库或 LLM classifier。

所以第九篇先做一个简单但可用的规则分类器。

七、新增 risk_tools.py

risk_tools.py 是本篇新增的核心文件。

它定义了三组关键词:

CRITICAL_KEYWORDS = [
    "sql injection",
    "command injection",
    "remote code execution",
    "rce",
    "xss",
    "csrf",
    "hardcoded secret",
    "secret key",
    "api key",
    "password",
    "token",
    "authentication",
    "authorization",
    "permission",
    "privilege",
    "prepared statement",
    "parameterized",
    "drop table",
]
WARNING_KEYWORDS = [
    "error handling",
    "exception",
    "null",
    "undefined",
    "validation",
    "not exported",
    "missing",
    "async",
    "timeout",
    "resource leak",
    "race condition",
]
SUGGESTION_KEYWORDS = [
    "style",
    "readability",
    "naming",
    "comment",
    "documentation",
    "refactor",
    "simplify",
]

这三组关键词分别对应:

Critical
Warning
Suggestion

八、classify_comment:给单条评论分级

核心函数是:

def classify_comment(comment: dict[str, Any]) -> dict[str, Any]:
    result = dict(comment)
    text = build_comment_text(comment)

    severity, reason, matched_keywords = match_severity(text)
    result["severity"] = severity
    result["risk_reason"] = reason
    result["matched_keywords"] = matched_keywords
    return result

它做了几件事:

  1. 复制原始 comment。
  2. 把 comment 里的文本拼成一个待匹配字符串。
  3. 调用 match_severity 判断风险等级。
  4. 给 comment 添加三个新字段。

新增字段是:

字段 含义
severity 风险等级
risk_reason 为什么判成这个等级
matched_keywords 命中的关键词

这样做的好处是:原始 OCR comment 不丢失,只是在它上面增加风险信息。

九、build_comment_text:为什么要拼多个字段

风险分级不能只看 content

因为有些风险关键词可能出现在:

content
existing_code
suggestion_code

所以代码里这样处理:

def build_comment_text(comment: dict[str, Any]) -> str:
    parts = [
        str(comment.get("content", "")),
        str(comment.get("existing_code", "")),
        str(comment.get("suggestion_code", "")),
    ]
    return " ".join(parts).lower()

这样做的目的很简单:扩大匹配范围。

例如 content 里可能没有明确写 “password”,但 existing_code 里有:

db.query("UPDATE users SET password = ...")

这种情况下也应该提高风险等级。

十、match_severity:按优先级匹配

风险匹配函数是:

def match_severity(text: str) -> tuple[str, str, list[str]]:
    matched = find_keywords(text, CRITICAL_KEYWORDS)
    if matched:
        return "Critical", "Security-sensitive or data-risk keyword matched.", matched

    matched = find_keywords(text, WARNING_KEYWORDS)
    if matched:
        return "Warning", "Reliability, correctness, or maintainability keyword matched.", matched

    matched = find_keywords(text, SUGGESTION_KEYWORDS)
    if matched:
        return "Suggestion", "Code quality or readability keyword matched.", matched

    return "Suggestion", "No high-risk keyword matched; treat as a general suggestion.", []

这里的顺序很重要:

Critical -> Warning -> Suggestion

为什么 Critical 要放在最前面?

因为同一条评论里可能同时出现多个关键词。

例如:

SQL Injection + missing validation

如果先匹配 Warning,就可能把 SQL 注入误判成 Warning。

所以应该优先匹配高风险关键词。

十一、classify_review_risks:处理整份 JSON

单条评论分级之后,还需要处理整份 OCR JSON。

代码是:

def classify_review_risks(review_data: dict[str, Any]) -> dict[str, Any]:
    result = deepcopy(review_data)
    comments = result.get("comments") or []
    classified_comments = [classify_comment(comment) for comment in comments]
    result["comments"] = classified_comments
    result["risk_summary"] = summarize_risks(classified_comments)
    return result

这里做了两件事:

  1. comments 里的每条评论加风险字段。
  2. 在顶层新增 risk_summary

为什么要用 deepcopy

因为不希望直接修改原始 review_data 对象。

这样函数更像一个纯转换:

原始 OCR JSON -> 带风险分级的 JSON

十二、risk_summary:生成风险统计

风险统计函数是:

def summarize_risks(comments: list[dict[str, Any]]) -> dict[str, int]:
    summary = {severity: 0 for severity in SEVERITY_ORDER}
    for comment in comments:
        severity = comment.get("severity", "Suggestion")
        if severity not in summary:
            severity = "Suggestion"
        summary[severity] += 1
    return summary

生成结果类似:

"risk_summary": {
  "Critical": 1,
  "Warning": 1,
  "Suggestion": 0
}

这个字段很适合展示在报告顶部。

因为读者一打开报告,就能马上知道:

高危问题有几个
中风险问题有几个
普通建议有几个

十三、修改 agent_graph.py:加入风险分级节点

第八篇的流程是:

OCR JSON
  ↓
Generate Markdown Report

第九篇改成:

OCR JSON
  ↓
Classify Risk
  ↓
Generate Markdown Report

agent_graph.py 中新增导入:

from tools.risk_tools import classify_review_risks

然后在 run_workflow 中加入:

state.review_data = classify_review_risks(review_data)
state.review_json_path.write_text(
    json.dumps(state.review_data, ensure_ascii=False, indent=2) + "\n",
    encoding="utf-8",
)

这说明 review-result.json 保存的不再只是 OCR 原始结果,而是增强后的结果。

增强后的结果包含:

原始 OCR 字段
severity
risk_reason
matched_keywords
risk_summary

这一步可以理解成一个新的 Agent 节点:

risk_classification_node

虽然现在还没有用 LangGraph,但节点思想已经很清楚了。

十四、修改 report_tools.py:展示风险信息

report_tools.py 主要改了两处。

第一处:增加 Risk Summary。

lines.extend(["", "## Risk Summary", "", "| Severity | Count |", "|---|---:|"])
for severity in SEVERITY_ORDER:
    lines.append(f"| {severity} | {risk_summary.get(severity, 0)} |")

生成报告后会显示:

## Risk Summary

| Severity | Count |
|---|---:|
| Critical | 1 |
| Warning | 1 |
| Suggestion | 0 |

第二处:Finding 标题增加 severity。

lines = [
    f"### {index}. [{severity}] `{path}:{start_line}-{end_line}`",
    "",
]

生成后会变成:

### 1. [Critical] `s01/src/user.js:37-37`

相比第八篇,这个变化很明显。

开发者不需要读完整段评论,就能先看到风险等级。

十五、修复第八篇发现的问题

第八篇真实运行时发现一个问题:

OCR 的 --exclude 生效了,但 Markdown 报告里的 Changed Files 仍然显示被排除文件。

原因是:

changed_files 来自 git status --porcelain
没有按 exclude 过滤

第九篇在 git_tools.py 里加入了:

def filter_paths(paths: list[str], excludes: list[str] | None = None) -> list[str]:
    patterns = [pattern for pattern in (excludes or []) if pattern]
    return [path for path in paths if not is_excluded(path, patterns)]

并在 agent_graph.py 中使用:

all_changed_files = get_changed_files(repo_root)
state.changed_files = filter_paths(all_changed_files, effective_excludes)

这样报告里的 Changed Files 会和 exclude 规则保持一致。

测试时加了:

--exclude outputs/** --exclude outputs-review-result.json --exclude s09/**

最终报告里的 Changed Files 只显示:

s01/src/user.js

十六、顺手优化 Windows 下 ocr.cmd 解析

第八篇还遇到一个问题:

PowerShell 能运行 ocr,但 Python subprocess 找不到 ocr。

原因是 PowerShell 找到的是 ocr.ps1,而 Python 更适合调用 ocr.cmd

第九篇在 ocr_tools.py 里新增了:

def resolve_ocr_executable(ocr_bin: str = "ocr") -> str:
    candidate = Path(ocr_bin)
    if candidate.exists():
        return str(candidate)

    if os.name == "nt" and not Path(ocr_bin).suffix:
        cmd_path = shutil.which(f"{ocr_bin}.cmd")
        if cmd_path:
            return cmd_path

    resolved = shutil.which(ocr_bin)
    if resolved:
        return resolved

    raise RuntimeError(...)

这样在 Windows 下,如果传入默认的:

ocr

代码会优先查找:

ocr.cmd

这比第八篇要求用户手动传 --ocr-bin 更稳。

十七、运行语法检查

先做语法检查:

python -B -c "from pathlib import Path; files=[r'D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp\agent_graph.py',r'D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp\tools\git_tools.py',r'D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp\tools\ocr_tools.py',r'D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp\tools\risk_tools.py',r'D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp\tools\report_tools.py']; [compile(Path(f).read_text(encoding='utf-8'), f, 'exec') for f in files]; print('syntax ok:', len(files), 'files')"

输出:

syntax ok: 5 files

说明 5 个核心 Python 文件语法正常。

十八、离线运行测试

这里继续用已有 OCR JSON 做离线测试:

python -B D:\agent\open-code-review-main\ocr-practice-demo\s09\open-code-review-agent-mvp\agent_graph.py --repo D:\agent\open-code-review-main\ocr-practice-demo --output-dir "C:\Users\ad\Documents\New project\s09-final-output-filtered" --use-existing-json D:\agent\open-code-review-main\ocr-practice-demo\outputs-review-result.json --exclude outputs/** --exclude outputs-review-result.json --exclude s09/**

输出:

Code Review Agent MVP finished.
Repo: D:\agent\open-code-review-main\ocr-practice-demo
Changed files: 1
JSON: C:\Users\ad\Documents\New project\s09-final-output-filtered\review-result.json
Report: C:\Users\ad\Documents\New project\s09-final-output-filtered\review-report.md

这里的 Changed files: 1 说明 exclude 过滤生效了。

因为当前仓库里有:

 M s01/src/user.js
?? outputs-review-result.json
?? s09/

但加了 exclude 后,报告里只展示业务代码:

s01/src/user.js

十九、查看增强后的 review-result.json

生成的 JSON 里,每条 comment 已经带上风险字段。

第一条 SQL 注入评论:

{
  "path": "s01/src/user.js",
  "content": "**SQL Injection Vulnerability**: ...",
  "start_line": 37,
  "end_line": 37,
  "severity": "Critical",
  "risk_reason": "Security-sensitive or data-risk keyword matched.",
  "matched_keywords": [
    "sql injection",
    "prepared statement",
    "parameterized"
  ]
}

第二条函数未导出评论:

{
  "path": "s01/src/user.js",
  "content": "The `updateUserEmail` function is defined but not exported...",
  "start_line": 36,
  "end_line": 39,
  "severity": "Warning",
  "risk_reason": "Reliability, correctness, or maintainability keyword matched.",
  "matched_keywords": [
    "not exported"
  ]
}

顶层新增了:

"risk_summary": {
  "Critical": 1,
  "Warning": 1,
  "Suggestion": 0
}

这说明风险分级模块已经生效。

二十、查看增强后的 Markdown 报告

报告里新增了 Risk Summary:

## Risk Summary

| Severity | Count |
|---|---:|
| Critical | 1 |
| Warning | 1 |
| Suggestion | 0 |

Changed Files 也只显示业务代码:

## Changed Files

- `s01/src/user.js`

Findings 标题现在带风险等级:

### 1. [Critical] `s01/src/user.js:37-37`

并且会展示分级原因:

**Risk Reason**

Security-sensitive or data-risk keyword matched.

**Matched Keywords**

`sql injection`, `prepared statement`, `parameterized`

第二条问题是:

### 2. [Warning] `s01/src/user.js:36-39`

命中的关键词是:

`not exported`

这样报告的可读性明显比第八篇更好。

二十一、这版代码的工程价值

第八篇的报告只能说明:

发现了哪些问题

第九篇的报告可以进一步说明:

哪些问题更严重
为什么判定为这个等级
命中了哪些关键词

这就让报告从“结果展示”升级成了“处理优先级建议”。

这也是一个 Code Review Agent 应该具备的能力。

因为审查结果如果很多,开发者不可能逐条从头读到尾。

风险分级可以帮助开发者先处理:

Critical

再处理:

Warning

最后再看:

Suggestion

二十二、当前规则方案的不足

当前风险分级仍然是一个 MVP 版本,有明显不足:

  1. 关键词规则比较粗糙。
  2. 可能出现误判。
  3. 不能理解复杂上下文。
  4. 不同语言、不同框架的风险关键词不一样。
  5. 不能判断某个问题是否真的可利用。

例如:

password

这个词出现在代码里,不一定一定是 Critical。

但如果它和 SQL 拼接、token、secret、authorization 等词一起出现,风险就明显更高。

所以后续可以升级成:

规则库 + 权重打分

或者:

LLM Risk Classifier

二十三、后续怎么升级成 LangGraph 节点

现在的风险分级是在 run_workflow 里直接调用:

state.review_data = classify_review_risks(review_data)

如果后续引入 LangGraph,可以把它变成一个独立节点:

check_git_changes
  ↓
run_ocr_review
  ↓
parse_review_json
  ↓
classify_risk
  ↓
generate_report

其中:

classify_risk

就是第九篇新增的能力。

也就是说,第九篇虽然还没有引入 LangGraph,但已经为 LangGraph 的节点拆分做准备了。

二十四、本篇总结

本篇完成了 Code Review Agent MVP 的风险分级能力。

核心成果如下:

  1. 新增 tools/risk_tools.py
  2. 为每条 OCR comment 增加 severity
  3. 为每条 OCR comment 增加 risk_reason
  4. 为每条 OCR comment 增加 matched_keywords
  5. 在 JSON 顶层增加 risk_summary
  6. 在 Markdown 报告中增加 Risk Summary
  7. 在 Findings 标题中展示 [Critical][Warning][Suggestion]
  8. 修复 Changed Files 未按 exclude 过滤的问题。
  9. 优化 Windows 下 ocr.cmd 解析问题。

到这里,项目已经从:

能生成报告

升级成:

能生成带风险优先级的报告

二十五、下一篇计划

下一篇可以正式进入 LangGraph:

从 0 学 OpenCodeReview:引入 LangGraph 管理 Agent 工作流

下一篇可以把当前线性流程:

run_workflow

拆成:

check_git_changes
run_ocr_review
classify_risk
generate_report
save_outputs

这样项目会更像一个真正的 Agent Workflow。

更多推荐