Agent PR中的缓存键缺陷:跨租户数据泄露与测试验证

拆解一个Agent生成缓存PR中的跨租户泄露、失败缓存与无界进程本地字典三大缺陷及十分钟复现方案。
文章以一个凌晨两点提交、测试全绿的小型缓存PR为切入点,揭示其通过审查的假象。该PR在complete()中接受tenant_id却未将其纳入缓存键,导致相同prompt在不同租户间共享缓存条目,构成直接的跨租户数据泄露;同时空值或失败结果被无条件缓存,把上游瞬时抖动放大为持久故障;模块级字典还造成多worker部署下缓存既不共享又无界增长。文章解释了为何happy path测试无法暴露这些问题,并给出十分钟内可复现的修复路径:写隔离测试、把tenant_id与model纳入键、证明失败不被缓存、测量跨worker命中率。最终主张Agent PR只是草稿,审查应将直觉转化为可运行的失败测试。
凌晨两点,一个PR被提交上来,附带绿色通过标记,只改了三个文件。它的声明很克制:跳过重复的模型调用,为重复的prompt降低延迟。审查者的第一反应是信任测试套件。但测试只覆盖了happy path,而这恰恰是缓存变更最不容易出错的地方。

本文以这个Agent生成的PR为例,拆解其中隐藏的三个缓存缺陷,并给出一个十分钟内可复现、可验证的修复方案。
被审查的Diff:一眼可见的问题
核心代码很短:一个模块级的CACHE字典,complete()函数接受prompt、tenant_id和model参数。缓存键是prompt的SHA-256哈希,命中则返回,未命中则调用模型并写入缓存。
三处问题在第一次阅读时就已浮现。tenant_id参数被接受却从未被使用;没有任何检查判断响应是否可用就将其存入缓存;字典没有大小限制、没有TTL、没有淘汰路径。
这个diff之所以能通过审查,是因为它小且可读。但小且可读,并不等于正确。
为什么Happy Path测试会通过
随附的测试用同一个prompt和同一个租户调用了两次complete(),断言模型客户端只被调用了一次。
这个断言是正确的。而且在修复以下所有缺陷之后,它仍然保持正确。这就是为什么这个测试套件给审查者提供不了任何信号——它从未触及缓存系统真正脆弱的地方:边界条件、失败路径、并发场景。
现有测试从未触及的三个缺陷
1. 缓存键忽略租户身份
缓存键仅仅是prompt文本的哈希。两个发送相同字符串的租户共享同一个缓存条目。
对于类似"Summarize the incident report."这样的prompt,碰撞不是理论上的可能性。客服工具、模板和系统prompt每天都在不同账户间重复。在一个多租户产品中,这意味着租户A可以读取到租户B的响应内容——这是直接的跨租户数据泄露。
2. 空白和失败结果被缓存
如果call_model因上游瞬时故障返回None或空字符串,这个值会被当作答案存储。之后所有调用者都会读到这个失败结果,直到进程重启。
负面缓存把一个一秒的抖动转化为数小时的故障。这个diff从未区分好答案和坏答案。
3. 缓存是进程本地且无界的
四个worker意味着四个独立的字典和四个独立的命中率。内存随唯一prompt数量增长,且永不收缩。
这个diff看起来像一个共享缓存,行为上却像一个没有橡皮擦的每个worker各自的笔记本。
什么该保留,什么该回退,什么该测试
面对这样一个PR,决策可以归纳为五条:
- 保留纯函数。
hashlib.sha256是合理的键原语。 - 保留
tenant_id参数,并将其纳入键的计算。 - 回退模块级可变字典。
- 回退任何空、失败或不完整响应的写入。
- 用隔离、失败和并发测试替换单一的happy-path测试。
十分钟可运行的复现方案
第一步:先写隔离测试
测试的核心是创建一个假的call_model,记录每次调用传入的tenant_id。然后用同一个prompt分别以"acme"和"globex"两个租户调用complete(),断言两次调用都到达了模型层,且返回值不同。
运行pytest -k tenant -x,它会失败。这个失败就是证据,不是观点。
第二步:修复键并重新运行
修复方案是将tenant_id、model和prompt三者用null字节分隔符连接后再哈希。null字节分隔符防止了歧义拼接。同一个测试无需修改就应通过。
第三步:证明失败不被缓存
通过monkeypatch让call_model抛出TimeoutError,连续调用两次complete(),断言底层被调用了两次。如果异常被缓存,只会产生一次调用。
第四步:测量跨worker命中率
对本地端点发送四十次相同prompt的请求,统计日志中model_call的出现次数。如果计数接近四十,说明缓存在worker之间没有共享。
决策表:每一项变更的裁决
| 变更项 | 裁决 | 理由 |
|---|---|---|
cache_key辅助函数 | 保留 | 小、纯、易测试 |
键中包含tenant_id | 保留且强制 | 阻断跨租户读取 |
模块级CACHE字典 | 回退 | 进程本地、无界、无TTL |
| 缓存空或失败结果 | 回退 | 把抖动变成持久故障 |
| 单一happy-path测试 | 替换 | 从未覆盖隔离或失败场景 |
复用这套审查循环
说个细节,生成这个diff的Agent运行在MonkeyCode的免费模型访问上。上述复现套件运行在该项目的免费服务器选项上,足以在一次会话中完成数百次测试调用。
运营方表示免费层还包含促销token额度,目前宣传为1000万tokens。在围绕任何数字做规划之前,请先确认仓库中的当前条款。披露:本文是作为MonkeyCode产品推广的一部分准备的。
这里模型不是重点。循环才是:生成、复现、回退、重测。
局限性:谁应该跳过这套方案
有几类团队需要格外谨慎。有严格数据驻留要求的团队不应将prompt发送到任何托管端点,无论免费还是付费。多租户产品的缓存键必须同时从租户、模型和prompt版本派生。进程本地字典仅适用于单worker原型,在其他任何部署中都是错误的。如果命中率在大规模场景下重要,应使用带TTL、淘汰策略和大小上限的共享存储。
本文的审查不尝试验证模型输出质量,只检查一个租户能否读到另一个租户的答案。
结语
Agent PR是草稿,不是决策。信任纯函数,回退共享可变状态,并在做任何其他事之前测试租户边界。这套审查循环的价值在于它把"我觉得有问题"变成了"运行这个测试,看它失败"。在一个Agent越来越频繁地生成代码的世界里,这种从直觉到证据的转化,比任何单个缓存修复都更重要。
相关推荐

零成本搭建AI超级员工系统:智能体自动工作流全解析
深度拆解B站演示的AI超级员工系统:Agent智能体编排、数字人口播、地图与企业获客、24小时智能客服及法务模块全覆盖。分析零成本开户背后的商业逻辑与合规风险,帮助创业者判断这套AI数字员工系统的真实投产比。

Chatbot与AI Agent的本质区别:从对话到执行的跨越
Chatbot和AI Agent有什么区别?本文基于Learn Tech频道的MCP系列讲解,用订机票、笔记摘要等实例说明:Chatbot只给答案,Agent通过思考-行动-观察循环真正完成任务,并拆解模型、工具、记忆、目标四大部件及MCP的桥梁作用,同时讨论Agent的适用边界与权限风险。

免费GitHub仓库:342小时AI课程,从数学到MCP服务器
一个免费GitHub仓库提供342小时AI课程,包含523节课、20个阶段,覆盖数学基础到Agent构建和MCP服务器。支持目标驱动学习,每节课产出可留存的prompt、skill或agent,已有近10万人开始学习。