Agent 测试面试宝典(五):上下文、记忆与会话测试——别让 Agent 把“前任”认成现任
2026/9/7 22:25:25 网站建设 项目流程

很多 Agent 单轮对话时聪明得像学霸:

用户:北京天气怎么样?
Agent:正在为你查询。

一进入多轮会话,画风突然变了:

用户:那上海呢?
Agent:请问你想查询上海的什么?

更刺激的是:

  • A 用户问的内容,跑到了 B 用户的回答里;
  • 新会话已经创建,Agent 还惦记着上一轮;
  • 用户把“预算 5000”改成“预算 8000”,Agent 坚持按 5000 推荐;
  • 页面传了一遍聊天记录,Checkpointer 又补了一遍,模型看到两份完全相同的历史。

这已经不是“记性不好”,而是上下文、状态和会话管理失控


一、先分清:上下文、短期记忆和长期记忆

测试前,先别把所有历史数据统称为“记忆”。

概念主要作用常见生命周期示例
页面聊天记录展示消息页面或本地存储用户看到的历史气泡
当前上下文提供给模型推理单次请求System Prompt、历史消息
Checkpointer保存运行状态会话级消息、节点状态、工具结果
长期记忆跨会话保存信息用户级或业务级用户偏好、常用地址
业务数据保存真实业务状态由业务决定订单、工单、审批记录

一个重要原则

页面看到了历史,不等于模型拿到了历史;模型拿到了历史,也不等于系统应该永久记住历史。

例如:

  • “我刚才说的预算是多少?”依赖当前会话上下文;
  • “以后都用中文回复我。”可能属于长期偏好;
  • “帮我创建了一张工单。”属于业务数据;
  • “工具节点已经执行到哪一步。”可能由 Checkpointer 保存。

测试工程师要验证的不只是“记住没有”,还要验证:

  1. 该记的是否记住;
  2. 不该记的是否清除;
  3. 更新后的信息是否覆盖旧信息;
  4. 不同用户、租户和会话之间是否隔离。

二、单轮对话测试:先确认每一轮能独立站稳

单轮测试主要验证:只根据当前输入,Agent 能否给出正确响应。

例如:

用户:查询订单 A1001 的物流状态

需要检查:

  • 是否正确理解当前问题;
  • 是否携带正确用户身份;
  • 是否调用正确工具;
  • 是否引用其他会话中的信息;
  • 当前请求结束后是否错误写入长期记忆。

典型反例

用户第一次打开系统:

用户:帮我推荐一款手机。 Agent:按照你之前 5000 元的预算……

如果这是新用户、新会话,而且用户从未提供过预算,那 Agent 很可能:

  • 读取了错误的用户记忆;
  • 复用了测试环境中的历史状态;
  • 使用了共享缓存;
  • 出现了会话串线。

这类问题的危险程度,远高于“回答不够优雅”。


三、多轮对话测试:Agent 能不能听懂“它、那个、刚才的”

多轮对话的核心,是验证 Agent 能否正确解析上下文中的省略、指代和条件。

基础用例

用户:查询北京今天的天气。 Agent:北京今天晴,最高温度 28℃。 用户:那上海呢?

第二轮中的“上海”应继承:

  • 查询动作:天气查询;
  • 时间条件:今天;

同时覆盖:

  • 城市:北京 → 上海。

测试矩阵

场景第一轮后续输入预期
指代继承推荐电脑 A它支持扩展内存吗“它”指向电脑 A
条件补充推荐北京酒店要能带宠物保留北京,增加宠物条件
条件修改预算 5000改成 8000使用 8000
条件删除只看五星酒店不限制星级了移除星级条件
对象切换查询订单 A再看看订单 B后续围绕订单 B
话题切换查询天气帮我写请假邮件不应继续调用天气工具
回到旧话题先聊订单,再聊天气回到刚才的订单正确恢复订单上下文

多轮测试不能只验证 Agent“还记得”,还要验证它是否知道:

什么时候继承、什么时候覆盖、什么时候清空。


四、条件修改测试:新信息必须压住旧信息

Agent 最常见的上下文 Bug,是“初恋条件忘不掉”。

用户:帮我推荐一台 5000 元以内的笔记本。 用户:预算改成 8000 元。 用户:主要用于视频剪辑。

最终条件应为:

{"budget":8000,"usage":"video_editing"}

而不是:

{"budget":[5000,8000],"usage":"video_editing"}

更不能在推荐理由中继续写:

考虑到您的预算不超过 5000 元……

重点测试三种更新方式

1. 覆盖
预算从 5000 改成 8000

预期:旧预算失效。

2. 追加
还需要支持触控

预期:保留原条件,新增触控要求。

3. 删除
不限制品牌了

预期:移除品牌限制,而不是把品牌设置成字符串“不限制”。

自动化断言示例

state=get_agent_state(thread_id)assertstate["budget"]==8000assertstate["usage"]=="video_editing"assert5000notinstate.get("active_constraints",[])

除了检查最终回答,还要检查 Agent 内部的结构化状态。否则回答看着正常,下一轮可能又把旧条件“考古”出来。


五、新建会话测试:开新聊天,不是换个聊天框继续失忆式表演

点击“新建会话”后,通常应生成新的thread_idsession_id

测试步骤:

会话 A:我的预算是 8000 元。 会话 A:记住我主要做视频剪辑。 新建会话 B:我的预算是多少?

预期结果取决于产品定义:

  • 如果预算只属于会话上下文:会话 B 不应知道;
  • 如果预算已明确写入长期记忆:会话 B 可以知道;
  • 如果产品没有长期记忆功能:绝不能凭空回答 8000 元。

测试时必须先确认记忆边界

建议产品明确以下规则:

信息是否跨会话
当前任务的临时条件
未完成工作流状态根据产品设计
用户语言偏好可以
用户明确要求记住的信息可以
敏感数据默认不应长期保存
其他用户的信息永远不可以

“新建会话”不是简单清空页面,而应同步处理:

  • 前端消息列表;
  • 后端会话标识;
  • Checkpointer 状态;
  • 缓存键;
  • 工具调用中的会话参数。

六、用户与租户隔离:串线一次,安全同学连夜上线

1. 不同用户隔离

准备两个用户:

用户 A:我的项目代号是“猎鹰”。 用户 B:我的项目代号是什么?

用户 B 不应得到“猎鹰”。

需要重点检查:

  • 缓存是否只按thread_id区分;
  • thread_id是否可能被用户猜测;
  • 服务端是否校验会话归属;
  • 长期记忆查询是否包含user_id
  • 日志和向量库是否混用了用户数据。

2. 不同租户隔离

多租户系统中,推荐使用类似的隔离键:

tenant_id + user_id + thread_id

不能只依赖:

thread_id

因为不同租户可能存在相同用户名、相同业务编号,甚至相同的会话编号。

高危测试用例

场景操作预期
修改用户身份用 B 用户访问 A 的 thread_id拒绝访问
修改租户标识将 tenant_id 从 T1 改成 T2无法读取 T1 数据
猜测会话编号枚举相邻 thread_id不返回其他会话
并发登录A、B 同时发送请求上下文严格隔离
缓存命中两个用户发送相同问题不复用私有上下文

会话串线不是普通功能 Bug,而是可能升级为数据泄露漏洞


七、长对话测试:Token 满了,谁先被请出会议室

模型上下文窗口不是无限的。随着对话增长,系统通常会采用:

  • 删除最早消息;
  • 保留最近 N 轮;
  • 压缩历史为摘要;
  • 提取关键条件保存到结构化状态;
  • 按相关性检索历史消息。

测试重点

在长对话开头设置关键条件:

第 1 轮:不能使用含花生的食材。 第 2~50 轮:讨论菜谱、人数、时间和价格。 第 51 轮:请输出最终菜单。

即使第 1 轮原始消息已被截断,最终菜单仍不能出现花生。

哪些信息不应该随意截断

  • 安全限制;
  • 用户明确约束;
  • 当前任务目标;
  • 已确认的关键参数;
  • 审批状态;
  • 工具执行结果;
  • 不可重复执行的业务操作记录。

建议验证三个层次

  1. 原始历史是否被截断;
  2. 关键条件是否被摘要或结构化保留;
  3. 摘要是否出现信息失真。

例如,原条件是:

预算不能超过 8000 元

摘要却变成:

预算约 8000 元

“不能超过”和“大约”只差几个字,账单可能差很多钱。


八、并发请求测试:别让两条消息在会话里“抢方向盘”

用户可能连续点击两次发送:

请求 A:把目的地改成上海。 请求 B:把出发日期改成下周一。

如果两个请求同时处理,可能出现:

  • 后写入覆盖先写入;
  • 状态只保留一个修改;
  • 消息顺序错乱;
  • 同一个工具被调用两次;
  • Checkpoint 版本冲突;
  • A 的结果使用了 B 尚未提交的状态。

常见处理策略

  • 同一会话请求串行执行;
  • 使用状态版本号进行乐观锁校验;
  • 为每个请求生成唯一request_id
  • 工具调用使用幂等键;
  • 冲突时重试或提示用户;
  • 明确消息排序规则。

自动化测试示例

results=awaitasyncio.gather(send_message(thread_id,"目的地改成上海"),send_message(thread_id,"出发日期改成下周一"))state=get_agent_state(thread_id)assertstate["destination"]=="上海"assertstate["departure_date"]=="下周一"assert_no_duplicate_tool_calls(results)

并发测试不要只跑一次。很多竞态条件平时像隐士,压测时才突然下山。


九、应用重启测试:服务重启后,Agent 是续聊还是失忆

应用重启后的行为取决于 Checkpointer 的存储方式。

内存型 Checkpointer

特点:

  • 实现简单;
  • 适合本地调试;
  • 服务重启后状态通常丢失;
  • 多实例之间无法天然共享。

持久化 Checkpointer

可能使用:

  • 数据库;
  • Redis;
  • 专用状态存储;
  • 框架提供的持久化组件。

需要验证:

  1. 应用重启后是否恢复正确会话;
  2. 是否从正确节点继续执行;
  3. 已成功调用的工具是否重复调用;
  4. 中断时的半完成状态是否安全;
  5. Checkpoint 版本是否兼容新代码;
  6. 恢复失败时是否有降级策略。

尤其是支付、发邮件、创建工单等副作用操作,恢复时不能再来一遍:

Agent:刚才支付成功了。 Agent 重启后:怕您没付够,我又帮您支付了一次。

这不是贴心,这是事故。


十、页面历史与 Checkpointer:最容易“双份投喂”

假设 Checkpointer 中已经保存:

用户:我的预算是 5000 元。 Agent:好的。

下一轮页面又把完整历史提交给后端:

{"messages":[{"role":"user","content":"我的预算是5000元"},{"role":"assistant","content":"好的"},{"role":"user","content":"改成8000元"}]}

后端再从 Checkpointer 恢复旧消息,就可能变成:

我的预算是 5000 元 好的 我的预算是 5000 元 好的 改成 8000 元

可能造成的后果

  • 消息重复;
  • Token 消耗翻倍;
  • 模型误判用户反复强调旧条件;
  • 工具调用记录重复;
  • 上下文更快达到窗口上限;
  • 调试日志与真实会话不一致。

推荐方案

明确谁是运行时上下文的唯一事实来源。

方案一:Checkpointer 管历史

页面每次只提交:

{"thread_id":"t_1001","message":"预算改成8000元"}

后端根据thread_id恢复历史。

方案二:客户端传完整历史

后端不再自动拼接同一份 Checkpointer 消息,或只将 Checkpointer 用于其他状态恢复。

方案三:消息去重

为每条消息设置唯一标识:

{"message_id":"msg_20250308_001","request_id":"req_10086","content":"预算改成8000元"}

服务端根据消息 ID、请求 ID 和 Checkpoint 版本进行去重。


十一、高频面试题:页面已经保存聊天记录,为什么还需要 Checkpointer?

推荐回答

页面聊天记录和 Checkpointer 的职责不同。

页面状态主要用于展示聊天内容,可能保存在浏览器内存、本地存储或前端状态管理中;它不能可靠表示 Agent 后端的完整运行状态。

Checkpointer 主要用于保存 Agent 运行过程中的状态,例如:

  • 多轮消息上下文;
  • 当前工作流节点;
  • 工具调用结果;
  • 中断与审批状态;
  • 可恢复的执行进度。

如果只依赖页面历史,刷新页面、切换设备、服务端恢复任务时,Agent 可能无法继续之前的运行流程。

但如果 Checkpointer 已经保存历史,页面又在每次请求中重复提交完整消息,就可能造成:

  • 上下文重复;
  • Token 浪费;
  • 状态不一致;
  • 重复执行工具。

因此系统应明确上下文的唯一事实来源,并通过thread_idmessage_idrequest_id和版本控制保证状态一致性。

一句话版本

页面历史负责“给人看”,Checkpointer 负责“让 Agent 接着干”;两边都传完整历史,就可能让 Agent 把同一段剧情看两遍。


十二、上下文与会话测试矩阵

测试维度测试内容核心断言
单轮对话独立请求不引用无关历史
多轮继承指代、省略、条件补充正确继承有效信息
条件修改覆盖、删除、追加最新条件生效
话题切换新任务进入不错误继承旧参数
新建会话创建新 thread临时上下文清除
长期记忆跨会话偏好仅恢复允许的信息
用户隔离A、B 用户并行数据不串线
租户隔离T1、T2 相同业务编号租户数据严格隔离
长对话超过上下文窗口关键条件不丢失
并发请求同一会话同时写入无覆盖、无污染
应用重启中断后恢复状态正确且副作用不重复
历史去重页面与后端同时存储消息不重复
删除会话删除后重新访问无法恢复已删除数据
权限变化用户权限被收回历史权限同步失效

十三、自动化断言应该检查什么

不要只断言回答文本中出现某个关键词。

建议至少覆盖以下四层。

1. 回复层

assert"8000"inresponse.contentassert"5000"notinresponse.content

2. 状态层

assertcheckpoint["budget"]==8000assertcheckpoint["thread_id"]==expected_thread_id

3. 调用层

asserttool_call.arguments["budget"]==8000asserttool_call.arguments["user_id"]==current_user_id

4. 存储与隔离层

assertquery_memory(user_b).does_not_contain(user_a_secret)assertcheckpoint.tenant_id==current_tenant_id

还可以增加这些通用断言:

assert_no_duplicate_messages()assert_no_duplicate_tool_calls()assert_message_order_is_correct()assert_checkpoint_version_increases()assert_sensitive_data_not_cross_session()

十四、更多高频面试题

1. 如何测试 Agent 使用的是最新信息?

先设置旧条件,再通过覆盖、删除和追加分别修改,最后检查:

  • 最终回答;
  • 工具参数;
  • Checkpoint 状态;
  • 摘要内容;
  • 后续轮次引用结果。

不能只看当前一轮,因为旧条件可能在下一轮“诈尸”。


2. 如何测试会话隔离?

建立多个用户、租户和会话,同时写入容易辨认的唯一标记,然后交叉访问。

例如:

T1-U1:标记 ALPHA T1-U2:标记 BETA T2-U1:标记 GAMMA

分别查询历史,确保只能获取自己权限范围内的数据。同时尝试篡改thread_iduser_idtenant_id


3. 长对话超出上下文窗口怎么办?

常见方案是历史截断、摘要压缩、结构化状态保存和相关性检索。测试重点不是要求保留所有原文,而是验证:

  • 关键约束不能丢;
  • 摘要不能篡改原意;
  • 旧信息被修改后不能重新生效;
  • 安全条件要比闲聊内容具有更高保留优先级。

4. Checkpointer 和长期记忆有什么区别?

Checkpointer 通常保存某个会话或工作流的运行状态,重点是“任务如何继续”。

长期记忆通常保存跨会话使用的用户信息,重点是“未来还要记住什么”。

简单理解:

  • Checkpointer:上次任务干到哪了;
  • 长期记忆:这个用户长期喜欢什么。

二者的生命周期、权限边界和清理策略都应分别测试。


十五、面试综合回答模板

我会从上下文正确性、状态更新、会话隔离、长对话处理、并发一致性和恢复能力几个方面测试。

首先验证单轮与多轮对话,包括指代解析、条件继承、覆盖、追加和删除,确保 Agent 始终使用最新有效信息。

其次验证新建会话后的清理策略,以及长期记忆和临时上下文的边界。

在安全方面,我会重点测试不同用户、不同租户和不同 thread 之间的数据隔离,并尝试篡改会话标识进行越权访问。

对长对话,我会验证截断、摘要和结构化状态是否保留关键约束。对并发和应用重启,则检查状态覆盖、Checkpoint 冲突、恢复正确性及工具调用幂等性。

最后,我会检查页面聊天历史与 Checkpointer 的职责边界,避免重复提交历史造成消息重复、Token 浪费和工具重复执行。


十六、速记口诀

上下文与记忆测试,可以记住这五句:

单轮不乱猜,多轮能接话;
旧条件让位,新会话清家;
用户不串门,租户不串线;
长聊不丢重点,并发不打架;
页面负责展示,Checkpoint 负责续命。

Agent 真正可靠的表现,不是“什么都记得”,而是:

该记的记得住,该忘的忘得掉,改过的别反悔,别人的绝不碰。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询