综合小项目:用 Python 构建可验证的 AI 代码审查器

举报
时光不写 发表于 2026/09/24 09:02:17 2026/09/24
【摘要】 综合运用提示词、结构化输出、校验和离线测试,构建一个只读的 AI 代码审查命令行工具。
【技术专栏】 AI开发
【内容摘要】 综合运用提示词、结构化输出、校验和离线测试,构建一个只读的 AI 代码审查命令行工具。

前面的系列已经覆盖了模型调用、提示词、结构化输出、错误处理、评估和安全边界。本篇在路线完成后做一个小型综合项目:输入一段 Python 代码,让模型给出有限格式的审查报告,程序负责校验、排序和保存结果。它不会执行被审查的代码,也不会自动修改文件,重点是把“模型提出建议”和“程序控制流程”清楚地分开。

先确定功能边界

这个工具只回答一个问题:代码中有哪些值得人工检查的问题。每条问题包含行号、严重程度、类别和建议。严重程度只能是 lowmediumhigh;类别只能是 correctnesssecuritymaintainabilityperformance。模型不能运行代码、读取其他文件、安装依赖,也不能把建议直接当成补丁执行。

输入从命令行参数读取,输出保存为 JSON。这样做虽然简单,却保留了真实项目中的几个重要边界:外部文本必须限制长度,模型输出必须解析和校验,文件写入只能发生在校验成功之后。审查意见最终仍由开发者决定是否采纳。

准备环境和配置

在独立虚拟环境中安装官方 Python SDK 和环境变量加载库:

1
2
3
python -m venv .venv
source .venv/bin/activate
python -m pip install openai python-dotenv

在项目目录创建 .env,只填写自己的配置;下面的值是占位符:

1
2
3
OPENAI_API_KEY=替换为你的真实密钥
MODEL_NAME=替换为你可用的模型名称
OPENAI_BASE_URL=

不要把 .env 提交到 Git。若使用兼容服务,只有在服务商文档明确说明兼容该 SDK 接口时,才设置 OPENAI_BASE_URL。模型名也从环境变量读取,避免把某个具体模型写死在示例中。

编写最小审查器

新建 reviewer.py。代码使用 Responses API 的 instructionsinputoutput_text,但仍把返回文本当作不可信输入处理。提示词是软约束,真正的边界在 parse_report

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import json
import os
import sys
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
MODEL = os.environ["MODEL_NAME"]
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
base_url=os.environ.get("OPENAI_BASE_URL") or None,
)

INSTRUCTIONS = """
你是只读的 Python 代码审查助手。只返回 JSON 数组,不要 Markdown 围栏或解释。
每项必须包含 line(正整数)、severity(low/medium/high)、
category(correctness/security/maintainability/performance)、
summary(不超过80字)和 suggestion(不超过160字)。
只指出能从输入代码直接观察到的问题;没有问题时返回空数组。
不要执行代码,不要臆测缺失的上下文。
""".strip()
SEVERITIES = {"low", "medium", "high"}
CATEGORIES = {"correctness", "security", "maintainability", "performance"}


def parse_report(raw: str, line_count: int) -> list[dict]:
try:
report = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError("模型没有返回合法 JSON") from exc
if not isinstance(report, list):
raise ValueError("报告必须是数组")
checked = []
for item in report:
required = {"line", "severity", "category", "summary", "suggestion"}
if not isinstance(item, dict) or set(item) != required:
raise ValueError("报告字段不完整或包含未知字段")
if not isinstance(item["line"], int) or not 1 <= item["line"] <= line_count:
raise ValueError("line 不在代码行范围内")
if item["severity"] not in SEVERITIES or item["category"] not in CATEGORIES:
raise ValueError("severity 或 category 无效")
for key, limit in (("summary", 80), ("suggestion", 160)):
if not isinstance(item[key], str) or not item[key].strip():
raise ValueError(f"{key} 必须是非空字符串")
if len(item[key]) > limit:
raise ValueError(f"{key} 超过长度限制")
checked.append(item)
return sorted(checked, key=lambda x: (x["line"], x["severity"]))


def review(source: str) -> list[dict]:
if not source.strip():
raise ValueError("代码不能为空")
if len(source) > 12000:
raise ValueError("示例只接受不超过12000字符的代码")
response = client.responses.create(
model=MODEL,
instructions=INSTRUCTIONS,
input=source,
)
return parse_report(response.output_text, source.count("\n") + 1)


def main() -> None:
if len(sys.argv) != 2:
raise SystemExit("用法:python reviewer.py path/to/file.py")
source = Path(sys.argv[1]).read_text(encoding="utf-8")
report = review(source)
Path("review-report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"发现 {len(report)} 条建议,已保存到 review-report.json")


if __name__ == "__main__":
main()

运行:

1
python reviewer.py example.py

模型回复会因代码、模型和服务状态而变化,因此不能把某个固定的审查结果当作运行保证。可验证的部分是:输入为空或过长会在请求前失败;返回不是数组、字段多余、行号越界或枚举值非法时不会写入报告;只有通过校验的列表才会排序并保存。

用固定数据做离线测试

不必每次测试都消耗 API 配额。把 parse_report 作为纯函数单独测试,创建 test_reviewer.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import json
import unittest

from reviewer import parse_report


class ReportTest(unittest.TestCase):
def test_valid_report_is_sorted(self):
raw = json.dumps([
{"line": 3, "severity": "low", "category": "style",
"summary": "x", "suggestion": "y"}
])
# 这里故意会失败:category 不在允许集合中,证明校验边界生效。
with self.assertRaises(ValueError):
parse_report(raw, 3)

def test_line_must_exist(self):
raw = json.dumps([
{"line": 4, "severity": "high", "category": "security",
"summary": "x", "suggestion": "y"}
])
with self.assertRaises(ValueError):
parse_report(raw, 3)


if __name__ == "__main__":
unittest.main()

运行 python -m unittest -v test_reviewer.py,测试只覆盖确定性的解析逻辑。实际项目中还应补充合法报告排序、未知字段、空数组和超长文本等案例。注意,测试数据里的 category 必须使用程序允许的四个值;上面的第一个测试特意使用非法值,目的是演示拒绝路径,而不是模拟一次成功审查。

常见问题

为什么不让模型直接改代码? 审查和修改是不同风险等级的动作。只读报告容易人工复核;自动修改还需要补丁格式校验、应用前后测试和回滚机制,不能仅凭一段自然语言建议完成。

行号为什么需要程序检查? 模型可能因为理解偏差给出不存在的行号。越界行号会让审查者浪费时间,甚至误改其他代码,所以它应在程序边界被拒绝,而不是被默默修正。

为什么限制输入长度? 上限同时控制请求成本、上下文压力和敏感信息暴露范围。大型文件应先由确定性代码按模块分块,再分别审查并汇总,而不是无限增大一次请求。

这能代替人工代码审查吗? 不能。它适合发现重复性线索和生成检查清单,不保证理解完整业务约束。高风险安全问题、权限逻辑和生产变更仍需要人工与确定性工具复核。

小结

这个项目把一次 AI 功能组织成“读取代码—请求模型—解析 JSON—严格校验—排序保存—离线测试”的闭环。模型负责提出候选意见,Python 代码负责限制格式、范围和副作用;两者职责清晰,才有可能继续加入重试、日志、评估集或人工审批。综合项目不应把所有能力堆在一个函数里,而应优先保留可替换、可测试和可拒绝的边界。


技术分享自:时光笔记 (wxy.email) | 华为云开发者社区

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。