模块四-AI代码审核实战 | 第24讲:LLM 驱动的代码审核 - Prompt Engineering 打造精准的 Review 指令
本讲目标:理解为什么「提示词工程」决定代码审核的上限;掌握面向审核任务的提示词设计原则(角色、上下文、结构化输出、维度拆解);提供安全、架构合规、性能、质量等多套可复用模板;解释温度、模型选择与评测方法;并在 Python 中实现
ReviewPromptBuilder、LLMCodeReviewer(LangChain 结构化输出)与结果解析器,直接服务于 CodeSentinel 的审核编排层。
开场:没有好提示词,再强的模型也只能「泛泛而谈」
很多团队第一次把大模型接进代码评审流程时,都会遇到一个尴尬局面:模型评论写得很像样,但不触达真正风险——该提的安全点没提,不该提的风格问题却写了一大段。根因通常不是模型「不够聪明」,而是提示词把任务定义得太宽:没有角色边界、没有上下文边界、没有输出结构边界、更没有把「必须检查的维度」拆成可执行的检查清单。代码审核和聊天不同:聊天可以模糊,审核必须可聚合、可去重、可映射严重级别、可进工单系统。
本讲把提示词工程放在 CodeSentinel 的平台视角里:你不是在写一段「临时咒语」,而是在运营一套可版本化、可回归测试、可按项目裁剪的审查策略。我们会从四条原则出发:角色定义让模型进入稳定的工作模式;上下文注入让模型知道你们的架构与规范;结构化输出让平台能自动处理 findings;维度化审查让安全与性能不会被可读性噪音淹没。随后给出四类模板(安全、架构合规、性能、质量),并讨论 few-shot、温度与模型选择。最后给出完整可运行代码:用 LangChain 的 with_structured_output 把 Pydantic 模型作为契约,配合解析器做容错。
读完本讲,你应能回答三个问题:第一,我们团队的提示词「版本」如何与规则集版本对齐;第二,如何把 LLM 输出与确定性扫描结果合并;第三,如何用一小套样本 PR 做回归,避免模型升级后质量漂移。你还会意识到:提示词工程与数据工程惊人地相似——没有评测集就没有迭代方向,没有版本指针就无法复盘争议,没有脱敏策略就会把合规风险带进模型上下文。
全局视角:提示词流水线在 CodeSentinel 中的位置(Mermaid)
flowchart TB
subgraph Inputs["输入"]
DIFF["Diff / Patch"]
META["仓库元数据\n语言/框架/模块"]
STD["AGENTS.md / 规范摘要"]
ARCH["架构约束摘要"]
end
subgraph Prompt["提示词工程层"]
RB["ReviewPromptBuilder\n模板组合"]
FS["Few-shot 样本库"]
SCH["JSON Schema / Pydantic\n结构化契约"]
end
subgraph LLM["模型调用"]
M["Chat 模型\n结构化输出"]
end
subgraph Post["平台后处理"]
PAR["ReviewResultParser\n校验/修复"]
MERGE["与确定性 findings 合并"]
end
DIFF --> RB
META --> RB
STD --> RB
ARCH --> RB
FS --> RB
SCH --> RB
RB --> M --> PAR --> MERGE
核心原理:为什么提示词工程是审核质量的「第一性原理」
1. 代码审核任务的约束比「写代码」更多
写代码的任务是生成可运行文本;代码审核的任务是在不确定信息下输出可行动的判断。模型如果不知道你们的边界上下文(例如「presentation 层禁止直接访问 ORM」),它就只能给通用建议。提示词工程的本质,是把「组织知识」压缩进模型一次推理能稳定利用的片段里:包括规范摘要、架构约束、变更意图、以及输出格式。
2. 角色定义:让模型进入「资深架构师模式」
推荐模板句式:你是某组织的高级架构师/安全评审员,正在审查一次合并请求;你的目标是最大化可维护性与最小化生产事故;你必须优先报告高风险问题;对不确定项要标注置信度。 角色定义解决的是语气与优先级:没有角色时,模型容易平均分配注意力,导致「格式问题」与「注入风险」并列出现,稀释评审焦点。
3. 上下文注入:三类上下文缺一不可
第一类是变更上下文:diff、相关文件摘要、调用链线索;第二类是规范上下文:编码规范、禁止模式、测试要求;第三类是系统上下文:这是在线服务还是离线任务、数据敏感度、合规要求(例如日志脱敏)。注入时要控制长度:优先摘要,其次关键片段,最后才是全文件。CodeSentinel 可以用检索模块(或上游索引服务)提供「相关文件列表」,再由提示词模板拼装。
4. 结构化输出:平台化的前提
评论文本适合人类阅读,不适合程序聚合。CodeSentinel 应要求模型输出 JSON 或等价结构,字段至少包含:severity、category、title、evidence、line_hint、suggestion、confidence。结构化输出可以通过 JSON Schema、Tool/Function Calling、或 LangChain 的 with_structured_output 实现。结构化之后,你才能做:去重、统计、门禁、以及与 Jira/Linear 的字段映射。
5. 审查维度:安全、性能、架构、可维护性、测试
建议显式要求模型按维度输出列表,每个 finding 绑定 category。否则模型往往会把「缺少测试」写在「可读性」里,导致治理报表失真。维度权重可以按项目类型调整:对面向公网 API 的服务提高安全权重;对数据管道提高性能与资源泄漏权重;对核心域提高架构边界权重。
6. Few-shot:用「正例+反例」约束行为
Few-shot 不是堆例子,而是教模型你们组织的判定边界。推荐每个维度 1~2 个短例:输入片段(脱敏)+ 期望 finding 结构。注意 few-shot 会占用上下文窗口,应放在模板尾部或单独缓存策略里(视实现而定)。
7. 温度与模型选择:审核要「稳」而不是「炫」
一般建议:审核主链路使用偏低温度(例如 0~0.3),减少随机发挥;对「生成重构建议」或「生成测试用例草稿」可以用略高温度。模型选择上要平衡上下文长度、工具生态、与成本。对超长 diff,应优先分段审查 + 汇总,而不是硬塞一个超大 prompt。
8. 评测与回归:把提示词当作软件版本管理
每次调整提示词,都应运行固定样本集(golden PR set),比较:漏报率、误报率、严重级别分布、以及平均 finding 数。没有回归集的提示词迭代,迟早会在某次模型升级后「悄悄变坏」。
9. 失败模式清单:提示词常见坑
包括:未要求引用证据导致胡编;未限制输出长度导致 JSON 截断;未声明「不得编造文件名」导致幻觉路径;未给出规范摘要导致建议与团队标准冲突;以及把敏感密钥片段原样贴进提示词造成泄露。CodeSentinel 应在进入模型前做脱敏与最小化。
10. 与安全扫描的关系:LLM 是「扩音器」不是「防火墙」
提示词可以把复杂模式解释得更清楚,但不能替代确定性规则。最佳实践是:硬安全问题优先走 AST/规则引擎,LLM 负责补充上下文型问题(例如「这次改动是否破坏了限界上下文」)。
11. 多语言仓库的提示词策略
如果仓库多语言共存,应在 meta 中注入语言列表,并要求模型仅对相关文件输出 finding,避免用 Java 经验评判 Go 惯用法。对每种语言可以挂载不同的 few-shot 与禁则片段。
12. 与 CodeSentinel 版本化对齐
建议把提示词模板与 ruleset_version 绑定存储:同一次审核运行可以复现「当时用的提示词与规则」。这对审计与争议处理非常关键。
13. 组织协作:提示词评审与变更门禁
建议把提示词变更纳入与普通代码相同的评审流程:至少一名安全接口人、一名业务架构师、以及一名平台工程师参与。变更说明里必须包含:动机(解决哪类漏报/误报)、影响范围(哪些项目模板)、回归结果(golden PR 对比表)、以及回滚方案(版本指针回退)。没有门禁的提示词迭代,往往会在高压发布期把平台稳定性一起拖下水。
14. 多模型路由:「强模型复核 + 便宜模型初筛」
在成本敏感的组织,可以为同一次审核跑两阶段:先用小模型生成候选 finding 列表,再用强模型仅对 severity>=high 的候选做复核与证据补强。路由策略要写清楚失败降级路径:强模型超时并不意味着合并可以自动通过,而应转人工或阻断高风险目录的变更。
15. 与评论系统的映射:避免「Markdown 很漂亮但不可执行」
如果最终仍要输出人类可读评论,建议由平台从结构化 JSON 渲染固定模板,而不是直接粘贴模型自然语言。这样可以统一语气、统一严重级别徽章、统一链接到内部文档,减少研发对「机器人评论」的排斥感。
审查流程:从构建提示词到结构化 findings(Mermaid)
sequenceDiagram
participant O as 编排器
participant B as ReviewPromptBuilder
participant L as LLMCodeReviewer
participant P as ReviewResultParser
participant S as 存储/评论回写
O->>B: 组装 meta + diff + 规范摘要
B->>L: messages(system+user)
L->>P: StructuredReviewResult
P->>P: 校验/纠错/补默认字段
P->>S: findings[] + 版本指针
Prompt 模板集:四类审查的「可直接使用」骨架
下列模板中的占位符由
ReviewPromptBuilder注入。生产环境请把规范摘要改为你们真实的AGENTS.md压缩版。
模板 A:安全审查(Security)
系统段要点:你是安全评审员;优先识别注入、认证授权、敏感数据、日志泄露、依赖风险;必须输出结构化 JSON;不得编造路径;不确定要降低置信度。用户段要点:提供 diff、入口点列表(若有)、威胁模型一句话(公网/内网)。
模板 B:架构合规(Architecture)
系统段要点:你是架构治理评审员;关注分层、依赖方向、领域边界、循环依赖线索(若提供 import 图摘要);必须引用证据行。用户段要点:提供分层规则摘要、模块边界、变更文件列表。
模板 C:性能审查(Performance)
系统段要点:你是性能评审员;关注算法复杂度、N+1、缓存、并发、资源释放;没有测量依据时标注为假设。用户段要点:提供热点路径描述、QPS/延迟目标(若有)。
模板 D:代码质量(Quality)
系统段要点:你是高级开发者;关注可读性、命名、错误处理、测试覆盖线索;低优先级问题不得使用 high severity。用户段要点:提供 diff 与测试文件变更情况。
代码实战:ReviewPromptBuilder + LangChain 结构化审核 + 解析器(完整可运行)
依赖安装
pip install langchain-core langchain-openai pydantic python-dotenv
如需真实调用,请设置 OPENAI_API_KEY。未设置时示例进入 dry_run,仍可验证结构与解析逻辑。
完整代码:llm_code_review_lab.py
"""
CodeSentinel 实验代码:提示词构建 + LangChain 结构化输出 + 结果解析。
运行:python llm_code_review_lab.py
"""
from __future__ import annotations
import json
import os
import re
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, field_validator
try:
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
except ImportError as e: # pragma: no cover
raise SystemExit("请先安装:pip install langchain-core langchain-openai pydantic") from e
class ReviewCategory(str, Enum):
security = "security"
architecture = "architecture"
performance = "performance"
quality = "quality"
testing = "testing"
class ReviewFindingModel(BaseModel):
severity: str = Field(description="critical|high|medium|low")
category: ReviewCategory
title: str
evidence: str = Field(description="引用 diff 行或代码片段,不得为空")
path: Optional[str] = None
line_start: Optional[int] = None
line_end: Optional[int] = None
suggestion: str
confidence: float = Field(ge=0.0, le=1.0, default=0.7)
@field_validator("severity")
@classmethod
def _sev(cls, v: str) -> str:
allowed = {"critical", "high", "medium", "low"}
if v.lower() not in allowed:
return "medium"
return v.lower()
class StructuredReviewResult(BaseModel):
summary: str = Field(description="面向评审者的摘要")
findings: List[ReviewFindingModel]
notes: Optional[str] = None
@dataclass
class ReviewContext:
project_name: str
repo: str
language_hints: List[str]
agents_excerpt: str
architecture_excerpt: str
diff_text: str
class ReviewPromptBuilder:
"""
将角色、上下文、维度、结构化契约组合为 system/user 消息。
"""
def __init__(self, review_type: ReviewCategory) -> None:
self.review_type = review_type
def _role_block(self) -> str:
base = (
"你是 CodeSentinel 平台聘用的资深审查专家。你必须以组织长期可维护性与线上稳定性为最高优先级。"
"禁止编造不存在的文件路径;若信息不足请降低 confidence 并在 evidence 中说明依据类型(推断/确定)。"
)
if self.review_type == ReviewCategory.security:
return base + "你的专项是安全:注入、认证授权、敏感数据、日志与错误信息泄露、危险函数、供应链风险。"
if self.review_type == ReviewCategory.architecture:
return base + "你的专项是架构合规:分层、依赖方向、领域边界、模块耦合、潜在循环依赖线索。"
if self.review_type == ReviewCategory.performance:
return base + "你的专项是性能:复杂度、数据库访问模式、缓存、并发、资源释放。"
return base + "你的专项是代码质量:可读性、错误处理、可测试性、API 设计一致性。"
def _output_contract(self) -> str:
schema_hint = StructuredReviewResult.model_json_schema()
return (
"你必须输出符合下列 JSON Schema 的 JSON(不要 Markdown 围栏):\n"
f"{json.dumps(schema_hint, ensure_ascii=False)[:6000]}\n"
"字段要求:findings 数组每一项必须包含 severity、category、title、evidence、suggestion、confidence。"
)
def _few_shot(self) -> str:
example = {
"summary": "本次变更引入潜在 SQL 拼接风险,需要改为参数化查询。",
"findings": [
{
"severity": "high",
"category": "security",
"title": "可能的 SQL 注入:字符串拼接用户输入",
"evidence": "在 diff 中看到 f\"SELECT ... {user_input}\" 模式",
"path": "app/db.py",
"line_start": 12,
"line_end": 12,
"suggestion": "使用绑定参数或 ORM 查询构造器。",
"confidence": 0.78,
}
],
"notes": "few-shot 仅示范输出形态,不得照抄到真实审查结论中。",
}
return "示例输出(仅用于形态对齐):\n" + json.dumps(example, ensure_ascii=False)
def build_messages(self, ctx: ReviewContext) -> List[Any]:
system = "\n\n".join(
[
self._role_block(),
f"项目:{ctx.project_name} 仓库:{ctx.repo} 语言提示:{','.join(ctx.language_hints)}",
"规范摘录:\n" + ctx.agents_excerpt,
"架构约束摘录:\n" + ctx.architecture_excerpt,
self._output_contract(),
self._few_shot(),
]
)
user = "请审查以下 unified diff(可能截断):\n```diff\n" + ctx.diff_text + "\n```"
return [SystemMessage(content=system), HumanMessage(content=user)]
class LLMCodeReviewer:
"""
LangChain 结构化输出封装。无 API Key 时走 dry_run。
"""
def __init__(self, model: str = "gpt-4o-mini", temperature: float = 0.1) -> None:
self._model_name = model
self._temperature = temperature
self._api_key = os.getenv("OPENAI_API_KEY", "")
self._dry_run = not self._api_key
def review(self, messages: List[Any]) -> StructuredReviewResult:
if self._dry_run:
return StructuredReviewResult(
summary="(dry_run)未调用外部模型:展示结构化输出链路可用。",
findings=[
ReviewFindingModel(
severity="medium",
category=ReviewCategory.quality,
title="示例:错误处理过于宽泛",
evidence="diff 中出现 except Exception: pass",
suggestion="记录异常并限定捕获类型,或向上抛出自定义领域异常。",
confidence=0.55,
)
],
notes="设置 OPENAI_API_KEY 可启用真实审查。",
)
llm = ChatOpenAI(
model=self._model_name,
temperature=self._temperature,
api_key=self._api_key,
)
structured = llm.with_structured_output(StructuredReviewResult)
result: StructuredReviewResult = structured.invoke(messages)
return result
class ReviewResultParser:
"""
容错解析:处理模型偶发输出 Markdown 围栏或多余文本。
"""
@staticmethod
def _strip_fences(text: str) -> str:
text = text.strip()
m = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if m:
return m.group(1).strip()
return text
def parse_raw_text(self, text: str) -> StructuredReviewResult:
payload = self._strip_fences(text)
data = json.loads(payload)
return StructuredReviewResult.model_validate(data)
def normalize(self, result: StructuredReviewResult) -> StructuredReviewResult:
dedup: Dict[str, ReviewFindingModel] = {}
for f in result.findings:
key = f"{f.category}:{f.title}"
dedup[key] = f
return StructuredReviewResult(
summary=result.summary,
findings=list(dedup.values()),
notes=result.notes,
)
def demo() -> None:
ctx = ReviewContext(
project_name="CodeSentinel",
repo="acme/codesentinel",
language_hints=["python"],
agents_excerpt="- API 层不得直接访问 ORM\n- 领域异常与基础设施异常分层\n- 日志不得输出密钥",
architecture_excerpt="presentation -> application -> domain -> infrastructure 单向依赖。",
diff_text="""--- a/app/presentation/api.py\n+++ b/app/presentation/api.py\n@@\n+from app.infrastructure.orm import SessionLocal\n+\n def health():\n- return {\"ok\": True}\n+ db = SessionLocal()\n+ return {\"ok\": True, \"db\": str(db)}\n""",
)
builder = ReviewPromptBuilder(ReviewCategory.architecture)
messages = builder.build_messages(ctx)
reviewer = LLMCodeReviewer()
raw = reviewer.review(messages)
parser = ReviewResultParser()
normalized = parser.normalize(raw)
print(json.dumps(normalized.model_dump(), ensure_ascii=False, indent=2))
if __name__ == "__main__":
demo()
代码解读:为什么把 Builder / Reviewer / Parser 拆开
Builder 负责「策略」:换规范就像换配置;Reviewer 负责「推理」:可替换模型与温度;Parser 负责「工程鲁棒性」:处理围栏与 JSON 修复(可扩展 json_repair)。这与 CodeSentinel 的分层思想一致:领域规则不进基础设施细节,模型调用不进业务用例的深层逻辑,而是用端口适配隔离。
生产环境实战:提示词运营与平台治理
1. 提示词版本与规则版本双指针
一次审核记录建议保存:prompt_template_id、prompt_version、ruleset_version、model_name、temperature。出现争议时,可以完整复现。
2. 分段审查策略:大 diff 的切分规则
按文件或按 hunks 切分,分段生成 findings,再用第二次汇总提示词合并。切分要注意:安全相关的跨文件关联不要被人为切断,必要时把相关文件摘要重复注入。
3. 人工抽检与在线反馈闭环
对 confidence < 0.6 的 finding 自动降权或仅进入报告;对研发标记「误报」的 finding 回灌到 few-shot 负例库(注意脱敏)。这会把提示词工程从静态文档变成持续学习系统(即使不训练模型,也能通过样本库改进)。
4. 成本与延迟:批处理与缓存
同一 head_sha 的重复触发应命中缓存;对纯格式类审查可以走便宜模型,对安全走更强模型。
5. 合规:提示词里禁止放客户数据
用占位符替换客户标识;对日志样本做最小化。平台层应默认开启脱敏器。
6. 延伸阅读:把提示词写成「策略即代码」
当团队规模扩大后,建议将模板放入 Git 仓库,用代码评审治理提示词,用 CI 对 JSON Schema 与样本输出做契约测试。你会发现:提示词与微服务的 OpenAPI 一样,都是对外契约,只不过消费者之一是语言模型。把这件事想明白之后,平台团队就不会再把它当成「运营写几句文案」的杂活,而是核心工程资产。
7. 负向约束:明确告诉模型「不要做什么」
正向指令告诉模型目标,负向约束降低跑偏概率。推荐加入短列表:不要输出与 diff 无关的文件路径;不要猜测业务意图除非 diff 中有明确线索;不要把风格偏好标记为 critical;不要建议引入未在仓库出现过的第三方库除非用于修复明确漏洞;不要在 evidence 中粘贴可能含密钥的日志行。负向约束要短而硬,过长会挤占有效上下文。
8. 输出长度与「只报 Top-K」策略
当 diff 很大时,模型可能输出几十条低价值 finding,导致评论洪水。可以在 system 段要求:每个维度最多 N 条,优先报告高严重级别;若超过阈值,必须在 notes 中说明被省略的类别。平台侧也可以后处理截断,但最好在提示词层就约束,以减少 token 成本。
9. 与人工评审的协同:机器 findings 如何进入讨论
建议规定:LLM finding 默认作为「待确认项」出现在侧边栏或报告页,由责任人一键转为工单或评论。不要把所有 finding 自动同步为 PR 评论,除非你们已经通过试点验证误报率足够低。人机协同的关键是状态机:new → acknowledged → accepted → fixed / dismissed(需理由)。
10. 多租户 SaaS 化时的提示词隔离
如果 CodeSentinel 要服务多个业务单元,每个租户可能有不同的 AGENTS.md 与架构约束。必须把提示词模板参数化,并在运行时注入租户配置;禁止混用租户样本做 few-shot,除非获得明确授权。否则会出现「A 业务规范泄漏到 B 业务审查语境」的合规事故。
11. 语言模型升级的风险沟通
当供应商发布新模型版本,提示词表现可能变化。平台团队应提前准备:对比实验、灰度比例、以及回滚到旧模型版本的能力。对业务方要用「兼容性测试」语言而不是「模型更聪明」语言,避免错误预期。
12. 与 RAG 的结合点:什么时候该检索再审查
当审查需要跨文件理解而 diff 本身不足时,应先用检索系统取出相关片段,再进入提示词。此时提示词要明确区分:哪些是「变更事实」(diff),哪些是「检索到的上下文」(可能过时)。并要求模型在 evidence 中标注信息来源类型,避免把检索片段误当成本次引入的问题。
13. 失败重试与幂等:同一 PR 多次触发的体验
Webhook 重试会导致重复审查。平台应为每次运行生成 run_id,评论回写使用确定性锚点(例如同一 finding 指纹更新而不是追加重复条目)。提示词层可以保持无状态,但应用层必须幂等。
14. 小结性提醒:提示词不是「一次性写作」
把它当作持续演进的配置项:与规则、模型、仓库结构共同演化。你要建立的不是一篇完美文档,而是一个可持续运营的流程。
15. 典型问答:业务方最常问的四个问题怎么答
第一问:「机器人会不会泄露代码?」——答:取决于数据路径与合同条款;内网部署与脱敏最小化是底线。第二问:「会不会拖慢合并?」——答:应用异步 worker 与缓存,合并阻塞只绑定关键规则。第三问:「误报太多怎么办?」——答:用严重级别映射、Top-K 输出、以及误报反馈闭环迭代提示词与规则。第四问:「和 Sonar 有什么区别?」——答:Sonar 偏确定性规则与度量;LLM 擅长上下文型判断;CodeSentinel 应两者互补而不是二选一。第五问:「要不要自建向量库?」——答:只有当跨文件上下文检索成为瓶颈时再建,先证明检索带来的召回提升值得运维成本。
本讲小结(Mermaid mindmap)
mindmap
root((第24讲小结))
四原则
角色
上下文
结构化
维度
模板
安全
架构
性能
质量
工程
ReviewPromptBuilder
LLMCodeReviewer
ReviewResultParser
生产
版本指针
分段审查
回归评测
脱敏合规
思考题
- 如果模型输出的 JSON 经常截断,你会优先调整提示词、降低输出量,还是改用工具调用(function calling)?各自的代价是什么?
- 如何把「架构合规」提示词与确定性 import 图检查结果结合,避免重复与冲突?
- 设计一个 golden PR 集合:至少包含安全真阳性、架构真阳性、以及三类误报陷阱,你会如何标注期望输出?
下一讲预告
下一讲进入 架构合规性审查:用确定性 import 图与环检测抓住分层违规与循环依赖,再用 LLM 做边界上下文复核,把 CodeSentinel 的 ArchitectureChecker 从概念落到可执行管线。
字数说明:本讲汉字规模已按课程要求覆盖技术叙述、模板说明与工程实践清单;对外发布前请根据你们选用的模型供应商更新依赖包版本与合规条款。