前言:
断言写好了、Case 结构化了、Mock 也分层了------你以为输入已经完全可控。但跑完一轮调整计划,再跑创建计划,9 条结果全偏了。排查半天发现:上一轮写进去的用户画像,悄悄成了这一轮的隐形输入。你控制了你能看见的输入,但没控制你看不见的那个 - 记忆。
1、一次真实坑
我先跑了调整计划的用例(修改训练日),再跑创建计划的 9 条核心用例。结果全部偏了------训练日排布、频次、甚至运动类型都跟预期对不上。
排查了半个下午,最后发现:调整计划那一轮执行时,系统把"用户将训练日调整为周一、三、五"写进了长期画像。之后跑创建计划时,模型看到的不是我 Mock 的干净输入,而是我 Mock 的输入 + 上一轮留下的画像。两边冲突,模型自己决定听谁的。
我以为在测"给定输入生成计划",实际测的是"给定输入 + 一堆历史偏好生成计划"。输入被污染了,而我完全不知道。
2、记忆是什么(两行讲清)
Agent 系统通常有两类记忆,存在数据库里,每次请求时被塞进给模型的提示词:
| 类型 | 内容 | 比喻 |
|---|---|---|
| 短期记忆(history) | 最近几轮对话流水 | "你上次说了啥" |
| 长期记忆(memory) | 用户画像、偏好、伤病、事件日志 | "你是个什么样的人" |
模型看到的不只是你这句话,它看的是:你这句话 + 你是谁 + 你刚才说过什么。
这意味着:即使你的请求参数完全一样,只要记忆不同,模型的输入就不同,输出就可能不同。
3、它怎么影响创建计划
创建计划的正式输入应该只有产品定义的那些:目标、周数、训练日、设备。评测里我们用 Mock 把这些全写死了,就是为了控制变量。
但记忆是从旁边溜进来的第二个输入:
你 Mock 的输入: 8周 / 增肌 / 周一、周四、周五
记忆里写着: 偏好训练时间:周一、周三、周五上午
模型看到的: 两个都看到了,自己决定听谁的
你以为控制了变量,其实有个你没控制的变量在悄悄起作用。Mock 再精确,如果记忆没清干净,你测的就不是"给定输入生成计划",而是"给定输入 + 未知历史生成计划"。
4、它怎么影响修改计划
修改计划的输入是三样:用户这句话 + 当前计划(Mock 的) + 记忆。记忆在两个方向同时插手:
4.1 读取方向:影响模型怎么理解你的话
理解层面: "下周""周三"这种相对时间表达,模型可能参考历史对话来定位------如果历史里有"用户之前把训练改到周三",它可能会和新请求里的"周三"产生关联。
猜测层面: 长期画像里的偏好会被顺手带上。比如画像里有一条"用户确认只用某设备,另一设备停用"。你现在跑的 Case 只想改日期,模型看到那条画像,可能顺手把设备也给你换了。你以为"改日期把设备改错了"是 Bug,其实是记忆在起作用。
4.2 写入方向:每次对话完,系统把总结写进记忆
更麻烦的是写入方向。每次对话结束后,系统会自动把这轮的摘要写进长期记忆。而它写得不一定准:
实际发生的:用户问了"能不能改成周一三五",没点确认,没保存
记忆写的: 用户将后续训练日调整为每周一、三、五,系统已保存 ← 编的
实际发生的:这只是一个未来的训练安排
记忆写的: [2026-09-02] 运动:低强度恢复骑行 ← 未来的事写成已发生
于是上一条 Case 的执行结果(甚至是错误的总结),变成了下一条 Case 的前提。
5、为什么这对测试特别致命
5.1 同一条 Case 不可复现
第 1 次跑 Case-01 → 输入 = 用户话 + 计划 + 空记忆
第 2 次跑 Case-01 → 输入 = 用户话 + 计划 + 第 1 次留下的记忆 ← 输入变了
同一条 Case,两次输入不同,结果不可复现。断言是基于"相同输入应得到相同结果"这个前提的------如果输入本身在变,断言就失去意义了。 你的 PASS/FAIL 不反映产品质量,反映的是"这次记忆里恰好有什么"。
5.2 污染跨套件
这更要命:不同测试套件之间会互相污染。调整计划写的画像,会影响创建计划;A 账号的用例结果,如果复用同一个账号跑 B 套件,B 的输入就不干净了。
你以为每个套件是独立的,其实它们通过记忆这个隐形通道连在一起。
5.3 污染是累积的
第 1 条 Case 跑完,写入 1 条记忆。 第 2 条 Case 跑完,写入 2 条记忆。 第 15 条 Case 跑完,记忆里堆了 15 条历史。
越往后跑,记忆里的噪声越多,结果越不可信。 如果你不在每条 Case 前清记忆,你的第 15 条 Case 的输入里包含了前 14 条的所有副作用------你测的根本不是第 15 条 Case 本身,而是"前 14 条的残留 + 第 15 条"。
6、但:目前观察到的影响有限
老实说,在当前这一轮实测中:
- 清干净记忆跑某条 Case,和带着脏记忆跑,结果逐字段对比完全一样
- 修改计划这条链路,大多数时候压根不写记忆(15 次执行里只清出 2 次有东西)
所以准确的说法是:记忆是个不受控的变量,理论上会影响结果,当前这几条 Case 上没有观测到它真的改变输出。
但不能因此说它无害------它是随时可能起作用的变量。 换一个账号(长期画像更丰富的老用户),换一个模型版本(对记忆更敏感的),换一个 Case(刚好触发了画像里的某条偏好),它可能就发作了。
测试的规矩是:把不受控的变量摁住,让结果可复现,而不是赌它今天不发作。
7、怎么处理:三步摁住记忆
7.1 每条 Case 前清记忆
这是最基本的。跑每条 Case 之前,清掉该账号的所有短期和长期记忆,让模型看到的只有你 Mock 的输入,没有任何历史残留。
清记忆
→ 验证清干净(查询记忆条数 = 0)
→ 发请求
→ 跑断言
"验证清干净"这一步不能省。 我们实测遇到过清记忆脚本报成功、但实际没清干净的情况(依赖没装好、环境变量没设)。如果你只清不验,你以为输入是干净的,其实不是------这比不清还危险,因为你带着错误的信心在看结果。
7.2 跨套件隔离
不同测试套件(创建计划、调整计划)用不同账号,或者确保每个套件开始前完整清一次所有测试账号的记忆。
我们这次踩的坑就是:先跑了调整计划(写了画像),再跑创建计划(读到了画像)。如果两个套件用不同账号,这个污染就不会发生。如果必须用同一个账号,那跑完调整计划后、跑创建计划前,必须清一次。
7.3 第一条 Case 清记忆失败 → 整批终止
我们实测还有一个教训:清记忆失败时,脚本没有终止,而是继续跑了 15 条------全部带着脏记忆跑完,全部结果不可信,白跑了一下午。
正确的做法是:第一条 Case 清记忆失败 → 立即终止整批,打印错误原因。 不要让 15 条 Case 在脏环境里逐条重复失败,那不是测试,是浪费时间。
8、记忆还暴露了一个更深的问题
清记忆只是"摁住变量"。但记忆这件事本身,暴露了 Agent 评测一个更深的挑战:Agent 的输入不是你发的那一句话,而是"你这句话 + 系统悄悄塞进去的一切"。
记忆是一种"悄悄塞进去的输入"。但它不是唯一一种:
| 隐形输入 | 例子 | 对评测的影响 |
|---|---|---|
| 长期记忆 | 用户画像、历史偏好 | 本文重点 |
| 短期记忆 | 最近几轮对话 | 多轮场景中前一轮的回答影响后一轮 |
| 系统时间 | "下周四"→ 具体哪天取决于今天几号 | Case 里写死时间会过期(前一篇讲过) |
| 账号状态 | 已有的计划、订阅状态、设备绑定 | 不同账号跑同一个 Case,结果可能不同 |
| 模型版本 | 同一个 prompt,不同模型版本输出不同 | 模型升级后回归结果波动 |
所有这些"隐形输入",都需要在评测时显性地控制或记录。 记忆要清、时间要相对化、账号要隔离、模型版本要锁定。如果你只控制了"你发的那句话"和"你 Mock 的数据",而忽略了这些从旁边溜进来的东西,你的评测结果就不完全可信。
这也是为什么 Agent 评测比传统接口测试难------不是断言难写,是输入本身就不完全在你手里。 传统接口测试,你发什么参数,接口就收到什么参数,干干净净。Agent 评测,你发的只是输入的一部分,系统还会自己加料。你要做的不只是"控制你发的",还要"摁住系统自己加的"。
9、总结
Agent 评测的前提不是"断言写对了",而是"输入是干净的"。断言再好、Case 再结构化、Mock 再分层,如果记忆没清、时间在变、账号在串,你测的就不是你以为在测的东西。
先保证输入可控、可复现,然后断言才有意义。这是所有评测方法论的第零步------前面几篇讲的三层断言、松紧度、Mock 分层,全都建立在这一步之上。