前面的系列已经覆盖了模型调用、提示词、结构化输出、错误处理、评估和安全边界。本篇在路线完成后做一个小型综合项目:输入一段 Python 代码,让模型给出有限格式的审查报告,程序负责校验、排序和保存结果。它不会执行被审查的代码,也不会自动修改文件,重点是把“模型提出建议”和“程序控制流程”清楚地分开。
先确定功能边界 这个工具只回答一个问题:代码中有哪些值得人工检查的问题。每条问题包含行号、严重程度、类别和建议。严重程度只能是 low、medium、high;类别只能是 correctness、security、maintainability 或 performance。模型不能运行代码、读取其他文件、安装依赖,也不能把建议直接当成补丁执行。
输入从命令行参数读取,输出保存为 JSON。这样做虽然简单,却保留了真实项目中的几个重要边界:外部文本必须限制长度,模型输出必须解析和校验,文件写入只能发生在校验成功之后。审查意见最终仍由开发者决定是否采纳。
准备环境和配置 在独立虚拟环境中安装官方 Python SDK 和环境变量加载库:
1 2 3 python -m venv .venv source .venv/bin/activatepython -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 jsonimport osimport sysfrom pathlib import Pathfrom dotenv import load_dotenvfrom openai import OpenAIload_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 jsonimport unittestfrom reviewer import parse_reportclass ReportTest (unittest.TestCase): def test_valid_report_is_sorted (self ): raw = json.dumps([ {"line" : 3 , "severity" : "low" , "category" : "style" , "summary" : "x" , "suggestion" : "y" } ]) 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 代码负责限制格式、范围和副作用;两者职责清晰,才有可能继续加入重试、日志、评估集或人工审批。综合项目不应把所有能力堆在一个函数里,而应优先保留可替换、可测试和可拒绝的边界。