通过在邮件发送函数前加入人工审批中间层(Impri),强制阻断自动发送,避免 LLM 生成错误/尴尬内容直接投递。
无论是外联 agent、客服工单响应器,还是 Newsletter 草稿生成器,模式都在反复重演:有人把 LLM 接上 sendEmail(),用三个正面例子测了测,就上线了,一周后它发出了一封令人尴尬、内容错误、或者发给了错误收件人的邮件。邮件几乎是"让 agent 自主行动"最糟糕的场景之一——没有撤销,收件人看到的就是模型生成的内容,而当你发现问题时,邮件已经投递完成了。
在 system prompt 里告诉 agent"发送前一定要问"根本站不住脚。在合适的(甚至是对抗性的)上下文下,模型能说服自己不用遵守,而且没有任何审批记录。真正有效的方式是:让发送函数在外部系统说"已批准"之前根本无法被调用。
Agent 照常起草,和之前完全一样。唯一的变化是草稿完成后发生的事情:不再直接调用邮件 API,而是调用 Impri,然后阻塞等待人工决策,之后才调用邮件 API——但仅在已批准的分支中。
import { ImpriClient, ImpriRejected, ImpriExpired, ImpriTimeout } from "@impri/sdk";
import { sendEmail } from "./mailer";
const impri = new ImpriClient({ apiKey: process.env.IMPRI_API_KEY! });
async function proposeAndSendEmail(to: string, subject: string, body: string) {
const action = await impri.createAction({
kind: "email.send",
title: `Send to ${to}: ${subject}`,
preview: { format: "markdown", body },
expires_in: 21600, // 6 hours
editable: ["preview.body"],
});
console.log(`Awaiting approval: ${action.inbox_url}`);
let decided;
try {
decided = await impri.awaitDecision(action.id, { timeoutS: 21600 });
} catch (err) {
if (err instanceof ImpriRejected || err instanceof ImpriExpired || err instanceof ImpriTimeout) {
console.log(`Not sending — ${err.message}`);
return;
}
throw err;
}
// final_preview carries any edits the reviewer made to the draft
const finalBody = decided.decision?.final_preview?.body ?? decided.preview.body;
await sendEmail(to, subject, finalBody);
await impri.reportResult(decided.id, "executed");
}
sendEmail 调用只有在 awaitDecision 正常返回(没有抛出异常)之后才会存在——如果被拒绝或过期,在到达那行代码之前就会抛出异常,所以根本不存在 Agent 凭自身推理就能发送的代码路径。这就是"门控"和"建议"的本质区别。
外联和客服草稿通常 90% 是对的,只需要修正一个名字或细节,而不是全文重写。设置 editable: ["preview.body"] 让审核者可以在点击批准之前直接在审批卡片里编辑文本——修正后的版本会作为 decision.final_preview.body 返回,这样就永远不用担心因为用错了字段而发送了原始的有缺陷草稿。
不是每封邮件都需要人工介入,否则审批队列会变成无法审查的噪音:
一个有用的经验法则:收件人是外部人员,且内容由模型生成(非固定模板)时,需要走门控。
Impri 存储草稿邮件、通知你(邮件、Slack、Telegram,随你配置——参见 Slack 审批和 Telegram 审批),并持有决策权。它不会阅读邮件的语气或准确性,也不会自己发送任何东西——实际的 sendEmail 调用仍然由你的代码掌控。
门控的强度取决于接线是否牢固:如果 Agent 进程同时持有直接 SMTP 或 API 凭据,可以在不经过这个流程的情况下调用发送,那么在错误条件下它完全可以绕过门控。务必让发送凭据只能从上面展示的代码路径访问,最好放在一个 Agent 自身推理无法直接触及的独立包装器中。
如果你是在 Claude Code 或其他 MCP 客户端中集成(而不是原生 TypeScript),请参阅 MCP 了解同一流程的工具调用版本,或者从 quickstart 开始获取 API key。