目录:
之前写过一篇用 AI 把 VOC 变成自动化流水线,讲的是整条流水线的架构——采集、分析、路由、执行。反馈不错,但也有读者指出一个关键问题:中间的部分(分类、路由)其实相对好做,真正难的是一头一尾。
“头"是用户问题理解——用户说"页面打不开”,你能不能自动搞清楚是哪个页面、什么场景、能不能复现、根因是什么?
“尾"是自动验证——修完 bug 之后,你能不能自动生成测试用例来证明"确实修好了,而且没有搞坏别的东西”?
这两个环节恰好是 AI Native 流程区别于传统自动化的核心:传统自动化靠规则,处理的是确定性问题;AI Native 靠理解和推理,处理的是模糊性问题。这篇文章就把这一头一尾拆开讲透。
一、为什么"一头一尾"最难
先看一条真实的用户反馈:
“你们最新版的导出功能是不是改了?我之前导出的报表里有客户联系方式的,现在导出来没有了,这个很影响我的工作。”
一个有经验的工程师看到这条反馈,脑子里会自动做这些事:
- 定位功能:“导出功能”——是哪个模块的导出?报表导出?客户列表导出?
- 还原场景:“之前有,现在没有”——是最近哪个版本改的?是字段被删了还是权限变了?
- 评估影响:“客户联系方式”——这是敏感数据,可能涉及隐私策略变更
- 制定验证标准:修完之后怎么确认?“导出的 CSV 中包含联系方式列” 就够了吗?要不要跑不同角色的权限组合?
这四步中,第 1-3 步就是"头"——把模糊描述转化为精确的问题定义;第 4 步就是"尾"——把问题定义转化为可执行的验证用例。
传统流水线在这两步上的处理方式基本是"甩给人":前面甩给技术支持做人工分类,后面甩给 QA 写测试用例。AI Native 的做法是把这两步也自动化——不是用规则,而是用理解。
二、头:用户问题的深度理解
2.1 从"表面文本"到"结构化问题"
用户反馈的最大挑战是:用户不会用工程师的语言描述问题。 他们说的是症状,不是原因;是感受,不是事实。
一个 AI Native 的问题理解系统需要做四层转化:
用户原话 → "导出的报表里没有客户联系方式了"
↓
症状提取 → 数据丢失 / 字段缺失
↓
上下文关联 → 最近版本变更 + 导出功能 + 联系方式字段
↓
工程化定义 → 报表导出模块的 CSV 生成逻辑在 v3.2 后
不再包含 contact_phone / contact_email 字段
关键在于第三层和第四层。简单的 NLP 分类(“这是一个 bug”)只完成了第一层。真正有用的理解必须关联上下文——代码变更历史、配置变更、权限策略——才能从症状推导出可能的根因。
2.2 实现:多轮推理 + 工具调用
一个设计良好的问题理解 Agent 不是"一次性分析",而是像一个 L2 技术支持工程师一样,主动调查:
from anthropic import Anthropic
client = Anthropic()
INVESTIGATOR_SYSTEM = """你是一个高级技术支持工程师。你的任务是将用户反馈转化为精确的工程问题定义。
你可以使用以下工具来调查问题:
1. search_changelog(keyword) - 搜索最近的版本变更记录
2. search_code(query) - 搜索代码库中的相关代码
3. query_config(module) - 查询模块的当前配置
4. get_error_logs(user_id, timerange) - 获取用户相关的错误日志
5. check_permissions(user_id, feature) - 检查用户的功能权限
每一步都要说明你的推理过程。最终输出一个结构化的问题定义。"""
TOOLS = [
{
"name": "search_changelog",
"description": "搜索最近的版本变更日志,找出可能相关的代码改动",
"input_schema": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "搜索关键词"},
"since_version": {"type": "string", "description": "起始版本号"}
},
"required": ["keyword"]
}
},
{
"name": "search_code",
"description": "搜索代码库中与问题相关的代码片段",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索查询"}
},
"required": ["query"]
}
},
{
"name": "get_error_logs",
"description": "获取指定用户在指定时间范围内的错误日志",
"input_schema": {
"type": "object",
"properties": {
"user_id": {"type": "string"},
"hours": {"type": "integer", "description": "最近N小时"}
},
"required": ["user_id", "hours"]
}
}
]
def investigate_issue(user_feedback: str, user_id: str = None) -> dict:
"""
对用户反馈进行多轮推理调查,输出结构化问题定义。
Agent 会自主决定调用哪些工具来获取上下文。
"""
messages = [
{
"role": "user",
"content": (
f"用户反馈:{user_feedback}\n"
f"用户ID:{user_id or '未知'}\n\n"
"请调查这个问题,给出精确的工程问题定义。"
)
}
]
# Agent loop:让 LLM 自主决定调查步骤
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2000,
system=INVESTIGATOR_SYSTEM,
tools=TOOLS,
messages=messages,
)
# 如果 LLM 不再调用工具,说明调查完成
if response.stop_reason == "end_turn":
return extract_issue_definition(response.content)
# 执行工具调用,把结果喂回去
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
# generated by claude code
2.3 案例:Agent 的调查过程
以开头那条反馈为例,一个训练有素的 Agent 的调查过程是这样的:
第 1 步:search_changelog("导出" OR "export" OR "联系方式" OR "contact")
→ 发现 v3.2 的 changelog 中有一条:
"feat: 导出报表默认不再包含联系方式字段,需在导出设置中手动勾选(隐私合规)"
第 2 步:search_code("export report contact field")
→ 找到 ReportExporter.java 中的变更:
contactFields 从 defaultIncluded 改为 optIn
第 3 步:check_permissions(user_id, "export.include_contact")
→ 用户的导出设置中,include_contact_info = false(新版本默认值)
结论:这不是 Bug,是有意的产品变更(隐私合规)。
但用户未被通知这个变化,导致体验断裂。
最终输出的结构化问题定义:
{
"issue_type": "expected_behavior_change",
"root_cause": "v3.2 隐私合规改动将联系方式导出改为 opt-in,但未通知存量用户",
"affected_module": "report_export",
"affected_versions": ["v3.2+"],
"related_changes": ["commit abc123: privacy compliance for export"],
"suggested_fix": "为存量用户发送变更通知;在导出页面增加字段选择的引导提示",
"severity": "medium",
"is_bug": false,
"requires_code_change": true,
"verification_hints": [
"导出设置页面应展示联系方式字段的勾选入口",
"勾选后导出的 CSV 应包含 contact_phone 和 contact_email 列",
"未勾选时导出的 CSV 不应包含联系方式(隐私合规要求)"
]
}
2.4 问题理解的三个最佳实践
实践一:让 Agent 带着工具去调查,而不是靠一次 prompt 猜答案。
最常见的错误是把用户反馈直接扔给 LLM,让它"猜"根因。这种做法准确率不会超过 40%。正确的做法是给 Agent 配备工具——changelog 搜索、代码搜索、日志查询、配置检查——让它像真人工程师一样主动调查。
实践二:区分"症状"和"问题",最终输出必须是工程可执行的。
“导出没有联系方式"是症状。“ReportExporter.java 中 contactFields 的默认值在 v3.2 被改为 optIn,需要增加存量用户迁移逻辑"才是问题。你的 Agent 输出的 issue definition 必须精确到模块、文件、甚至行号,后续的修复和验证才能自动化。
实践三:把"验证线索"作为问题定义的一部分输出。
注意上面 JSON 中的 verification_hints 字段。问题理解阶段就应该产出验证的方向——因为此时 Agent 对问题的理解最完整。这些线索会直接传递给"尾"端的测试生成模块,形成闭环。
三、尾:自动生成测试用例验证修复
3.1 为什么自动验证是 AI Native 的必选项
传统流程中,验证靠 QA 手工测试。问题在于:
- 时间差:Bug 修完到 QA 验证,中间可能隔几小时甚至几天
- 信息损耗:QA 拿到的往往只有一个 Jira ticket 的标题,缺少复现步骤和边界条件
- 回归盲区:只测了修复点,没覆盖可能被影响的关联功能
AI Native 的做法是:在修复完成的瞬间,自动生成验证用例并执行。这需要解决两个问题——生成什么用例、怎么验证。
3.2 从问题定义到测试用例
测试用例的生成不是凭空创造,而是基于三个输入:
问题定义(来自"头"的输出)
+
代码变更(git diff 的内容)
+
已有测试(项目中相关的测试文件)
↓
生成的测试用例
import subprocess
def generate_test_cases(
issue_definition: dict,
repo_path: str,
commit_sha: str,
) -> str:
"""
基于问题定义和代码变更,自动生成验证测试用例。
"""
# 1. 获取本次修复的代码变更
diff = subprocess.run(
["git", "diff", f"{commit_sha}~1..{commit_sha}"],
cwd=repo_path, capture_output=True, text=True,
).stdout
# 2. 获取相关的已有测试文件,作为风格参考
existing_tests = find_related_tests(
repo_path,
issue_definition["affected_module"],
)
# 3. 让 LLM 生成测试用例
prompt = f"""基于以下信息,生成验证测试用例。
## 问题定义
{json.dumps(issue_definition, ensure_ascii=False, indent=2)}
## 代码变更(git diff)
{diff}
## 已有相关测试(参考风格和约定)
{existing_tests}
## 生成要求
1. **正向验证**:确认修复后的行为符合预期
2. **回归验证**:确认修复没有破坏已有功能
3. **边界条件**:覆盖问题定义中 verification_hints 提到的所有场景
4. 测试代码必须可直接运行,遵循项目已有的测试框架和命名约定
5. 每个测试方法要有清晰的 docstring 说明测试意图
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4000,
system="你是一个资深测试工程师,擅长根据 bug 修复的上下文生成高质量的自动化测试用例。",
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
# generated by claude code
3.3 案例:从修复到验证的完整链路
继续上面导出功能的例子。假设开发者提交了修复(在导出页面增加联系方式字段的引导提示),Agent 会自动生成以下测试用例:
import pytest
from app.export import ReportExporter
from app.models import User, ExportSettings
class TestExportContactFieldsFix:
"""验证 v3.2 导出联系方式字段的引导提示修复"""
def test_export_settings_shows_contact_field_option(self, client, login_user):
"""正向验证:导出设置页面应展示联系方式字段的勾选入口"""
resp = client.get("/api/export/settings")
fields = resp.json()["available_fields"]
contact_fields = [f for f in fields if f["category"] == "contact"]
assert len(contact_fields) > 0, "导出设置中应包含联系方式字段选项"
assert any(f["key"] == "contact_phone" for f in contact_fields)
assert any(f["key"] == "contact_email" for f in contact_fields)
def test_export_with_contact_fields_enabled(self, client, login_user):
"""正向验证:勾选后导出的 CSV 应包含联系方式列"""
# 先启用联系方式导出
client.put("/api/export/settings", json={
"include_fields": ["contact_phone", "contact_email"]
})
resp = client.post("/api/export/report", json={"format": "csv"})
csv_content = resp.text
header = csv_content.split("\n")[0]
assert "contact_phone" in header
assert "contact_email" in header
def test_export_without_contact_fields_default(self, client, login_user):
"""回归验证:默认不勾选时,CSV 不应包含联系方式(隐私合规)"""
# 使用默认设置(不包含联系方式)
resp = client.post("/api/export/report", json={"format": "csv"})
csv_content = resp.text
header = csv_content.split("\n")[0]
assert "contact_phone" not in header
assert "contact_email" not in header
def test_existing_user_migration_prompt(self, client, legacy_user):
"""边界条件:存量用户首次访问导出页面时应看到引导提示"""
resp = client.get("/api/export/settings")
assert resp.json().get("show_migration_guide") is True
def test_new_user_no_migration_prompt(self, client, new_user):
"""边界条件:新用户不应看到迁移引导"""
resp = client.get("/api/export/settings")
assert resp.json().get("show_migration_guide") is False
# generated by claude code
注意这组测试的设计逻辑:
- 测试 1-2:验证修复本身是否生效(正向)
- 测试 3:验证隐私合规的约束没被破坏(回归)
- 测试 4-5:覆盖边界条件——存量用户 vs 新用户的不同体验
这些用例不是随机生成的,它们直接来源于"头"端输出的 verification_hints 和代码变更的 diff。
3.4 自动执行与结果判定
生成测试用例只是第一步。完整的验证闭环还需要自动执行和结果判定:
import subprocess
import json
def run_verification(
test_file_path: str,
repo_path: str,
) -> dict:
"""执行生成的测试用例,收集结果"""
result = subprocess.run(
["pytest", test_file_path, "-v", "--tb=short", "--json-report",
"--json-report-file=test_result.json"],
cwd=repo_path,
capture_output=True,
text=True,
timeout=300,
)
# 解析测试结果
with open(f"{repo_path}/test_result.json") as f:
report = json.load(f)
summary = {
"total": report["summary"]["total"],
"passed": report["summary"].get("passed", 0),
"failed": report["summary"].get("failed", 0),
"errors": report["summary"].get("error", 0),
}
# 如果有失败,收集详细信息
failures = []
for test in report.get("tests", []):
if test["outcome"] == "failed":
failures.append({
"test_name": test["nodeid"],
"message": test.get("call", {}).get("crash", {}).get("message", ""),
})
summary["failures"] = failures
summary["all_passed"] = summary["failed"] == 0 and summary["errors"] == 0
return summary
def verify_and_report(
issue_definition: dict,
test_file_path: str,
repo_path: str,
) -> dict:
"""完整的验证流程:执行测试 → 判定结果 → 生成报告"""
result = run_verification(test_file_path, repo_path)
if result["all_passed"]:
verdict = {
"status": "verified",
"confidence": "high",
"message": f"所有 {result['total']} 个验证用例通过,修复确认有效",
}
else:
# 让 LLM 分析失败原因:是修复不完整,还是测试本身有问题
analysis = analyze_test_failures(
issue_definition, result["failures"]
)
verdict = {
"status": "needs_review",
"confidence": "medium",
"message": analysis,
"failed_tests": result["failures"],
}
return verdict
# generated by claude code
3.5 测试生成的三个最佳实践
实践一:让测试"抄"项目已有的风格,不要生成"外星代码”。
把项目中已有的相关测试文件作为参考喂给 LLM,让它学习命名约定、fixture 用法、断言风格。生成的测试看起来应该像团队成员写的,而不是像从 Stack Overflow 上复制的。这不是审美问题——风格一致的测试更容易被团队接受并纳入 CI。
实践二:生成的测试必须区分"正向验证"“回归验证"“边界条件"三类。
大多数自动生成的测试只覆盖正向路径——“修了之后能用了”。但真正的价值在于后两类:回归验证确保没有引入新问题,边界条件覆盖用户没明确说但可能出问题的场景。要求 LLM 显式输出这三类,可以用 verification_hints 作为边界条件的种子。
实践三:测试失败时,让 AI 判断"是修复不完整"还是"测试本身有问题”。
自动生成的测试不是 100% 正确的。当测试失败时,不能盲目认为修复有问题——可能是测试的前置条件设置不对,或者断言逻辑有误。加一层 LLM 分析来区分这两种情况,可以减少误报带来的人力浪费。
四、一头一尾的连接:闭环设计
“头"和"尾"不是独立的两个模块,它们通过 verification_hints 形成闭环:
┌──────────────────────────────────────────────────────────┐
│ │
│ 用户反馈 ──→ 问题理解Agent ──→ 结构化问题定义 │
│ │ │
│ │ verification_hints │
│ ▼ │
│ 修复代码 ──→ 测试生成Agent ──→ 自动化测试用例 │
│ │ │
│ ▼ │
│ 自动执行 & 判定 │
│ │ │
│ ┌─────────┴──────────┐ │
│ ▼ ▼ │
│ 全部通过 存在失败 │
│ │ │ │
│ ▼ ▼ │
│ 自动关闭工单 通知开发者复查 │
│ 发送解决通知 附带失败分析 │
│ │
└──────────────────────────────────────────────────────────┘
这个闭环的关键设计点是信息不断流:
- “头"端理解问题时产生的上下文(涉及哪些模块、影响哪些用户、根因是什么),完整传递给"尾"端
- “尾"端不需要重新理解问题,它只需要把已有的理解翻译成测试代码
- 如果验证失败,失败信息又可以回流给"头"端,修正问题定义
这就像一个好的工程团队:Tech Lead 理解完问题后写了一份清晰的 issue description(包含验证标准),QA 拿到之后知道该测什么。AI Native 流程做的是把这个协作过程自动化,关键是不丢信息。
五、落地建议
如果你想在自己的团队落地这套流程,建议分三步走:
第一步:先做"头”,单独跑两周。
只做问题理解,不做自动验证。让 Agent 把用户反馈转化为结构化 issue,人工 review 准确率。目标是 >80% 的 issue definition 不需要人工修改。这一步的 ROI 最高——光是省掉工程师理解用户反馈的时间就够了。
第二步:加"尾”,从单元测试开始。
先不要尝试生成端到端测试,从单元测试和集成测试开始。这类测试的确定性高,生成质量更容易保证。等团队积累了信心之后,再逐步扩展到 API 测试和 UI 测试。
第三步:打通闭环,接入 CI/CD。
把测试生成和执行接入 CI pipeline,实现"代码提交 → 自动生成验证用例 → 自动执行 → 自动判定"的全流程。到这一步,从用户反馈到验证关闭的平均时间可以从几天缩短到几小时。
结语
AI Native 流程和传统自动化的本质区别在于:传统自动化处理的是"已知的确定性”——你事先想好规则,代码去执行;AI Native 处理的是"未知的模糊性”——用户的描述是模糊的,问题的根因需要推理,验证的边界需要判断。
在 VOC 自动解决这个场景里,一头一尾恰好覆盖了两种最核心的模糊性:
- 头——“用户到底遇到了什么问题”(理解的模糊性)
- 尾——“怎么证明问题真的被解决了”(验证的模糊性)
把这两个环节做好,中间的分类、路由、派发反而是最简单的部分。这也是为什么我说,AI Native 不是在现有流程上"加个 AI”,而是用 AI 的理解能力重新定义流程中最难的环节。
Note: 本文是 VOC 自动化流水线 的续篇,聚焦于流水线中最有技术深度的两个环节。