讨论 AI Agent 执行退款等资金操作的风险,指出单一错误会在批量工单中重复 200 次,必须在 Stripe 调用前增加人工关卡。
让支持 Agent 能够通过退款来解决工单,也就意味着:一旦它状态不佳,便可能耗尽你的 Stripe 余额。将每一笔退款都置于人工审批之后,Agent 依然可以快速解决工单,只是不能在无人监督的情况下执行退款。
AI 支持 Agent 的大多数错误只会让人感到恼火:语气不对、重复回答,或遗漏了细微信息。但错误退款相当于错误地发起了一笔银行转账。如果 Agent 误读了政策(“30 天内的任何投诉都可以全额退款”)、被熟悉触发短语的客户诱导,或者只是凭空编造了订单总金额,钱就会真的转出去。Stripe 不会询问 Agent 是否确定——refunds.create 执行的是 API 调用,而不是调用背后的意图。
这也是最有可能产生连锁影响的故障模式:一个正在处理 200 个工单的支持 Agent,不会只犯一次错误;如果问题来自系统性的错误推理(例如 prompt 中的退款政策配置有误),它就会把同一个错误重复 200 次。在调用 Stripe 之前设置人工检查点,可以在第一个工单时发现问题,而不是等到第二百个工单。
Agent 的退款工具绝不能直接调用 Stripe——它应该调用一个 wrapper,将退款提案推送到 Impri,并且只有在获得批准后才调用 Stripe:
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const IMPRI_KEY = process.env.IMPRI_API_KEY!;
const BASE = "https://api.impri.dev";
interface RefundProposal {
chargeId: string;
amountCents: number;
customerEmail: string;
reason: string;
}
async function proposeRefund(p: RefundProposal): Promise<string> {
const res = await fetch(`${BASE}/v1/actions`, {
method: "POST",
headers: {
Authorization: `Bearer ${IMPRI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
kind: "payment.refund",
title: "`Refund $${(p.amountCents / 100).toFixed(2)} — ${p.customerEmail}`,"
preview: {
format: "markdown",
body: `**Charge:** ${p.chargeId}\n**Amount:** $${(p.amountCents / 100).toFixed(2)}\n**Reason given by agent:** ${p.reason}`,
},
expires_in: 43200, // 12h — support tickets shouldn't sit longer than that
idempotent: false,
undo: "No automatic undo — a reversed refund requires a new manual charge",
}),
});
const { id } = await res.json();
return id;
}
async function waitForDecision(actionId: string) {
while (true) {
const res = await fetch(`${BASE}/v1/actions/${actionId}`, {
headers: { Authorization: `Bearer ${IMPRI_KEY}` },
});
const data = await res.json();
if (data.status !== "pending") return data;
await new Promise((r) => setTimeout(r, 5000));
}
}
async function refundWithApproval(p: RefundProposal) {
const actionId = await proposeRefund(p);
const decision = await waitForDecision(actionId);
if (decision.status !== "approved") {
return { executed: false, status: decision.status };
}
const refund = await stripe.refunds.create({ charge: p.chargeId, amount: p.amountCents });
await fetch(`${BASE}/v1/actions/${actionId}/result`, {
method: "POST",
headers: { Authorization: `Bearer ${IMPRI_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ status: "executed", payload: { refundId: refund.id } }),
});
return { executed: true, refundId: refund.id };
}
stripe.refunds.create 调用位于 decision.status !== "approved" 的提前返回之后——这正是整个设计的关键。Agent 的工单处理逻辑可以判断是否应该退款、起草退款理由,并调用 refundWithApproval;但实际的资金流转只隔着一个 if,而这个 if 取决于人工决策,而不是 Agent 自己的判断。
上面的 preview.body 特意展示了 charge ID、退款金额,以及 Agent 给出的理由。审批者通过手机批准退款时,只需要这三项信息,就能快速做出正确判断,不需要其他内容。之所以设置 undo,是因为通过这个 API 发起的退款确实无法撤销;审批者应该在点击批准之前知道这一点,而不是事后才发现。
通过 payload: { refundId: refund.id } 上报结果,意味着 Stripe refund ID 会附加到审批记录中。当财务人员询问“这笔退款是谁批准的,为什么批准”时,答案会集中在一个地方,而不是分散在 Stripe dashboard 和支持工具的日志里。有关这些记录如何保留和查询,请参阅审计日志。
Impri 不知道你的退款政策,也不会判断这个工单退款 340 美元是否合理——它只会向审批者展示 Agent 提出的方案,并负责承载“批准或拒绝”的决定。Agent 仍然负责推理;审批者仍然负责做出判断;Impri 则负责确保只有两者都参与后,Stripe 调用才可能发生。有关完整的三次调用模式(REST 和 MCP),以及如何让 wrapper 成为真正的强制关卡,而不只是一项建议,请参阅主要集成指南。如果你的团队希望使用基于 Slack 的批准/拒绝流程,而不是 Web 收件箱,请参阅通过 Slack 审批 Agent 操作。
第一次使用 Impri?请从快速入门开始。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。