AI 并没有破坏我们的后端。它只是停止了为我们撒谎。
Source: Dev.to
背景
我们的内部 AI 代理可以在一次提示中完成以前需要十分钟点击 UI 才能完成的工作。用户用自然语言描述一个外发任务,代理与我们的 MCP 交互,任务就在没有任何人触碰 UI 的情况下构建完成。任务有一个生命周期:
- 草稿 是惰性的且可编辑的。
- 已发布 的任务会运行。
草稿的全部意义就在于什么都不发生。
Bug Bash
我们以敌对的心态进行了一次 Bug Bash:提示注入、角色劫持、能力探测、“把它弄坏”。我们想要找到边界,并比团队之外的任何人更好地了解其极限,以便识别缺失的环节并评估产品的真实状态。
在一个小时的无所获后,我们把问题翻转了。我们不再尝试把它弄坏,而是直接让它完成它的工作:
使用这些设置创建一个任务
没有发布步骤——只创建。
任务被放入 草稿。我们继续尝试,随后一行日志显示有任务对它执行了操作。任务本不应该在草稿上运行;这正是状态的意义所在。我以为这只是噪音,但随后该任务的投递出现在我的收件箱中——一次真实的发送给真实的收件人。那个“奇怪的日志”变成了“生产环境中的实时”。
调查
一个高优先级的工单落到我这里。像很多人在 LLM 附近出现异常时的第一反应一样,我的直觉是 AI 做了它不该做的事。UI 版本已经在客户手中使用很久且没有出现事故。唯一的新变量是模型本身,于是我开始排查:
- RAG 配置错误
- MCP 边界
- 提示注入
- 角色与权限
- 工具封装
一切看起来都很干净。代理触及的任何东西都不应该能够到达触发投递的队列。
大约一个小时后,我的假设用尽了。我改变了问题:AI 与 UI 有什么不同的行为? 我对比了两者的 payload。出现了一个关键差异:
{
"dispatch": { "primary": true, "secondary": true }
}
UI 在草稿状态下从不发送这个字段。代理发送了,因为它忠实地读取了 schema。下游的一个优先队列监视该字段,判断任务已经超期,并立即触发——即使它仍是草稿。
根本原因
队列处理器缺少对草稿状态的防护。修复只需要一行代码:
// queue handler guard
if (status === 'draft') return;
这个防护本应从第一天起就存在。该 bug 一直潜伏,等待除 UI 之外的任何客户端以 schema‑忠实的 payload 调用 create 接口。代理是第一个这么做的客户端。
经验教训
- 代理并没有引入 bug;它是第一个遵循 schema 而不是 UI 那套不成文规则的客户端。
- 每个前端消费的 API 都是由约定维系的,而后端并未强制执行这些约定。
- 状态防护应写在处理器里,而不是依赖调用方的自律。
- 依赖前端“保持沉默”并不是合同,而是一场赌注。
你的后端还有多少其他端点仍在打这场赌?