资讯动态

AI 去后台导个账,它把 A 公司的数据记到了 B 公司名下——一个过期上下文的翻车复盘

发布时间:2026/10/23 2:41:42 来源:尧图企业网站定制
本文是「Agent 工程实战」系列的第 10 篇属于界面线。系列索引21 篇阅读路线Agent 放权前先补权限、出站、发现这三道口子摘要 Agent 在多租户后台长跑时界面适配只能保证点得对却兜不住以为的自己是哪个自己。本文用一个沙箱演练复盘了这类翻车运营账号从 A 公司切到 B 公司后Agent 仍带着缓存的 orgId 组装请求最终导出文件名是 B、数据范围却是 A的错乱文件。根因是错上下文——Agent 把界面展示当成服务端会话里的事实。解法是把当前身份从靠猜变成可确认跨上下文写操作前强制核对会话身份、把当前租户显性化到界面可读、关键写操作走结构化接口。方案有边界真实环境身份模型更复杂、结构化通道不解决身份被顶替、本案例属上下文一致性而非越权。文章目录从点得对到以为自己是谁现象切换租户后它带着上一家的上下文干完了活排查回放里它每一步都很对根因它看到的 ≠ 它该用的解决把当前身份变成一次可核对的动作边界这套方案的适用边界回头看从点得对到以为自己是谁上一篇聊后台界面适配讲了感知层、操作层、反馈层三层。有读者问了个很扎的问题你前面说的都是Agent 点得对可万一它点得挺对但整件事都搞错了界面适配还能兜住吗我觉得问得好。界面的三层层层加码护的是动作执行这一环。可 Agent 干活的第一前提不是动作是上下文——它得知道我是谁、我面前是哪家公司、我现在处于什么状态。OpenAI 的 Agents API 上线那天我在 HN 上刷到官方博客把自动压缩上下文、长会话单独拎出来当主打能力讲。这让我想起前阵子在隔离沙箱里做的一次演练。当时搭完两个租户的后台正准备演示结果我差点把演示搞成事故现场。下面这个翻车案例就是在那个沙箱里复现出来的——是演练不是生产。现象切换租户后它带着上一家的上下文干完了活沙箱里搭的是个典型的多租户后台A、B 两家公司共用同一套系统运营进系统后通过右上角切换租户在不同公司之间切换。登录态在服务端会话里界面上有一个当前租户的展示右上角公司名 URL 里的组织路径。技术栈就是常规的 Node 22 Express 5.x 服务端 session没有引入任何特殊组件——为了复现一个只靠标准 Web 会话就能出现的场景。演练任务很简单让 Agent 把上个月的订单导出成 Excel。前面几步很顺利。Agent 用 A 公司身份登录选中筛选条件点导出界面提示成功。然后我手动在后台切到了 B 公司——这一步是关键。URL 变了/org/a-company/… → /org/b-company/…页面顶部公司名也变成了 B 公司。我没有刷新整页而是继续让 Agent 执行第二趟“把上个月的订单再导一份”。Agent 听话地打开了导出页选了筛选条件点导出界面提示成功。结果拿到文件就傻眼了文件名里是 B 公司打开里面的数据却混着 A 公司的订单。一个文件里两家的账记到了一起。这件事到底怎么发生的画成链路是这个样子A 登录会话A 我切到 BURL/页头变 B Agent 继续干 │ │ │ ▼ ▼ ▼ 缓存 orgIda-company 页面展示 b-company 请求里还是 a-company │ │ │ └──────── 界面展示它看见≠ 服务端会话它看不见 ─────────┘ → 文件名为 B数据范围却是 A排查回放里它每一步都很对第一反应是看操作回放。我担心它是不是点错了页面、选错了筛选——毕竟上次沙箱里看它点串过行。回放很清楚每一步都没点错。它打开的就是 B 公司的导出页筛选条件也没选歪。我把 Agent 当时拿到的页面快照调出来一行一行跟它的动作对齐——它每一步依赖的信息都指向我还在 A 公司。再往后挖。Agent 导出的时候需要知道当前是哪家公司来填请求参数。它看了一眼手里能拿到的信息URL 里有组织路径页面右上角有公司名。可它取的不是当前这次请求的页面状态而是它进入角色时缓存下来的那份上下文——第一次干活时从 A 公司页面拿到的那个 orgId组织标识就是 URL 里/org/后面那一小段。于是查询参数里带的还是 A 公司的组织标识请求却落在了 B 公司的导出接口上。服务端权限校验没任何问题——它本来就是 A、B 两家都有权限的运营账号。校验全过了只是“给出 B 的文件名装 A 的数据范围”。我猜它是这么推理的URL 变了、标题变了但当前组织这个核心上下文没跟着刷新它照旧用。至于为什么它没用页面新状态——你看它的处理里根本没有确认当前身份这个步骤它默认上下文没变过。根因它看到的 ≠ 它该用的复盘下来问题的核心不是界面点错了是三个字错上下文。Agent 判断我是谁用的是它眼睛看到的东西——URL、标题、公司名文字。这些是界面展示是给人看的。而决定这次请求到底归哪家的是服务端会话里维护的当前租户那个它看不见。人的经验里有个肌肉记忆切完租户下意识瞄一眼右上角心里核一遍我现在是 A 还是 B然后才放心操作。Agent 没有这个。它把界面上的展示当成了服务端的事实而且一次性拿到的旧值会一直伴随到会话结束。顺着这个案例往下理我把 Agent 干活时容易踩的隐形上下文分成了三类一张表说清类型Agent 能看到的Agent 该用的翻车后果身份我是谁URL、右上角公司名、下拉值服务端会话的当前账号/租户数据混租户、操作落错对象环境在哪个环境/阶段页面上的环境角标、地址栏域名服务端路由/发布状态在测试环境以为在生产、改错列表瞬时状态加载中/禁用/弹窗按钮文案、页面形态真实请求是否完成、控件是否可点重复提交、把半成品当成品三类都有同一个根看不见的服务端事实 ≠ 界面上看得见的展示而 Agent 只认眼见的。解决把当前身份变成一次可核对的动作排查阶段最容易翻车的修法我也踩过——一开始我想着既然 Agent 拿错 orgId那就把缓存清掉、让它在每次导出前重新读一遍页面右上角的公司名// 错误尝试让 Agent 每次点导出前重新读一遍页面右上角当前租户// 看似解决了旧值残留但读的还是界面渲染出来的展示不是服务端会话asyncfunctionexportMonthlyWrongFix(month){constorgIdreadCurrentTenantFromPage();// 读的是页面显示的公司名constresawaitfetch(/org/${orgId}/reports/export,{method:POST,body:JSON.stringify({month}),});returnres.json();}在沙箱里这么改完跑了两轮 第一轮页面稳定显示 B正常 await exportMonthlyWrongFix(上个月) { ok: true, file: b-company-orders.xlsx, rows: 27 } 第二轮切换瞬间页面顶部公司名还没刷成 B又错了 await exportMonthlyWrongFix(上个月) { ok: true, file: b-company-orders.xlsx, rows: 180 } rows 多了一大截仔细看A 公司的订单又混进来了 为什么页面右上角在切换后是异步刷新的过渡帧里还挂着 A 的名字。 Agent 读页面→读到当前是 A→请求里带 A 的组织标识→错。 你以为清掉了旧缓存其实只是换了个更隐蔽的方式让它再次读到旧值。这个方向死路一条原因就是前面那张表说的——界面展示永远可能跟不上服务端会话。你要的不是让 Agent 正确读到展示而是让 Agent 停止拿展示当身份把它变回一次可核对的动作让 Agent 每次跨上下文操作前重新确认而不是靠它自己推理。第一件事触发前提核对。切换租户属于上下文边界操作——跨过这道边之后的第一笔写操作必须强制重新确认当前身份。做法很土但有效给 Agent 一个专门查会话身份的工具切完租户后必须调用一次拿到当前租户是 X的确认才允许继续。// 正确做法身份由服务端会话决定前端只展示不传参// 写操作前先调用一个查当前会话身份的工具拿到确认再继续asyncfunctionexportMonthly(month){constsessionawaitapi.getCurrentSession();// 这一步叫触发前提核对if(session.tenant!expectedTenant){thrownewError(当前租户是${session.tenant}和预期不一致中止);}constresawaitfetch(/api/reports/export,{method:POST,body:JSON.stringify({month}),// 不再携带 orgIdheaders:{Content-Type:application/json},});returnres.json();}修复后在沙箱里跑了两轮日志 第一轮切到 B 之后 await api.getCurrentSession() { tenant: b-company } await exportMonthly(上个月) { ok: true, file: b-company-orders.xlsx, rows: 27 } 第二轮切换后再执行session 落在 A await api.getCurrentSession() { tenant: a-company } 预期是 B抛出中止——宁可停不再肩挂 A 的上下文去导 B 的账第二件事把当前身份显性化到界面可读。页面右上角那个公司名加个可被语义树读到的属性让 Agent 拿语义树就能确认当前租户切换租户成功时给一个明确的状态变化而不是静默改 URL。第三件事它自己兜底不了的关键写操作走结构化通道/接口界面操作只用于读场景。导出这种一执行就落地的动作本来就该走接口——上次那篇讲的结构化后门在这里也是同一个道理。请求链路对比出错 vs 修复后 出错Agent 用缓存的 orgIda-company 组装参数 → POST /org/b-company/reports/export { orgId: a-company, month: 上个月 } → 服务端权限校验通过账号两租户都有权限但数据范围错位 修复后Agent 先调 getCurrentSession() → 返回 { tenant: b-company } → 导出请求不再携带 orgId服务端按会话当前租户执行 → 文件名与数据范围一致这版的要点不是让 Agent 更聪明是把我当时是哪个身份从靠猜变成可确认。边界这套方案的适用边界说明白三件事免得方案被滥用第一沙箱里的机制可以迁移到真实环境的灰度验证但真实环境的身份模型更复杂——SSO、联合身份、多个标签页同时在登录态里不能把切换租户导致上下文过期当成唯一翻车姿势。演练是简化模型真实系统里要在灰度里逐步收窄范围。第二结构化通道解决的是拿到对的身份它不解决身份本身被顶替——那是协议和认证层面的问题接口做得再干净也兜不住被人盗号。第三这不是越权。服务端的权限校验全程没出问题账号在 A、B 都有权限本来就是设计如此。问题出在 Agent 拿了过期上下文去执行操作。以后别再拿这个案例当权限漏洞讲它归上下文一致性这一类。回头看Agent 会话越来越长界面端的状态和服务端的状态之间那条缝只会越来越大。界面上写着你是 A服务端说该按 B 算Agent 永远信界面上写的——直到出问题。界面适配解决点得对上下文核对解决以为的自己是哪个自己。两件事都做到Agent 在后台长跑才谈得上落地。这个案例初期演示的时候我还真捏了把汗——因为那个沙箱要是没隔离好这就是一次真实的跨租户数据串。所以演练这件事值得单独给它一句忠告沙箱和生产的语义隔离是从演练那一刻就开始的不是上线那天才有的。会话身份这一块想深挖的话推荐两份资料OWASP 的会话管理速查表把身份该放哪、怎么维持讲得很细OpenAI 官方那篇 Introducing the Agents API 能看到他们怎么把上下文管理做进 harness 里。两篇都不长值得花十几分钟。「Agent 工程实战」系列路线界面线computer use / Agent UI上一篇09 给 AI 看的界面三层适配下一篇11 computer use 成本账系列索引21 篇阅读路线Agent 放权前先补权限、出站、发现这三道口子专栏Agent 工程实战如果这篇对你有用欢迎收藏想追更可以关注「Agent 工程实战」专栏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑