AI Agent执行API调用后可能过度承诺成功,导致用户获得错误结果,需关注状态问题而非仅优化prompt。
我以为是 prompt 的问题。
其实是状态管理的问题。
我的客服 Agent 能读取 Zendesk 工单、找到 Shopify 订单、发起 Stripe 退款、更新工单、回复客户。在 staging 环境下表现很好。在生产环境中,它开始做一些比「答错」更糟糕的事。
它自信满满地告诉客户退款已完成,而实际上工作流只是触发了一个 API 调用然后听天由命。
这个区别很重要。
因为一旦 LLM 能够调用 Shopify、Stripe、Zendesk 这类真实系统,失败模式就变了。模型不需要凭空捏造一个工具名来害你。它只需要在一次有效的工具调用之后,过度陈述发生的事实。
核心 bug:有效的工具调用,无效的结论
很多团队在遇到「Agent 任务失败」时会立刻做以下其中一件事:
强制执行更严格的 JSON schema
有时候这确实有用。
但如果你的 Agent 已经能用正确的参数调用正确的 API,那 prompt 工程就不再是主要杠杆。
OpenAI 的 Structured Outputs 在这方面确实是真正的改进。他们的公开评测显示 gpt-4o-2024-08-06 在复杂 JSON schema 上达到了 100% 的 schema 遵从率,而 gpt-4-0613 低于 40%。
但 schema 遵从不等于操作层面的真实。
你可能输出了完美的 JSON,但仍然在做一个这样的客服工作流:
在资金实际流动之前就报告成功
重试时不保留确定性
与异步系统失去同步
这是我的第一个线索。
Agent 调用了 Shopify 的 refundCreate,拿到了 Refund 对象,然后告诉客户:
您的退款已处理。
看起来合理,对吧?
Shopify 的文档明确指出:Refund 对象的存在并不能保证金融交易已完成。实际结果存在于相关的 OrderTransaction 对象中,可能是 pending、processing、success 或 failure。
所以这个模式是有问题的:
告诉客户完成了
这不是确认。这是带着 JSON 的乐观主义。
mutation 本身不是问题
mutation RefundOrder @idempotent(key: "refund-order-123") {
refundCreate(input: {
orderId: "gid://shopify/Order/123",
note: "Customer requested partial refund"
}) {
refund { id }
userErrors { field message }
}
}
Bug 发生在 mutation 之后。
你需要在面向客户的消息之前再检查一步交易状态。
async function refundInShopify(orderId: string) {
const refund = await shopify.refundCreate({ orderId });
if (refund.userErrors?.length) {
return { status: "failed", reason: refund.userErrors };
}
const txns = await shopify.getOrderTransactions(orderId);
const refundTxn = txns.find(t => t.kind === "refund");
if (!refundTxn) {
return { status: "unknown", reason: "No refund transaction found" };
}
switch (refundTxn.status) {
case "success":
return { status: "completed", refundId: refund.refund.id };
case "pending":
case "processing":
return { status: "pending", refundId: refund.refund.id };
case "failure":
return { status: "failed", refundId: refund.refund.id };
default:
return { status: "unknown", refundId: refund.refund.id };
}
}
就这么一个额外的验证步骤,就把客户消息从猜测变成了可辩护的陈述。
Stripe 是这里变得危险的地方。
你的 Agent 发送了 POST /v1/refunds
响应从未被持久化
模型决定「再试一次」
现在你有了不确定性。
Stripe 创建退款了吗?失败了?还是成功了只是丢失了响应?
这正是 Stripe 幂等键存在的原因。
而太多 Agent 工作流仍然把幂等性当作可选项。
幂等性让你可以重试而不会破坏证据链。
curl https://api.stripe.com/v1/refunds \
-u "sk_test_...:" \
-H "Idempotency-Key: 8b5b9f2e-6f8d-4f3d-a6d8-2f0f4d7f9c21" \
-d charge=ch_123
Stripe 会为给定的幂等键存储第一次结果,并在重试时返回相同的状态码和响应体,包括 500 错误。如果你用相同的键但不同的参数,Stripe 会拒绝。
这意味着你的工作流需要在调用之前持久化这个键,而不是之后。
async function badRefundRetry(chargeId: string) {
try {
return await stripe.refunds.create({ charge: chargeId });
} catch {
// terrible: new request identity, no certainty
return await stripe.refunds.create({ charge: chargeId });
}
}
import { randomUUID } from "crypto";
async function createRefundWithRecovery(chargeId: string, existingKey?: string) {
const idempotencyKey = existingKey ?? randomUUID();
await db.refundAttempts.upsert({
chargeId,
idempotencyKey,
status: "started"
});
try {
const refund = await stripe.refunds.create(
{ charge: chargeId },
{ idempotencyKey }
);
await db.refundAttempts.update({
chargeId,
idempotencyKey,
status: "completed",
refundId: refund.id,
rawResponse: JSON.stringify(refund)
});
return refund;
} catch (err) {
await db.refundAttempts.update({
chargeId,
idempotencyKey,
status: "unknown",
error: String(err)
});
throw err;
}
}
那个 unknown 状态很重要。
很多团队试图避免它,因为它看起来很混乱。但「unknown」是诚实的。告诉模型去猜不是。
Zendesk 是精心打磨的 demo 通常崩溃的地方。
限速不是建议
Zendesk Support 和 Help Center API 限制因计划不同而异。响应可能包含如下响应头:
X-Rate-Limit: 700
X-Rate-Limit-Remaining: 699
如果你触达限速,会收到 429 Too Many Requests 和 Retry-After。
一个因为想帮忙就持续猛击 API 的模型不是在帮忙。
它只是一个昂贵的循环。
有些动作是任务,不是即时完成
批量工单更新就是典型例子。
工作流发送更新、收到确认,然后就假设工单已更改。
但 Zendesk 任务状态可能处于:
如果你的 Agent 退了 40 个订单并批量更新了 40 个工单,你不能因为第一次调用返回 200 或 202 就假设工单端完成了。
你需要轮询任务 URL 并核对失败。
最小轮询示例
async function waitForZendeskJob(jobStatusUrl: string) {
for (let attempt = 0; attempt < 20; attempt++) {
const job = await zendesk.get(jobStatusUrl);
switch (job.status) {
case "completed":
return job;
case "failed":
throw new Error(`Zendesk job failed: ${job.id}`);
case "queued":
case "working":
await sleep(3000);
continue;
default:
throw new Error(`Unknown Zendesk job state: ${job.status}`);
}
}
throw new Error("Zendesk job did not reach terminal state in time");
}
这不是什么光鲜的工程。
然而,这就是靠谱的客服 Agent 和只是听起来靠谱的客服 Agent 之间的区别。
Prompt 修复 vs 编排修复
这是我希望早点分清的。
这就是为什么我认为很多「LLM 工具使用可靠性」的讨论格局太小了。
一旦你接触到资金、工单、订单或客户记录,你就不再是在调试一个聊天机器人了。
你在做分布式系统的工作。
LLM 只是链中的一个组件。
如果我必须把这条压缩成一条规则:
永远不要让模型从第一个有副作用的响应中传达成功。
这是速查表。
| 检查项 | Shopify 退款流程 | Stripe 退款流程 | | ---------- | ---------- | | 初始对象能保证资金流动吗? | 不能。单独的 Refund 对象不是退款已结算的证明 | 通常退款响应是主要记录,但重试必须保持幂等性以维持确定性 | | 需要调用后验证吗? | 需要。检查关联的 OrderTransaction 状态,如 pending、processing、success 或 failure | 需要,尤其是在超时或网络故障后;使用相同的幂等请求历史或后续检索来验证 | | 重试应该依赖什么? | 存储的工作流状态和显式验证逻辑 | 相同参数下的相同幂等键 |
最后那行是很多 Agent 工作流悄悄失败的地方。
不是因为 Shopify 或 Stripe 不稳定。
是因为工作流在重试时是无状态的。
你需要 LangGraph 吗,还是 n8n / Make 就够了?
我的看法:不是每个客服自动化都需要完整的 Agent 运行时。
如果流程大多是确定性的:
那么 n8n、Make,甚至带明确分支的 Zapier,可能比自主循环更安全。
人工 escalation 分支
如果工作流更开放、需要跨长时间窗口的可恢复性、局部失败恢复或分支调查,那么 LangGraph 开始更有意义。
Tiny LangGraph 骨架
from langgraph.graph import StateGraph, MessagesState, START, END
def mock_llm(state: MessagesState):
return {"messages": [{"role": "ai", "content": "hello world"}]}
graph = StateGraph(MessagesState)
graph.add_node(mock_llm)
graph.add_edge(START, "mock_llm")
graph.add_edge("mock_llm", END)
graph = graph.compile()
graph.invoke({"messages": [{"role": "user", "content": "hi!"}]})
这个例子很 trivial。
重要的部分是心态转变:状态转换、检查点、可恢复性、确定性恢复。
不是「也许 GPT-5 或 Claude Opus 4.6 下次会更谨慎」。
最终阻止说谎的模式
真正解决这个问题的方法很无聊。
我把工作流分成了几个阶段:
用验证过的输入准备操作
用幂等键或等效的请求标识执行
立即持久化外部 ID 和原始响应
在 Shopify、Stripe 或 Zendesk 中验证下游状态
只从已验证状态进行沟通
将模糊或非终态的情况 escalation 给人工
这比所有 prompt 调优加起来对可靠性的提升都大。
如果你在生产环境中运行 AI 代理,成本压力会让情况更糟
还有一件事人们谈得不够多:按 token 计价会推动团队做出糟糕的可靠性决策。
当每次重试、轮询、验证步骤和恢复分支都像在计费时,人们开始砍掉那些无聊的部分。
他们跳过验证。缩短重试。避免持久化状态。让模型即兴发挥,因为看起来当下更便宜。
这完全搞反了。
生产级 Agent 工作流需要空间来:
执行对账过程
运行长时间运行的自动化
这正是像 Standard Compute 这样的工具对构建客服 Agent、n8n 流程、Make 场景和自定义自动化的团队很有吸引力的一个大原因。如果你使用的是 OpenAI 兼容的 API 形态,但想要可预测的固定费用计算而不是按 token 焦虑,这会改变你设计可靠性时的激进程度。
你不再问「我们能承受再来一次验证吗?」
你开始问更好的问题:
「什么能让这个工作流停止对客户说谎?」
这才是正确的优化目标。
如果你的退款 Agent 听起来聪明但偶尔说谎,不要假设修复方法是更好的 prompt。
检查你的工作流是否在做这些事:
从初始 API 响应确认成功
重试时没有幂等性
未能持久化请求标识
跳过异步任务轮询
对账前发送面向客户的消息
真正的 bug 通常就在那里。
对我来说痛苦的教训很简单:
副作用不是聊天回合。
它们是穿着聊天机器人外衣的分布式事务。
一旦你这样对待它们,你的 Agent 就会变得不那么讨人喜欢,但更值得信赖。