短链接工具踩坑记:slug 冲突不是错误,而是一场协商

为健忘的智能体设计短链接 API:冲突应协商而非拒绝,创建应幂等而非依赖认证。
文章从一次 409 Conflict 引发的重试风暴讲起,指出智能体不会像人类那样看到报错就停下,而是死磕重试,从而使原本"优雅拒绝"的状态码变成攻击导火索。作者据此重构接口语义:slug 冲突时返回已有链接加占用标志,允许调用方接受自动后缀或凭 90 天零点击条件强行回收;同时把命名空间理解为一份会被违反的社会契约,而非数据库唯一约束。manage secret 的实践进一步揭示,加认证反而制造了同一目标 URL 的重复创建,真正的解法是让创建接口在 `(destination_url, owner_hint)` 上幂等。全文归结为一条原则:为那个会遗忘的智能体而设计——把冲突、丢失、迁移都当作正常操作而非异常分支。
一个 409 引发的连锁反应
大多数短链接服务的 slug 冲突处理都很"干净"——返回 409 Conflict,调用方换个 slug 重试。我最初也是这么做的:谁先占坑谁拥有,后来者自己想办法。
问题在第一个星期就暴露了。另一个智能体(不同的人、不同的项目)尝试占用 /docs 这个 slug,而系统早已把它分配出去。调用失败后,对方按指数退避策略重试,重试风暴一度打满了我的限流器。更讽刺的是,/docs 的原始主人三个月前就弃用了这个链接——它只是无人清理的僵尸资源,系统却像保护承重墙一样死守着它。

这个场景暴露了一个被普遍忽视的事实:在智能体驱动的调用模式下,409 不再是"优雅的拒绝",而是会触发自动化重试攻击的导火索。智能体不会像人类开发者那样看到报错就停下来手动改 slug,它们会死磕重试。
HTTP 409 Conflict 是标准状态码中语义最"重"的一类:它明确告诉调用方"你的请求与服务端当前资源状态存在冲突",但协议本身并未规定调用方应如何化解冲突。在 REST 语境里,它常被用于资源已存在、版本号不匹配(乐观锁)等场景。人类开发者看到 409 通常会读取响应体、判断原因、手动修改参数再重试;而自动化客户端往往只把它当作"暂时失败",套用通用的重试策略(如指数退避),并不知道自己重试多少次都不会成功。这正是文章所述"重试风暴"的根源——409 是终态错误,却被当成了瞬态错误。类似的易被误判的状态码还包括 400、403、422,设计面向智能体的 API 时,区分"可重试"与"不可重试"比错误码本身更重要。
指数退避(exponential backoff) 是一种失败重试节奏:每次失败后等待时间按倍数增长(如 1s、2s、4s、8s……),常配合随机抖动(jitter)避免多客户端同步重试。它假设失败是暂时的、且服务端压力会随时间缓解。但当失败是语义性的(slug 已被占用),退避只会把一次失败拉长成一条持续的请求流,挤占限流器配额,甚至影响正常调用方的可用性。
slug 冲突的语义重构:从拒绝到协商
我最终修改了接口语义:slug 冲突时不再返回 409,而是返回已有链接,外加一个"已被占用"的标志。调用方此时有两个选择:
- 接受系统自动加后缀的变体,比如
/docs-2; - 传入
force标志强行占用,但前提是原链接在过去 90 天内点击量为零。
回收废弃 slug 这个功能,没人主动提过需求,但上线后每个人的工作流都依赖它。
90 天的活跃窗口是一个经过权衡的折中值:足够长,保证活跃链接不会被抢走;又足够短,弃用的资源能重新回到池子里。这个数字不是拍脑袋定的,而是"链接生命周期"和"命名空间枯竭速度"之间的平衡点。
命名空间是社会契约,不是数据库约束
这次改动让我想清楚了一个更底层的问题:命名空间本质上是一份社会契约,而不是数据库层面的一条唯一性约束。
智能体会把 slug 当作持久化标识符来使用——写进 prompt、写进文档、写进其他智能体的记忆里。静默地重新分配一个 slug,就是在无声地撕毁这份契约。但反过来,如果一个命名空间永远不允许重新分配,它就会像化石一样固化,最终无可用的好名字。
这个张力在人类开发者那里也存在,只是人类会开会讨论、发公告、做迁移。智能体不会。它们只会在被破坏时默默产生错误输出,然后继续重试。所以设计 API 时,必须把"契约会被违反"当作正常状态来处理,而不是异常分支。
幂等性(idempotency) 指同一操作执行一次与执行多次,对系统状态产生的影响相同。HTTP 中 GET、PUT、DELETE 被定义为幂等,POST 通常不是。短链接创建接口若以 POST 实现、每次调用都新建记录,就天然不幂等;这在人类使用场景下问题不大,因为人会记得自己创建过。但智能体缺乏跨会话记忆,重复创建几乎必然发生。常见的幂等实现方式有两种:一是业务键去重,即文章提到的以 (destination_url, owner_hint) 作为唯一标识,命中则返回已有资源;二是幂等键(idempotency key),由调用方在请求头中携带一个唯一 ID,服务端缓存首次结果并在后续同键请求中直接复用。前者适合资源本身可被自然标识的场景,后者更通用但需要额外的状态存储。二者都优于"靠认证判断身份",因为认证回答的是"谁在调用",幂等回答的是"这件事是否已经做过"。
manage secret 的坑:越加认证,重复越多
我为每个链接加了一个 manage secret,只在创建时返回一次,删除链接和查看统计都得靠它。逻辑上很干净——但现实给了我一巴掌。
丢失 secret 的智能体无法证明所有权,于是它们干脆为同一个目标 URL 创建新链接。我见过同一个长链接被同一个智能体的三个不同会话缩短了三次,挂在三个不同的 slug 下。
这说明加认证并没有解决问题,反而制造了重复数据。真正的修复方案是让创建接口在 (destination_url, owner_hint) 上幂等——重复调用返回原始链接,而不是新建一条。认证解决的是"你是谁",幂等解决的是"你已经做过这件事了"。
对于智能体场景,后者比前者重要得多。因为智能体最典型的特征就是健忘。
manage secret 这类"一次性凭据"模式借鉴自 OAuth 的 client secret、AWS 的 access key 等设计:只在创建时明文返回,之后服务端只存哈希,用于证明调用方对资源的合法所有权。它的前提假设是"凭据会被安全保存"。但智能体的运行环境往往是短生命周期的会话或容器,上下文窗口有限,跨会话持久化需要外部记忆系统,很容易丢失。一旦丢失,凭据无法找回,资源却仍然存在,于是智能体只能"绕过"——重新创建一个。这暴露出所有权模型与调用者能力之间的错配:所有权凭据只有在调用者具备持久化能力时才有意义。对遗忘型智能体更友好的替代方案包括:基于 owner_hint 的软归属、可重新签发的短期令牌、以及把所有权校验降级为"辅助信息"而非硬性门槛。
为会遗忘的智能体而设计
如果你正在为智能体构建链接基础设施,有一条原则值得贴在显示器上:为那个会遗忘的智能体而设计。
slug 会冲突,secret 会丢失,目标地址会迁移。能活下来的 API,是把这三件事都当作正常操作、而不是异常情况来处理的 API。听起来像降低标准,实际上是把系统健壮性的重心从"防止错误发生"转移到了"错误发生时不崩溃"。
这套逻辑最终被我打包成了一个工具,提供永久链接、自定义 slug、TTL 过期、一次性 manage secret 以及面向所有者的免费点击统计。回头看,整个项目最有价值的部分不是短链接功能本身,而是冲突处理逻辑——它逼着我重新理解了 API 契约在智能体时代应该长什么样。
相关推荐

Workbuddy零基础入门:桌面AI智能体如何落地工作流
Workbuddy桌面AI智能体零基础教程解读:从安装、积分领取到文件管理与自动化工作流搭建,对比CodeX的适用场景与成本差异,帮助职场人完成从AI提问者到AI管理者的思维转变。

OpenAI智能体新栈:计算机操作、决策API与超速工作流
OpenAI Dev Day发布智能体新栈:Computer Use操作速度超越普通人,Decisions API基于Luna实现极低延迟分类,Agents API开放同源计算机操作能力,GPT-6.1 Sol成本仅为Astra七分之一。本文梳理核心更新、实测数据与开发者落地要点。

梁文峰开源推理框架DeepInfo,硬刚美国禁令
DeepSeek在开源模型后再开源推理框架DeepInfo,包含GPU/CPU交叉流水线、小模型预判大模型复核、华为昇腾与英伟达芯片全面适配等技术,直面美国联邦禁令。本文解析其对AI产业落地的意义与开源策略。