调用模型的 n8n 工作流,只要理想流程成功一次,看起来就像已经完成。但在生产环境中,同一个 Webhook 可能再次投递,首次调用已经成功却因超时而重试,草稿也可能因为无人负责审批步骤而被自动发送。
下面为包含 AI 步骤的工作流增加一层防护:幂等性、重试策略、人工审批关卡和日志记录。它与 n8n 中的第一个 AI 智能体以及人机协作设计中的审核模式配合使用。
如果一个节点可能已经创建 CRM 备注、发送消息或将邮件放入队列,那么在结果未知时启用重试可能造成重复副作用。在确认提供方的幂等性或对账机制之前,应将每一次外部写入都视为不可安全重复的操作。
为何 AI 步骤需要不同的失败处理
普通 HTTP 请求和模型调用可能因状态码、超时、响应格式错误或提交状态未知而失败。包含模型调用的步骤还会引入其他故障模式,例如:
- 慢本地推理上的超时(本地 OpenAI 兼容端点)。
- 模型返回散文而非 JSON 时的解析失败。
- 软失败:有效但错误的 JSON。
- 部分成功:模型已回答,但后续工具写入失败。
盲目重试可以解决部分超时,却会放大其他问题。应区分传输层重试(服务器从未提交任务时才安全)与业务重试(只有使用幂等键时才安全)。
n8n 允许操作者从执行历史中重试失败的执行(n8n 执行文档)。这一操作功能并不能证明副作用可以安全重复;工作流仍需要下文所述的任务认领、对账和发件箱控制。
幂等性从原子认领开始
在触发器提供足够信息后,尽早选择稳定的键:
| 触发器 | 候选键 |
|---|---|
| 来自表单/CRM 的 Webhook | 上游 lead_id / ticket_id |
| 邮件 | 规范化的 Message-ID |
| 定时处理队列 | (job_id, logical_period) 或行主键 |
| 手动重新运行 | 现有键;真正的更正/替换是一个新的、显式关联的业务事件 |
切勿依次实现 SELECT key 和 INSERT key,也切勿将电子表格行用作锁。两个 n8n 工作进程都可能观察到 “缺失” 并继续执行。应使用数据库的唯一性约束和一个原子语句;PostgreSQL 将唯一性约束描述为确保键唯一性的机制 (PostgreSQL 约束)。
最小化的 PostgreSQL 表结构(根据系统需求调整类型、保留期和迁移):
CREATE TABLE workflow_runs (
idempotency_key text PRIMARY KEY,
state text NOT NULL CHECK (state IN (
'processing', 'awaiting_human', 'approved',
'completed', 'failed_retryable', 'failed_terminal'
)),
payload_hash text NOT NULL,
lease_owner uuid,
lease_expires_at timestamptz,
version bigint NOT NULL DEFAULT 0,
result jsonb,
updated_at timestamptz NOT NULL DEFAULT now()
);
为每个 n8n 执行生成一个随机的 lease_owner UUID。在一条语句中认领新键,或仅恢复一个明确可重试或已过期的租约:
INSERT INTO workflow_runs (
idempotency_key, state, payload_hash, lease_owner, lease_expires_at
)
VALUES ($1, 'processing', $2, $3, now() + interval '5 minutes')
ON CONFLICT (idempotency_key) DO UPDATE
SET lease_owner = EXCLUDED.lease_owner,
lease_expires_at = EXCLUDED.lease_expires_at,
state = 'processing',
version = workflow_runs.version + 1,
updated_at = now()
WHERE workflow_runs.payload_hash = EXCLUDED.payload_hash
AND (workflow_runs.state = 'failed_retryable'
OR (workflow_runs.state = 'processing'
AND workflow_runs.lease_expires_at < now()))
RETURNING idempotency_key, lease_owner, version;
返回零行表示另一个执行已拥有该键,或运行已达到不可重试状态:获取其状态后不再处理,或返回先前结果。如果同一键携带不同的 payload_hash,则应停止并调查;在未明确告知的情况下把已更改的业务输入视为同一事件,会掩盖上游数据损坏。
租约必须有明确时限,且只能由其所有者续期。每次状态转换都必须使用条件更新(compare-and-set,CAS):
UPDATE workflow_runs
SET state = $4, version = version + 1, updated_at = now()
WHERE idempotency_key = $1
AND lease_owner = $2
AND version = $3
AND lease_expires_at > now()
RETURNING version;
如果没有返回任何行,表示本次执行已失去所有权,不得执行任何操作。根据实际工作时长设定初始租约,在到期前提前续期,限制租约的总生命周期,并在租约多次被其他执行接管时发出告警。租约可防止被遗弃的任务无限期阻塞,但无法让非幂等的外部发送变得安全。
Webhook 投递和工作进程执行通常采用至少一次语义。数据库中的原子认领可以确定并发任务的所有权,但无法保证电子邮件、支付或 CRM 操作跨越网络边界后恰好执行一次。为此,需要下游幂等键,或者能够对账未知结果的发件箱与调度器。
AI 节点的重试策略
使用简短的规则矩阵,并把规则直接编码到工作流中,不要依赖未成文的团队经验:
| 故障情况 | 是否重试? | 备注 |
|---|---|---|
| 模型服务器返回 HTTP 429 / 503 | 通常,当操作可以安全重复时 | 提供 Retry-After 时请遵循;使用设有上限且带随机抖动的指数退避,并在持续压力时发出告警 |
| 超时且提交状态未知 | 仅当调用为只读或带键时 | 优先使用状态查询,而非盲目重放 |
| 模型返回无效 JSON | 有限次重新提示(1–2 次) | 然后将请求路由给人类,并附带原始输出 |
| 业务验证失败(无效枚举、空草稿) | 不得静默循环重试 | 修复提示词或数据结构,或者升级处理 |
| 下游 CRM 返回 409 冲突 | 在视为成功之前请验证 | 读取该资源或进行对账,并确认是否使用了相同的幂等键和预期状态 |
| 写入不确定性后下游 CRM 返回 500 | 请调查;不要自动重新发送邮件 |
为智能体节点设置有限的最大迭代次数。如果智能体内部已经循环调用工具,外层再套一层重试,token 费用和重复工具调用很容易失控。
对本地端点,按测得延迟设定超时;不要在同步客户 webhook 上堆“每次 60 秒重试三次”。
阻止副作用执行的人工审批关卡
人工审批关卡不是一条仅供知悉的 Slack 消息,而是一种明确状态:在收到显式批准信号前,不得执行任何客户可见或不可逆的操作。
在 n8n 中有效的三种模式:
1. 先审批,后执行
AI 节点 → 验证数据结构 → 将草稿和键写入存储 → 创建一次性审批挑战 → 只有经过身份验证且未过期的审批事务才能将发送任务入队。
2. 延迟执行并保留取消窗口
排队稍后发送并带取消窗口。仅当动作足够可逆,即使稍后取消仍有实际意义时使用。
3. 仅在例外时审批
仅在范围狭窄且可逆的案例中自动执行,而且这些案例必须满足确定性的适用规则,并有经过校准的评测证据达到批准阈值;对这些案例进行抽样和监控,在存在不确定性时升级处理或不执行。模型自行报告的置信度不是强制控制手段。
根据操作后果选择审批关卡,采用与人机协作设计相同的决策模型。在取得实测证据并得到政策许可前,客户邮件、退款、账户或 CRM 变更以及日常运营财务操作都必须先审批、后执行。医疗治疗、法律建议、受监管的财务建议、儿童安全决策以及结构或建筑决策需要合格专业人员参与;自动化可以准备或转交记录,但不得替代专业审核。
审批卡上的字段清单示例:
- 幂等键
- 源记录链接
- 模型输出(草稿 / 标签 / 分数)
- 若有校验错误
- 要记录的审批者身份
- 待定状态的过期时间
持有审批链接即拥有操作凭据
绝不要发送 https://n8n.example/webhook/approve?id=ticket-42&action=approve。任何猜中、转发、扫描或重放该 URL 的人都能执行操作。请生成至少 256 位密码学安全的随机 token,只通过 HTTPS 发送不透明 token,并仅存储其 SHA-256 哈希值,同时记录:
- 该运行对应的幂等键和允许的决策;
- 预期的审批人、受众或 SSO 策略;
- 绝对过期时间;
consumed_at、决策和审批人身份;- 一次性使用限制。
GET 请求应显示确认页面,不应改变状态。完成身份验证并通过 CSRF 防护后,再使用 POST 提交决策。对于简单场景,当前 n8n 节点可以暂停并请求审批;n8n 本身建议在更复杂的审批流程中使用 Wait 节点(n8n Gmail 审批操作)。请验证实际部署的节点与版本如何处理身份验证、过期、转发和审计;邮件中的按钮并不天然适合支付或法律审批。
创建一个与不可变业务幂等键关联的审批记录:
CREATE TABLE approvals (
approval_id uuid PRIMARY KEY,
idempotency_key text NOT NULL REFERENCES workflow_runs(idempotency_key),
token_hash bytea NOT NULL UNIQUE,
allowed_decisions text[] NOT NULL,
expires_at timestamptz NOT NULL,
consumed_at timestamptz,
decision text,
approver_subject text,
created_at timestamptz NOT NULL DEFAULT now()
);
在应用程序中对原始令牌进行哈希处理,并仅将摘要作为 $1 传递。以原子方式核销该令牌:
UPDATE approvals
SET consumed_at = now(), decision = $2, approver_subject = $3
WHERE token_hash = $1
AND consumed_at IS NULL
AND expires_at > now()
AND $2 = ANY (allowed_decisions)
RETURNING idempotency_key;
返回零行表示链接已过期、无效、已使用或提交了错误决策,此时不得发送。请在一个事务中执行这条语句,然后锁定匹配的 workflow_runs 行,确认其仍处于 awaiting_human,将其更新为 approved,并插入唯一的发件箱行。任何一步失败,都要回滚整个事务。对于高后果操作,还必须使用登录态 SSO,并检查角色与职责分离;仅持有电子邮件链接并不充分。
不要让模型选择
auto_reply后,在工作流没有强制阈值的情况下直接执行。提示词只能提出建议,节点必须执行约束。
为外部副作用建立事务性发件箱
核销审批、更新运行状态和记录预期的外部副作用,应在同一个数据库事务中完成。不要在审批 Webhook 内直接发送。最小的发件箱约束如下:
CREATE TABLE effect_outbox (
effect_id uuid PRIMARY KEY,
idempotency_key text NOT NULL REFERENCES workflow_runs(idempotency_key),
effect_type text NOT NULL,
target text NOT NULL,
payload jsonb NOT NULL,
state text NOT NULL CHECK (state IN ('pending', 'sending', 'completed', 'unknown', 'failed')),
lease_owner uuid,
lease_expires_at timestamptz,
provider_id text,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (idempotency_key, effect_type, target)
);
发件箱工作进程通过有时限的租约认领待处理记录(PostgreSQL FOR UPDATE SKIP LOCKED 专为队列式消费者设计;参见锁定子句文档),在服务提供方支持时使用同一个幂等键发起调用,存储服务提供方返回的外部 ID,再通过条件更新(CAS)将该记录标记为完成。
如果工作进程在提供方可能已经接受非幂等操作后超时,应将该副作用标记为 unknown,并在重试前向提供方查询或对账。例如,本地数据库事务无法保证 SMTP 邮件恰好发送一次。在结果未知时自动重发,正是客户收到重复邮件的常见原因。
事故后仍可用于追查的日志
n8n 执行历史是起点。它本身不是合规档案。对 AI 步骤,应按幂等键记录结构化事件:
- 时间戳和工作流版本 / 提交 ID(如果对工作流进行版本控制)
- 幂等键和触发源
- 脱敏输入哈希或允许的字段(不包括原始机密信息)
- 已批准的服务提供方和端点类别,以及模型与修订标识;避免在可被广泛访问的日志中暴露内部主机或凭证
- 已批准并经过精简的模型输出字段或受控引用;采集原始输出需要单独确定目的、访问权限和保留规则
- 验证结果
- 审批决定和操作人
- 带有外部 ID 的下游写入
- 错误类别和重试次数
不要为了调试而在共享渠道保存模型内部思维链的完整转储。只存储你愿意接受审计的决策摘要与工具参数。
执行日志常含工单与邮件中的个人数据。在生产 AI 节点启用详细日志之前,应明确保留、访问与脱敏规则。若你处理个人数据,使用本地模型并不能免除 GDPR 等法规要求的责任。
出事时,你需要回答:我们处理过这个键吗?我们发送了吗?谁批准的?哪个模型版本起草的?
线索或工单路径的参考序列
- Webhook 接收载荷 → 验证数据结构(采用 n8n 中的第一个 AI 智能体所示的门控方式)。
- 计算键和载荷哈希 → 以原子方式认领一个有时限的
processing租约。 - 调用 AI 节点或智能体,并使用结构化输出契约。
- 验证 JSON(枚举、必填字段、最大长度)。
- 如果在有限修复后仍无效 →
failed_terminal+ 人工警报。 - 如果有效且为高风险操作 → 使用 CAS 更新到
awaiting_human;创建一个仅保存哈希、会过期且只能使用一次的审批 token。 - 在经过身份验证的审批 POST 请求后 → 以原子方式核销审批 token、更新状态,并插入唯一的发件箱操作记录。
- 分发器为发件箱记录取得租约,在服务提供方支持时使用相同的键调用它,存储其 ID,并通过 CAS 将外部操作与运行标记为完成。
- 拒绝时 → 标记为终止状态并附上原因;不要入队。
- 在重复投递时 → 返回先前结果或报告当前状态;绝不在未明确告知的情况下重复模型或发送路径。
可选方案是通过 Hermes 使用 Bearer 身份验证的 API 服务器,把需要大量判断的起草任务交给 Hermes;如果事件入口和配置化交付契约适合该工作流,也可以明确采用独立的 Webhook 适配器。无论选择哪种方式,持久化键、审批关卡和连接器都由 n8n 或业务系统保管。示例见 n8n → Hermes:选择 API 调用还是事件 Webhook。
不破坏幂等的强制重跑
操作者会从 n8n UI 重新运行失败的执行。这本身是正常操作,但如果一次部分成功的任务已经把键置为 completed,重跑可能悄然创建第二条 CRM 备注;更糟的是,如果键从未写入,邮件可能被再次发送。
定义显式重跑协议:
- 可重试的恢复:只有
failed_retryable或已过期的processing租约可通过上述原子认领重新取得。继续保留相同的业务键。 - 禁止重放已终止或已完成的运行:
failed_terminal、awaiting_human、approved和completed键将返回其先前或当前状态,而不会重新开始。 - 有意的更正或替换:创建新的业务事件,使用上游为它签发的独立幂等键,将其关联到原始键和外部结果,记录操作者与原因,并通过全新的审批和发件箱路径处理。不要临时拼接后缀,也不要直接修改原运行记录。
在审批卡上展示这套规则,避免夜班操作者在压力下临时制定策略。
值得关注的可观测性指标
第一天不需要完整可观测性平台。每周跟踪:
- 重复 webhook 率(同键见两次)
- 审批关卡等待时间(p50 / p95,并标注为实际测量值)
- AI 节点后的校验失败率
- 自动行动 vs 人工批准比率
- 重试耗尽次数
验证失败率骤升时,需要调查模型、提示词、schema、输入分布或集成变更。重复投递率骤升时,需要调查上游重传、认领失败、重放或服务提供方结果不明确等情况;该指标本身无法诊断原因。
紧急停止开关与所有权
在副作用边界或调度器上强制实施默认拒绝的紧急停止开关,不能只在工作流第一个节点检查:即使运行从中途恢复或绕过前置分支,AI_ACTIONS_ENABLED=false 也必须阻止所有外部发送。请测试开关关闭时队列中及执行中的副作用,明确哪些信息仍会记录,并指定有权操作和验证该控制的负责人。
同时定义:
- 谁可以批准
- 谁可以强制重新运行,以及如何让替换事件获得一个新的上游颁发的键,该键链接到原始键而无需随意添加后缀
- 审批仍在等待时,支持服务 SLA 中的“完成”意味着什么
交付清单
- 在 AI 调用之前选择并持久化幂等性键
- 同一键的十次并发投递仅产生一个活动租约
- 测试过期租约恢复和陈旧所有者 CAS 拒绝
- 按失败类别记录重试规则
- 测试审批令牌哈希、过期时间、SSO/角色、POST/CSRF,以及单次使用约束能否阻止重放
- 人工审批关卡插入发件箱记录;它不能直接调用发送节点
- 提供方在可能接受后超时进入
unknown状态且不会自动重发 - 结构化日志包含键、验证、审批人和外部 ID
- 测试紧急停止开关
- 为日志设置数据保留与隐私规则
只有在故障发生时仍表现得稳定而可预期,AI 节点才真正值得投入生产。幂等性避免重试掩盖事实,人工审批关卡防止错误输出直接成为客户可见的结果,日志则让这两项保证都可被核查。



