用证据记录让邮件验证 Agent 可排障
邮件验证 Agent 应记录尝试、观察和终止原因,而非只返回是否找到邮件。文章区分产品未发送、邮箱展示延迟、邮件属于其他测试及验证码无效等情况,强调验证结果必须能追溯。
AI 深度解读
让 LLM Agent 注册账号、收验证码、点击验证链接,并不难。难的是邮件迟到、重复投递、混入上次测试之后,团队还能知道这次验证到底发生了什么。作者提出的办法是:给每次邮件验证定义一份「证据契约」,把尝试、观察和结束原因记录下来。
这套设计不追求让模型包办所有步骤,而是把关联、过滤、超时交给有明确规则的程序。对正在用 Agent 做注册和登录 QA 的团队,它提供了一个很实用的判断标准:自动化的价值,还包括失败之后能不能查清原因。
为什么一句「没收到邮件」不够用
常见流程是创建账号、点击发送验证码,然后轮询邮箱。看起来只有一个等待动作,实际至少藏着四种情况:
- 产品还没有发出邮件。
- 邮件服务商已经接受邮件,但收件箱还没有显示。
- 邮件已经到了,却属于另一轮测试。
- 邮件属于当前测试,但验证码或链接无效。
统一返回 timeout,会把这些情况揉在一起。Agent 随后继续重试,既增加调用成本,也可能掩盖第一次失败的线索。
这里需要区分「验证没有完成」和「已经确定哪里出了问题」。作者列出了四种原因,但没有给出如何接入产品发送事件、服务商投递状态的完整方案。仅凭查不到邮件,仍然不能断言产品没发送。
证据契约的作用,是让结论能被复查,而不是让每次失败都自动得到准确归因。
它是一份结构固定的记录:谁触发了验证、允许等多久、看见了什么、为什么接受或拒绝。它记录验证过程,也不承诺整个产品都正常。
一份回执应该留下哪些信息
作者建议先定义记录结构,再编写 Agent 的 prompt。下面的示例用于保存一次验证的判断依据:
{
"run_id": "signup-7f31",
"inbox_id": "inbox-204",
"triggered_at": "2026-11-04T02:00:00Z",
"deadline_at": "2026-11-04T02:01:00Z",
"observations": [
{"id": "msg-81", "subject": "Verifica tu cuenta", "received_at": "2026-11-04T02:00:21Z"}
],
"decision": "accepted",
"reason": "message matched run token and contained one verification link"
}
重点看 run_id、triggered_at 和 deadline_at:它们分别负责关联当前运行、排除旧邮件、限制等待预算。示例允许等待一分钟,观察到的邮件在触发后 21 秒到达。
run_id 要能串起浏览器、后端和邮箱,不能只存在于模型对话里。失败时,记录也应该保留 timeout、wrong_message、invalid_link 或 provider_error 等结果,并附上人能读懂、统计时也能使用的原因。
不过,这个 JSON 只是最小示意。它没有展示 token 如何进入邮件,也没有保存链接校验的具体证据。若要让另一位工程师复查 accepted 是否合理,还得补齐这些字段和校验规则。
回执应当独立于模型对话保存,并附到测试结果上。否则对话被压缩、模型只总结最后状态时,关键过程仍然可能丢失。
哪些步骤应该交给确定性程序
作者给出的流程是:Agent 发起操作,应用产生事件,隔离邮箱接收邮件,过滤器验证,最后生成回执。
Agent 提交关联 token;worker 在限定时间内观察邮箱;过滤器检查日期、主题、收件人和 token,只提取必要的数据,例如验证 URL。模型可以解释结构化结果,在有真实歧义时协助判断。
下面的伪代码解决的是「只在固定预算内接受属于当前运行的有效邮件」:
deadline = now() + 60s
while now() < deadline:
for message in inbox.messages(since=triggered_at):
if owns_run(message, run_id) and valid_verification(message):
return receipt("accepted", message.id)
sleep(backoff())
return receipt("timeout", reason="no matching message before deadline")
要看住第一行的 60s:模型不应为了拿到成功结果自行延长它。backoff() 用于降低查询压力,也必须服从总预算。
这段代码还没有展开拒绝记录、服务商异常和请求自身的超时处理,因此不能直接当完整实现。它表达的是边界:到期就返回 timeout,保留证据,再单独调查。
邮箱隔离同样重要。作者建议每次运行使用一个账号,或者至少使用带唯一 token 的 alias。共享 inbox 会让测试 A 的邮件被测试 B 读走,「取最新邮件」无法解决归属问题。
证据也不能无限保存。作者倾向于保留消息标识、时间、拒绝原因和链接指纹,用收件人 hash 替代完整地址,断言结束后删除正文。临时邮箱还应明确负责人、保留期限、服务限制和允许接收的数据类型。
在指标上,作者建议记录回执接受率、首封邮件等待时间、失败原因分布。等待时间增加但投递情况稳定,可能需要检查轮询;wrong_message 增多,则可能存在隔离问题。这些是排查方向,不能直接当作因果证明。
我的判断
我认同作者对职责的划分。邮件归属、截止时间、结果持久化都适合用明确规则处理,让模型负责解释结果,可以减少不可追踪的自由决策。
但原文没有提供实测成功率、成本变化或与其他方案的对照数据。这是一套工程设计建议,不能据此断言它已经提高了多少稳定性。
它也不是 LLM 专属方案。普通浏览器自动化同样需要运行标识、邮箱隔离和失败回执。如果现有测试框架已经具备这些能力,优先复用更合适。
还有两个边界需要团队自己定义。第一,valid_verification(message) 到底验证链接格式,还是实际访问后确认账号状态?提取出一个 URL,不能独立证明验证成功。第二,最小化留存会减少诊断材料。链接指纹可以帮助比对,但无法还原所有内容错误,需要根据排障需求决定保存哪些证据。
最值得借鉴的,是让每个成功或失败都附带可检查的依据。
能拿来做什么
- 给现有注册验证测试增加
run_id,让浏览器操作、Node 测试 worker 和邮件过滤使用同一个关联标识,并验证它确实能证明邮件归属。 - 把邮箱查询封装成有固定 deadline 的确定性流程,让 Agent 消费回执;发生 timeout 时保留记录,避免自动延长等待。
- 并行运行测试前,落实独立邮箱或唯一 alias,并按时间、收件人和 token 组合过滤,停止使用「最新一封」作为判断依据。
- 在测试报告里区分错误邮件、无效链接、服务商异常和等待超时,同时保留必要元数据,让重试之前先有可查的失败现场。