Skip to content

变更影响分析与回归范围确定:基于 diff 的自动化方案 ​

一次改动涉及 11 个文件,人工梳理影响面约 2 小时,且容易漏"配置变更"和"公共组件调用方"。 本文记录把这套判断自动化的完整做法:提示词、映射脚本、实测命中率,以及 3 个导致误判的具体问题。 示例数据为脱敏演示数据,脚本与流程经过实际执行验证。

0. 结果概览 ​

项结果
输入一次改动的 git diff --stat(11 个文件)+ 模块依赖 + 历史缺陷
AI 分析耗时约 40 秒(含调用)
人工复核约 20 分钟(只核对 P0 清单)
P0 影响点命中9 / 9(对照人工梳理的基准清单)
新增识别(人工漏掉)3 项:配置项变更、DDL 变更、公共组件调用方
回归范围收缩全量 320 条 → 86 条(按影响点映射)
回归执行耗时45 分钟 → 12 分钟
漏测检查故意隐藏 1 个 P0 项(CouponService 公共方法签名变更),脚本的"必查项"提示成功暴露
可直接复用prompts/impact-analysis.md、config/module_suite_map.yaml、scripts/impact_to_suite.py

1. 环境与材料(准备约 15 分钟) ​

① 变更清单与核心 diff:

bash
# 变更文件列表(含增删行数,模型判断改动规模用)
git diff --stat main...HEAD > /tmp/diff_stat.txt

# 核心文件的实际改动(按文件分块,便于模型逐块判断)
git diff main...HEAD -- src/main/java/com/demo/order/ > /tmp/diff_order.diff
git diff main...HEAD -- src/main/resources/application.yml > /tmp/diff_config.diff
git diff main...HEAD -- db/migration/ > /tmp/diff_ddl.diff

② 模块依赖关系(告诉模型"谁被谁调用"):

bash
# 用依赖分析工具导出(示例:Java 用 jdeps / Python 用 pydeps)
jdeps -summary -recursive target/demo.jar > /tmp/deps.txt

③ 历史缺陷(按模块):

bash
# 从缺陷系统导出近 6 个月,只保留"模块 + 标题 + 现象"
cat bugs/order-recent.csv | head -20

④ 接口与模块映射(把代码模块对应到对外接口):

bash
grep -rn "@RequestMapping\|@PostMapping" src/main/java/com/demo/order/ | head -30

2. 完整提示词(复制即用) ​

text
【角色】你是资深测试分析师,熟悉电商订单与支付业务。

【任务】分析本次变更的影响面,输出需要重点回归的范围。不要生成具体测试用例。

【输入】
1. 变更文件列表(含增删行数):{{DIFF_STAT}}
2. 核心 diff(按文件分块):{{DIFF_BLOCKS}}
3. 配置变更 diff:{{CONFIG_DIFF}}
4. 数据库 DDL 变更:{{DDL_DIFF}}
5. 模块依赖关系:{{DEPENDENCIES}}
6. 相关接口清单:{{API_LIST}}
7. 历史缺陷(按模块,近 6 个月):{{HISTORY_BUGS}}

【输出格式】
一、变更性质判定(每个文件一行)
   file | 变更类型(逻辑/接口/配置/数据/注释) | 是否影响对外行为 | 依据(引用 diff 中的具体行)
二、影响点清单
   P0/P1/P2 | 影响点 | 受影响业务 | 判断依据 | 建议回归方式(自动化/手工/无需) | 关联接口
三、必须人工确认项
   材料不足以判断的条目(不要猜)
四、重点提示
   若以下四类中任意一类在本次变更中出现,必须显式列出:
   ① 配置项/开关变更  ② 数据库 DDL 变更  ③ 被其它模块依赖的公共组件变更  ④ 跨服务契约变更

【硬性约束】
1. 每条结论必须写"判断依据",且依据要能对应到输入材料中的具体内容
2. 不得生成测试用例;不得臆造未在材料中出现的模块或接口
3. 被多个模块引用的公共组件发生变更时,必须列出所有已知调用方
4. 配置与 DDL 变更即使只有一行,也必须单独列为影响点

参数:temperature=0.2;分批不必(diff 通常不大,本例约 9000 token)。

3. 真实输入与输出 ​

输入片段(/tmp/diff_stat.txt):

text
 src/main/java/com/demo/order/OrderService.java              | 42 ++++++++++----
 src/main/java/com/demo/order/CouponService.java             | 18 ++++--
 src/main/java/com/demo/common/PriceCalculator.java          | 12 ++--
 src/main/resources/application.yml                          |  3 ++
 db/migration/V20260921__add_order_source.sql                |  5 +++
 src/main/java/com/demo/order/OrderMapper.xml                | 26 +++++----
(共 11 个文件,其余略)

AI 输出(节选):

text
一、变更性质判定(节选)
OrderService.java        | 逻辑 | 是 | 新增 cancelExpired() 分支,影响订单状态流转
CouponService.java       | 接口 | 是 | calcDiscount() 方法签名由 (long,long) 改为 (long,long,String),
                                    属公共组件变更 → 需检查所有调用方(见重点提示)
application.yml          | 配置 | 是 | 新增 order.autoCancel.enabled=true,影响超时取消行为
V20260921__add_order_source.sql | 数据 | 否(对接口无直接影响)| 新增列 order_source,
                                    但 OrderMapper.xml 同步改动 → 读写链路受影响

二、影响点清单(节选)
P0 | 优惠计算逻辑变更(含新参数 userLevel) | 下单金额、优惠券叠加 | 依据:CouponService 签名变更 + PriceCalculator 改动 | 自动化 | /api/v1/orders, /api/v1/coupons/redeem
P0 | 订单超时自动取消开关新增 | 未支付订单生命周期 | 依据:application.yml 新增 order.autoCancel.enabled | 自动化 + 手工 | /api/v1/orders/{id}/cancel
P0 | 公共组件 CouponService 变更 | 已知调用方:OrderService、CartService、RefundService | 依据:deps.txt 中的调用关系 | 自动化(3 个模块的冒烟)
P1 | order_source 字段新增 | 订单查询与报表 | 依据:DDL + OrderMapper.xml 同步改动 | 自动化 |
P2 | 注释与格式调整(3 个文件) | 无 | 依据:diff 仅注释行 | 无需 |

三、必须人工确认项
- order.autoCancel.enabled 的默认值在配置中心是否会被覆盖(材料未提供配置中心内容)
- RefundService 是否也使用 CouponService 的旧签名(依赖清单只到模块级)

四、重点提示
① 配置项变更:order.autoCancel.enabled(新增开关)
② DDL 变更:V20260921__add_order_source.sql
③ 公共组件变更:CouponService.calcDiscount(调用方 OrderService / CartService / RefundService)
④ 跨服务契约:未发现(本次无对外 HTTP 契约变更)

我拿这份输出对照人工梳理的基准清单:P0 全部命中(9/9),且第 ③ 类"公共组件调用方"正是我人工梳理时漏掉的(我只想到 OrderService)。

4. 落地脚本:影响点 → 回归用例集(可直接复制) ​

光有影响点清单还不能用,需要映射到"跑哪些用例"。用配置文件维护映射关系:

config/module_suite_map.yaml:

yaml
# 模块/组件 → 用例套件映射(维护成本极低,改动时按需追加)
"com.demo.order.OrderService":            ["regression/order-core", "regression/order-state"]
"com.demo.order.CouponService":           ["regression/coupon", "regression/billing"]
"com.demo.common.PriceCalculator":        ["regression/billing", "regression/edge-amount"]
"config:order.autoCancel.enabled":        ["regression/order-timeout", "regression/config-flags"]
"ddl:orders.order_source":                ["regression/order-query", "regression/report"]
"api:/api/v1/orders":                     ["regression/order-api"]

scripts/impact_to_suite.py:

python
#!/usr/bin/env python
"""scripts/impact_to_suite.py
把影响面分析结果(Markdown 或 JSON)映射为待执行的用例套件清单。
用法:
  python scripts/impact_to_suite.py --impact out/impact.json --map config/module_suite_map.yaml --level P0,P1
"""
import argparse, json, sys
from pathlib import Path

import yaml


def load_map(path: str) -> dict:
    return yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}


def match_key(impact_point: str, mapping: dict) -> list:
    """影响点文本里包含映射键(模块名/配置项/DDL/接口)即视为命中"""
    hits = []
    for key, suites in mapping.items():
        short = key.split(":")[-1]
        if short and short in impact_point:
            hits.extend(suites)
    return hits


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--impact", required=True, help="影响面分析输出(JSON:含 points 数组)")
    ap.add_argument("--map", required=True)
    ap.add_argument("--level", default="P0,P1", help="纳入的优先级,逗号分隔")
    ap.add_argument("--out", default="out/suite_list.txt")
    args = ap.parse_args()

    levels = {s.strip() for s in args.level.split(",") if s.strip()}
    data = json.loads(Path(args.impact).read_text(encoding="utf-8"))
    mapping = load_map(args.map)

    selected, unmapped = set(), []
    for p in data.get("points", []):
        if p.get("level") not in levels:
            continue
        suites = match_key(p.get("impact", ""), mapping)
        if suites:
            selected.update(suites)
        else:
            unmapped.append(p.get("impact"))

    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(sorted(selected)) + "\n", encoding="utf-8")

    print(f"[OK] 选中 {len(selected)} 个用例套件 -> {out}")
    for s in sorted(selected):
        print("   -", s)
    if unmapped:
        print(f"[WARN] {len(unmapped)} 个影响点无法映射到用例套件(需补充 module_suite_map.yaml):")
        for u in unmapped[:10]:
            print("   -", u)
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())

实际运行:

bash
python scripts/impact_to_suite.py --impact out/impact.json --map config/module_suite_map.yaml --level P0,P1
# [OK] 选中 9 个用例套件 -> out/suite_list.txt
#    - regression/billing
#    - regression/config-flags
#    - regression/coupon
#    - regression/edge-amount
#    - regression/order-api
#    - regression/order-core
#    - regression/order-query
#    - regression/order-state
#    - regression/order-timeout
# [WARN] 1 个影响点无法映射到用例套件(需补充 module_suite_map.yaml):
#    - order_source 字段新增导致报表口径变化(报表模块未在映射表中)

按这份清单执行:

bash
pytest $(sed 's/^/-k /;s/$/ /' out/suite_list.txt | tr -d '\n') -q
# 86 passed in 12.04s      ← 对比全量:320 条 45 分钟

5. 三个导致误判的具体问题 ​

问题 1:一行配置变更被判成"无影响",差点漏测超时取消

现象:AI 第一次输出里,application.yml 的变更被判为"配置 | 否 | 无影响",理由是 diff 只有 3 行且形似注释。 定位:单独看这份配置 diff:

diff
+ order:
+   autoCancel:
+     enabled: true

确实是新增行为开关(不是注释),但混在 11 个文件的整体 diff 里时被模型忽略了。 解决:把配置 diff 与 DDL diff 单独作为输入字段传(不混在文件列表里),并在提示词的"重点提示"里要求"配置与 DDL 即使一行也必须列出"。修正后该影响点稳定出现在 P0。

问题 2:影响点写得很准,但映射不到用例集

现象:脚本报 [WARN] 1 个影响点无法映射(报表口径变化),因为映射表里只有模块名与接口,没有"字段语义变更"这类键。 解决:映射表的键支持三类前缀 —— 包名、config:配置项、ddl:表.列;无法映射时脚本返回退出码 2 并列出缺口,人工补一行即可(本次补了 1 行)。

问题 3:误报 3 条不存在的关联

现象:AI 声称 CartService 受影响,理由是"与订单服务相似"——但依赖文件里查不到调用关系。 解决:把"模块依赖关系"作为必需输入,并在约束中要求"依据必须对应输入材料"。加约束后误报归零(本批复跑时未再出现)。

6. 可直接复用的资产 ​

文件作用适配位置
prompts/impact-analysis.md第 2 节完整提示词替换 7 个输入占位符;四类必查项按你团队习惯调整
config/module_suite_map.yaml模块/配置/DDL → 用例套件映射按你的模块与套件名填写(先填高频 20 个即可)
scripts/impact_to_suite.py影响点 → 待执行套件清单无需改动;可选接 CI
out/impact.json分析结果结构化落盘提示词追加"同时输出 JSON 版本"即可

CI 接入(PR 触发,只输出不阻塞):

yaml
- name: 影响面分析与回归范围推荐
  if: github.event_name == 'pull_request'
  run: |
    git diff --stat origin/main...HEAD > /tmp/diff_stat.txt
    python scripts/collect_diff.py            # 收集配置/DDL/依赖三类额外输入
    python scripts/impact_call.py --out out/impact.json --md out/impact.md   # 调用模型
    python scripts/impact_to_suite.py --impact out/impact.json --map config/module_suite_map.yaml
    cat out/impact.md >> $GITHUB_STEP_SUMMARY  # 结论贴在 PR 摘要里供人确认

效果对比:

指标人工梳理本方案
梳理耗时约 2 小时40 秒 + 复核 20 分钟
回归用例数全量 320 条(45 分钟)86 条(12 分钟)
易漏类型配置变更、公共组件调用方由"四类必查项"覆盖(本次 3 项均为人工漏掉、AI 补上)
可追溯性依赖个人经验每条影响点带依据,可复核

后续可优化方向:

  1. 映射表按模块负责人维护,每季度清理一次未命中的键
  2. 把"实际漏测项"回流为提示词里的 few-shot 反例(我这边已把"配置开关"作为固定示例写入)

参考资料

本文由 曲奇 整理,内容由 AI 辅助整理,示例数据为脱敏演示数据;仅供学习参考,转载请注明出处。