前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯10189
  • Mercury 3 发布:主打高速生成与百万上下文
  • 用 Node 拆解向量池化与检索质量
  • 补齐 Oracle 方言约束,轻量模型首答率跃升
  • 拆解 Claude Code 的完整智能体循环
  • 邮件 Agent 重试前先校验退信
  • 用信号队列约束 Agent 等待邮件
  • 用 Cloudflare 搭建 Agent 收信中枢
  • 一次字符串替换如何让线上文章翻倍
  • AI 搭项目失败,先确认代码是否拉取成功
  • 检索匹配再高,也救不了过期知识库
  • 本地模型帮你给常用命令起快捷名
  • MCP 无状态改版涉及哪些迁移工作
  • 检查 Supabase 服务端鉴权的信任来源
  • 流式 AI 接口延迟该如何拆分测量
  • 模型故障切换如何追踪实际调用路径
  • 百灵开放五款语音模型供开发者组合
  • Drex Max 为电脑操作智能体输出决策概率
  • 无人值守的 Claude 循环如何耗尽额度
  • Bedrock为Claude提供严格结构化输出
  • 错误码搜不准?给向量检索补上 BM25
  • 让 Agent 崩溃后续跑,并处理重复副作用
  • 用 TypeScript 搭建带人工审批的客服智能体
  • Copilot 模型策略支持 REST API 管理
  • Copilot多种使用场景启动模型弃用
  • 把框架说明随包交付,让编程智能体可用
  • Nx 恶意包如何借编程智能体窃取秘密
  • 用分层防护限制 Agent 越权与误操作
  • 在表格中用自然语言分析完整BigQuery数据
  • 亚马逊推 TorchNeuron,拓宽 PyTorch 芯片选择
  • OpenCode平台遭入侵,密钥被盗刷
  • Cloudflare如何将AI审查成本降68%
  • 将Copilot任务无缝接续到VS Code
  • Gemini 4 Argon 主打多 Agent 三维制作
  • 用关联 ID 串起 Agent 日志与异常
  • MiniMax新模型以半价提升编程表现
  • 用ADK消除语音代理调用工具时的冷场
  • 62行Bash拆解编程代理的运行机制
  • 从调用轨迹定位代理的延迟与费用问题
  • AI 迁移脚本删库:备份失败为何未阻断
  • DeepSeek 推出 Agent 搜索与网页抓取接口
  • Bedrock新增长视频结构化解析模型
  • 语音识别迁移 Trainium2 的优化实战
  • vLLM 推送式 KV 传输降低推理等待
  • vLLM 双向复用 KV 缓存,降低多轮首字延迟
  • 英伟达发布面向企业与个人电脑的开放模型
  • Agent 工具报错,先判断副作用再重试
  • 在常驻虚拟机上搭建持久化编程 Agent 团队
  • 用DGX Spark承载本地n8n AI工作流
  • 训练数据权限如何随流水线变换传播
  • 用 AI 在 PR 中自动补齐架构决策记录
  • 用故障注入检验 AI 补丁的测试有效性
  • 已加载 51 / 10189
8.0
热点
AI SCORE
技术实践2026-10-20 07:28

让 Agent 崩溃后续跑,并处理重复副作用

dev.to · AI#Agent#持久化#幂等性
Editor brief · 编辑速览

Amoxi 将请求保存为任务,由后台工作进程执行,并在模型轮次和工具结果后写入检查点,以支持重启恢复。教程围绕退款与邮件场景处理副作用,并通过 MCP、A2A 向其他 AI 客户端开放 Agent。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

持久化运行可以从中断处继续,副作用不会重复发生,还能通过 MCP 和 A2A,让 Claude、ChatGPT 以及其他 Agent 调用你的 Agent。

在第 5 篇文章中,每次运行大约只需要一秒。真实的 Agent 任务要慢得多:多次模型调用、支付服务商、邮件服务器。在这几分钟里,某次部署可能会重启服务器,进程也可能崩溃。这时,正在执行的任务怎么办?客户的退款到底退了一次、两次,还是一次都没退?

本文继续使用第 5 篇文章中的客服助手。我们会添加一个退款工具,在退款过程中强行终止服务器,然后把这个 Agent 开放给其他 AI 客户端。

运行任务是后台作业

收到消息后,Amoxi 不会在 HTTP 请求中执行 Agent。它会保存一个运行任务,然后立即返回;后台 worker 负责执行,浏览器则通过事件流跟踪进度。每轮模型交互和每次工具返回结果后,运行任务都会向存储写入一个检查点,默认保存在 data/ai/ 目录下的文件中。

正是这些检查点,让运行任务具备了持久性。如果进程退出,另一个 worker,或者重启后的同一台服务器,就能从最后一个检查点接着执行。

一个会产生实际副作用的工具

退款有两个副作用:资金转移,以及向客户发送邮件。创建 src/orders/payments.ts,用它模拟支付服务商,把每笔退款写入文件,方便统计退款次数:

import { appendFileSync, mkdirSync } from 'node:fs';
import { Singleton } from 'amoxi';

/** Stands in for a payment provider: every refund is appended to data/refunds.log. */
@Singleton()
export class Payments {
  refund(orderId: string, idempotencyKey: string): { refundId: string } {
    mkdirSync('data', { recursive: true });
    const refundId = `R-${Date.now()}`;
    appendFileSync('data/refunds.log', `${refundId} ${orderId} key=${idempotencyKey}\n`);
    return { refundId };
  }
}

接着是工具 src/orders/refund-tools.ts:

import { Singleton } from 'amoxi';
import { Tool, type ToolContext } from 'amoxi/ai';
import { Payments } from './payments';

@Singleton()
export class RefundTools {
  constructor(private readonly payments: Payments) {}

  /** Refund an order and email the customer a receipt. */
  @Tool({ idempotent: true })
  async refundOrder(
    input: {
      /**
       * The order number, e.g. A-1001
       * @pattern ^A-\d{4}$
       */
      orderId: string;
    },
    ctx: ToolContext,
  ): Promise<{ refundId: string; emailed: boolean }> {
    const { refundId } = await ctx.step('refund', () => this.payments.refund(input.orderId, ctx.callId));
    ctx.progress('Refund done, sending the receipt...');
    await ctx.step('email', async () => {
      await new Promise((r) => setTimeout(r, 4000)); // a slow mail server
      console.log(`receipt for ${refundId} sent`);
      return true;
    });
    return { refundId, emailed: true };
  }
}

把 RefundTools 加入 Agent 的工具列表。这个工具里有三点很关键:

ctx.step('refund', fn) 会执行 fn,并把结果记录到运行任务中。如果工具在进程崩溃后再次执行,已经完成的步骤会直接返回记录的结果,不会重新执行。

@Tool({ idempotent: true }) 告诉 Amoxi,整个工具可以安全地重新执行,因为它的副作用都封装在步骤里。

ctx.callId 会作为幂等键传给支付服务商。中断的调用重试时,这个值保持不变;每次新调用则会得到不同的值。大多数支付 API 都接受这样的键,即使进程在调用支付服务商的过程中崩溃,它也能提供保护。

ctx.progress(...) 会在耗时操作执行期间,在聊天界面显示一行状态信息。

启动应用,输入“为订单 A-1001 退款”,然后在收据还处于“发送中”时强行终止进程(Ctrl+C 太温和了;可以关闭终端,或者使用任务管理器、kill -9)。此时,退款已经记录下来:

R-1792425180411 A-1001 key=offline_1

再次启动应用。等已经退出的 worker 对运行任务的占用租约到期后,任务就会恢复执行。租约默认是 30 秒,由 ai.runs.lease-ms 配置。工具会再次运行:退款步骤直接返回之前记录的结果,因此不会再次退款;尚未完成的邮件步骤则会在这时执行:

receipt for R-1792425180411 sent

data/refunds.log 仍然只有一行记录,运行任务最终返回的也是最初的退款 ID。对于看着聊天界面的用户来说,任务会继续执行,仿佛什么都没发生;浏览器会自行重新连接事件流。

如果没有把工具标记为幂等,会怎样?Amoxi 绝不会盲目重新执行中断的调用。它会告诉模型,这个操作可能已经完成,也可能没有完成,模型可以进行核查,例如查询订单,也可以询问用户。对于尚未实现幂等性的工具,这是安全的默认行为。

POST /agents/support-agent/runs/<id>/cancel 可以停止运行任务,即使任务正在执行工具。工具会收到 ctx.signal,这是一个标准的 AbortSignal;把它传给 fetch 和其他接受该信号的调用,这些操作也能随之停止。

一台服务器不够用时

文件存储适合单台服务器。如果需要多台服务器,就切换到带有 pgvector 的 PostgreSQL:

npm install pg

在 application.json 中设置 "ai": { "store": { "type": "postgres" } },并提供 DATABASE_URL。这样,运行任务就会组成一个共享队列:任意服务器都可以领取等待中的任务,而以 ai.runs.worker=false 启动的服务器只负责处理 Web 流量。Agent 的代码无需修改。

把你的 Agent 变成 MCP 服务器

Model Context Protocol(MCP)让 Claude、ChatGPT、Cursor 和 IDE 助手等 AI 客户端能够调用服务器上的工具。每个 Amoxi 应用都可以成为这样的服务器。为每个要连接的客户端生成一个 token。下面的命令在 bash 和 PowerShell 中都能运行:

node -e 'console.log(require(`crypto`).randomBytes(32).toString(`hex`))'

在 application.json 中启用 MCP:

{ "ai": { "mcp": { "enabled": true } } }

然后,通过环境变量 AI_MCP_CALLERS_<NAME> 为每个调用方分别配置 token,例如用 AI_MCP_CALLERS_DESKTOP 配置桌面客户端。不要把 token 放进 application.json,也不要提交到 git。如果 token 少于 16 个字符,Amoxi 会拒绝启动。

现在,任何支持远程服务器的 MCP 客户端,都可以带上请求头 Authorization: Bearer <token>,连接到 http://localhost:8080/mcp。没有 token,会返回 401;带上 token 后,tools/list 会返回:

list_notes          readOnlyHint
add_note
lookup_order        readOnlyHint
refund_order        idempotentHint
ask_assistant
ask_support_agent

留意哪些内容出现了,哪些没有:

你的工具会带上从装饰器提取的提示:readOnly 变成 readOnlyHint,idempotent 变成 idempotentHint。

cancel_order 没有出现。需要审批的工具绝不会通过 MCP 对外提供,因为那里没有人来审批。

每个 Agent 都会以 ask_<agent> 工具的形式出现,因此客户端可以把整个问题委派给它。

refund_order 被直接提供给了客户端。对于一个涉及资金转移的工具,这可能并不是你想要的行为。可以用 @Tool({ idempotent: true, expose: false }) 把它隐藏起来,Agent 仍然可以使用它。

调用 lookup_order 时,返回的订单既有文本形式,也有 structuredContent 形式,后者的类型来自你的返回类型。

如果客户端要求 Agent 执行一个需要审批的操作呢?

ask_support_agent  { "message": "Cancel order A-1002" }
-> "That didn't work: The person declined this action."

Agent 会获知原因,并据此作答。如果 MCP 调用方是已经登录你应用的用户,可以把 ai.mcp.approvals 设置为 wait。这样,运行任务就会暂停,直到该用户在应用中批准操作。

通过 A2A 实现 Agent 间通信

A2A 协议用于 Agent 之间的通信,例如,合作伙伴的采购 Agent 向你的客服 Agent 查询订单。启用方式与 MCP 类似:

{ "ai": { "a2a": { "enabled": true } } }

通过 AI_A2A_CALLERS_<NAME> 为每个合作伙伴配置一个 token。其他 Agent 可以通过 /.well-known/agent-card.json 发现你的 Agent。这个文件把整个应用描述为一个 A2A Agent,每个 Amoxi Agent 对应其中的一项 skill:

{ "name": "agent-app", "url": "http://localhost:8080/a2a", "skills": ["assistant", "support-agent"] }

合作伙伴向某项 skill 发送任务:

{ "jsonrpc": "2.0", "id": 1, "method": "message/send",
  "params": {
    "message": { "role": "user", "messageId": "m1", "parts": [{ "kind": "text", "text": "Where is order A-1001?" }] },
    "metadata": { "skill": "support-agent" } } }

随后收到一个已完成的任务,答案作为 artifact 返回:

Here's what I found:
ID: A-1001
Item: Trail backpack, 28 L
Status: shipped

运行任务映射为 A2A task,对话映射为 context,审批则映射为 input-required 状态。

调用方之间相互隔离

每个调用方都有自己的 token,因此 Amoxi 知道请求来自谁。携带 token 发起请求的调用方,会以 a2a:partner 或 mcp:desktop 这样的身份拥有自己的对话。它只能列出和继续自己的任务,Agent 对某个调用方保留的记忆也只属于该调用方。你的工具同样可以区分渠道:通过 MCP 或 A2A 启动的运行任务,会携带 principal,分别为 { channel: 'mcp' } 或 { channel: 'a2a' }。

MCP 和 A2A 默认都处于关闭状态,而且你自己的中间件会先于它们执行,因此可以像处理其他路由一样,添加 IP 规则或限流。

运行任务是后台作业,每一步之后都会保存检查点,并能在崩溃或重启后恢复。

ctx.step 让副作用只发生一次;ctx.callId 则提供了一个现成的幂等键。

对于不具备幂等性的工具,系统绝不会盲目重新执行。

只需启用相应配置,就能让应用成为 MCP 服务器和 A2A Agent,并为每个调用方提供独立的 token 和隔离的对话。

需要审批的工具绝不会直接提供给外部客户端,其他工具也可以通过 expose: false 隐藏。

最后一篇文章会把应用带到生产环境:在每个 CPU 核心上运行 amoxi start,使用 Docker,管理密钥和配置,执行健康检查,以及在第一位真实用户到来前逐项核对的检查清单。

Amoxi 基于 Apache License 2.0 开源。代码:github.com/karawash-projects/amoxi。文档:karawash-projects.github.io/amoxi。

如果需要采取进一步措施,可以考虑屏蔽此人,或者举报其滥用行为。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
错误码搜不准?给向量检索补上 BM25
下一篇
用 TypeScript 搭建带人工审批的客服智能体