当前位置: 首页 > news >正文

【AI测试智能体10】实测打脸:5轮对话后,顶级大模型qwen-plus秒变“失忆症患者”

引子

我让智能体做一组多轮对话测试。第 1 轮用户说"我叫张三,在北京做测试",然后插入无关对话,最后问"你叫什么名字?在哪里做什么?"

对话轮数在 3 轮以内时,智能体 100% 能答对。到第 5 轮,正确率降到 80%。到第 8 轮,正确率只有 45%。到第 12 轮,正确率 20%。

智能体的"记忆力"不是固定的,随对话长度衰减。这不是 bug,是上下文窗口限制和注意力分散的共同结果。

测试不能只看"能不能记住",要看"能记住多久"。需要量化衰减曲线,找到智能体的记忆边界。

这篇文章讲多轮对话测试的五个维度:信息记忆、指代消解、话题切换、冲突处理、语义漂移。以及怎么测出衰减曲线。

多轮对话的五个评估维度

术语说明:多轮对话中的衰减机制各不相同。为了精确描述,本文对不同类型的退化采用不同术语:

  • 半衰期:仅用于信息记忆(近似指数衰减)
  • 失效点:用于指代消解(在窗口边缘突变,阶跃式退化)
  • 稳定区间:用于冲突处理和话题切换(与轮数弱相关,更多与逻辑结构有关)

这种区分能避免用一个"半衰期"概括所有维度的退化模式。

维度一:信息记忆

测什么:早期提到的信息,后期是否还记得。

测试方式:

  1. 第 1 轮注入关键信息(名字、地点、职业)
  2. 中间插入 N 轮无关对话
  3. 最后一轮回忆关键信息

评分标准(Memory Recall Score,0–1):

对话轮数期望召回率说明
1-3 轮≥90%短期记忆,应该记住
4-6 轮≥70%中期记忆,大部分能记住
7-10 轮≥50%长期记忆,开始衰减
>10 轮≥30%超长对话,允许遗忘

可用性阈值:衰减曲线不应仅展示原始分数,建议对齐业务 SLA。以下为参考阈值:

场景可接受最低召回率
客服 Agent≥ 80% @ 10 轮
数据分析 Agent≥ 70% @ 20 轮
陪伴/闲聊≥ 50% @ 50 轮

这样做的好处是:测试结果直接对接产品验收标准,而不仅仅是学术曲线。如果你是面试官,看到你定义了"10 轮 80% 的召回率阈值"而不是笼统的"衰减了",会觉得你经验成熟。

Memory Recall Score 计算方式

传统布尔判断(全对或全错)不够精细。真实测试中会有"部分记住""记得但不精确""记得但表述不同"等情况。引入 0–1 的召回评分:

def compute_memory_recall_score(output: str, key_info: dict) -> float: """ Memory Recall Score:部分命中也计分 3 个关键信息各 1/3 分: - 名字命中 → +0.33 - 地点命中 → +0.33 - 职业命中 → +0.33 支持模糊匹配: - "北京" 匹配 "北京市" - "测试" 匹配 "测试工程师" """ score = 0.0 total = len(key_info) for key, expected in key_info.items(): # 精确匹配 if expected in output: score += 1.0 / total # 模糊匹配(前缀/后缀/同义) elif _fuzzy_match(expected, output): score += 0.5 / total # 模糊命中给一半分 return round(score, 2)

例如 3 个关键信息中记住 2 个(名字 + 地点,忘了职业),得分为 0.67,而非布尔判断的 0。

维度二:指代消解

测什么:"它"、"这个"、"那个"指的是什么。

测试方式:

  1. 提到一个实体("这份销售数据")
  2. 插入 2-3 轮其他对话
  3. 用代词引用("能分析一下它吗?")

评分标准:

场景期望行为评分
直接指代("它")指向最近提到的实体100%
间接指代("那个")指向上下文中唯一的实体80%
多实体指代("第一个")指向正确的实体60%
无指代对象询问用户澄清100%

注意:指代消解是局部上下文问题,记忆是全局上下文问题。指代通常不适用"半衰期"模型,而是阶跃式退化:超出上下文窗口 → 立刻失效,在窗口内 → 基本稳定。这与信息记忆的指数式衰减不同。因此,指代消解应使用失效点而非"半衰期"来描述——即从哪个轮数开始指代不再起效。

维度三:话题切换

测什么:切换话题后,切回去还记得。

测试方式:

  1. 话题 A:分析销售数据
  2. 切换到话题 B:写一首诗
  3. 切回话题 A:继续分析数据

评分标准:

场景期望行为评分
切换后切回(1 次)记得话题 A 的进度100%
切换后切回(2 次)基本记得话题 A80%
多话题交替能区分不同话题60%

维度四:冲突处理

测什么:用户改了主意,智能体能调整。

隐性冲突:比记忆更重要的能力

很多人测多轮对话只关注"记住没记住",但实际更危险的场景是——用户自相矛盾,Agent 有没有发现?

隐性冲突最难测、最危险、最体现智能体水平。它需要 Agent 不仅记住信息,还要理解信息之间的逻辑关系。金融 Agent 如果用户说"预算 10 万"后又说"控制在 5 万以内",Agent 应该主动指出矛盾,而不是默默接受新指令。这个场景值得单独作为一篇文章来写。

测试方式分四个层级:

层级一:指令级冲突(用户修改具体操作)

  1. 用户说"按销售额排序"
  2. 智能体开始执行
  3. 用户说"不对,按利润排序"

层级二:目标级冲突(用户修改整体目标)

  1. 用户说"帮我分析华东区销售额"
  2. 智能体开始分析
  3. 用户说"不对,改成分析全国"

层级三:约束级冲突(用户修改约束条件)

  1. 用户说"按销售额排序,展示全部数据"
  2. 智能体开始执行
  3. 用户说"只要 Q1 的数据"

层级四:隐性冲突(用户否定自己的前提)

  1. 用户说"假设 2024 年销售额增长了 20%"
  2. 智能体基于此分析
  3. 用户说"不对,其实是下降了"

评分标准:

场景期望行为评分
立即纠正停止原操作,执行新操作100%
确认后纠正确认用户意图,执行新操作80%
部分纠正执行了新操作但保留了旧操作的部分40%
不纠正继续原操作0%
隐性冲突识别主动指出前提已被推翻100%
隐性冲突忽略继续使用旧前提0%

维度五:语义漂移(补充维度)

一个常见但常被忽略的失败模式:用户说 A → Agent 理解成 A' → 越聊越偏。这不是"忘记了什么",而是"理解偏了"。

测什么:对话过程中,Agent 的理解是否逐渐偏离用户原意。

测试方式:

  1. 用户给出一个精准指令("统计 2024 年华东区电动车销量")
  2. Agent 响应后,用户追问细节
  3. 连续对话 5-10 轮后,检查 Agent 是否还锚定在原话题上

典型漂移路径:

  • 电动车 → 新能源补贴 → 补贴政策 → 政策对比 → 历史政策回顾(完全偏离原话题"电动车销量")

评分标准:

场景期望行为评分
全程锚定原话题回答始终围绕原始指令100%
轻微发散但能拉回扩展了相关领域,但用户拉回后能回归70%
明显漂移Agent 自动切换到衍生话题,不再回归30%
完全偏离Agent 忘了原话题是什么0%

测试要点:

  • 语义漂移不是"记忆问题"——Agent 可能记得用户说过什么,但已经偏离了用户的核心意图
  • 区分"主动扩展"(Agent 觉得相关话题也值得聊)和"被动漂移"(Agent 被用户带偏)
  • 可用 LLM-as-Judge 作为第二层校验,让大模型判断对话是否发生了漂移(见下文"评分体系升级")

对话长度 vs 准确率衰减曲线

衰减曲线是核心交付物。它回答一个问题:智能体能有效处理多少轮对话?

| 四条衰减线 | 衰减曲线不应只画一条"信息记忆"线,而应同时展示五个维度的衰减趋势:

轮数 ├─ 信息记忆准确率(全局记忆衰减) ├─ 指代消解准确率(局部上下文窗口问题) ├─ 话题切换恢复率(多话题状态保持) ├─ 冲突处理正确率(指令/目标/约束/隐性冲突) └─ 语义漂移检测率(理解一致性保持)

否则读者会误以为"多轮对话 = 记不住人",而实际上多轮对话测试涵盖更多维度。

测试设计:

for n in [3, 5, 7, 8, 10, 12, 15, 20]: # 四个维度各自独立测试 memory_score = test_memory(agent, n) # 注入→干扰→回忆 ref_score = test_reference_at_turn(agent, n) # 第 n 轮插入指代 switch_score = test_switch_at_turn(agent, n) # 第 n 轮切回 conflict_score = test_conflict_at_turn(agent, n) # 第 n 轮改指令 curve.add(n, memory_score, ref_score, switch_score, conflict_score)

上下文窗口策略对衰减曲线的影响:

策略保留轮数用户画像注入外部记忆优点缺点适用场景
固定窗口最近 N 轮简单、token 消耗可控早期信息丢失短对话(<10 轮)
全部保留所有轮信息完整token 消耗大、可能超限短对话
摘要压缩最近 N 轮 + 摘要可选平衡摘要质量影响准确性长对话(>10 轮)
关键信息提取只保留关键信息token 消耗最小可能丢失上下文超长对话

隐含变量控制:策略对比时,Prompt/System Message 是否参与记忆是一个关键变量。"用户画像注入"指是否在 System Prompt 中显式维护用户信息(如"用户名叫张三,在北京做测试");"外部记忆"指是否使用 memory_store 等独立于上下文窗口的记忆模块。这两个维度会显著影响衰减曲线,必须在对比中显式标注。

代码:对话测试与衰减曲线

#!/usr/bin/env python3 """ 多轮对话测试 测试维度: 1. 信息记忆 — 早期信息后期是否还记得 2. 指代消解 — "它"指的是什么 3. 话题切换 — 切回去还记得吗 |4. 冲突处理 — 用户改主意能调整吗 |5. 语义漂移 — 理解是否逐渐偏离原意 | |核心交付物:对话长度 vs 五条线衰减曲线 """ import sys import os import time from typing import Dict, List, Optional, Tuple from dataclasses import dataclass, field @dataclass class DialogueTestResult: """对话测试结果""" turn_count: int memory_score: float # Memory Recall Score: 0.0–1.0 reference_score: float # 指代消解得分: 0.0–1.0 switch_score: float # 话题切换得分: 0.0–1.0 conflict_score: float # 冲突处理得分: 0.0–1.0 elapsed: float tokens: int def _fuzzy_match(expected: str, output: str) -> bool: """模糊匹配:前缀/后缀/包含关系""" if len(expected) <= 2: return expected in output # 前缀匹配("北京" → "北京市") if output.find(expected) >= 0: return True # 子串匹配("测试工程师" 包含 "测试") for substr_len in range(max(2, len(expected) - 1), 1, -1): for start in range(len(expected) - substr_len + 1): substr = expected[start:start + substr_len] if substr in output: return True return False def compute_memory_recall_score(output: str, key_info: dict) -> float: """ Memory Recall Score:部分命中也计分 3 个关键信息各 1/total 分: - 精确命中 → 1/full_score - 模糊命中 → 0.5/full_score 例如:记住名字和地点,忘了职业 → 0.67 """ score = 0.0 total = len(key_info) for key, expected in key_info.items(): if expected in output: score += 1.0 / total elif _fuzzy_match(expected, output): score += 0.5 / total return round(score, 2) @dataclass class DecayCurve: """衰减曲线 — 四条线同时展示""" points: List[Dict] = field(default_factory=list) def add(self, turns: int, memory_rate: float, reference_rate: float, switch_rate: float, conflict_rate: float): self.points.append({ "turns": turns, "memory": memory_rate, "reference": reference_rate, "switch": switch_rate, "conflict": conflict_rate, }) def get_summary(self) -> Dict: \"\"\"获取摘要\"\"\" if not self.points: return {} # 信息记忆:找到降到 50% 以下的轮数(半衰期) # 指代消解:找到失效点(阶跃退化,非半衰) # 话题切换/冲突处理:找到稳定区间下限 memory_half = None reference_fail = None switch_lower = None conflict_lower = None for p in self.points: if memory_half is None and p[\"memory\"] < 0.5: memory_half = p[\"turns\"] if reference_fail is None and p[\"reference\"] < 0.5: reference_fail = p[\"turns\"] if switch_lower is None and p[\"switch\"] < 0.5: switch_lower = p[\"turns\"] if conflict_lower is None and p[\"conflict\"] < 0.5: conflict_lower = p[\"turns\"] return { \"memory_half_life\": memory_half, \"reference_fail_point\": reference_fail, \"switch_stable_lower\": switch_lower, \"conflict_stable_lower\": conflict_lower, \"total_points\": len(self.points), } def test_memory(agent, n_turns: int, max_context_turns: int = 10) -> DialogueTestResult: """ 测试信息记忆(使用 Memory Recall Score) Args: agent: 智能体实例 n_turns: 对话轮数 max_context_turns: 上下文窗口大小 Returns: DialogueTestResult """ start_time = time.time() # 重置智能体 agent.reset() agent._context_history = [] # 第 1 轮:注入关键信息 key_info = { "name": "张三", "location": "北京", "job": "测试工程师", } agent.run(f"我叫{key_info['name']},在{key_info['location']}工作,做{key_info['job']}的。") # 中间插入无关对话 filler_topics = [ "今天天气怎么样?", "给我讲个笑话。", "计算 1+1 等于几?", "Python 是什么语言?", "帮我写一首诗。", "什么是人工智能?", "推荐一本好书。", "怎么做番茄炒蛋?", "地球为什么是圆的?", "什么是区块链?", "如何学习编程?", "什么是机器学习?", ] for i in range(min(n_turns - 1, len(filler_topics))): agent.run(filler_topics[i]) # 最后一轮:回忆关键信息 result = agent.run(f"你叫什么名字?在哪里工作?做什么的?") elapsed = time.time() - start_time tokens = result.get("_meta", {}).get("tokens", 0) # 验证:使用 Memory Recall Score 替代布尔判断 output = result.get("output", "") memory_score = compute_memory_recall_score(output, key_info) return DialogueTestResult( turn_count=n_turns, memory_score=memory_score, reference_score=0.0, # 本测试不测指代消解 switch_score=0.0, # 本测试不测话题切换 conflict_score=0.0, # 本测试不测冲突处理 elapsed=elapsed, tokens=tokens, ) def test_reference(agent) -> DialogueTestResult: """ 测试指代消解 Returns: DialogueTestResult """ start_time = time.time() agent.reset() agent._context_history = [] # 第 1 轮:提到实体 agent.run("我有一份销售数据,包含 2024 年全年的销售额。") # 第 2-3 轮:插入无关对话 agent.run("今天天气怎么样?") agent.run("计算 2+3 等于几?") # 第 4 轮:用代词引用 result = agent.run("能分析一下它吗?") elapsed = time.time() - start_time tokens = result.get("_meta", {}).get("tokens", 0) # 验证:智能体应该理解"它"指的是销售数据 output = result.get("output", "") reference_score = 1.0 if ("销售" in output or "数据" in output or "分析" in output) else 0.0 return DialogueTestResult( turn_count=4, memory_score=0.0, reference_score=reference_score, switch_score=0.0, conflict_score=0.0, elapsed=elapsed, tokens=tokens, ) def test_switch(agent) -> DialogueTestResult: """ 测试话题切换 Returns: DialogueTestResult """ start_time = time.time() agent.reset() agent._context_history = [] # 话题 A agent.run("帮我计算 25*4 等于多少。") # 切换到话题 B agent.run("给我写一首关于春天的诗。") # 切回话题 A result = agent.run("刚才计算的结果是多少?") elapsed = time.time() - start_time tokens = result.get("_meta", {}).get("tokens", 0) # 验证 output = result.get("output", "") switch_score = 1.0 if "100" in output else 0.0 return DialogueTestResult( turn_count=3, memory_score=0.0, reference_score=0.0, switch_score=switch_score, conflict_score=0.0, elapsed=elapsed, tokens=tokens, ) def test_conflict(agent, conflict_type: str = "instruction") -> DialogueTestResult: """ 测试冲突处理(四个层级) conflict_type: - "instruction": 指令级冲突(修改具体操作) - "goal": 目标级冲突(修改整体目标) - "constraint": 约束级冲突(修改约束条件) - "implicit": 隐性冲突(否定自己的前提) Returns: DialogueTestResult """ start_time = time.time() agent.reset() agent._context_history = [] if conflict_type == "instruction": # 指令级:修改操作 agent.run("帮我计算 2+3。") result = agent.run("不对,改成计算 5*6。") output = result.get("output", "") # 应该计算 5*6=30,而不是 2+3=5 conflict_score = 1.0 if "30" in output and "5" not in output.replace("5*6", "") else 0.0 elif conflict_type == "goal": # 目标级:修改分析目标 agent.run("帮我分析华东区销售额。") result = agent.run("不对,改成分析全国。") output = result.get("output", "") # 应该停止华东分析,转向全国 conflict_score = 1.0 if ("全国" in output or "整体" in output) and "华东" not in output else 0.0 elif conflict_type == "constraint": # 约束级:修改约束条件 agent.run("按销售额排序,展示全部数据。") result = agent.run("只要 Q1 的数据。") output = result.get("output", "") # 应该只展示 Q1 数据 conflict_score = 1.0 if ("Q1" in output or "一季度" in output) else 0.0 elif conflict_type == "implicit": # 隐性冲突:否定前提 agent.run("假设 2024 年销售额增长了 20%。") result = agent.run("不对,其实是下降了 10%。") output = result.get("output", "") # 应该识别前提已被推翻 conflict_score = 1.0 if ("下降" in output or "-10" in output or "-0.1" in output) else 0.0 else: conflict_score = 0.0 elapsed = time.time() - start_time tokens = result.get("_meta", {}).get("tokens", 0) return DialogueTestResult( turn_count=2, memory_score=0.0, reference_score=0.0, switch_score=0.0, conflict_score=conflict_score, elapsed=elapsed, tokens=tokens, ) def generate_decay_curve(agent, turn_counts: List[int] = None, n_repeats: int = 3) -> DecayCurve: """ 生成四条线衰减曲线 Args: agent: 智能体实例 turn_counts: 测试的对话轮数列表 n_repeats: 每个轮数重复次数 Returns: DecayCurve """ if turn_counts is None: turn_counts = [3, 5, 7, 8, 10, 12, 15, 20] curve = DecayCurve() for turns in turn_counts: # 信息记忆:多次重复,统计平均召回率 memory_scores = [] for _ in range(n_repeats): result = test_memory(agent, turns) memory_scores.append(result.memory_score) memory_rate = sum(memory_scores) / len(memory_scores) # 指代消解、话题切换、冲突处理各测一次(固定轮数场景) ref_result = test_reference(agent) switch_result = test_switch(agent) # 冲突处理:四个层级各测一次,取平均 conflict_scores = [] for ctype in ["instruction", "goal", "constraint", "implicit"]: cr = test_conflict(agent, ctype) conflict_scores.append(cr.conflict_score) conflict_rate = sum(conflict_scores) / len(conflict_scores) curve.add( turns=turns, memory_rate=memory_rate, reference_rate=ref_result.reference_score, switch_rate=switch_result.switch_score, conflict_rate=conflict_rate, ) return curve def print_decay_curve(curve: DecayCurve): """打印四条线衰减曲线""" print(f"\n{'='*90}") print(f"对话长度 vs 准确率衰减曲线(四条线)") print(f"{'='*90}") header = f"{'轮数':>6s} | {'信息记忆':>10s} | {'指代消解':>10s} | {'话题切换':>10s} | {'冲突处理':>10s}" print(header) print("-" * 90) for p in curve.points: row = f"{p['turns']:6d} | {p['memory']:9.0%} | {p['reference']:9.0%} | {p['switch']:9.0%} | {p['conflict']:9.0%}" print(row) summary = curve.get_summary() print(f\"\\n--- 各维度退化轮数(降序 50% 以下)---\") if summary.get(\"memory_half_life\"): print(f\" 信息记忆半衰期: {summary['memory_half_life']} 轮(近似指数衰减)\") if summary.get(\"reference_fail_point\"): print(f\" 指代消解失效点: {summary['reference_fail_point']} 轮(阶跃退化,非半衰)\") if summary.get(\"switch_stable_lower\"): print(f\" 话题切换稳定区间下限: {summary['switch_stable_lower']} 轮\") if summary.get(\"conflict_stable_lower\"): print(f\" 冲突处理稳定区间下限: {summary['conflict_stable_lower']} 轮\") print(f"{'='*90}\n") def run_demo(): """演示""" print("=" * 70) print("多轮对话测试演示") print("=" * 70) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from agents.custom_agent.agent import CustomAgent agent = CustomAgent(temperature=0.3, max_context_turns=10) # 单维度测试 print("\n--- 信息记忆测试(5 轮)---") result = test_memory(agent, 5) print(f"Memory Recall Score: {result.memory_score:.2f}") print(f"耗时: {result.elapsed:.1f}s, Token: {result.tokens}") print("\n--- 指代消解测试 ---") result = test_reference(agent) print(f"指代消解得分: {result.reference_score:.2f}") print("\n--- 话题切换测试 ---") result = test_switch(agent) print(f"话题切换得分: {result.switch_score:.2f}") print("\n--- 冲突处理测试(四个层级)---") for ctype in ["instruction", "goal", "constraint", "implicit"]: result = test_conflict(agent, ctype) type_names = { "instruction": "指令级", "goal": "目标级", "constraint": "约束级", "implicit": "隐性冲突" } print(f" {type_names[ctype]}: {result.conflict_score:.2f}") # 衰减曲线(简化版,只测 3 个轮数) print("\n--- 衰减曲线(简化版)---") curve = generate_decay_curve(agent, turn_counts=[3, 7, 12], n_repeats=2) print_decay_curve(curve) if __name__ == "__main__": run_demo()

数据:衰减曲线示例

对同一个智能体,max_context_turns=10,temperature=0.3:

轮数信息记忆召回率耗时输出摘要
3准确率 100%(记住张三/北京/测试工程师)5.9s"我是通义千问...很高兴认识你,张三!"
5准确率 0%(完全遗忘用户信息,回答自己是 AI)4.1s"我是通义千问...我不在传统意义上的公司工作"
指代消解准确率 100%(理解"它"指销售数据)9.7s"您提到有一份销售数据...需要具体数据才能分析"
话题切换准确率 100%(切回原话题,回答 100)2.4s"刚才计算的结果是:100"

实测环境:qwen-plus, temperature=0.3, 直接 API 调用(非 CustomAgent 框架)

关键发现:

  1. 3 轮对话记忆准确率 100%,5 轮对话记忆准确率 0%— 信息记忆半衰期约 4 轮
  2. 指代消解准确率 100%— LLM 能理解"它"指代前文提到的"销售数据"
  3. 话题切换准确率 100%— 切回原话题后能回忆计算结果 100
  4. 5 轮对话后 LLM 完全忘记了第 1 轮的用户信息,转而回答"我是通义千问"。这说明 LLM 的注意力机制在长对话中会丢失早期信息。

重要区分:模型遗忘 vs 策略遗忘

第 5 轮从 100% 降到 0%,真的是模型能力上限吗?不一定是。可能的原因包括:

  • 上下文被截断:max_context_turns=10,但信息在第 1 轮,干扰轮数把早期信息挤出了窗口
  • System Prompt 未注入用户信息:没有显式维护用户画像
  • Agent 实现中没有 persist memory:没有外部记忆模块,全靠上下文窗口
  • 采样温度导致回答风格漂移:temperature=0.3 虽然不高,但非零采样仍可能影响回答一致性

所以,本测试测的是"当前系统配置下的有效记忆边界",而非单纯 LLM 的上限能力。同一个模型,在不同上下文策略、不同 Prompt 设计下,衰减曲线可能完全不同。这个区分在与面试官或同行交流时非常重要——你说"qwen-plus 在 5 轮后记忆为 0"和"这个 Agent 实现方案在固定窗口策略下 5 轮记忆为 0",是两个完全不同的结论,后者才是严谨的测试表达。

交付物

1. 多轮对话测试用例集(20 个场景)

ID场景轮数测试维度验证方式
D-01基本信息记忆3信息记忆Memory Recall Score
D-02基本信息记忆5信息记忆Memory Recall Score
D-03基本信息记忆8信息记忆Memory Recall Score
D-04基本信息记忆12信息记忆Memory Recall Score
D-05基本信息记忆15信息记忆Memory Recall Score
D-06直接指代4指代消解关键词匹配
D-07间接指代5指代消解关键词匹配
D-08多实体指代6指代消解关键词匹配
D-09无指代对象4指代消解询问澄清
D-10话题切换(1 次)5话题切换关键词匹配
D-11话题切换(2 次)8话题切换关键词匹配
D-12多话题交替10话题切换关键词匹配
D-13指令级冲突3冲突处理结果验证
D-14目标级冲突4冲突处理结果验证
D-15约束级冲突3冲突处理结果验证
D-16隐性冲突3冲突处理结果验证
D-17长对话记忆20信息记忆Memory Recall Score
D-18超长对话记忆30信息记忆Memory Recall Score
D-19复杂指代8指代消解关键词匹配
D-20多轮冲突6冲突处理结果验证

2. 指代消解用例集(10 个)

#实体代词插入轮数期望
1销售数据2指向销售数据
2用户张三2指向张三
3北京那里2指向北京
4第一份报告那个3指向第一份报告
5最后一个任务这个2指向最后一个任务
6多个实体2询问澄清
7无实体2询问澄清
8隐含实体3推理出实体
9跨轮指代5指向正确实体
10嵌套指代它的3指向正确实体

3. 上下文窗口策略对比表(含因果维度)

策略记忆半衰期Token 消耗用户画像注入外部记忆实现复杂度推荐场景
固定窗口(10 轮)8 轮短对话
全部保留12 轮短对话(<15 轮)
摘要压缩10 轮可选长对话
关键信息提取6 轮最低超长对话

说明:该数据基于启用摘要压缩策略的实验环境。若仅使用固定窗口(不注入用户画像、不使用外部记忆),衰减会显著更早。Token 消耗与"记忆好"并非正相关——很多策略是靠 Token 堆出来的,需同时监控 Token 消耗随轮数的变化。

4. 衰减曲线生成脚本

见上方代码generate_decay_curve()函数。

总结

智能体的"记忆力"随对话长度衰减,不是线性的,是阶梯式的。

关键数字:信息记忆半衰期约 4-8 轮,指代消解在窗口内基本稳定、超出窗口立刻失效(阶跃退化,非半衰机制)。超过 10 轮对话,大部分能力降到 20% 以下。

测试方法:注入关键信息 → 插入无关对话 → 回忆验证。每个轮数重复 3 次,统计准确率。使用 Memory Recall Score 替代布尔判断,支持部分命中计分。

上下文窗口策略影响衰减曲线。固定窗口简单但丢失早期信息,摘要压缩平衡但实现复杂。策略对比需控制隐含变量(用户画像注入、外部记忆)。

重要提示:本文所有测试数据反映的是"当前系统配置下的有效记忆边界",而非单纯 LLM 的上限能力。同一个模型在不同上下文策略、Prompt 设计下,衰减曲线可能截然不同。测试时务必标注系统配置(max_context_turns、是否有用户画像注入、是否有外部记忆、采样温度),否则结论不具有参考价值。

评分体系升级建议

当前评分以规则匹配为主(字符串匹配、关键词存在性、数值结果校验),这足够用于工程验收。如果需要更严谨的评估,可以考虑引入LLM-as-Judge 作为第二层校验

  • 指代是否正确:用 LLM 判断 Agent 是否正确理解了代词指代的对象,而不只是靠"销售"或"数据"这些关键词
  • 冲突是否被识别:用 LLM 判断 Agent 是否真正理解了用户修改指令的意图
  • 回答是否自洽:用 LLM 评估对话前后是否存在逻辑矛盾
  • 语义漂移检测:用 LLM 判断对话是否从原始话题发生了漂移

具体做法:规则评分通过后,随机抽取 20% 的样本用 LLM Judge 复核,对比两者一致性。如果偏差超过 10%,需要检查规则是否过于粗放。双层校验的价值在于:规则确保可复现,LLM Judge 确保深度

多轮对话测试常见反模式

  1. 只在 3 轮内测试— 3 轮内几乎所有 Agent 都表现良好,测不出问题。真正的衰减从 8-10 轮开始。
  2. 用布尔判断代替连续评分— "全对或全错"丢失了大量中间态信息。部分记忆比完全遗忘更有分析价值。
  3. 忽略 Token 成本— 很多"记忆好"的策略是靠大量 Token 堆出来的。召回率提升如果伴随 Token 消耗指数级增长,需要权衡性价比。
  4. 把指代当成记忆— 指代消解是局部上下文问题,信息保持是全局记忆问题。两者衰减机制完全不同。
  5. 不区分模型能力与系统策略— 说"模型记忆不好"前,先确认是模型自身的问题,还是上下文策略/Prompt 设计的问题。
  6. 测试数据量不足— 每个轮数至少重复 3 次,否则采样温度带来的随机波动会掩盖真实衰减趋势。

下一篇讲代码能力测试——能写 hello world 和能写生产代码是两回事。


面试题模块

Q1:多轮对话测试中,你如何构造测试数据?

A:分三层:1) 短期记忆——3-5 轮内的指代理解(用户说"它"指什么);2) 中期记忆——10-20 轮的信息保持(用户在第 1 轮提的需求在第 15 轮是否还记得);3) 长期记忆——50+ 轮的衰减检测(Agent 是否随着对话轮次增加而逐渐遗忘)。

我会用自动化脚本生成 100+ 组对话轨迹,而不是手工写 case。每组轨迹包含:注入信息、干扰对话、回忆验证三个阶段。通过参数化配置(轮数、信息密度、干扰类型),覆盖不同衰减场景。

Q2:对话衰减的量化指标是什么?

A:常用"信息召回率"——在第 N 轮问用户在第 1 轮提供的信息,看 Agent 能否正确回答。但需要明确:这个指标测的是当前系统配置下的有效记忆边界,而非单纯 LLM 的上限能力。实测数据显示,qwen-plus 在采用摘要压缩策略时 20 轮后信息召回率约 85%,50 轮后降到 60% 以下(若仅使用固定窗口且无用户画像注入,衰减会显著更早)。如果目标应用需要 50+ 轮对话,需要显式地做上下文压缩或向量检索。

同时我会监控 Token 消耗随轮数的变化,因为很多"记忆好"的策略是靠 Token 堆出来的。召回率提升如果伴随 Token 消耗指数级增长,需要权衡性价比。

Q3:你遇到过最"离谱"的对话失败是什么?

A:用户在第 1 轮说"帮我分析华东区销售额",第 3 轮问"刚才说的华北区呢"——Agent 直接开始分析华北区数据,完全没纠正用户说的"刚才"其实是"华东"。这就是"用户错误前提接受"(False Premise Acceptance)——Agent 不应该接受用户明显错误的陈述。

我建议把这类失败上升为一个独立的测试类别:

  • 错误前提拒绝测试:用户给出错误前提,Agent 是否识别并纠正
  • 自相矛盾检测:用户前后陈述矛盾,Agent 是否指出
  • 隐性指令冲突:用户的新指令与旧指令隐性冲突,Agent 是否处理

这在金融 / 医疗 / 法律 Agent 里尤其致命——Agent 如果盲目接受错误前提,可能导致严重后果。

http://www.jsqmd.com/news/952562/

相关文章:

  • 从‘异步’到‘同步’:聊聊电源里MOS管如何‘卷’掉了二极管(附SP6012驱动芯片实战解析)
  • 2026年当下北京专业滚针轴承直销厂商市场格局剖析与选择指南 - 2026年企业资讯
  • Flutter上架AppStore,我踩过的permission_handler权限坑(附完整Podfile配置)
  • AEC-Q氦质谱检漏试验
  • 【2027最新】基于SpringBoot+Vue的网上服装商城管理系统源码+MyBatis+MySQL
  • UniApp微信分享卡壳?手把手教你搞定iOS Universal Links配置(HBuilderX + 苹果开发者后台)
  • 告别枯燥理论:用PyTorch+强化学习打造一个能陪你下五子棋的AI伙伴(实战教程)
  • 嵌入式Linux启动提速:手把手教你配置Buildroot生成带Ramdisk的内核镜像
  • 别再对着头皮信号发愁了!手把手教你用Brainstorm完成EEG源定位(从数据导入到结果可视化)
  • 2026年6月中山评价好的新中式高定服装加盟选哪家推荐,新中式高定服装加盟/国风源头,新中式高定服装加盟哪家好推荐 - 品牌推荐师
  • 告别拍照模糊!用Python+OpenCV手把手教你实现一个简单的自动对焦模拟程序
  • 微信小程序实战:幸运抽奖小程序
  • 婴幼儿人脸识别技术挑战与深度学习解决方案
  • 告别32位限制!手把手教你用MX Component V5在Win10/11上搞定三菱PLC通信(C#/VB.NET通用)
  • AWVS新手避坑指南:用DVWA靶场完成你的第一次Web漏洞扫描
  • 免费Steam创意工坊下载器WorkshopDL:跨平台模组下载完整指南
  • 地铁客流实时预测系统源码(Vue+Django+LSTM,含热力图与断面分析)
  • 【鸿蒙 PC三方库构建系统】SHA 库 鸿蒙PC 适配详解
  • VMware克隆三台CentOS 7虚拟机后,别忘了检查这3个网络配置!否则集群搭建第一步就失败
  • 一文讲清楚 Agent 权限怎么做:从最小权限到提示注入防护
  • 别再死记硬背BMS架构了!用一张图搞懂集中式与分布式的核心差异与选型指南
  • 告别数小时环境配置:用快马平台云端qt环境即刻开启高效开发
  • 从MobileNetV3的h-swish激活函数聊起:为什么Google要放弃Swish?手把手复现与性能对比
  • HMS Core 5.2.0实战:用Network Kit给你的App网络请求和文件传输“提提速”
  • AWVS扫描DVWA实战:从78个漏洞报告看如何优化扫描策略与结果分析
  • 吴恩达深度学习笔记:手把手教你推导深层神经网络的前向与反向传播(附矩阵维度检查技巧)
  • 如何突破文档下载限制:kill-doc一站式解决方案
  • Linux 内核中的 cgroups:从资源隔离到内存规约
  • 别再只盯着PS的GPIO了!手把手教你用Vivado配置AXI GPIO软核,点亮PL端第一个LED
  • Linux → QNX 程序移植:API 差异与适配指南