《游戏服务端架构·平台化与架构治理》11.3 AI 辅助测试与压测

AI 生成三百条用例只留下十一条,问题不在生成能力,而在准入与选择。本文把 AI 放在用例生成、回归选择、失败归因三个位置,给出带来源与版本的人工评审准入、可解释的回归优先级与预算选择,并说明压测场景生成为什么不能替代容量结论,以及幻觉、泄漏与不可复现的防线。

三百条用例,最后留下十一条

某项目把一次线上事故的描述丢给模型,要求“生成完整的回归用例”。模型很快返回约 300 条。评审用了两天,结果是:约 220 条与现有用例重复,40 条断言了不存在或已改名的 API,30 条依赖无法构造的测试数据,真正值得入库的只有 11 条。以上数字是示例值,但比例并不夸张。

这次尝试不能算失败——11 条有效用例里,有 3 条覆盖了人工没想到的边界。问题在于流程:生成没有成本,评审有成本;如果不设准入,AI 只是把工作量从“写用例”转移到了“读用例”。

AI 在测试里的三个位置

AI 辅助测试不是一个工具,而是三个不同的位置。每个位置的产出物、评审方式和失败模式都不同。

位置AI 的产出人的门禁主要失败模式
用例生成候选用例、边界条件、测试数据逐条评审后入库幻觉 API、重复、不可构造
回归选择本次变更应跑的子集与理由抽查选择结果漏选高风险路径、过度选择
失败归因失败日志的聚类与假设排序确认根因并记录把相关当因果、忽略环境差异
压测场景流量模型、脚本骨架、异常场景审核参数与上限场景失真、数据泄漏、不可复现

一个常见的越界是把“失败归因”当成“自动修复”。归因可以做,修复必须由人确认,因为测试失败可能暴露的是产品语义问题,不是代码 bug。

用例候选的准入

生成用例的第一原则是:没有来源和版本的用例,不进入测试库。 来源可以是事故编号、需求文档、接口契约或线上日志片段;版本是生成时使用的模型、提示词模板和输入快照。没有这些,三个月后没人能判断这条用例为什么存在、是否还有效。

// file: internal/aitestload/cases.go
// tier: L1
// verify: go test ./internal/aitestload/
package aitestload

import (
	"errors"
	"fmt"
	"sort"
	"strings"
)

var (
	ErrNoSource      = errors.New("aitestload: candidate has no source")
	ErrNoVersion     = errors.New("aitestload: candidate has no version")
	ErrNoReviewer    = errors.New("aitestload: candidate needs human review")
	ErrBadRisk       = errors.New("aitestload: risk must be 1..5")
	ErrMissingID     = errors.New("aitestload: candidate needs an id")
	ErrNoSteps       = errors.New("aitestload: candidate needs at least one step")
	ErrNoRequirement = errors.New("aitestload: candidate needs a requirement")
)

// Candidate is a generated test case before it enters the suite. Source and
// Version make the origin reproducible; ApprovedBy is the human who accepted
// it. A generated case without all three is a draft, not a test.
type Candidate struct {
	ID          string
	Source      string
	Requirement string
	Steps       []string
	Risk        int
	GeneratedBy string
	Version     string
	ApprovedBy  string
}

// Admit validates a candidate and records the reviewer. It performs no model
// calls: generation is a separate, replaceable step, while admission is the
// control point that keeps unreviewed output out of CI.
func Admit(c Candidate, reviewer string) (Candidate, error) {
	if strings.TrimSpace(c.ID) == "" {
		return Candidate{}, ErrMissingID
	}
	if strings.TrimSpace(c.Source) == "" {
		return Candidate{}, ErrNoSource
	}
	if strings.TrimSpace(c.Requirement) == "" {
		return Candidate{}, ErrNoRequirement
	}
	if len(c.Steps) == 0 {
		return Candidate{}, ErrNoSteps
	}
	if strings.TrimSpace(c.Version) == "" {
		return Candidate{}, ErrNoVersion
	}
	if c.Risk < 1 || c.Risk > 5 {
		return Candidate{}, ErrBadRisk
	}
	if strings.TrimSpace(reviewer) == "" {
		return Candidate{}, ErrNoReviewer
	}

	c.ApprovedBy = reviewer
	return c, nil
}

// Fingerprint is the dedupe key for candidates. Steps are sorted so the same
// scenario expressed in a different order does not enter twice. It is not a
// semantic comparison; reviewers still decide whether two cases overlap.
func Fingerprint(c Candidate) string {
	steps := make([]string, len(c.Steps))
	for i, s := range c.Steps {
		steps[i] = strings.TrimSpace(s)
	}
	sort.Strings(steps)

	req := strings.TrimSpace(c.Requirement)
	return fmt.Sprintf("%s::%s", req, strings.Join(steps, "|"))
}

这段代码把“生成”和“准入”分开:

  • Admit 不调用模型,只做校验和记录。模型可以换,准入规则不变。
  • Source、Version、ApprovedBy 三个字段缺一不可。缺少任何一个,用例都只是草稿。
  • Risk 限定在 1 到 5。风险等级决定评审人级别和回归优先级,不能让生成器随便填。
  • Fingerprint 对步骤排序后做去重。它不懂语义,只解决“同一场景换了个说法”的明显重复;语义重叠仍由人判断。

评审时建议按风险分级:高风险用例(涉及资产、匹配、存档、支付)必须由对应模块负责人看;低风险用例可以由测试负责人批量评审。批量评审不是不评审,而是把注意力放在断言和数据构造上。

回归选择:可解释优先

全量回归越来越慢,按变更选择子集是刚需。AI 可以参与,但选择逻辑必须可解释:为什么选这条、为什么跳过那条,评审人要能复核。

下面的选择器没有使用模型,而是用三个可解释的信号:是否触及变更、上次是否失败、风险等级。它故意保持简单,因为回归选择一旦变成黑盒,漏选就无法复盘。

// file: internal/aitestload/regression.go
// tier: L1
// verify: go test ./internal/aitestload/
package aitestload

import "sort"

// Case is an existing test case in the regression suite. Touches lists the
// packages or modules the case exercises; LastFail marks a case that failed
// in the last run.
type Case struct {
	ID       string
	Touches  []string
	LastFail bool
	Risk     int
	Duration int // seconds
}

// Selection is the regression plan. Skipped carries a reason per case so a
// reviewer can challenge the filter instead of trusting a black box.
type Selection struct {
	Chosen  []string
	Skipped map[string]string
	Seconds int
}

// Selector chooses cases under a time budget. Scores are intentionally
// simple and explainable: a changed dependency dominates, a recent failure
// is next, and risk breaks ties. No model output is involved in the decision.
type Selector struct {
	BudgetSeconds int
}

// Select returns a deterministic plan. Cases with no changed dependency and
// no recent failure are skipped as no_signal; the rest are ordered by score,
// then by shortest duration, then by ID.
func (s Selector) Select(changed []string, cases []Case) Selection {
	out := Selection{Skipped: make(map[string]string)}
	changedSet := make(map[string]bool, len(changed))
	for _, c := range changed {
		changedSet[c] = true
	}

	type scored struct {
		c     Case
		score int
	}
	var candidates []scored
	for _, c := range cases {
		score := 0
		if hitsChanged(c, changedSet) {
			score += 100
		}
		if c.LastFail {
			score += 50
		}
		if score == 0 {
			out.Skipped[c.ID] = "no_signal"
			continue
		}
		score += c.Risk * 2
		candidates = append(candidates, scored{c: c, score: score})
	}

	sort.SliceStable(candidates, func(i, j int) bool {
		if candidates[i].score != candidates[j].score {
			return candidates[i].score > candidates[j].score
		}
		if candidates[i].c.Duration != candidates[j].c.Duration {
			return candidates[i].c.Duration < candidates[j].c.Duration
		}
		return candidates[i].c.ID < candidates[j].c.ID
	})

	for _, cand := range candidates {
		if out.Seconds+cand.c.Duration > s.BudgetSeconds {
			out.Skipped[cand.c.ID] = "over_budget"
			continue
		}
		out.Chosen = append(out.Chosen, cand.c.ID)
		out.Seconds += cand.c.Duration
	}
	return out
}

func hitsChanged(c Case, changed map[string]bool) bool {
	for _, t := range c.Touches {
		if changed[t] {
			return true
		}
	}
	return false
}

选择器的行为:

  • 触及变更的权重最高。Touches 由维护用例的人填写,不靠模型猜。
  • 上次失败其次。失败过的用例更可能再次暴露问题,但不能压过本次变更。
  • 风险只做加分,不做一票否决。高风险但完全不相关的用例不应该挤占预算。
  • 预算是硬约束。按分数排序后贪心选择,超预算的用例记录 over_budget,而不是静默丢弃。
  • 结果确定。同分时先选耗时短的,再按 ID 排序;同样的输入永远得到同样的计划。

它的局限也要写清楚:Touches 可能过期,选择器看不到调用链和覆盖率,无法发现“没被任何用例标记但实际受影响”的模块。因此高风险发布仍然需要保留一个固定的冒烟集和核心链路集,不能完全依赖变更选择。

发布类型回归策略是否可只用 AI 选择
日常小改变更选择 + 冒烟集可以,需抽查
活动配置活动专项 + 支付链路不可以,活动脚本需人工确认
核心系统重构全量回归 + 核心链路压测不可以
紧急热修冒烟集 + 受影响模块可以,但必须记录跳过项

压测:AI 只生成场景,不产出容量结论

压测场景可以借助 AI 起草:根据玩法描述生成流量阶段、思考时间、并发比例、异常注入点。但容量结论必须来自真实执行,且要附命令、环境、版本和原始结果。

一个可审核的压测场景至少包含:

字段含义审核要点
目标环境压测环境、数据规模、网络条件是否与生产同量级
流量模型阶段、时长、并发、RPS、思考时间峰值是否超过安全上限
用户分布新老玩家、地区、设备比例是否与目标活动一致
异常注入断线、超时、依赖降级是否有止损开关
观测指标延迟分位、错误率、资源水位是否有明确通过标准
退出条件错误率或延迟超限即停止是否自动生效

AI 生成的脚本必须经过三道检查:参数上限、数据脱敏、可复现性。特别是用线上日志生成流量模型时,日志里可能包含玩家 ID、设备信息、聊天内容,必须先脱敏再进入生成流程。

压测报告里不要写“AI 预测可支撑十万在线”。预测不是结论。可以说“在某某环境、某某版本、某某流量模型下,真实执行到多少 RPS 时错误率超过阈值”,并把原始数据附上。没有这些,数字只是营销。

三类风险

风险表现防线
幻觉断言不存在的 API、编造配置项、引用不存在的文档编译或运行验证;来源必须可点开
数据泄漏把线上日志、玩家信息、内部地址带进提示词或生成物脱敏、最小化、禁止外发;生成物入库前扫描
不可复现同一提示词两次生成不同结果,无法解释用例来源固定模型版本与参数,保存输入快照和生成时间

幻觉最危险的形态不是明显错误,而是“看起来合理但语义反了”的断言。例如把“断线后不扣体力”写成“断线后扣体力”,编译能过、测试能跑,但验证的是错误行为。这类问题只能靠人读断言,不能靠自动校验。

不可复现则要求把 AI 产物当成构建制品管理:有版本、有哈希、有来源、有评审记录。聊天窗口里的输出不算制品,复制到文件里才算。

来源与版本:让每个产物可追溯

建议为每类 AI 产物定义最小元数据:

产物必填元数据存放位置
测试用例来源、模型版本、提示词版本、生成时间、评审人用例文件头或测试管理库
回归计划变更集、选择器版本、预算、跳过原因CI 构建记录
压测场景目标环境、流量模型版本、脱敏记录、审核人压测仓库
失败归因失败日志哈希、聚类版本、假设列表事故记录

这些字段看起来繁琐,但它们决定了半年后还能不能回答“这条用例为什么在这里”和“这次压测的数字能不能比较”。

检查表

  • 生成用例是否有来源、模型版本和提示词版本?
  • 是否逐条人工评审后才入库,而不是批量导入?
  • 高风险用例是否由模块负责人评审?
  • 回归选择是否可解释,跳过项是否记录原因?
  • 高风险发布是否保留固定冒烟集和核心链路集?
  • 压测场景是否设置 RPS 上限、止损开关和退出条件?
  • 线上日志进入生成流程前是否脱敏?
  • 压测结论是否附环境、版本、命令和原始结果?
  • AI 产物是否作为带版本的制品管理,而不是聊天记录?

小结

AI 在测试与压测里的正确位置是“扩大候选集、加快整理”,不是“替人做结论”。生成能力越强,准入、去重、溯源和人工评审越重要。把这三件事做成流程,11 条有效用例就是净收益;跳过它们,300 条用例只是把评审成本推迟到上线之后。下一章回到架构治理本身:如何把这些能力纳入决策记录、风险台账与技术路线图。

阅读导航:上一节:11.2 智能客服与内容审核 · 下一节:12.1 决策记录、风险台账与路线图 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查