一、前言
第八篇已经把 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 件事:
- 新增
risk_tools.py风险分级模块。 - 给每条 OCR comment 添加
severity。 - 生成顶层
risk_summary。 - 在 Markdown 报告中展示 Risk Summary。
- 在每条 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:可以后续优化六、为什么第一版用关键词规则
风险分级有很多做法:
- 再调用一次 LLM 判断风险。
- 使用规则库匹配。
- 结合静态扫描器结果。
- 使用人工标注数据训练分类器。
第一版我选择关键词规则。
原因是:
- 不额外消耗 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:给单条评论分级
核心函数是:
defclassify_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_keywordsreturnresult它做了几件事:
- 复制原始 comment。
- 把 comment 里的文本拼成一个待匹配字符串。
- 调用
match_severity判断风险等级。 - 给 comment 添加三个新字段。
新增字段是:
| 字段 | 含义 |
|---|---|
severity | 风险等级 |
risk_reason | 为什么判成这个等级 |
matched_keywords | 命中的关键词 |
这样做的好处是:原始 OCR comment 不丢失,只是在它上面增加风险信息。
九、build_comment_text:为什么要拼多个字段
风险分级不能只看content。
因为有些风险关键词可能出现在:
content existing_code suggestion_code所以代码里这样处理:
defbuild_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:按优先级匹配
风险匹配函数是:
defmatch_severity(text:str)->tuple[str,str,list[str]]:matched=find_keywords(text,CRITICAL_KEYWORDS)ifmatched:return"Critical","Security-sensitive or>,matched matched=find_keywords(text,WARNING_KEYWORDS)ifmatched:return"Warning","Reliability, correctness, or maintainability keyword matched.",matched matched=find_keywords(text,SUGGESTION_KEYWORDS)ifmatched:return"Suggestion","Code quality or readability keyword matched.",matchedreturn"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。
代码是:
defclassify_review_risks(review_data:dict[str,Any])->dict[str,Any]:result=deepcopy(review_data)comments=result.get("comments")or[]classified_comments=[classify_comment(comment)forcommentincomments]result["comments"]=classified_comments result["risk_summary"]=summarize_risks(classified_comments)returnresult这里做了两件事:
- 给
comments里的每条评论加风险字段。 - 在顶层新增
risk_summary。
为什么要用deepcopy?
因为不希望直接修改原始review_data对象。
这样函数更像一个纯转换:
原始 OCR JSON -> 带风险分级的 JSON十二、risk_summary:生成风险统计
风险统计函数是:
defsummarize_risks(comments:list[dict[str,Any]])->dict[str,int]:summary={severity:0forseverityinSEVERITY_ORDER}forcommentincomments:severity=comment.get("severity","Suggestion")ifseveritynotinsummary:severity="Suggestion"summary[severity]+=1returnsummary生成结果类似:
"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中新增导入:
fromtools.risk_toolsimportclassify_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 |","|---|---:|"])forseverityinSEVERITY_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里加入了:
deffilter_paths(paths:list[str],excludes:list[str]|None=None)->list[str]:patterns=[patternforpatternin(excludesor[])ifpattern]return[pathforpathinpathsifnotis_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里新增了:
defresolve_ocr_executable(ocr_bin:str="ocr")->str:candidate=Path(ocr_bin)ifcandidate.exists():returnstr(candidate)ifos.name=="nt"andnotPath(ocr_bin).suffix:cmd_path=shutil.which(f"{ocr_bin}.cmd")ifcmd_path:returncmd_path resolved=shutil.which(ocr_bin)ifresolved:returnresolvedraiseRuntimeError(...)这样在 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>,"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>### 2. [Warning] `s01/src/user.js:36-39`命中的关键词是:
`not exported`这样报告的可读性明显比第八篇更好。
二十一、这版代码的工程价值
第八篇的报告只能说明:
发现了哪些问题第九篇的报告可以进一步说明:
哪些问题更严重 为什么判定为这个等级 命中了哪些关键词这就让报告从“结果展示”升级成了“处理优先级建议”。
这也是一个 Code Review Agent 应该具备的能力。
因为审查结果如果很多,开发者不可能逐条从头读到尾。
风险分级可以帮助开发者先处理:
Critical再处理:
Warning最后再看:
Suggestion二十二、当前规则方案的不足
当前风险分级仍然是一个 MVP 版本,有明显不足:
- 关键词规则比较粗糙。
- 可能出现误判。
- 不能理解复杂上下文。
- 不同语言、不同框架的风险关键词不一样。
- 不能判断某个问题是否真的可利用。
例如:
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 的风险分级能力。
核心成果如下:
- 新增
tools/risk_tools.py。 - 为每条 OCR comment 增加
severity。 - 为每条 OCR comment 增加
risk_reason。 - 为每条 OCR comment 增加
matched_keywords。 - 在 JSON 顶层增加
risk_summary。 - 在 Markdown 报告中增加
Risk Summary。 - 在 Findings 标题中展示
[Critical]、[Warning]、[Suggestion]。 - 修复 Changed Files 未按 exclude 过滤的问题。
- 优化 Windows 下
ocr.cmd解析问题。
到这里,项目已经从:
能生成报告升级成:
能生成带风险优先级的报告二十五、下一篇计划
下一篇可以正式进入 LangGraph:
从 0 学 OpenCodeReview:引入 LangGraph 管理 Agent 工作流下一篇可以把当前线性流程:
run_workflow拆成:
check_git_changes run_ocr_review classify_risk generate_report save_outputs这样项目会更像一个真正的 Agent Workflow。