[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fbzaJ3OJtjAcLVMrO9pV4Ki6D1_YbNK0m_G1R9OFA5Sg":3,"$fimZzYcWjBXq2rbcXh4vXE0CUi_UmaAUWhYX2yBpfcpc":48},[4],{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":39,"sourceName":40,"sourceUrl":40,"status":41,"seoTitle":40,"seoDescription":40,"canonicalUrl":40,"isFeatured":42,"sno":43,"sortOrder":44,"publishedAt":45,"updatedAt":46,"createdAt":47},"9c77fbc0-6422-4ae3-bf41-9a592e3a53e1","article","A2UI：AI Agent 为什么不应该只返回一段文字","a2ui","A2UI 把 Agent 的界面意图与客户端的组件实现分开：模型选择要展示的卡片、表单或动作，宿主应用负责白名单、权限、渲染和安全。本文用订票场景讲清 A2UI 的四层结构、跨端复用、流式更新与落地护栏。","## 从文字到界面：Agent 为什么需要一层 UI 协议\n\n聊天机器人返回一段文字，用户还要自己判断下一步做什么；但在真实产品里，用户往往需要的是一组可以直接操作的选项：选择日期、确认金额、填写地址、比较方案，或者继续编辑一份表单。\n\n这就是 A2UI（Agent-to-User Interface）试图解决的问题。它不是让模型随意生成 HTML，也不是把整个前端交给模型，而是让 Agent 用一种声明式格式表达“我想给用户展示什么”，再由宿主应用用自己已有的组件库把它渲染出来。\n\n![A2UI 项目概念图](https:\u002F\u002Fopengraph.githubassets.com\u002F1\u002Fgoogle\u002FA2UI)\n\nGoogle 在 2026 年发布的 A2UI 0.9，强调了一个很实用的分工：Agent 负责理解意图和选择界面结构，应用负责组件实现、样式、权限与交互安全。A2UI 可以通过 MCP、WebSocket、REST、A2A 等不同传输方式工作，但它本身更像“界面声明层”，而不是又一个网络传输协议。可以先看[官方介绍](https:\u002F\u002Fdevelopers.googleblog.com\u002Fen\u002Fa2ui-v0-9-generative-ui\u002F)，再把它放回自己的前端架构里理解。\n\n## 为什么纯文本不够用了\n\n假设用户说：“帮我订下周去上海的高铁，最好下午出发。”纯文本 Agent 可能返回一串车次。用户还需要复制车次、确认时间、选择座位，再回到对话里输入“就这个”。\n\n如果 Agent 能返回一个经过审核的车次卡片，卡片里有“出发时间”“到达时间”“余票状态”和“确认”按钮，交互就从“读一段答案”变成了“完成一个任务”。\n\n但直接让模型生成 HTML 或 JavaScript 有三个问题：\n\n- 模型可能生成宿主应用没有实现的组件。\n- 任意脚本会扩大 XSS、数据外传和权限越界风险。\n- Web、Flutter、原生移动端各自有不同的组件体系，HTML 很难直接复用。\n\nA2UI 的关键取舍是：只允许 Agent 从一个组件目录里选择组件，并用数据绑定和动作描述它们如何组合。模型表达的是 UI 意图，最终执行仍由客户端掌控。\n\n## A2UI 的四层结构\n\n可以把一次 A2UI 响应拆成四层：\n\n1. **Agent 层**：理解用户问题，决定需要展示卡片、表单还是列表。\n2. **Schema 层**：用版本化的结构描述组件树、数据和动作。\n3. **Catalog 层**：规定当前应用允许使用哪些组件，以及每个组件需要什么字段。\n4. **Renderer 层**：把声明转换成 React、Lit、Angular、Flutter 或其他宿主框架的真实组件。\n\n这四层让“生成内容”和“执行内容”分开。Agent 可以说“需要一个日期选择器”，但不能凭空执行一个未登记的浏览器 API。\n\n```mermaid\nflowchart LR\n    U[\"用户意图\"] --> A[\"Agent 理解任务\"]\n    A --> S[\"生成版本化 UI 声明\"]\n    S --> V{\"通过 Schema 与 Catalog 校验?\"}\n    V -->|否| F[\"降级为文本或修复\"]\n    V -->|是| R[\"宿主 Renderer 渲染\"]\n    R --> I[\"用户操作组件\"]\n    I --> E[\"动作回传 Agent 或业务 API\"]\n```\n\n## 一个具体例子：订票卡片怎么生成\n\nAgent 不需要返回完整 HTML，可以只表达类似下面的意图：\n\n```json\n{\n  \"surface\": \"train_options\",\n  \"components\": [\n    {\n      \"type\": \"option_card\",\n      \"data\": {\n        \"departure\": \"2026-08-12 15:20\",\n        \"arrival\": \"2026-08-12 19:48\",\n        \"price\": 553,\n        \"available\": true\n      },\n      \"actions\": [\"select_train\"]\n    }\n  ]\n}\n```\n\n真正的客户端还要做几件事：检查 `option_card` 是否在白名单里，验证价格和车次数据来自可信工具，确认 `select_train` 是否需要登录或二次确认，然后才把它映射成产品自己的卡片组件。\n\n这也是 A2UI 与“模型直接写前端代码”的根本区别：模型输出的是受限的数据，不是可以立即执行的程序。\n\n## 为什么组件目录比“万能组件”更重要\n\n组件目录不是简单的 UI 列表，它实际上是 Agent 的能力边界。\n\n一个金融应用可以只开放余额卡片、转账表单和收款人选择器；一个客服系统可以开放订单时间线、退款原因选项和人工转接按钮。不同用户、设备和权限，还可以使用不同的目录。\n\n这样做有三个好处：\n\n- **安全**：Agent 不能调用目录之外的组件和动作。\n- **一致**：生成式交互仍然遵守产品的设计系统。\n- **可演进**：升级客户端组件时，不必要求 Agent 学会新的 HTML 细节。\n\n但组件目录也会带来维护成本。每新增一个组件，都要补齐 schema、校验规则、渲染器、无障碍语义和失败回退。如果目录太小，Agent 只能输出僵硬的卡片；如果目录太大，模型更容易选错或生成难以测试的组合。\n\n## 声明的不只是组件，还有数据和动作\n\n把 A2UI 简化成“模型返回一棵组件树”还不够。一个真正能工作的界面声明，至少要回答三件事：显示什么、数据从哪里来、用户操作后发生什么。\n\n| 部分 | 解决的问题 | 典型约束 |\n| --- | --- | --- |\n| Component | 画出卡片、表单、列表还是进度状态 | 必须存在于 Catalog |\n| Data | 给组件填充价格、时间、状态和选项 | 字段类型、来源和权限可验证 |\n| Action | 用户点击后向谁发送什么事件 | 动作白名单、参数校验、确认级别 |\n\n例如“确认订票”不应该只是一个叫 `confirm` 的字符串。客户端至少要检查车次 ID 是否来自当前查询结果、价格是否仍然有效、用户是否登录，以及这个动作是否需要二次确认。A2UI 负责表达动作意图，但最终的业务授权仍然应该发生在服务端。\n\n还要区分两类数据：**Agent 生成的数据**和**工具返回的事实数据**。前者可以是标题、解释和排序建议；后者则可能是余额、库存和订单状态。后者不能因为模型把它写进 JSON 就自动变成可信事实，最好携带来源标识和过期时间，由客户端或服务端再次验证。\n\n## A2UI 与三种相邻方案有什么区别\n\n**直接生成 HTML \u002F JavaScript**：自由度最高，但安全边界最差。模型生成的代码需要经过沙箱、静态检查和运行时隔离，复杂度很快超过“做一个界面”的收益。\n\n**固定 JSON Schema**：比 HTML 安全，也容易解析，但如果 schema 只描述数据、不描述组件语义，前端仍要为每一种业务自定义协议。A2UI 更强调组件目录、版本协商和跨端渲染。\n\n**MCP Apps 一类的工具 UI**：可以让工具返回一个交互式资源，适合把工具自己的小界面带进宿主。A2UI 更像一层面向 Agent 的通用界面声明，适合由宿主统一控制组件和设计系统。二者可以组合，并不是非此即彼。\n\n实际选型可以这样判断：如果 UI 主要属于某个工具，优先考虑工具资源；如果 UI 要跨多个 Agent、多个设备，并且必须服从宿主设计系统，A2UI 的抽象更合适。\n\n## 流式渲染与跨端复用\n\nA2UI 0.9 的一个重要方向是流式更新：客户端不一定要等 Agent 生成完整结果后才渲染，可以先显示骨架，再逐步补齐数据或组件。对于需要搜索、比价和多轮工具调用的任务，这会明显降低“什么都没发生”的等待感。\n\n跨端复用则依赖各端的 Renderer。Web 端可以映射到 React，移动端可以映射到 Flutter，二者共享的是组件语义和数据，而不是具体的 DOM。前提是各端的组件目录足够一致，否则同一个“日期选择器”可能在不同设备上出现不同能力。\n\n流式界面还要处理“半成品状态”。例如 Agent 先生成了一个空的结果卡片，后来工具调用失败；客户端应该把卡片标记为“暂时无法获取”，而不是保留一个看起来像最终结果的旧状态。一个成熟的声明格式通常需要区分 `loading`、`partial`、`complete` 和 `error`，并携带可恢复动作，例如“重试查询”或“改用文字回答”。\n\n这会改变前端测试方式。过去测试的是“点击按钮后组件是否出现”，现在还要测试：声明版本不兼容时是否降级、数据缺失时是否显示错误、Agent 重复发送同一事件时是否幂等、用户在流式更新中点击时状态是否一致。\n\n## 无障碍与设计系统不能交给模型猜\n\n生成式 UI 很容易只关注“能不能显示”，忽略“能不能被所有人操作”。组件目录应该直接绑定无障碍语义：按钮的可访问名称、表单字段的标签、错误提示与输入框的关联、键盘焦点顺序和屏幕阅读器状态。\n\n同样，颜色、间距、字体和交互反馈最好来自设计系统 token，而不是让模型自由生成。Agent 可以选择“警告状态”或“强调操作”，但不应该自己决定用什么十六进制颜色。这样既保持视觉一致，也避免模型在不同回答中生成一套套互相冲突的 UI。\n\n## 一条更稳的落地路线\n\n不要一开始就让 Agent 生成任意页面。可以按四步推进：\n\n1. 选一个闭环任务，例如筛选商品或填写报销单。\n2. 只开放 5–8 个组件，每个组件配 schema、示例和失败状态。\n3. 先让 Agent 只生成“组件选择 + 数据填充”，动作由固定代码处理。\n4. 用真实任务记录无效组件率、校验失败率、用户完成率和人工接管率，再扩大目录。\n\n这样可以把问题拆开：如果用户没完成任务，到底是 Agent 选错组件、数据不可信、动作失败，还是流程本身设计得太长。没有这些指标，生成式 UI 很容易变成一组看起来漂亮但无法完成业务的卡片。\n\n## 适合什么时候使用\n\nA2UI 更适合这些场景：\n\n- Agent 需要引导用户完成多步骤任务。\n- 同一套 Agent 要服务 Web、移动端和桌面端。\n- 产品已经有成熟设计系统，希望 AI 复用现有组件。\n- 需要对 Agent 能展示和执行的 UI 做权限控制。\n\n如果只是问答、摘要或一次性文本生成，直接返回 Markdown 通常更简单。不要为了“看起来像 AI”而把每个回答都包装成动态界面。\n\n## 落地时的四个护栏\n\n第一，**所有组件和动作都采用白名单**。未知类型直接拒绝，不要尝试“宽松解析”。\n\n第二，**把数据权限放在工具和服务端**。UI 声明里的 `price`、`balance`、`status` 只能作为展示数据，不能成为业务决策的最终依据。\n\n第三，**动作需要幂等和确认机制**。支付、下单、删除、发消息等副作用操作，不应因为 Agent 重试或用户重复点击而执行两次。\n\n第四，**始终保留文本回退**。客户端版本过旧、schema 不兼容、数据校验失败时，用户至少应该得到一段清楚的文字说明，而不是空白区域。\n\n## 结语：Agent 的下一层抽象是“意图”，不是“代码”\n\nA2UI 的价值不在于让模型生成更漂亮的卡片，而在于重新划分边界：Agent 负责决定“用户此刻需要什么交互”，客户端负责决定“这个交互以什么安全、可访问、可维护的方式呈现”。\n\n如果你准备尝试它，建议先选一个窄流程，例如“筛选商品”或“填写报销单”，建立小型组件目录，给每个动作加校验和审计，再逐步扩大范围。生成式 UI 的可靠性，最终取决于目录和边界设计，而不只是模型聪不聪明。\n\n**动手前的检查清单：**\n\n- 是否能把每个可生成组件写成明确 schema？\n- 是否有未知组件、未知动作的拒绝路径？\n- 是否支持客户端版本协商和文本降级？\n- 是否对副作用动作做了确认、幂等和审计？\n- 是否用真实用户任务而不是 Demo 截图评估体验？\n\n## 一手资料\n\n- [A2UI 0.9 官方发布说明](https:\u002F\u002Fdevelopers.googleblog.com\u002Fen\u002Fa2ui-v0-9-generative-ui\u002F)\n- [A2UI 项目主页](https:\u002F\u002Fa2ui.org\u002F)\n- [Google 对 Agent-driven UI 的介绍](https:\u002F\u002Fdevelopers.googleblog.com\u002Fintroducing-a2ui-an-open-project-for-agent-driven-interfaces\u002F)","\u002Fuploads\u002F2026-08-05\u002Fb6291296-7a17-44a7-b288-83ebc0072068.jpg",[],[],"Foundit AI","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27,31,35],{"id":24,"name":25,"slug":26},"88d2bc27-0e0f-468a-b907-2991cb97b87b","人工智能","ai",{"id":28,"name":29,"slug":30},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":32,"name":33,"slug":34},"a202d639-99a6-488a-a712-4d4c6ffd7e15","开发","dev",{"id":36,"name":37,"slug":38},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse","资料来源",null,"published",false,67,0,"2026-08-02T00:00:00.000Z","2026-08-05T03:13:23.142Z","2026-08-05T02:12:50.323Z",[49,68,86],{"id":50,"type":6,"title":51,"slug":52,"summary":53,"body":54,"coverUrl":55,"productScreenshots":56,"productLinks":57,"authorName":14,"authorUrl":58,"authorSubject":16,"category":59,"tags":60,"sourceLabel":40,"sourceName":40,"sourceUrl":40,"status":41,"seoTitle":40,"seoDescription":40,"canonicalUrl":40,"isFeatured":42,"sno":64,"sortOrder":44,"publishedAt":65,"updatedAt":66,"createdAt":67},"544fc658-c911-4de6-93b0-d2520087119a","MCP：AI 的「USB-C」时刻","mcp-ai-usb-c-moment","以前每个 AI 应用都要为 GitHub、数据库、日历各写一套私有连接器，这是 M×N 的集成噩梦，直到 MCP 的出现","如果你用过笔记本电脑，一定熟悉那种「每个设备一根专属线」的烦躁：鼠标一个接口、打印机另一个、硬盘又一个。2025 年之前的 AI 应用，几乎就是这种状态——想让一个助手同时读你的代码仓库、查数据库、发日历邀请，开发团队得为每一个系统写一套私有「连接器」，又脆又难维护。\n\n## 背景：每个 Agent 都曾是孤岛\n\n大模型本身只会「说话」，它要真正干活，得去调工具、读数据。在 MCP（Model Context Protocol，模型上下文协议）出现之前，这套对接是组合爆炸：假设市面上有 M 个 AI 客户端、N 个工具，开发者就要写 M×N 套集成。一个代码助手要读 Git、查 Jira、搜文档，就得维护三条互不相通的管线。\n\n更糟的是，这些连接器大多只服务某一个产品，换个助手就得重写。结果就是：每个 Agent 都困在自己的小岛上，能力被锁死在少数几个硬编码的集成里。\n\n## MCP 是什么：AI 世界的「USB-C」\n\n2024 年底，Anthropic 发布了 MCP。它的目标很朴素：给「AI 连工具」定义一个统一接口，就像 USB-C 给「设备连外设」定义统一接口一样。\n\n打个比方——如果大模型是大脑，那 MCP 就是手。大脑再聪明，没有手也打不开文件、点不了按钮、查不了数据库。MCP 让任意符合规范的「大脑」（Claude、ChatGPT、Gemini、Cursor、VS Code Copilot）都能使用任意符合规范的「手」（一个封装好的工具服务），而且不用为每个组合单独适配。\n\n2025 年 12 月，Anthropic 把 MCP 捐给了 Linux 基金会，OpenAI、Google、Microsoft 作为联合发起人。到 2026 年，它的 SDK 月下载量超过 9700 万次，ChatGPT、Claude、Gemini 都支持同一个协议——某种意义上，这场标准之战已经赢了。\n\n## 它是怎么运作的：三层结构\n\nMCP 把「连工具」拆成三个角色，理解这三层就理解了全部：\n\n- **Host（宿主）**：你直接使用的应用，比如 Claude 桌面端、VS Code、一个自定义聊天机器人。\n- **Client（客户端）**：住在 Host 内部、专门负责管理 MCP 连接的小组件。\n- **Server（服务端）**：一个轻量程序，把某个能力「暴露」出来，比如一个 GitHub 服务、一个数据库查询服务。\n\n每个 Server 通过三种「原语」提供能力：`Tools`（AI 可以调用的可执行函数，如 `create_issue`）、`Resources`（AI 可以读取的数据，如文件内容、数据库表结构）、`Prompts`（可复用的提示词模板）。它们底层用 **JSON-RPC**（一种简单的远程调用格式）通信，远程服务走 HTTP 传输，本地服务走标准输入输出。\n\n整个调用流程是这样的：\n\n```mermaid\nflowchart LR\n    U[用户] --> H[Host 应用\u003Cbr\u002F>Claude \u002F Cursor \u002F VS Code]\n    H --> C[MCP Client\u003Cbr\u002F>连接管理器]\n    C -->|JSON-RPC| S1[MCP Server: GitHub]\n    C -->|JSON-RPC| S2[MCP Server: 数据库]\n    C -->|JSON-RPC| S3[MCP Server: 天气 API]\n    S1 --> D1[(代码仓库)]\n    S2 --> D2[(业务数据)]\n    S3 --> D3[(外部 API)]\n```\n\n关键点在于：Host 只要实现一次 Client 协议，Server 只要实现一次 Server 协议，从此任意 Host 能连任意 Server。集成成本从 M×N 降到了 M+N。\n\n## 一个最小可运行的例子\n\n下面用官方 Python SDK 写一个「天气查询」MCP 服务，只暴露一个工具：\n\n```python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"weather\")  # 服务名叫 weather\n\n@mcp.tool()\ndef get_weather(city: str) -> str:\n    \"\"\"查询某城市的天气（示例返回静态数据）\"\"\"\n    return f\"{city} 今天晴，25°C。\"\n\nif __name__ == \"__main__\":\n    mcp.run()  # 默认以 stdio 方式启动，等待 Host 来连\n```\n\n运行前只需 `pip install mcp`，然后用任意支持 MCP 的客户端（Claude 桌面端、Cursor 等）配置这个服务路径即可。AI 在对话里说「查下北京天气」，客户端就会通过 MCP 调用 `get_weather(\"北京\")`，拿到结果再组织成自然语言回答你。注意：这只是最小骨架，真实服务里要把静态返回值换成真正的天气 API 调用。\n\n## 取舍与边界：它解决了什么，没解决什么\n\nMCP 解决的是「连接标准」问题，但它不是银弹：\n\n- **它让集成变简单，但不保证工具安全。** 一个 MCP Server 可以是任何人所写，工具描述会直接喂给模型。如果 Server 既能读私有数据、又能访问不可信内容、还能对外发消息，就构成了安全风险（业界称之为「致命三件套」）。企业通常会加一层 **Gateway（网关）** 来做鉴权和审计——Uber、Amazon 都用了这种「网关 + 注册表」的控制平面。\n- **上下文膨胀是个真问题。** 接的 Server 一多，工具定义会塞满模型的上下文窗口。2026 年的常见解法是「按需加载」：只把当前 Agent 真正需要的工具暴露出来，而不是一次全塞进去。\n- **它定义「怎么连」，不定义「连上去说什么」。** 多 Agent 之间的协作语义，由另一套协议 A2A（Agent-to-Agent）负责——MCP 接工具，A2A 连同伴。\n\n## Tips\n\n- 下次看到「AI 连不上我的系统」，先问：有没有现成的 MCP Server？多数数据库、SaaS、开发工具都已有官方或社区实现。\n- 想自己动手：用官方 SDK（Python\u002FTypeScript 等）把内部的一个 API 包成 MCP Server，比写一套专属集成快得多。\n- 评估风险时记住三件事：私有数据、不可信输入、对外通信，三者叠加要格外小心，尽量放进网关管控。\n- 分清两层协议：接工具看 MCP，多 Agent 协作看 A2A，别混为一谈。\n- 把 MCP 当「基础设施」而非「功能」：它赢是因为无聊、通用、可复用，这正是它值得长期投入的原因。","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-19\u002Fc3c06d99-a0ac-40ac-a283-7e77aabb4c4c.jpg",[],[],"https:\u002F\u002Ffoundit.cn\u002Fabout",{"id":18,"name":19,"slug":20,"description":21},[61,62,63],{"id":32,"name":33,"slug":34},{"id":28,"name":29,"slug":30},{"id":36,"name":37,"slug":38},46,"2026-07-20T00:00:00.000Z","2026-07-19T17:39:20.060Z","2026-07-19T16:13:40.316Z",{"id":69,"type":6,"title":70,"slug":71,"summary":72,"body":73,"coverUrl":74,"productScreenshots":75,"productLinks":76,"authorName":14,"authorUrl":15,"authorSubject":16,"category":77,"tags":78,"sourceLabel":39,"sourceName":40,"sourceUrl":40,"status":41,"seoTitle":40,"seoDescription":40,"canonicalUrl":40,"isFeatured":42,"sno":82,"sortOrder":44,"publishedAt":83,"updatedAt":84,"createdAt":85},"03ca8001-f2c1-4b68-9a9a-1d2225483c68","Durable Execution：如何让长任务 Agent 崩溃后接着工作","durable-execution-for-long-running-agents","长任务 Agent 会遇到进程崩溃、网络故障、工具超时和人工等待。本文用 Workflow、Activity 与 Event History 拆解 Durable Execution，解释它与普通重试的区别、幂等副作用的边界，以及如何设计可暂停、恢复和审计的 Agent 工作流。","## Agent 最难的不是“会思考”，而是“不会丢进度”\n\n一个 Agent 要完成“调研供应商、比较报价、提交审批”这样的任务，往往需要十几个步骤。模型调用可能超时，工具服务可能暂时不可用，执行进程可能在第八步重启，用户也可能隔几个小时才回来确认。\n\n如果系统只把整个过程写成一段普通函数，失败后通常只能从头再来。这不仅浪费时间和模型调用，还可能重复扣款、重复发邮件或重复创建订单。\n\nDurable Execution（持久化执行）提供了另一种思路：把长任务写成可恢复的工作流，持续保存每一步的状态和结果。进程挂掉后，系统从最近一次完成的位置继续，而不是把 Agent 当成一次性请求重新启动。\n\n![Temporal 工作流项目概念图](https:\u002F\u002Fopengraph.githubassets.com\u002F1\u002Ftemporalio\u002Ftemporal)\n\nGoogle 的 Gemini 官方示例使用 Temporal 构建可恢复的 Agent 循环：模型调用和工具调用作为可重试的活动执行，工作流负责组织顺序和状态。[Temporal 官方文档](https:\u002F\u002Fdocs.temporal.io\u002F)把这种能力概括为让应用在崩溃、网络故障或基础设施中断后从原位置恢复。\n\n## 重试不等于持久化执行\n\n最简单的重试是：请求失败后，再调用一次同一个函数。\n\n但长任务通常有多个步骤：\n\n```text\n读取客户资料 → 查询库存 → 生成报价 → 请求审批 → 创建订单 → 发送通知\n```\n\n如果“创建订单”之后通知服务超时，系统无法判断订单到底创建成功没有。此时直接重试，可能得到两个订单；不重试，用户又可能永远收不到通知。\n\n持久化执行关注的不是“把异常捕获住”，而是把工作流历史、每一步的输入输出和重试状态保存下来。系统可以知道哪些步骤已经完成，哪些步骤还没有拿到确定结果。\n\n不过，持久化执行也不会自动把外部世界变成 exactly-once。数据库写入、付款、发邮件等外部副作用仍然需要幂等键、去重表或业务状态机配合。它解决的是“执行进度可恢复”，不是“所有外部系统天然只执行一次”。\n\n## 三个核心概念\n\n### Workflow：稳定的流程骨架\n\nWorkflow 描述任务的顺序、分支、等待和超时。例如：先并行查询三个供应商，等用户选择后再发起审批。它应该尽量保持确定性，因为系统可能会根据历史事件重放 Workflow 代码。\n\n### Activity：可以失败的具体动作\n\nActivity 承担模型调用、HTTP 请求、数据库读写、文件处理等不稳定工作。它们可以单独设置超时、重试策略和并发限制，也可以记录调用结果。\n\n### Event History：可重放的执行历史\n\n每完成一步，系统都会留下事件。恢复时，Workflow 根据历史跳过已完成的 Activity，重新计算下一步应该做什么。对 Agent 来说，这相当于把“上下文”从一段容易丢失的内存，变成可审计的执行记录。\n\n```mermaid\nflowchart TD\n    Q[\"用户提交长任务\"] --> W[\"启动持久化 Workflow\"]\n    W --> A1[\"Activity：读取资料\"]\n    A1 --> A2[\"Activity：调用模型与工具\"]\n    A2 --> H{\"需要人工确认?\"}\n    H -->|是| P[\"等待外部信号\"]\n    P --> A3[\"Activity：执行副作用\"]\n    H -->|否| A3\n    A3 --> C[\"记录完成事件\"]\n    C --> D[\"返回结果\"]\n    A2 -. \"进程崩溃或网络失败\" .-> R[\"从最近事件恢复并重试\"]\n    R --> A2\n```\n\n## Agent 为什么特别需要这个能力\n\n传统 CRUD 请求通常几百毫秒到几秒就结束，失败后重新请求的代价有限。Agent 任务则经常包含：\n\n- 多轮模型调用，且每轮可能选择不同工具。\n- 长时间等待人工审批、第三方回调或定时条件。\n- 需要跨越多个服务，任何一个依赖都可能短暂失败。\n- 不能重复执行的外部副作用。\n\n例如“整理一批合同并生成风险清单”可以分成：上传文件、解析文本、并行抽取条款、合并结果、人工复核、生成报告。解析到第六份文件时进程崩溃，如果前五份结果已经被保存，系统就不该从第一份重新开始。\n\n## Agent 工作流应该怎样拆\n\n一个实用原则是：**模型负责做判断，Workflow 负责保存进度，Activity 负责接触外部世界。**\n\n可以把 Agent 循环写成下面的逻辑：\n\n```python\nwhile not task_done:\n    decision = await call_model(state)\n    if decision.kind == \"tool_call\":\n        result = await run_tool_activity(decision.tool, decision.args)\n        state = update_state(state, result)\n    elif decision.kind == \"needs_human\":\n        await wait_for_signal()\n    else:\n        return decision.answer\n```\n\n这里的 `call_model` 和 `run_tool_activity` 不应被当成普通内存函数。模型请求应有超时、重试和版本记录；工具调用应有幂等键、权限检查和结果快照；`state` 应能在任务恢复后重新获得。\n\n对于高风险动作，最好把“决定要做”和“真正执行”拆成两步：先生成待确认计划，再由用户或策略引擎发出批准信号。这样 Agent 可以长时间等待，而不需要占用一个一直在线的 HTTP 请求。\n\n## 重放为什么要求 Workflow 保持确定\n\n持久化引擎恢复任务时，可能会重放 Workflow 代码，让它重新读取历史事件并走到当前节点。因此 Workflow 里不应该直接调用随机数、当前时间、网络请求或 LLM。否则同一份历史在第二次计算时得到不同分支，系统就无法判断哪些 Activity 已经执行过。\n\n正确的拆法是：Workflow 只负责调度，外部世界交给 Activity。当前时间可以由引擎提供一个可重放的时间值；随机 ID 可以在 Workflow 外生成后作为输入；模型调用必须作为 Activity 保存请求和结果。伪代码看起来相似，但责任边界不同：\n\n```python\n@workflow\nasync def order_workflow(request):\n    plan = await execute_activity(make_plan, request)\n    await workflow.wait_condition(lambda: workflow_state.approved)\n    result = await execute_activity(create_order, plan)\n    await execute_activity(send_notification, result)\n    return result\n```\n\n这里的 `make_plan`、`create_order` 和 `send_notification` 都可能失败，但 Workflow 本身只在事件历史上推进。尤其是 `send_notification`，不能因为它超时就假设“肯定没发出去”；它需要一个业务侧的幂等键，例如 `workflow_id + step_name`，让重复尝试最终只产生一条通知。\n\n## 幂等、副作用与“结果未知”\n\n工程上最危险的不是明确失败，而是**结果未知**：客户端发出创建订单请求，连接在服务端返回之前断开。此时 Agent 无法仅靠异常判断订单是否存在。\n\n常见处理方式是把副作用设计成三段：\n\n1. 生成全局幂等键，并把它写入请求。\n2. 服务端在事务中记录“幂等键 → 业务结果”。\n3. 重试前先用幂等键查询；如果已有结果，直接复用，不再创建新副作用。\n\n邮件、支付、工单、仓储扣减都可以采用类似模式。若第三方 API 不支持幂等键，就要在自己的系统里增加状态表或中间层，至少能够区分“尚未执行”“执行中”“已确认成功”和“需要人工核查”。\n\n这也是为什么 Durable Execution 不能单独解决一致性问题：它能可靠地恢复你的流程，却无法替你修改银行、邮件服务或供应商系统的语义。\n\n## 人工确认其实是工作流的一部分\n\n很多 Agent Demo 把人工确认做成一个同步接口：模型问“要不要继续”，用户必须立刻回答。生产系统更常见的情况是用户关掉页面，第二天才点批准。\n\n持久化 Workflow 可以把等待设计成显式状态：\n\n```text\n准备计划 → 等待审批 → 已批准 \u002F 已拒绝 \u002F 已过期\n```\n\n用户批准时发送一个带有 `workflow_id`、审批人、审批时间和审批版本的信号。执行前再次检查计划是否被修改、权限是否仍然有效、价格或库存是否过期。这样“用户点过同意”不会被误当成对任何未来状态都永久授权。\n\n还可以设置补偿动作。例如订单已创建但通知失败，补偿动作不是删除订单，而是把通知标记为待补发；如果支付已扣款但库存预留失败，则进入人工处理队列，而不是让 Agent 自己随意退款。\n\n## 重试策略不能一刀切\n\n不同错误应该使用不同策略：\n\n- **网络暂时不可用**：指数退避后重试。\n- **限流**：尊重服务端的 Retry-After，并降低并发。\n- **参数错误**：先让 Agent 修正参数，不要盲目重复。\n- **权限错误**：暂停并请求用户授权。\n- **副作用结果未知**：先查询业务状态，再决定是否重试。\n\n模型调用通常可以重试，但重试也可能产生不同答案。若下游依赖结构化输出，恢复时应保存原始响应、解析结果和模型版本，避免同一任务在重放中悄悄换成另一种决策。\n\n建议把每次重试都记录成一个可查询的事件，而不是只在应用日志里写一行“retry”。至少保留：步骤名、尝试次数、错误类别、等待时长、依赖版本和最终结果。这样可以回答两个很实际的问题：一次任务失败是依赖偶发抖动，还是某个工具从根本上不稳定；以及重试成功到底为平均延迟和成本增加了多少。\n\n版本升级也要谨慎。Workflow 可能持续运行数天，旧任务的历史需要由旧代码解释，新任务才使用新逻辑。实际落地时要为流程定义做版本兼容或迁移策略，不要直接修改一个正在执行的分支含义。\n\n## 什么时候不值得上 Durable Execution\n\n它不是所有 Agent 的默认基础设施。一次性问答、无副作用的摘要、几秒内完成的简单分类，普通请求加超时和日志就够了。\n\n引入工作流平台会增加服务部署、事件存储、版本迁移和运维成本。只有当任务真的跨步骤、跨时间、跨服务，且失败恢复的价值高于基础设施成本时，才值得采用。\n\n## 落地清单\n\n- 把每一步标成“可重试”“不可重试”或“需要人工确认”。\n- 为所有外部副作用设计幂等键和状态查询接口。\n- 把模型调用、工具调用、解析和业务写入拆成可观测的 Activity。\n- 记录模型版本、提示模板版本、工具参数和返回摘要。\n- 为 Workflow 设置总时限、单步时限和最大重试次数。\n- 设计暂停、恢复、取消和人工接管，而不只是成功路径。\n\n持久化执行的核心价值，可以用一句话概括：Agent 不再是一段“运行时可能忘记一切”的循环，而是一条可以暂停、恢复、审计和接管的业务流程。\n\n## 一手资料\n\n- [Temporal 官方文档](https:\u002F\u002Fdocs.temporal.io\u002F)\n- [Gemini + Temporal 的 Durable AI Agent 示例](https:\u002F\u002Fai.google.dev\u002Fgemini-api\u002Fdocs\u002Ftemporal-example?hl=en)\n- [Temporal Durable Execution 介绍](https:\u002F\u002Ftemporal.io\u002Fhow-it-works)","\u002Fuploads\u002F2026-08-05\u002F6ea439e7-4d7e-46cc-ace8-efc556719f28.jpg",[],[],{"id":18,"name":19,"slug":20,"description":21},[79,80,81],{"id":24,"name":25,"slug":26},{"id":36,"name":37,"slug":38},{"id":28,"name":29,"slug":30},65,"2026-08-04T00:00:00.000Z","2026-08-05T03:15:43.428Z","2026-08-05T02:13:44.156Z",{"id":87,"type":6,"title":88,"slug":89,"summary":90,"body":91,"coverUrl":92,"productScreenshots":93,"productLinks":94,"authorName":14,"authorUrl":15,"authorSubject":16,"category":95,"tags":96,"sourceLabel":39,"sourceName":40,"sourceUrl":40,"status":41,"seoTitle":40,"seoDescription":40,"canonicalUrl":40,"isFeatured":42,"sno":100,"sortOrder":44,"publishedAt":101,"updatedAt":102,"createdAt":103},"74dedc2e-6a66-4481-aef2-5cffc3ba338d","嵌入模型（Embeddings）：向量数据库能搜「意思」，全靠它","embedding-models-vector-search","向量库怎么懂「意思相近」？靠嵌入模型把文字变成向量。本文讲清它的工作原理、余弦相似度检索，给出 sentence-transformers 最小示例，以及模型选型、维度统一、中英差异等取舍。","你让向量库「找意思相近的句子」，它怎么懂「意思」？靠嵌入模型（Embeddings）：把文字变成一串数字（向量），意思越近，数字越近。它是语义搜索和 RAG 真正的地基——没有它，模型只能靠关键词硬匹配。\n\n## 为什么需要嵌入\n\n传统搜索靠关键词匹配，搜「怎么给猫降温」找不到「猫咪中暑怎么办」。嵌入把文本映射到向量空间，把相近语义聚在一起，才能按「意思」而不是「字面」检索。\n\n## 它是怎么工作的\n\n嵌入模型（如 BGE、OpenAI text-embedding）是个神经网络，把变长文本压成定长向量（常见 768 或 1536 维）。训练目标是「语义相近的文本，向量距离小」。检索时把 query 也编码，算余弦相似度，找最近的那些。\n\n```mermaid\nflowchart LR\n    A[文本] --> B[嵌入模型]\n    B --> C[向量]\n    C --> D[存入向量库]\n    E[查询] --> B\n    D --> F[相似度检索]\n    B --> F\n    F --> G[返回相近文本]\n```\n\n## 取舍与边界\n\n- **模型要选对**：通用嵌入未必适合你的领域（法律、医疗），必要时用领域数据微调。\n- **维度与成本权衡**：维度越高通常越准，但存储、检索都更贵更慢，按场景取舍。\n- **中英文差异**：混用中英文语料要选多语言模型，否则跨语言检索会崩。\n- **维度必须统一**：检索和入库一定要用同一个模型、同一维度，否则向量不可比，检索全乱。\n\n## Tips\n\n- 任何「按意思搜」的需求，第一步就是选好嵌入模型。\n- 中文场景优先试 BGE、m3e 等多语言\u002F中文模型，别直接套英文默认。\n- 入库和检索用同一模型同一维度，这是铁律。\n- 领域强相关的语料，用该领域样本微调嵌入，召回率提升明显。\n- 嵌入质量直接决定 RAG 上限，值得在它上面多花时间。","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-22\u002Facd40af8-276a-48bc-8d62-dcd52c124590.jpg",[],[],{"id":18,"name":19,"slug":20,"description":21},[97,98,99],{"id":36,"name":37,"slug":38},{"id":28,"name":29,"slug":30},{"id":32,"name":33,"slug":34},68,"2026-07-22T00:00:00.000Z","2026-07-23T01:16:32.032Z","2026-07-20T10:23:37.966Z"]