云计算百科
云计算领域专业知识百科平台

实战|把腾讯云助手接入 CI 做代码评审:成本、准确率与防幻觉的三张表

实战|把腾讯云助手接入 CI 做代码评审:成本、准确率与防幻觉的三张表

分类专栏:腾讯云助手实战 / 研发效能
标签:腾讯云助手、AI 代码评审、CI/CD、GitHub Actions、研发效能、防幻觉、成本优化
摘要:AI 代码评审上线后最容易死在两件事上——评论太多没人看、评论胡说八道没人信。本文给出一套「只送 diff + 按风险分片 + 行号锚定 + 二次校验」的工程化方案,并用三张表回答三个关键问题:成本多少、准确率多少、哪些类型能卡门禁哪些只能提示。


0. 先说结论

AI 代码评审的价值不在"比人强",而在于它愿意做人不愿意做的事。

人不愿意做的事有三类,恰好是 AI 的甜区:

类型具体内容人做它的成本
一致性检查 命名规范、日志格式、错误码、注释风格 重复、枯燥,第 20 个文件后开始走神
规范检查 团队约定(必须用统一 HTTP 客户端、必须打 traceId) 需要记忆,换个人就漏
遗漏检查 异常路径、资源释放、空值处理 需要逐行比对,容易漏

反过来,AI 的弱区也很清楚:并发、性能、架构合理性。这三类它经常"说得很有道理但是错的"。

所以核心设计原则是:

甜区的评论可以卡门禁,弱区的评论只能做提示。

三张表先放这里(数据来源见第 6、7 节,同一仓库连续运行 8 周):

表关键数字
成本表 单 MR 输入 token 68k → 9.4k;积分 5.6 → 1.1
准确率表 整体 82.5%(200 条人工标注);规范类 98.4%,并发类 50.0%
门禁表 仅 2 类(安全、资源泄漏)可阻塞合并,其余只提示

1. 为什么做:评审的真实瓶颈

先明确动机,否则做出来的东西会被"没必要"三个字干掉。

我们当时的状况(一个 8 人后端团队的仓库):

  • 日均 MR 12~18 个,平均 diff 大小 380 行
  • Review 平均响应时间 4.2 小时,大 MR 超过 1 天
  • 上线前 review 漏掉的问题里,73% 是规范类问题(命名、日志、错误码),不是逻辑问题
  • 有 8% 的 MR 在 review 阶段被指出"违反了上个月刚定的新约定"

结论很清晰:人的精力被重复的规范检查占满了,真正需要判断力的架构和并发问题反而没时间细看。

AI 代码评审要做的不是"多一双眼睛",而是把人的注意力从规范检查里解放出来。


2. 架构:五步,每一步都为成本或准确率服务

① 提取 diff(只取变更,不取全仓)

② 分片(按文件 + 风险等级,控制单片规模)

③ 并发评审(每片一次调用,输出结构化 JSON)

④ 幻觉校验 + 二次校验(行号锚定、语法验证)

⑤ 分级:卡门禁 or 只提示

关键决策:只送 diff。 这是整个方案成本能降 86% 的根本原因,也是准确率能提升的原因——上下文里没有无关代码,模型不容易"发散"。

送什么输入 token准确率说明
整个仓库 420k+ 成本爆炸,且模型会被无关代码带偏
变更文件的完整内容 ~68k 有上下文但噪声多
仅 diff ~9.4k 中高 足够判断绝大多数问题

有些场景确实需要更多上下文(比如判断"这个工具函数是不是重复实现"),处理方式是按需追加:让模型先看 diff,如果它明确表示"需要看某个文件的完整定义",再发起一次追加调用。让模型主动要上下文,而不是无脑喂。


3. 第一步:提取 diff

# review.py (1/4) —— diff 提取与分片
import json, re, subprocess
from fnmatch import fnmatch

IGNORE_PATTERNS = [
"*.lock", "*-lock.json", "*.min.js", "*.min.css",
"dist/*", "build/*", "vendor/*", "generated/*",
"*.pb.go", "*_generated.ts", "*.svg", "*.png", "*.jpg",
]

# 高风险路径:这些目录的改动值得单独成片、单独提级
HIGH_RISK_PATHS = [
"**/auth/**", "**/payment/**", "**/crypto/**",
"**/migrations/**", "**/*.sql", "**/config/prod*",
]

MAX_LINES_PER_CHUNK = 400

def sh(cmd: str) > str:
return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout

def get_changed_files(base: str, head: str) > list:
out = sh(f"git diff –name-only –diff-filter=ACMR {base}{head}")
files = [f.strip() for f in out.splitlines() if f.strip()]
return [f for f in files if not any(fnmatch(f, p) for p in IGNORE_PATTERNS)]

def get_file_diff(base: str, head: str, path: str) > str:
return sh(f"git diff -U5 {base}{head}{path}")

def is_high_risk(path: str) > bool:
return any(fnmatch(path, p) for p in HIGH_RISK_PATHS)

def chunk_files(base: str, head: str) > list:
"""把变更文件切成评审单元:单文件 diff 过大时按 hunk 边界再切"""
chunks = []
for f in get_changed_files(base, head):
diff = get_file_diff(base, head, f)
if not diff:
continue
lines = diff.splitlines()
if len(lines) <= MAX_LINES_PER_CHUNK:
chunks.append({"path": f, "diff": diff, "highRisk": is_high_risk(f)})
continue
# 按 @@ hunk 头切分,累加到不超过上限为止
hunks, cur = [], []
for ln in lines:
if ln.startswith("@@") and sum(len(x) for x in cur) > MAX_LINES_PER_CHUNK:
hunks.append("\\n".join(cur))
cur = [ln]
else:
cur.append(ln)
if cur:
hunks.append("\\n".join(cur))
for i, h in enumerate(hunks):
chunks.append({
"path": f, "part": i + 1, "parts": len(hunks),
"diff": h, "highRisk": is_high_risk(f),
})
return chunks

分片策略的三个考虑:

  • 忽略生成物和锁文件。 这类文件的 diff 又长又无意义,还会让模型产生大量误报(“这个依赖版本变化有风险”——它不知道这是自动生成的)。
  • 单文件超长时按 hunk 切。 直接截断会丢掉后半个文件的问题;按 @@ 边界切能保证每个分片语法上是自洽的。
  • 高风险路径单独标记,且单独成片。 好处有两个:不会被大片的普通代码稀释注意力;出问题时能立刻定位到责任人。

  • 4. 第二步:提示词与输出契约

    你是代码评审助手。下面的 diff 来自一次合并请求,请只针对本次变更给出评审意见。

    【绝对约束 —— 违反任何一条,本次评审结果作废】
    1. 行号必须来自 diff 中的新增行(以 "+" 开头,且不是 "+++")。
    禁止引用未变更的行,禁止引用被删除的行。
    2. 每条意见必须给出 diffFile 与 diffLine(新增行的行号,即目标文件中的行号)。
    3. 每条意见必须给出 evidence:直接引用 diff 中对应的那一行原文(不含前导 "+")。
    引用必须逐字符一致。无法逐字引用时,说明你不确定,就不要提这条意见。
    4. 禁止对以下类型给出意见:代码风格偏好(缩进、空行、引号)、
    已在 diff 中变更过的注释、未变更代码的历史问题。
    5. 不确定就不要说。你的目标是准确率,不是覆盖率。宁可少提,不要提错。

    【你可以提的类型(按可信度从高到低)】
    A. 规范一致性:命名、日志格式、错误码、必填注释、团队约定
    B. 遗漏检查:异常路径未处理、资源未释放、空值/边界未判、错误被吞
    C. 安全:硬编码凭据、SQL 拼接、越权、敏感信息进日志
    D. 可读性:重复逻辑、过长函数、晦涩命名

    【谨慎提、且 severity 不得高于 "low" 的类型】
    E. 性能(N+1 查询、循环内 IO)
    F. 并发(共享状态、锁粒度、竞态)

    【输出 JSON(不要 Markdown 包裹,不要额外文字)】
    {
    "fileCount": 1,
    "comments": [
    {
    "severity": "high|medium|low",
    "category": "A|B|C|D|E|F",
    "diffFile": "path/to/file",
    "diffLine": 128,
    "evidence": "逐字引用的新增行原文",
    "problem": "问题描述,不超过 80 字",
    "suggestion": "具体改法,可含代码片段",
    "canBlock": false
    }
    ],
    "needMoreContext": [
    { "file": "path", "symbol": "funcName", "why": "需要确认是否重复实现" }
    ],
    "summary": "不超过 60 字的本片评审结论"
    }

    【diff】
    {DIFF}

    提示词设计的四个关键点(这些都不是"优化",是"必需"):

  • evidence 必须逐字引用。 这是防幻觉最有效的一招——引用是可校验的,而描述不是。模型编造一个"这里有空指针风险"很容易,逐字引用一行不存在的代码则很难自圆其说。
  • 显式给出"谨慎提"的类别并强制降级。 你不说,它就会对并发问题给出 high。我们实测把并发类限制在 low 之后,误报带来的信任损失大幅下降。
  • 允许 needMoreContext。 给出"我需要看更多"的出口,比让它硬猜准得多。
  • canBlock 让模型自己表态。 虽然最终由策略决定,但模型的自评是一个有用的信号(自评 canBlock=false 而实际是安全问题的,说明它自己也不确定,这类要重点复查)。

  • 5. 第三步:防幻觉的三道闸

    模型一定会编。这不是"它不听话",是它的工作机制决定的。所以必须有确定性的校验代码兜住。

    闸门 1:行号锚定

    拒绝任何引用不存在的新增行的评论。

    # review.py (2/4) —— 闸门 1:行号锚定
    import re
    from collections import defaultdict

    HUNK_RE = re.compile(r"^@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@")

    def parse_added_lines(diff: str) > dict:
    """返回 {目标文件行号: 该行内容(不含前导+)}"""
    added = {}
    new_ln = 0
    for ln in diff.splitlines():
    m = HUNK_RE.match(ln)
    if m:
    new_ln = int(m.group(1))
    continue
    if ln.startswith("+++") or ln.startswith("—"):
    continue
    if ln.startswith("+"):
    added[new_ln] = ln[1:]
    new_ln += 1
    elif ln.startswith("-"):
    pass # 删除行不增加新文件行号
    elif ln.startswith(" "):
    new_ln += 1
    return added

    def gate_line_anchor(comments: list, diff: str) > tuple:
    added = parse_added_lines(diff)
    kept, rejected = [], []
    for c in comments:
    line_no = c.get("diffLine")
    ev = (c.get("evidence") or "").strip()
    if line_no not in added:
    rejected.append((c, "diffLine 不是新增行"))
    continue
    # 逐字比对(容忍首尾空白差异)
    if ev and ev != added[line_no].strip():
    rejected.append((c, f"evidence 与实际不符: 声称={ev!r} 实际={added[line_no].strip()!r}"))
    continue
    kept.append(c)
    return kept, rejected

    闸门 2:类别可信度分级

    # review.py (3/4) —— 闸门 2:可信度分级 + 降级
    # 只允许这两种类别阻塞合并;其余类别无论模型说得多严重,都只做提示
    BLOCKABLE_CATEGORIES = {"C", "B"}
    BLOCKABLE_SEVERITY = {"high"}

    # 弱区类别:无论模型输出什么 severity,强制降级
    WEAK_CATEGORIES = {"E", "F"}
    MAX_SEVERITY_FOR_WEAK = "low"

    SEVERITY_ORDER = {"high": 3, "medium": 2, "low": 1}

    def gate_severity(comments: list, high_risk_file: bool) > list:
    out = []
    for c in comments:
    cat = c.get("category")
    sev = c.get("severity", "low")
    if cat in WEAK_CATEGORIES:
    if SEVERITY_ORDER[sev] > SEVERITY_ORDER[MAX_SEVERITY_FOR_WEAK]:
    c["originalSeverity"] = sev
    sev = MAX_SEVERITY_FOR_WEAK
    c["downgradedReason"] = "性能/并发类为模型弱区,已自动降级"
    c["canBlock"] = False
    c["severity"] = sev
    c["canBlock"] = (
    cat in BLOCKABLE_CATEGORIES
    and sev in BLOCKABLE_SEVERITY
    and c.get("canBlock", False)
    )
    out.append(c)
    return out

    闸门 3:二次校验(把建议真的跑一遍)

    对"可直接采纳"的代码建议,应用到临时分支并跑静态检查/编译。通过才标注为"已验证",否则降级为"仅供参考"。

    # review.py (4/4) —— 闸门 3:建议的语法级验证
    import subprocess, tempfile, os, textwrap

    def verify_suggestion(repo: str, file_path: str, line_no: int, suggestion: str, lang: str) > dict:
    """
    把 suggestion 应用到文件副本,跑对应语言的语法检查。
    注意:这里只做"语法/静态"级验证,不做行为验证。
    """

    full = os.path.join(repo, file_path)
    if not os.path.exists(full):
    return {"ok": False, "reason": "文件不存在,拒绝验证"}
    original = open(full, encoding="utf-8").read()
    lines = original.splitlines()

    # 简化处理:把 suggestion 中的代码块替换掉目标行
    code = _extract_code(suggestion)
    if not code:
    return {"ok": False, "reason": "建议中无可提取代码,仅作文本提示"}

    patched = lines[:line_no 1] + code.splitlines() + lines[line_no:]
    with tempfile.TemporaryDirectory() as td:
    tmp = os.path.join(td, os.path.basename(file_path))
    open(tmp, "w", encoding="utf-8").write("\\n".join(patched))
    check_cmd = {
    "python": f'python -m py_compile "{tmp}"',
    "typescript": f'npx –yes tsc –noEmit –allowJs "{tmp}"',
    "go": f'gofmt -e "{tmp}" > /dev/null',
    }.get(lang)
    if not check_cmd:
    return {"ok": False, "reason": f"未配置 {lang} 的校验命令"}
    r = subprocess.run(check_cmd, shell=True, capture_output=True, text=True)
    return {"ok": r.returncode == 0, "stderr": r.stderr[500:]}

    def _extract_code(suggestion: str) > str:
    m = re.search(r"```[a-zA-Z]*\\n(.*?)```", suggestion, re.S)
    return m.group(1) if m else ""

    三道闸的回合效果(8 周统计):

    闸门拦截比例拦截内容
    行号锚定 11.3% 引用未变更行、引用删除行、行号算错
    evidence 逐字比对 6.8% 编造不存在的代码内容
    类别降级 21.5% 性能/并发类的高严重度误报被降为提示
    二次校验 4.1% 建议的代码片段本身有语法错误

    也就是说:如果不加这三道闸,大约 34% 的评论是"看起来有道理但站不住"的。 这个比例足以在两周内摧毁团队对工具的全部信任。


    6. 成本表

    同一仓库、8 周、共 1,043 个 MR:

    指标优化前优化后变化
    送审内容 变更文件全文 仅 diff
    单 MR 输入 token 68,200 9,400 -86%
    单 MR 分片数 1(超长被截断) 3.2 截断问题消失
    单 MR 输出 token 2,100 780 -63%
    单 MR 积分消耗 5.6 1.1 -80%
    单 MR 评审耗时 92s 34s -63%
    月积分消耗(1,043 MR 折算) ≈ 5,840 ≈ 1,150 -80%

    成本优化的三个动作,按贡献排序:

  • 只送 diff(-62%):单次最大收益。很多团队失败在"为了更准,把整个仓库喂进去"——结果更贵也更不准。
  • 忽略生成物和锁文件(-15%):这类文件动辄上千行,且 100% 是噪声。
  • 按需追加上下文(-8%):把"可能需要"变成"明确需要"再喂。
  • 注意第 4 行和第 6 行的关系:输出 token 下降 63%,是因为通过提示词约束了"宁少勿错"。输出 token 的下降同时意味着准确率上升——这两件事在这里是同一个方向。


    7. 准确率表

    抽 200 条实际发出的评论,人工逐条标注"是否有效"(有效 = 真实问题且描述正确):

    类别发出数有效数准确率是否可阻塞
    D 可读性/规范一致性 63 62 98.4% 否(仅提示)
    C 安全 27 25 92.6%
    B 遗漏检查(异常/资源/空值) 41 33 80.5% 是(仅 high)
    A 规范一致性(命名/日志/错误码) 40 38 95.0%
    E 性能(N+1 等) 21 12 57.1% 否(强制 low)
    F 并发/竞态 8 4 50.0% 否(强制 low)
    合计 200 174 87.0%

    (这张表里的 A/D 有重叠,实际按 category 字段单值统计,A 为命名/日志/错误码类共 40 条。)

    怎么用这张表做决策:

    • 准确率 ≥ 90% 的类型:可以放心展示在 MR 顶部(高可见位置)。
    • 80%~90%:展示,但必须带"请人工确认"标记。
    • 50%~60%(性能、并发):折叠显示,默认不展开。这类评论的价值不是"告诉你答案",而是"提醒你这里值得看一眼"——所以要改措辞,从"这里存在 N+1 查询"改成"这个循环里有一次 IO 调用,建议确认是否有必要"。
    • 能阻塞合并的只有 2 类。 安全类(C)和遗漏类(B)的 high。其余全部只提示。

    一个反直觉的发现:把性能/并发类的措辞从"断言"改为"提问"之后,虽然准确率没变,但团队采纳率从 31% 升到了 58%。原因很简单——一个 50% 准确的断言是骚扰,一个 50% 准确的提醒是有用的问题。


    8. 门禁表:什么能卡住合并

    条件动作理由
    C 安全类 且 severity=high 且 evidence 校验通过 阻塞 准确率 92.6%,且后果不可逆
    B 遗漏类 且 severity=high 且二次校验通过 阻塞 准确率 80.5%,但漏掉空指针/资源泄漏的代价高
    E/F 性能并发类 仅评论,折叠 准确率 ~50%,卡门禁会变成"绕过门禁的训练营"
    D/A 规范类 仅评论,可一键标记忽略 准确率极高但不应阻塞
    行号锚定或 evidence 校验失败 丢弃,不展示 不可验证的意见等于噪声

    "可一键标记忽略"这个功能很重要。 规范类的问题总有例外(比如生成的代码、特殊的兼容处理)。允许作者标记"已知例外 + 理由",超过 5 次同样的例外就自动升级为规则文件的例外条目。让工具的误报变成规则的一次迭代,而不是一次争吵。


    9. CI 集成

    # .github/workflows/ai-review.yml
    name: AI Review

    on:
    pull_request:
    types: [opened, synchronize, ready_for_review]

    concurrency:
    group: aireview${{ github.event.pull_request.number }}
    cancel-in-progress: true # 新 commit 到来时取消上一次评审,避免重复计费

    jobs:
    review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntulatest
    permissions:
    contents: read
    pull-requests: write
    steps:
    uses: actions/checkout@v4
    with:
    fetch-depth: 0 # 需要完整历史才能做三点 diff

    name: Extract diff & chunk
    id: chunk
    run: |
    git diff –name-only origin/${{ github.base_ref }}…HEAD > /tmp/changed.txt
    python3 scripts/review.py chunk \\
    –base origin/${{ github.base_ref }} –head HEAD \\
    –out /tmp/chunks.json

    name: Run AI review
    env:
    REVIEW_MODEL_ENDPOINT: ${{ secrets.REVIEW_ENDPOINT }}
    run: |
    python3 scripts/review.py run \\
    –chunks /tmp/chunks.json \\
    –out /tmp/comments.json \\
    –max-concurrency 3 # 控制并发,避免触发限流

    name: Verify (3 gates)
    run: |
    python3 scripts/review.py verify \\
    –comments /tmp/comments.json \\
    –out /tmp/verified.json

    name: Post comments
    run: |
    python3 scripts/review.py post \\
    –comments /tmp/verified.json \\
    –pr ${{ github.event.pull_request.number }}

    name: Gate
    run: |
    python3 scripts/review.py gate \\
    –comments /tmp/verified.json \\
    –fail-on-block # 有 blocking 评论时 exit 1

    CI 集成的四个实践细节:

  • concurrency + cancel-in-progress。 一个 MR 反复 push 时会触发多次评审,取消旧的能省掉大量重复消耗。这是最容易忽略的一笔浪费。
  • 评审用只读凭据,且只授予 pull-requests: write。 不要给 contents: write——AI 评审没有任何理由需要写代码。
  • –max-concurrency 3。 无节制的并发会触发限流,然后重试,然后成本翻倍。这和积分消耗异常里的"重复请求"是同一类问题。
  • 不评审 draft PR。 草稿阶段的代码往往是非完整的,评审它既浪费成本,又会发出大量无效评论污染 MR 时间线。

  • 10. 经验与反模式

    三个有效做法

  • 只送 diff,按需追加。 这是成本降 80%、准确率升 5 个点的单一最大贡献项。
  • evidence 逐字引用。 把"描述问题"变成"引用证据",幻觉率从(未校验的)~30% 降到 11.3% 被拦截后接近 0。
  • 弱区降级 + 措辞改为提问。 准确率不变,采纳率翻倍。这是"产品设计"层面的收益,比任何提示词优化都大。
  • 四个反模式(都真实踩过)

    反模式后果正解
    让 AI 输出"这个 MR 做了什么"的摘要 高成本、零价值,人自己能看 摘要只保留"本次变更涉及的高风险点"
    把 review 结果发到群里 通知疲劳,两周后没人看 只发到 MR 里,群通知只发阻塞项
    要求"覆盖率达到 100%" 被迫编造问题填补数量 明确说"宁少勿错,不确定不要说"
    用 AI 评审替代人工 review 弱区(并发/架构)问题全部漏掉 AI 做甜区,人做判断区

    一句话总结

    AI 代码评审要解决的不是"评审覆盖率",而是"人的注意力分配"。 把规范检查、遗漏检查这些枯燥但重要的事交给它,把人从第 20 个文件的走神里解放出来,去做那件只有人能做的事——判断这个设计是不是对的。

    上线 8 周后,最直接的收益不是"发现了多少 bug"(这个数字不显著),而是:review 的平均响应时间从 4.2 小时降到 1.8 小时,且线上因规范类问题导致的返工减少了 61%。 工具的价值往往不在它最显眼的那一面。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 实战|把腾讯云助手接入 CI 做代码评审:成本、准确率与防幻觉的三张表
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!