【技术专栏】 AI开发
【内容摘要】 综合运用提示词、结构化输出、校验和离线测试,构建一个只读的 AI 代码审查命令行工具。
前面的系列已经覆盖了模型调用、提示词、结构化输出、错误处理、评估和安全边界。本篇在路线完成后做一个小型综合项目:输入一段 Python 代码,让模型给出有限格式的审查报告,程序负责校验、排序和保存结果。它不会执行被审查的代码,也不会自动修改文件,重点是把“模型提出建议”和“程序控制流程”清楚地分开。
先确定功能边界
这个工具只回答一个问题:代码中有哪些值得人工检查的问题。每条问题包含行号、严重程度、类别和建议。严重程度只能是 low、medium、high;类别只能是 correctness、security、maintainability 或 performance。模型不能运行代码、读取其他文件、安装依赖,也不能把建议直接当成补丁执行。
输入从命令行参数读取,输出保存为 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 的 instructions、input 和 output_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)