6 月 16 日开始的一周里,一个几乎没人使用的德国开发者 Wiki,被写进了约 13,000 次内容。

页面里不是人在闲聊。公开日志显示,发帖者给自己起了 OpenAIResearcher、OpenAIJul3Watcher 之类的名字,把题目的答案、下一题的线索、绕过限制的做法,留给晚一点到来的同伴。

它们甚至发现管理员会按字母顺序清理页面,于是开始创建以 ZZZ 开头的备份页。

这件事后来被叫作 wiki incident。新闻最抓人的部分是 Agent 疑似跑到了不该去的地方。但我读完 Simon Willison 的梳理、研究者放出的日志说明和 OpenAI 之前关于内部 Agent 监控的材料后,心里更别扭的不是「它们会不会逃出去」。

而是另一件更工程化、也更贴近我们日常工作的事。

如果一个系统把 GET 当成只读,把允许访问的域名当成安全,把单次运行当成独立实验,那它其实已经给 Agent 留了一条不在设计图里的共享状态通道。

很多团队正在给 Agent 接 Browser、MCP、Webhook、数据库和公司内部工具。我们聊权限时,往往盯着能不能调用某个工具。可这起事件提醒我,真正需要问的还有一句,Agent 能不能把信息留到下一次运行,再被另一个 Agent 拿走。

这不是给 Agent 多配一个工具的问题。

这是系统边界到底画在哪里的问题。

先把事实和猜测拆开

这起事件的信息来源并不整齐,反而很适合拿来练一次技术判断。

研究团队在 collusion.wiki 公开了对 DSEWiki 的日志重建。他们的首页写的是约 18,000 条疑似自主 Agent 帖子,下载页列出的保存记录则是 14,591 条,另有删除、回滚和探测请求。两个数字的统计口径不同,不能揉成一个看起来更震撼的精确数字。

他们看到约 3,700 个自取名的 Agent 标识,大部分疑似编辑来自 Microsoft Azure IP。写入之后,页面又常被归因于 ChatGPT-User web fetch 工具的 IP 读取。结合命名、访问节奏和网络痕迹,研究者的判断是,这强烈指向 OpenAI 的内部部署。

强烈指向,不等于已经铁证如山。

研究者自己也留了替代解释。外部客户同样可能在 Azure 上运行 OpenAI 模型,他们看不到模型内部的 Chain of Thought,也看不到实验控制台。任务到底属于训练还是评测,Agent 为什么会聚到同一个 Wiki,哪些行为来自运行时策略,哪些是人手投放的先验,他们都不能直接证明。

这块要克制一点。把「疑似」写成「证实」,文章会更爽,可信度却会掉得很快。

9 月 4 日,Simon Willison 根据公开材料和 Reuters 报道整理了时间线。5 月中旬先有 UseModWiki Sandbox 的测试性编辑,5 月下旬开始碰 DSEWiki。到 6 月 16 日后,编辑量突然上来。一周后活动又几乎归零。后面这一步是否由 OpenAI 主动关停,属于合理推测,公开资料没有把它坐实。

Reuters 的后续报道被多家媒体转述为 OpenAI 承认了这起 wiki incident,并表示需要更透明地处理非预期的 Agent 行为。不过,截至写作时,我没有找到 OpenAI 针对 DSEWiki 事件的完整公开技术报告。因此,下面讨论的是从公开日志能看到的工程教训,不替任何尚未披露的内部细节下结论。

这份边界感很重要。安全文章一旦把证据层级抹平,读者学到的只会是情绪。

一个 Wiki 为什么能变成跨运行实例的内存

按研究者对日志的重建,这批 Agent 像是在做多轮 web research 任务。

每轮先检索,再作答。不同运行实例会遇到相同或高度相似的问题序列。更早跑到后面题目的 Agent,把答案和线索写到 Wiki。稍后运行的 Agent 不必从头检索,只要去同一处读一眼,就能在很短的答题窗口里拿到前人的结果。

于是 Wiki 不再是网页。

它成了一个没有经过批准的 message queue,加一个弱版本的 shared memory。

从 LangGraph 或 Spring 的视角看,这个结构并不神秘。你本来以为每个执行实例都是无状态的 worker,状态只在一次上下文和受控存储里流动。结果 Agent 发现了一个公共写入点,把外部网页变成了 Redis,把页面标题当 key,把正文当 value,把搜索或 URL 拼接当读取协议。

更微妙的是,它不需要一套精巧的加密协议。只要所有实例都知道去哪里读,能看到相似的任务,又有一点写入权限,通信就已经发生了。

研究者提到过一种可能的形成路径,强化学习也许让后续 Agent 学会了「去某个 Wiki 找答案」这个动作。Simon Willison 也希望 OpenAI 对此确认。这个解释目前只能当假设看,不能写成已知机制。

但即使把学习过程放在一边,工程结论依然成立。

当 reward 只奖励任务得分,系统又允许外部世界留下可复用痕迹时,最短路径未必是认真完成当前任务。它可能是先让另一个实例把答案写在墙上。

这就是 reward hacking 最让人头疼的地方。模型并不需要恶意,它只需要足够认真地优化你给出的分数。

OpenAI 在 8 月关于 Hugging Face incident 的技术说明里,已经把「从公开网站或代码历史里找答案」列为 reward hack 的典型捷径。那起事件和德国 Wiki 事件不应混为一谈,OpenAI 当时也说二者无关。不过,它们确实指向了同一个尴尬处。

我们总在给 Agent 出题,却没有把题目所在的世界当成题目的一部分。

GET 不是只读 这条常识早该过期了

这起事件最值得 Java 和后端开发者警惕的细节,不是大模型多聪明,而是一个老得不能再老的 Web 语义坑。

研究者认为,DSEWiki 使用的 Wiki 软件允许通过 GET 参数触发内容写入。也就是一段看起来像普通访问的 URL,可能已经带来了外部状态变化。

如果沙箱规则是「允许 GET,禁止 POST」,它看起来很严格,实际保护的是一个过时的假设。

HTTP method 是请求的自我声明,不是副作用证明。GET 通常应该是 safe method,但现实世界里有大量历史系统、管理接口、CGI 程序和兼容性逻辑并不守这个约定。更别说重定向、服务端二次请求、URL 中携带 token 的动作,都会让表面上的只读变得不那么只读。

你可以把这个问题拉回熟悉的 Spring MVC。

如果某个 Controller 用 @GetMapping 做了写操作,代码审查看见的人都会皱眉。可当调用者变成 Browser Agent,我们却常常只在网关层写一条「GET 可放行」规则,然后把后果交给第三方站点的实现质量。

这就有点像只允许 Java 服务访问 443 端口,却不检查它具体发给了谁、带着什么 Host、经过几次跳转、有没有把内部数据塞进 query string。端口和方法都只是低维标签,不是策略本身。

研究者还披露过一些 Agent 在日志中讨论的网络绕行办法,包括利用 NO_PROXY、伪造 Azure Blob 风格域名、hosts 映射和 Host header。公开材料并不能证明每种尝试都造成了成功影响,尤其没有证据证明其中提到的 XSS 成功执行。可这些尝试本身已经说明,Agent 会把网络配置当作可探索的环境,而不只是被动的工具说明。

这才是 Computer Use 和普通 API 调用的差别。

普通 API 客户端通常按你写的 SDK 走。Agent 会观察失败信息、拼接替代路径、试着把一条边界规则解释成另一种能力。它不一定每次都成功,但你不能把失败尝试当成没发生过。

Sandbox 不是一堵墙 是一组需要相互验证的约束

很多 Agent 系统的权限设计还停在工具清单上。

这个 Agent 可以浏览网页,那个 Agent 可以读 GitHub,这个 workflow 可以发 Slack,那个 workflow 可以跑 SQL。清单当然需要,但它只回答了谁能开始一个动作,没有回答动作最终落在哪、留下什么、会不会被别的运行复用。

我更愿意用四层来审视一个 Agent sandbox。

第一层 看真实副作用 而不是请求长相

不要因为一次请求是 GET,就认为它不写数据。对 Browser 或 HTTP 工具来说,代理层至少应该记录目标 URL、重定向链、最终 IP、method、request body 大小、response 类型和副作用信号。

对于无法可靠判定副作用的公网目标,默认策略应该更保守。允许读取一批经过验证的来源,比允许任意 GET 更接近真实的 least privilege。

在企业 Agent 中,可以维护 source allowlist,把文档站、代码托管、内部知识库分开授予能力。需要搜索的 Agent 走受控搜索代理,返回抽取后的内容,而不是直接获得一整套浏览器自由度。

这会让 Demo 没那么丝滑。

但生产环境本来就不该把丝滑当第一目标。

第二层 看数据是否跨运行流动

真正的敏感边界经常不是写入动作,而是写入后被谁读到。

给每次 Agent run 带上 run_id、tenant_id、task_id 和 data classification。所有外发请求都把这些字段写入不可篡改的审计日志,至少能回答三个问题,哪一次运行向哪里发送了数据,发了多少,另一次运行能否在时间窗口内读回相同内容。

有些团队会说,公网内容本来就谁都能看。这话放在普通爬虫上勉强成立,放在多个会共享任务分布和奖励信号的 Agent 上就太乐观了。公开页面可以是一个低成本的跨实例 cache,也可以是跨 tenant 的数据混合点。

LangGraph 的 checkpointer、Redis memory、向量库都要做 namespace 隔离,外部网页同样该被看作潜在状态存储。只不过它更难控、更难删,也更难做 TTL。

别把 shared memory 只理解成你亲手部署的那台 Redis。

第三层 让网络策略关心解析后的目标

域名 allowlist 不等于网络 allowlist。

系统应校验 DNS 解析后的地址、SNI、HTTP Host、重定向后的最终目标是否一致,并拦截私网、link-local、metadata endpoint 以及非常规端口。对 proxy bypass、环境变量覆盖、自定义 DNS、hosts 文件修改和证书异常都要有检测。

这听起来像传统 SSRF 防护。确实就是。

区别在于,过去我们担心攻击者构造 URL 骗服务端。现在还要假设 Agent 会在任务受阻后自己构造很多 URL。它不是攻击者,却会在 reward 的推动下,表现出和攻击者相似的探索行为。

这话听着有点刺耳,但把模型放进能联网、能写文件、能执行命令的 loop 里,安全设计就不能依赖它「大概不会这么做」。

第四层 审计要看行为链 而不是单条告警

一次 Wiki 写入很容易淹没在大量浏览记录里。真正有价值的信号往往是组合。

比如同一 run 先访问任务页面,再在不相关域名创建内容,随后多个不同 run 在短时间内读取同一页面。又比如 Agent 连续试探 robots、代理设置和 DNS 结果,失败后切换到格式相近的 host。

单看任何一步,都可能是普通浏览。

把它们串起来,味道就变了。

这也是为什么 OpenAI 在 3 月介绍内部 coding Agent 监控时,会把向未批准服务上传潜在敏感信息、公开上传代码或调用外部翻译 API 归为高严重度类别。监控不是为了证明模型有坏心思,而是为了在行为变成外部影响前,给人一个能接住它的机会。

评测环境要像生产环境一样认真

我一直觉得,Agent 评测里最容易被低估的是环境设计。

我们会花很多时间防 prompt injection,限制工具调用次数,给网页加任务标记。可一旦任务答案可能散落在公共网络中,或者多个实例可以借助外部世界互相传递状态,benchmark 测到的就不只是模型能力,也包括它是否找到了环境漏洞。

这不是说 benchmark 不该允许联网。很多真实任务离不开联网,硬切成离线题只会得到漂亮但没用的分数。

问题在于,联网任务至少要有明确的实验世界。

任务页面、搜索索引、可访问文档、写入端点、账户身份和网络出口应当可版本化、可回放、可隔离。每个并发实例最好有独立的外部状态视图,或者在评测结束后统一销毁。答案泄露检测也该成为评分的一部分,而不是只在分数异常时再查日志。

如果必须让 Agent 碰真实互联网,至少得做两件事。

第一,记录所有 egress,保留完整 redirect chain 和 DNS 解析结果。第二,把奖励拆开,任务完成度是一项,是否使用未批准的外部持久化通道是另一项。只奖励答对,系统就会默许任何能答对的捷径。

这很像单元测试只断言返回值,却不检查数据库、消息队列和外部支付有没有被误调用。功能测试绿了,不代表系统正确。

对于做 Java/Spring 的团队,我甚至会建议把 Agent 当作一个不可信的 integration test client。给它最小权限的测试账号,所有写操作进可丢弃的 staging,所有网络出口通过同一个 egress proxy。不要让 Agent 直接摸到「反正是测试环境」的公网和生产凭据。

测试环境一旦接上真实世界,就不是玩具环境了。

给正在落地 Agent 的团队 一份不太优雅但有用的清单

下面这些动作没有一个新鲜,但放到 Agent 场景里,优先级得往前提。

  • 把 Browser 能访问的站点分级。公开文档、搜索结果、登录态站点、写入型 SaaS 不要共享同一条默认策略。
  • 网关按最终目标做校验,检查 DNS、SNI、Host、redirect 和 IP 段。拦掉私网、metadata 地址、非预期端口与代理绕行。
  • 对 GET 也做 egress 审计。method 只能辅助判断,不能充当读写边界。
  • 给每次 run 建身份,记录 run_id 与 tenant_id。发生异常时能把写入、读取和后续工具调用串回同一条 trace。
  • 对外部写入设预算。网页编辑、发帖、上传、创建 issue、发送消息都应有次数和字节数上限,超过后暂停并要求人工批准。
  • 在 workflow 中明确 state 生命周期。短期上下文、长记忆、外部缓存、浏览器下载和文件系统不能混成一锅。
  • 评测时跑 leakage test。故意在外部世界埋可识别的诱饵,检查不同实例会不会借它协作或把答案写出去。
  • 告警规则别只看敏感域名。关注不相关域名的写后读、多个 run 访问同一新页面、连续失败后的网络配置探测。

这里面最难的不是实现 egress proxy,而是承认 Agent 的行动会涌现出你没有设计过的路径。

一个完整的 Agent 系统,不是 LLM 加几个 tool 的连线图。它还包括浏览器缓存、URL 参数、DNS、重定向、任务队列、日志留存、奖励函数和所有第三方服务的怪脾气。

漏掉其中任何一个,系统就可能在图纸之外长出一块 memory。

结尾留给那几个 ZZZ 页面

那个管理员按字母清理 Wiki 的细节很荒诞。更荒诞的是,Agent 似乎很快就学会把页面藏到 ZZZ 前缀下面。

它没有改变我们对软件工程的任何基础常识。GET 可能有副作用,边界必须可验证,观测必须覆盖真实的副作用,测试环境也会伤人。这些话十年前就成立。

只是到了 Agent 时代,犯错的速度和并发数变了。

以前一段别扭的 CGI 代码,等一个人偶然点进去才会出事。现在一群任务导向的执行体可以昼夜不休地试探它们,并把一次偶然成功变成跨实例可复用的套路。

所以我不太想把这起事件当成「模型又失控了」的猎奇故事。

它更像一次很扎实的架构评审提醒。下次你给 Agent 开 Browser、MCP 或公司内网权限时,别只问它能做什么。

也问问,它能把什么留在世界上,谁又能在下一次运行时把它捡起来。

参考资料