从写代码,到创作下一幕

探索 字节跳动 - 火山方舟 的 AI 编程与视频创作活动。

火山方舟 Agent Plan 与 Coding Plan 活动,价格 ¥9.9 起

Agent Plan & Coding Plan

一站体验多款热门模型,为 AI 编程与智能体开发提供更多选择。新用户可联系(微信: goo_lvyouyou)免费体验 9.9 agent plan。

查看套餐优惠
Seedance 2.5 视频创作活动:30 秒视频、多模态参考、局部编辑

Seedance 2.5

让创意,跃然成片。探索 30 秒视频、多模态参考与局部编辑,把脑海中的画面变成下一支作品。

探索视频创作

需要人工审批的退款 Agent

查看 Markdown

本指南使用 Grok API 构建一个客服 Agent,在退款前先请人审批。对于需要实际执行操作的 Agent,都可以使用这种做法:先停下并保存状态,之后恢复同一次运行,确保操作只执行一次。

客户发邮件要求退款。Agent 查询订单和退款政策,判断允许退还哪些款项,再提出退款方案。由于退款无法撤销,Agent 会停下来,将退款请求放进队列等待人工批准。一旦获批,Agent 就从暂停处继续同一次运行,执行一次退款,并向客户发邮件,即使服务器在此期间重启也能继续。审计日志会记录谁批准了哪笔退款。

你将学到什么

  • 在执行不可撤销的函数调用前暂停 Agent,并保存恢复所需的信息:已存储响应的 ID、调用 ID 和调用参数

  • 稍后恢复运行,使用 previous_response_id 及该次调用的输出,无需重发对话,因为 store: true 可将响应在 SpaceXAI 服务器上保留 30 天

  • 将调用 ID 用作幂等键,即使两次点击 Approve 或服务器中途崩溃,已批准的退款也只会执行一次

  • 记录审计日志,可追溯每笔退款由谁提出、批准或拒绝

  • 当客户回信时,从同一个已保存的响应继续之前已完成的运行,并让 Grok 扮演生气的客户来测试 Agent

  • 通过服务器发送事件(SSE)将 Agent 的推理过程和工具调用从 Node 服务器流式发送到网页,在服务器重启时继续中断的运行

运行示例

需要 Node.js 22.13 或更高版本。将 API 密钥放入 .env(位于仓库根目录),或导出环境变量 XAI_API_KEY。

Bash

cd examples/refund-agent-human-approval/typescript
npm install
npm run web

打开 http://localhost:3000。网页类似客服工作台,左侧为收件箱,中间显示工单,右侧是审批队列和审计日志。可以按以下步骤体验:

  1. 点击 Cracked on arrival。Maya 来信反映,收到的手冲咖啡壶破裂了,但同一订单里的杯子完好。工单展示 Agent 工作的每一步:推理过程、查到的订单、适用的政策,以及只退咖啡壶、不退杯子的 $48.00 方案。运行在此暂停,退款会出现在 Waiting for approval。

  2. 按 Ctrl+C 停止服务器,然后使用 npm run web 重启。页眉会显示服务器离线,然后以新的进程 ID 重新上线。退款会从以下文件加载,继续等待审批:output/state.json。

  3. 点击 Approve。新进程从旧进程存储的响应继续运行,工单会在相应位置标出 Server restarted。Agent 执行退款、给 Maya 发邮件,并为团队留下一条备注。审计日志中,请求来自旧进程,审批和退款则来自新进程。

  4. 试试其他案例。点击 Reject 会要求填写备注,Agent 会据此向客户解释决定。Past 30 days 的请求不符合退款政策,Agent 会直接拒绝,无需人工介入。Stop 可中途取消运行,Resume 则从上次保存的响应继续。

  5. 点击 Angry customer。Priya 要求退还订单 A1066 的全款 $94.95,其中包含售出后不可退还的礼品卡。Agent 提议只退平底锅的钱,扣除退货手续费后为 $31.50。获批后,Grok 扮演 Priya 回信:她拒绝接受,声称要发起拒付、留一星评价,并要求与经理交谈。Agent 会保持冷静,遵循政策,并在给团队的备注中标明这些威胁。每条客户回复都会注明由 Grok 撰写,三次回复后对话结束;如果客户更早放弃,则会提前结束。对于已解决的工单,可以选择 Reply as the customer,亲自扮演客户提出异议。

要测试崩溃恢复流程,请使用 CRASH_AFTER_REFUND=1 npm run web 启动服务器并批准一笔退款。退款完成后,进程会在保存结果前立即退出。去掉环境变量,重新启动服务器。它会自动继续运行,支付服务会返回已完成的退款,避免重复退款;审计日志也会记录这次复用。

终端版本执行相同流程,每条命令都会启动一个新进程:

Bash

npm start                               # sends Maya's email and stops at the approval
npm start -- approve --as "Dana Kim"    # approves it in a new process and finishes the run
npm start -- approve                    # nothing left to approve

npm start -- reject --note "Why not" 会拒绝退款,npm start -- "Your email" --from you@example.com 则发送你自己的请求。npm start -- angry 启动生气的客户场景,npm start -- reply "Your reply" 则以客户身份回复最近一次已完成的运行。按 Ctrl+C 可停止运行,使用 npm start -- resume 可继续最近一次被停止、失败或因崩溃而中断的运行,例如 CRASH_AFTER_REFUND=1 npm start -- approve,不会重复退款。终端和网页应用共享 output/,因此一次只使用其中一种。

处理 Maya 的请求需要五次调用 grok-4.7,审批前三次,审批后两次。我们测试时,多数调用耗时一至四秒,整次运行等待 Grok 的时间约为 10 至 15 秒,另加人工审批所需时间。API 繁忙时,单次调用有时需要 20 秒。一次运行约花费 1 美分,两种版本都会显示每次运行的费用。

工作原理

Agent 的代码位于 src/agent.ts,模拟商店位于 src/shop.ts,订单来自 orders.json,政策来自 refund-policy.md。还有两个模拟的外部系统:支付服务将退款写入 output/refunds.log,邮件服务则写入 output/outbox.log,不会真的发送邮件。

  1. 模型可使用四个函数工具:lookup_order,check_refund_policy,issue_refund 和 send_customer_message。政策工具同时返回政策及作出判断所需的事实,例如送达至今的天数、剩余可退款金额,这样模型不用自己数天数或汇总历史退款。send_customer_message 只能回复请求的发件人;商店会另起一行添加签名,让每封邮件都以相同方式结尾。

  2. advance() 逐轮运行 Agent。每轮发起一个流式 client.responses.create() 请求,并设置 store: true,让 API 将响应保存 30 天。未明确要求时,SDK 不保存响应。第一轮发送系统提示词和客户邮件。之后每轮只发送上一轮函数调用的输出,并将 previous_response_id 设为发起这些调用的响应 ID,其余对话由 API 补全。API 不允许将 instructions 与 previous_response_id 一起使用,因此提示词放进首轮的输入中。parallel_tool_calls: false 将每轮限制为一次调用,因此某轮暂停等待审批时,不会留下其他挂起的调用。每次运行的各轮都使用相同的 prompt_cache_key,请求会到达同一服务器,复用先前轮次的缓存。

  3. 模型请求调用 issue_refund,refund() 不会立刻执行退款。它先对照订单核对参数,将待审批记录写入 output/state.json,随后暂停运行。等待审批期间没有任何任务在后台执行,因此服务器可以重启,审批也可以挂起数天。审批记录包含继续运行所需的全部信息:

    JSON

    {
      "id": "call-358bffbd-f436-43ec-8031-6a57f0ca0d93-2",
      "run": "run_ce2290f3",
      "response_id": "0ba8f3c0-ea96-9805-8bbb-01da7e9a74f7",
      "call_id": "call-358bffbd-f436-43ec-8031-6a57f0ca0d93-2",
      "arguments": { "order_id": "A1043", "amount": 48, "reason": "Glass pour-over coffee maker arrived cracked; full item price refunded within 30 days, no return required." },
      "status": "pending"
    }
  4. decide() 会在等待任何异步操作之前,先将决定与决策者写入状态文件和审计日志。再次点击时,服务器发现该退款已经作出决定,返回 409,审计日志会记下这次尝试。接着,advance() 回到同一条调用。获批时执行退款,被拒时返回审批人的备注。将这个结果作为函数调用的输出,并将 previous_response_id 设为已保存的响应 ID,模型便可以继续,订单、政策和它此前的推理仍在上下文中。

  5. 每次调用一经执行就会保存其输出;恢复运行时直接发送保存的输出,不再执行原调用。仍有一种情况需要处理:退款已完成,服务器却在保存输出前崩溃。issueRefund() 通过接受调用 ID 作为幂等键来处理这一情况,与支付服务接受 Idempotency-Key 请求头的方式相同。第一次使用某个键会执行退款,使用相同键的后续请求都会返回原退款。发件箱也以相同方式处理邮件。

  6. output/audit.log 会为 Agent 提出的每个退款请求、每次批准和拒绝、每笔已执行或复用的退款,以及每个来得太晚的审批决定各记一行,并记录操作人、时间和进程。

  7. 如果进程在一轮执行过程中退出,该运行在状态文件中仍标记为进行中,src/server.ts 会在启动时恢复这些运行。如果响应超过 30 天已经失效,运行会失败,并提示相应原因。

  8. 客户回信后,可用与审批相同的方式继续已完成的运行。addReply() 会保存回信。下一轮将其作为新的用户消息发送,同时将 previous_response_id 设为上次存储的响应 ID。这样 Agent 可继续使用订单、政策和它写过的内容,无需重发。提示词说明如何处理异议:冷静回应,无论客户如何施压都坚持政策,并在给团队的备注中标出威胁或转接经理的要求。

  9. 对于生气的客户场景,converse() 位于 src/customer.ts 中,它封装了 advance()。每当一次运行完成,它就会请 Grok 扮演客户。这是一个独立请求,不提供工具,只给出简短的角色说明;它只读取此前的邮件,不读取 Agent 的推理,然后写出 Priya 的下一封回信,或在她表达完诉求后返回 DONE。回信会像真人输入的消息一样送入原运行,使其继续。最多三次回复,以免两个模型争论不休。Grok 作为客户发起的请求也计入运行费用。

每轮都使用 low 推理强度,因为只需选择下一个工具或写一封短邮件。我们用默认强度测试退款决定时,耗时从 2 至 9 秒增加到 15 至 47 秒,最终仍决定退款 $48.00。如果请求在 Grok 开始回答前失败(例如触发限流),客户端最多重试五次。每次运行的费用是 API 在各请求的 usage.cost_usd 中报告的费用之和。

src/server.ts 是一个小型 node:http 服务器。每个打开的网页都保持一条 SSE 流,接收每次运行的每项变更,所以各标签页显示同一个队列。服务器中断后,页面的 EventSource 会持续尝试重新连接。新进程会发送一份最新快照,数据加载自 output/。发起、停止或恢复运行,以及批准或拒绝退款,都通过 POST 请求完成。每次 API 调用都会收到一个 AbortSignal。点击 Stop 或关闭发起运行的网页会触发中止,运行停在最后一次保存的响应处,可用 Resume 恢复。等待审批时没有正在执行的请求,关闭网页仍会保留队列中的退款。public/index.html 使用原生 HTML 和 JavaScript 显示这些事件,而 src/index.ts 在终端中完成相同的工作。