10.4 评估自动化
前面几节我们建立了评估指标体系、标准测试集和自动评分工具。但如果每次改代码都得手动跑一遍评估,那和"手动测试"有什么区别?评估自动化就是把所有评估能力串联起来,嵌入 CI/CD 流水线,让质量把控变成"自动运转的流水线"。
生活类比:评估自动化就像汽车工厂的质检线——每辆车经过时,机器人自动检测刹车、排放、灯光,不合格的自动拦截。不需要质检员拿着检查表一辆一辆看。人工抽检只用于复核。
评估自动化的必要性
在 Agent 开发中,"改了 Prompt 会不会影响其他场景?"、"新模型上线后效果到底好没好?"——这些问题如果靠人工每次手动评估,不仅效率低下,还容易遗漏。
┌──────────────────────────────────────────────────────────────────┐
│ 手动评估 vs 自动化评估 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 手动评估流程: │
│ 开发者修改代码 -> 手动跑几个 case -> 肉眼判断 -> 上线 │
│ ├─ 问题:覆盖不全、主观性强、不可复现、耗时 │
│ │
│ 自动化评估流程: │
│ 开发者提交 PR -> 自动触发评估 -> 运行全部测试集 -> 生成报告 │
│ ├─ 优势:全面覆盖、客观量化、可复现、7×24 运行 │
│ │
│ 评估自动化 = 速度 × 覆盖度 × 一致性 × 可追溯性 │
│ │
└──────────────────────────────────────────────────────────────────┘CI/CD 集成评估流水线
评估自动化的核心是将评估嵌入持续集成/持续交付(CI/CD)流水线:
┌──────────────────────────────────────────────────────────────────┐
│ CI/CD 评估流水线架构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 代码提交 │───►│ 单元测试 │───►│ 评估测试 │───►│ 报告生成 │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
│ │ │
│ ├─ 基础能力测试集 │
│ ├─ 回归测试集 │
│ ├─ 安全测试集 │
│ └─ 性能测试集 │
│ │ │
│ ┌──────▼──────┐ │
│ │ 结果对比 │ │
│ │ vs 基线版本 │ │
│ └──────┬──────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ ┌────▼───┐ ┌────▼───┐ ┌────▼───┐ │
│ │ PASS │ │ WARN │ │ FAIL │ │
│ │ 自动合并│ │ 人工审核│ │ 阻塞合并│ │
│ └────────┘ └────────┘ └────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘关键设计决策:
| 决策点 | 选项 | 建议 |
|---|---|---|
| 触发时机 | PR 提交 / 定时 / 手动 | PR 提交 + 每日定时 |
| 测试集规模 | 全量 / 采样 / 分层 | 快速检查用采样,合并前用全量 |
| 阻塞策略 | 严格阻塞 / 软阻塞 / 仅报告 | 回归测试严格阻塞,新能力软阻塞 |
| 环境管理 | 共享 / 隔离 / 容器化 | 使用 Docker 保证环境一致性 |
| 成本控制 | 全量 / 动态 / 限额 | 设置每日预算上限 |
回归测试框架
回归测试确保新版本不在已有能力上"退化"——就像体检时不仅查新症状,还要复查之前的指标有没有变差。
import json # JSON 处理
from typing import Dict, List, Callable # 类型注解
from dataclasses import dataclass # 数据类
from datetime import datetime
@dataclass
class RegressionTestResult:
"""回归测试结果——单个测试用例的对比结果"""
test_id: str # 测试 ID
test_name: str # 测试名称
baseline_score: float # 基线版本得分
current_score: float # 当前版本得分
delta: float # 得分变化(正数=提升,负数=退化)
passed: bool # 是否通过(退化不超过阈值)
threshold: float # 允许的最大退化幅度
class RegressionTestSuite:
"""
回归测试套件——对比当前版本与基线版本的表现
工作原理:
1. 加载基线版本的评估结果(之前保存的)
2. 用当前版本运行相同的测试集
3. 逐个对比得分变化
4. 如果退化超过阈值,标记为失败
"""
def __init__(
self,
baseline_results_path: str, # 基线结果文件路径
degradation_threshold: float = 0.05, # 退化容忍度(默认 5%)
):
"""
初始化回归测试套件
Args:
baseline_results_path: 基线结果 JSON 文件路径
degradation_threshold: 允许的最大得分下降幅度(0.05 = 5%)
"""
self.threshold = degradation_threshold
self.baseline = self._load_baseline(baseline_results_path)
self.results: List[RegressionTestResult] = []
def _load_baseline(self, path: str) -> Dict:
"""加载基线结果——从 JSON 文件读取之前的评估得分"""
with open(path, "r") as f:
return json.load(f)
def run(
self,
agent_fn: Callable, # Agent 执行函数
test_cases: List[Dict], # 测试用例列表
) -> Dict:
"""运行回归测试——对比当前版本与基线"""
self.results = [] # 清空上次结果
regressions = [] # 退化列表
improvements = [] # 提升列表
for case in test_cases:
test_id = case["id"] # 测试 ID
baseline_score = self.baseline.get(test_id, 0.0) # 基线得分
# 运行当前版本 Agent
current_output = agent_fn(case["input"])
current_score = self._evaluate(case, current_output) # 评估当前输出
delta = current_score - baseline_score # 计算变化
passed = delta >= -self.threshold # 退化不超过阈值才算通过
result = RegressionTestResult(
test_id=test_id,
test_name=case.get("name", test_id),
baseline_score=baseline_score,
current_score=current_score,
delta=delta,
passed=passed,
threshold=self.threshold,
)
self.results.append(result)
if not passed: # 退化超过阈值
regressions.append({
"test_id": test_id,
"name": case.get("name"),
"baseline": baseline_score,
"current": current_score,
"delta": delta,
})
elif delta > 0.05: # 显著提升
improvements.append({
"test_id": test_id,
"name": case.get("name"),
"delta": delta,
})
total = len(self.results)
passed_count = sum(1 for r in self.results if r.passed)
return {
"summary": {
"total": total,
"passed": passed_count,
"failed": total - passed_count,
"pass_rate": f"{passed_count/total*100:.1f}%",
"regressions": len(regressions),
"improvements": len(improvements),
"avg_delta": sum(r.delta for r in self.results) / total,
},
"regressions": regressions, # 退化详情
"improvements": improvements, # 提升详情
"details": [ # 逐条详情
{
"test_id": r.test_id,
"baseline": r.baseline_score,
"current": r.current_score,
"delta": r.delta,
"passed": r.passed,
}
for r in self.results
],
}
def _evaluate(self, case: Dict, output: str) -> float:
"""
评估单个输出(简化版)
生产环境应使用 LLM-as-Judge 进行语义级评估。
这里使用文本相似度作为演示。
"""
expected = case.get("expected", "")
if not expected:
return 1.0 # 无期望答案时默认满分
from difflib import SequenceMatcher
return SequenceMatcher(None, expected, output).ratio()
def save_baseline(
path: str, # 保存路径
agent_fn: Callable, # Agent 执行函数
test_cases: List[Dict], # 测试用例
):
"""
保存基线结果——用于后续回归测试对比
首次建立回归测试时调用此函数,它会运行当前版本的所有测试,
将得分保存为基线。之后每次评估都与这个基线对比。
"""
baseline = {} # {test_id: score}
for case in test_cases:
output = agent_fn(case["input"]) # 运行 Agent
from difflib import SequenceMatcher
score = SequenceMatcher(
None, case.get("expected", ""), output
).ratio()
baseline[case["id"]] = score # 记录得分
with open(path, "w") as f:
json.dump(baseline, f, indent=2)
print(f"Baseline saved: {len(baseline)} test cases")A/B 实验框架
A/B 实验对比两个版本的 Agent 在同一测试集上的表现——就像盲测:把两种口味的可乐倒进没标签的杯子里,让人喝了打分,看哪种更好。
import numpy as np # 数值计算
from scipy import stats # 统计检验
from typing import Dict, List
class ABExperiment:
"""
A/B 实验框架——对比对照组和实验组的统计差异
核心流程:
1. 收集两组评分数据(对照组 A 和实验组 B)
2. 计算描述统计(均值、标准差)
3. 进行统计显著性检验(Welch's t-test)
4. 计算效应量(Cohen's d)
5. 给出结论(是否显著优于/差于对照组)
"""
def __init__(self, name: str, significance_level: float = 0.05):
"""
Args:
name: 实验名称,如 "prompt_v2_vs_v1"
significance_level: 统计显著性水平(默认 0.05,即 95% 置信度)
"""
self.name = name
self.alpha = significance_level # 显著性水平
self.results_a: List[float] = [] # 对照组得分列表
self.results_b: List[float] = [] # 实验组得分列表
def add_result_a(self, score: float):
"""添加对照组得分"""
self.results_a.append(score)
def add_result_b(self, score: float):
"""添加实验组得分"""
self.results_b.append(score)
def analyze(self) -> Dict:
"""分析 A/B 实验结果——统计检验 + 效应量"""
if len(self.results_a) < 2 or len(self.results_b) < 2:
return {"error": "Not enough data", "min_samples": 2}
arr_a = np.array(self.results_a) # 转为 numpy 数组
arr_b = np.array(self.results_b)
# 基本统计量
mean_a = np.mean(arr_a) # 对照组均值
mean_b = np.mean(arr_b) # 实验组均值
std_a = np.std(arr_a, ddof=1) # 标准差(ddof=1 表示样本标准差)
std_b = np.std(arr_b, ddof=1)
# 效应量(Cohen's d)——衡量差异的"大小"而非只是"有无"
pooled_std = np.sqrt((std_a**2 + std_b**2) / 2) # 合并标准差
cohens_d = (mean_b - mean_a) / pooled_std if pooled_std > 0 else 0
# Welch's t-test——不假设两组方差相等的 t 检验
t_stat, p_value = stats.ttest_ind(arr_b, arr_a, equal_var=False)
# 95% 置信区间
diff = mean_b - mean_a # 均值差异
se = np.sqrt(std_a**2 / len(arr_a) + std_b**2 / len(arr_b)) # 标准误
ci_lower = diff - 1.96 * se # 置信区间下限
ci_upper = diff + 1.96 * se # 置信区间上限
# 结论判断
if p_value < self.alpha: # p 值小于显著性水平
if diff > 0:
conclusion = f"✅ 实验组显著优于对照组(p={p_value:.4f})"
else:
conclusion = f"❌ 实验组显著差于对照组(p={p_value:.4f})"
else:
conclusion = f"➖ 无显著差异(p={p_value:.4f}),样本量可能不足"
return {
"experiment": self.name,
"sample_size": {
"control": len(self.results_a),
"treatment": len(self.results_b),
},
"statistics": {
"control_mean": round(mean_a, 4),
"treatment_mean": round(mean_b, 4),
"mean_difference": round(diff, 4),
"relative_change": f"{diff/mean_a*100:.1f}%" if mean_a != 0 else "N/A",
},
"statistical_test": {
"method": "Welch's t-test",
"t_statistic": round(t_stat, 4),
"p_value": round(p_value, 4),
"significant": p_value < self.alpha,
"cohens_d": round(cohens_d, 4),
"effect_size": self._interpret_cohens_d(cohens_d),
"confidence_interval_95": [round(ci_lower, 4), round(ci_upper, 4)],
},
"conclusion": conclusion,
}
def _interpret_cohens_d(self, d: float) -> str:
"""解释 Cohen's d 效应量——衡量差异的实际意义大小"""
d = abs(d)
if d < 0.2: # 可忽略
return "可忽略"
elif d < 0.5: # 小效应
return "小"
elif d < 0.8: # 中效应
return "中"
else: # 大效应
return "大"
# 使用示例
def run_ab_experiment():
"""运行 A/B 实验示例"""
experiment = ABExperiment(name="prompt_v2_vs_v1")
# 模拟测试结果(实际中从 Agent 执行获取)
# 对照组:原始 Prompt 的得分
control_scores = [0.82, 0.78, 0.85, 0.90, 0.76, 0.88, 0.81, 0.79,
0.84, 0.87, 0.75, 0.83, 0.80, 0.86, 0.77]
# 实验组:新 Prompt 的得分
treatment_scores = [0.85, 0.88, 0.87, 0.92, 0.84, 0.90, 0.86, 0.83,
0.89, 0.91, 0.82, 0.87, 0.85, 0.90, 0.84]
for s in control_scores:
experiment.add_result_a(s) # 添加对照组数据
for s in treatment_scores:
experiment.add_result_b(s) # 添加实验组数据
result = experiment.analyze() # 分析结果
print("=" * 60)
print(f"A/B 实验: {result['experiment']}")
print("=" * 60)
print(f"\n样本量: 对照组 {result['sample_size']['control']}, "
f"实验组 {result['sample_size']['treatment']}")
print(f"\n描述统计:")
print(f" 对照组均值: {result['statistics']['control_mean']}")
print(f" 实验组均值: {result['statistics']['treatment_mean']}")
print(f" 均值差异: {result['statistics']['mean_difference']}")
print(f" 相对变化: {result['statistics']['relative_change']}")
print(f"\n统计检验:")
print(f" p 值: {result['statistical_test']['p_value']}")
print(f" 效应量: {result['statistical_test']['cohens_d']} "
f"({result['statistical_test']['effect_size']})")
print(f" 95% 置信区间: {result['statistical_test']['confidence_interval_95']}")
print(f"\n结论: {result['conclusion']}")
if __name__ == "__main__":
run_ab_experiment()GitHub Actions 评估流水线
将评估嵌入 GitHub Actions,实现"代码提交即触发评估":
# 评估执行脚本——可被 GitHub Actions 调用
import argparse # 命令行参数解析
import json # JSON 处理
import sys # 系统操作(退出码控制)
from pathlib import Path # 路径处理
def run_evaluation(
test_suite: str, # 测试集名称
sample_size: int = None, # 采样数量(可选)
output: str = "results.json", # 输出文件路径
parallel: int = 1, # 并行度
):
"""
执行评估的核心逻辑
这个函数会被 CI/CD 流水线调用,执行步骤:
1. 加载测试集
2. (可选)随机采样
3. 逐个执行 Agent 并评估
4. 保存结果
5. 根据得分决定退出码(0=通过,1=失败)
"""
test_cases = load_test_suite(test_suite) # 加载测试集
# 采样(用于快速检查模式)
if sample_size and sample_size < len(test_cases):
import random
random.seed(42) # 固定随机种子,保证可复现
test_cases = random.sample(test_cases, sample_size)
print(f"Running evaluation: {len(test_cases)} test cases")
results = []
for i, case in enumerate(test_cases):
output = execute_agent(case["input"]) # 执行 Agent
score = evaluate_output(case, output) # 评估输出
results.append({
"id": case["id"],
"score": score,
"output": output,
})
if (i + 1) % 10 == 0: # 每 10 个打印进度
print(f" Progress: {i+1}/{len(test_cases)}")
# 保存结果
summary = {
"test_suite": test_suite,
"total": len(results),
"avg_score": sum(r["score"] for r in results) / len(results),
"results": results,
}
Path(output).parent.mkdir(parents=True, exist_ok=True)
with open(output, "w") as f:
json.dump(summary, f, indent=2)
print(f"Results saved to {output}")
print(f"Average score: {summary['avg_score']:.3f}")
# 根据得分决定 CI 退出码
if summary["avg_score"] < 0.7: # 低于阈值则失败
print("❌ Evaluation score below threshold!")
sys.exit(1) # 退出码 1 = CI 失败
else:
print("✅ Evaluation passed!")
# sys.exit(0) 默认退出码 0 = CI 通过
def load_test_suite(name: str):
"""加载测试集——从 JSON 文件读取"""
path = Path(f"eval-results/test_suites/{name}.json")
if not path.exists():
print(f"Test suite not found: {path}")
sys.exit(1)
with open(path) as f:
return json.load(f)["cases"]
def execute_agent(input_text: str) -> str:
"""执行 Agent(实际实现中调用 Agent 框架)"""
return f"Mock response for: {input_text[:50]}"
def evaluate_output(case: dict, output: str) -> float:
"""评估输出(实际实现中使用 LLM-as-Judge)"""
from difflib import SequenceMatcher
return SequenceMatcher(None, case.get("expected", ""), output).ratio()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--test-suite", required=True) # 测试集名称
parser.add_argument("--sample-size", type=int) # 采样数量
parser.add_argument("--output", default="results.json") # 输出路径
parser.add_argument("--parallel", type=int, default=1) # 并行度
args = parser.parse_args()
run_evaluation(args.test_suite, args.sample_size, args.output, args.parallel)对应的 GitHub Actions 配置(YAML):
# 文件路径:.github/workflows/agent-eval.yml
name: Agent Evaluation Pipeline
on:
pull_request:
branches: [main]
paths:
- 'agent/**'
- 'prompts/**'
- 'config/**'
schedule:
- cron: '0 2 * * *' # 每天凌晨 2 点运行全量评估
workflow_dispatch: # 允许手动触发
jobs:
quick-eval:
name: Quick Evaluation (Sampling)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-eval.txt
- name: Run quick evaluation
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python -m eval.run \
--test-suite quick \
--sample-size 50 \
--output eval-results/quick-eval.json
- name: Check regression
run: |
python -m eval.check_regression \
--current eval-results/quick-eval.json \
--baseline eval-results/baseline.json \
--threshold 0.05
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: quick-eval-results
path: eval-results/评估结果可视化
可视化是评估系统的"最后一公里"——好的可视化让团队能在 30 秒内判断版本质量。
┌──────────────────────────────────────────────────────────────────┐
│ 评估仪表盘核心面板 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 整体评分趋势图 │ │ 各维度雷达图 │ │
│ │ (折线图,按时间) │ │ (多维度对比) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 回归测试热力图 │ │ A/B 实验对比 │ │
│ │ (版本 × 用例) │ │ (箱线图/小提琴图) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 延迟分布直方图 │ │ 成本趋势图 │ │
│ │ (P50/P95/P99) │ │ (Token/费用) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘下面是可视化仪表盘的实现,代码逐行注释:
import json # JSON 处理
import matplotlib.pyplot as plt # 绘图库
import numpy as np # 数值计算
from pathlib import Path # 路径处理
class EvalDashboard:
"""评估结果可视化仪表盘——生成评分趋势、雷达图、延迟分布等可视化"""
def __init__(self, results_dir: str = "eval-results"):
"""
初始化仪表盘
Args:
results_dir: 评估结果文件目录
"""
self.results_dir = Path(results_dir)
# 设置中文字体(支持中文显示)
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
def plot_score_trend(self, history: list, save_path: str = None):
"""绘制评分趋势图——展示 Agent 得分随版本变化"""
fig, ax = plt.subplots(figsize=(12, 6))
dates = [h.get("date", f"v{i}") for i, h in enumerate(history)]
scores = [h["avg_score"] for h in history]
ax.plot(dates, scores, 'o-', linewidth=2, markersize=8, color='#2E86AB')
ax.axhline(y=0.8, color='green', linestyle='--', alpha=0.5, label='目标线') # 目标线
ax.axhline(y=0.7, color='orange', linestyle='--', alpha=0.5, label='警戒线') # 警戒线
ax.fill_between(range(len(scores)), scores, alpha=0.1, color='#2E86AB')
ax.set_ylim(0, 1.0)
ax.set_xlabel('版本')
ax.set_ylabel('平均分')
ax.set_title('Agent 评估得分趋势')
ax.legend()
ax.grid(True, alpha=0.3)
if save_path:
plt.savefig(save_path, dpi=150, bbox_inches='tight')
plt.show()
def plot_radar(self, dimensions: dict, save_path: str = None):
"""绘制能力雷达图——多维度能力可视化"""
categories = list(dimensions.keys()) # 维度名称列表
values = list(dimensions.values()) # 各维度得分
N = len(categories)
# 计算雷达图角度
angles = [n / float(N) * 2 * np.pi for n in range(N)]
values += values[:1] # 闭合雷达图
angles += angles[:1]
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.plot(angles, values, 'o-', linewidth=2, color='#D64933')
ax.fill(angles, values, alpha=0.25, color='#D64933')
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories)
ax.set_ylim(0, 5)
ax.set_title('Agent 能力雷达图', pad=20)
ax.grid(True)
if save_path:
plt.savefig(save_path, dpi=150, bbox_inches='tight')
plt.show()
# 使用示例
def demo_dashboard():
"""仪表盘演示"""
dashboard = EvalDashboard()
# 评分趋势示例数据
history = [
{"date": "2026-07-01", "avg_score": 0.72},
{"date": "2026-07-05", "avg_score": 0.75},
{"date": "2026-07-10", "avg_score": 0.78},
{"date": "2026-07-15", "avg_score": 0.82},
{"date": "2026-07-20", "avg_score": 0.85},
]
dashboard.plot_score_trend(history)
# 能力雷达图示例数据
dimensions = {
"任务完成率": 4.2,
"回答准确率": 4.5,
"响应速度": 3.8,
"工具调用": 4.0,
"安全性": 4.8,
"用户满意度": 4.3,
}
dashboard.plot_radar(dimensions)
print("可视化仪表盘演示完成!")
if __name__ == "__main__":
demo_dashboard()常见误区
误区一:评估只在上线前跑一次
评估不是"一次性考试",而是"持续体检"。每次代码变更(改 Prompt、换模型、加工具)都应该触发评估。只在上线前跑一次,就像只在出事前才做体检——太晚了。
误区二:A/B 实验样本量不够就下结论
"新 Prompt 在 5 个 case 上表现更好"——这可能是随机波动。统计显著性检验需要足够的样本量,建议每组至少 30 个样本。样本量太少时,p 值不可靠。
误区三:CI 中评估不设阻塞策略
如果评估只生成报告但不阻塞合并,那和"不评估"差不多——开发者看了报告觉得"差不多就合了吧"。回归测试必须设置严格阻塞:退化超过阈值就拒绝合并。
误区四:基线永远不更新
基线是"上一版的评估结果",应该随着版本迭代而更新。如果基线一直是半年前的版本,那对比意义就不大了。建议每次发布后自动更新基线。
本节小结
| 环节 | 关键实践 | 工具/方法 |
|---|---|---|
| CI/CD 集成 | PR 触发 + 定时全量 | GitHub Actions / GitLab CI |
| 回归测试 | 基线对比 + 退化阈值 | 自定义 RegressionTestSuite |
| A/B 实验 | 统计显著性检验 + 效应量 | Welch's t-test + Cohen's d |
| 结果可视化 | 趋势图 + 雷达图 + 分布图 | Matplotlib / Grafana / LangSmith |
| 告警机制 | 阈值告警 + 趋势告警 | Slack / PagerDuty / 邮件 |
关键原则:
- 评估自动化是 Agent 工程化的基石,没有自动化评估就无法持续迭代
- 测试集应分层:快速检查(采样)-> 回归测试(全量)-> 专项测试(安全/性能)
- A/B 实验需要足够的样本量才有统计意义,建议至少每组 30+ 样本
- 可视化不是装饰——好的可视化让团队在 30 秒内判断版本质量
- 保持基线文件的版本管理,每次通过评估后更新基线
有了自动化评估流水线,下一步需要从用户反馈中学习——这正是 10.5 节反馈闭环要解决的问题。