[{"data":1,"prerenderedAt":747},["ShallowReactive",2],{"article:agent-concurrency-state":3},{"meta":4,"raw":17,"references":18,"prev":49,"next":50,"body":58,"toc":736},{"slug":5,"title":6,"date":7,"updated":7,"tags":8,"summary":13,"draft":14,"lang":15,"minutes":16},"agent-concurrency-state","Agent 服务的并发与状态：异步、任务队列、幂等和故障恢复怎么配合","2026-10-11",[9,10,11,12],"Agent","服务端","并发","一致性","把厂商会话能力与业务一致性分开，解释 async I\u002FO、有限并发、事务 outbox、任务租约和稳定操作 ID；用崩溃窗口、慢客户端与外部结果未知来检验生产服务。",false,"zh-CN",8,"一条 Agent 请求可能等待模型、检索、工具和审批。请求多起来以后，问题不仅是“能开多少协程”，还有任务会不会丢、写入会不会重复、旧 worker 会不会覆盖新结果，以及客户端断开后系统怎样继续。\n\n本文综合官方运行框架、FastAPI、PostgreSQL 与 outbox 资料。代码片段是用于解释语义的简化示例；完整生产实现还要接入鉴权、存储、调度、取消、监控和故障演练。\n\n## 两家厂商保存的状态，与应用状态如何连接\n\n[OpenAI SDK Sessions](https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-python\u002Fsessions\u002F)解决跨运行对话历史；[人工审批机制](https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-python\u002Fhuman_in_the_loop\u002F)支持暂停、保存运行状态并继续。这些能力帮助恢复模型交互，但某个工具是否已改变业务对象，仍需要业务系统确认。\n\n[Claude Managed Agents](https:\u002F\u002Fwww.anthropic.com\u002Fengineering\u002Fmanaged-agents)公开了 session、harness 和 sandbox 的解耦思路。对应用的启发是把可靠事件记录与可替换的执行进程分开；该工程报告并没有给所有第三方工具定义事务或幂等保证。\n\n**工程推导**：即使用厂商托管状态，应用也应保存业务任务 ID 到厂商 session \u002F run ID 的映射，以及已确认结果与验收状态。重复回调、超时后的查询和用户取消，都必须落到同一个业务任务上。\n\n## async I\u002FO：提高等待期间的利用率\n\n[FastAPI 并发指南](https:\u002F\u002Ffastapi.tiangolo.com\u002Fasync\u002F)解释了并发等待与并行计算的区别。等待模型 HTTP、异步数据库或网络工具时，`await` 可以释放事件循环；同步库放进 `async def` 并不会自动变异步，反而可能阻塞所有同进程请求。\n\n同步 I\u002FO 可以使用适当的线程池与超时；较重的 Python CPU 计算通常应使用独立进程或专门计算服务，具体还受运行时与原生库行为影响。PDF 解析、OCR、embedding 批处理与交互式流式响应可使用不同 worker 池，避免批任务占满交互资源。\n\n下面展示**进程内**限制模型调用的语义，`invoke_model` 是待接入的异步函数：\n\n```python\nmodel_slots = asyncio.Semaphore(16)  # 示例值，按压测和上游预算调整\n\nasync def bounded_call(request, remaining_seconds):\n    # 等待并发槽也消耗截止时间，不能只给 HTTP 调用计时。\n    async with asyncio.timeout(remaining_seconds):\n        async with model_slots:\n            return await invoke_model(request)\n```\n\n这段代码不提供多进程或跨机器的总限流。启动四个进程后可能允许四倍调用；需要全局配额时，由入口或共享调度器分配预算，并防止所有实例同时重试。`asyncio.timeout` 也无法中断阻塞事件循环的同步计算。\n\n## 容量：协程数量不是吞吐承诺\n\n在稳态平均条件下，Little 定律给出 `在途任务数 ≈ 到达率 × 平均停留时间`。**示例推算**：接受 20 个任务\u002F秒、平均停留 15 秒，则平均约有 300 个任务处于系统中；这不是 300 个模型调用都应同时执行，也不是 p95 容量保证。\n\n一个任务可能调用模型三轮、并发检索两次，或暂停等审批。容量规划分别估计：HTTP 连接、运行中任务、模型调用 RPM\u002FTPM、数据库连接、工具并发和队列积压。限制应覆盖用户、租户、模型和工具，防止一个慢租户耗尽公共资源。\n\n队列等待过长时应拒绝新任务或返回明确的繁忙状态；可以按优先级与公平性调度。扩 worker 前先看瓶颈：若上游 token 配额已满，扩容只会增加排队与限流。\n\n## 持久队列：接收成功必须有恢复依据\n\n[FastAPI BackgroundTasks](https:\u002F\u002Ffastapi.tiangolo.com\u002Ftutorial\u002Fbackground-tasks\u002F)适合响应后的进程内工作，重型工作可以使用专门队列。它本身不能替代需要持久化、重试与跨进程恢复的任务系统。\n\n接收长任务时，推荐在一个数据库事务中写入任务与待发布事件，再返回 `202 + task_id`。发布器把 outbox 事件送到消息队列，确认后标记发布；worker 依据任务 ID 去重消费。[AWS transactional outbox](https:\u002F\u002Fdocs.aws.amazon.com\u002Fprescriptive-guidance\u002Flatest\u002Fcloud-design-patterns\u002Ftransactional-outbox.html)解释了如何避免数据库提交与消息发送之间的双写漏洞，也指出重复消息仍需消费者处理。\n\n![任务与外部副作用的恢复路径：同事务 outbox、可重投消息、租约 worker 与对账](\u002Fdiagrams\u002Fagent-concurrency-state.svg)\n\n*图 1 · 队列允许重投；任务接收、领取和业务动作分别有稳定身份。外部结果未知时先对账。*\n\n接口幂等键按租户或用户范围唯一，并绑定规范化请求摘要。同一键同一内容返回原任务；同一键不同内容明确拒绝。先查“是否存在”再插入仍有竞争，应通过唯一约束或原子写入完成。\n\n## 最小数据模型：哪些数据由谁负责\n\n| 实体 | 关键字段示意 | 职责与不变量 |\n| --- | --- | --- |\n| tenant \u002F principal | 租户、身份、角色与授权关系 | 服务端决定可访问范围 |\n| task | 业务 ID、请求摘要、状态、配置版本、deadline | 同一业务请求对应一个任务 |\n| attempt | 尝试 ID、worker、epoch、lease、进度 | 同时只有当前有效持有者提交任务结果 |\n| operation | 稳定操作 ID、参数摘要、对象版本、结果 | 重试不改变同一个逻辑动作的身份 |\n| event \u002F outbox | 事件 ID、任务 ID、序号、发布状态 | 可重投、可追踪；排序规则明确 |\n| artifact \u002F source | 产物或来源 ID、版本、定位、权限、留存 | 验收与引用可追溯，读取仍需授权 |\n\nMySQL \u002F PostgreSQL 可保存事务事实；Redis 可做缓存、短期配额或调度辅助；消息队列负责传输；向量库负责检索索引。没有必要强行使用每一种组件，也不要把可能淘汰的缓存当作任务成功的唯一记录。\n\n若业务确实有设备、监护关系或其他角色，在身份与授权模型上扩展关系；它们属于具体业务约束，不应硬编码进通用 Agent 循环。\n\n## 领取任务与 fencing：租约过期不代表旧进程已停止\n\nworker 用短事务领取任务，记录租约到期时间和递增的执行代次 `epoch`，事务提交后再调用模型或工具。不能持有数据库行锁等待几分钟的外部请求。\n\n[PostgreSQL SELECT 文档](https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-select.html)说明 `FOR UPDATE SKIP LOCKED` 可用于多个消费者访问队列式表，但返回的是跳过锁定行的视图，不适合普通一致查询。它也不提供长期任务租约或重试策略，仍需要应用设计。\n\n任务更新可以使用条件写入：`WHERE task_id = ? AND epoch = ? AND owner = ?`，并结合有效租约校验，只有当前执行者能提交状态。旧进程恢复后提交旧代次结果，应被拒绝。\n\n**局限在外部副作用**：数据库 fencing 只能挡住数据库所验证的写入。工具服务若不验证代次、稳定操作 ID 或幂等条件，旧 worker 仍可能执行外部动作。关键写入通过受控工具网关校验，或依赖上游唯一约束与查询对账，不能只加一个 Redis 锁就宣布“恰好执行一次”。\n\n## 三个崩溃窗口：如何避免盲目重跑\n\n| 故障发生处 | 恢复方式 | 应验证的结果 |\n| --- | --- | --- |\n| 数据库已提交，消息尚未发出 | outbox 发布器继续扫描 | 已接收任务最终能被领取 |\n| 消息已发出，发布确认未记录 | 允许重投，消费侧按 ID 去重 | 不创建第二个任务或重复确认 |\n| 外部动作已成功，工具结果未写回 | 查询稳定操作 ID，或复用幂等键对账 | 不再次产生业务副作用 |\n\n尝试 ID 每次执行可以变化；**逻辑操作 ID 跨重试保持稳定**。同一个操作若参数或目标版本改变，应被识别为新动作并重新授权，不能复用旧幂等键掩盖差异。未知结果没有查询能力时，暂停并升级处理比自动重试更可靠。\n\n## 流式接口、慢客户端与取消\n\nHTTP 接收确认、Agent 运行和事件流可以分开：客户端订阅任务事件，用事件序号或 SSE ID 恢复展示；服务器定义重放窗口，超出窗口时先读取任务快照再接续。确认、进度与模型文本是不同事件，不能把普通进度称为首 token。\n\n慢客户端会使缓冲增长。对可重放事件设置有限缓冲和明确断开策略；实时模型流若无法暂停或重放，定义中止与续接行为。不要因为展示端慢就无限占用内存，也不能静默丢掉用户需要的最终状态。\n\n取消是一项持久化请求：worker 在模型、工具和步骤边界检查，将取消尽量传播上游。已提交外部动作不能靠断开 SSE 撤销；还要处理“取消与完成同时发生”的状态竞争，依据已确认事实返回结果。多端协议与旧客户端兼容见[端云同步篇](\u002Farticles\u002Fdevice-cloud-api-sync)。\n\n## 优化与验证：用故障和负载测出边界\n\n固定模型、任务组合和上游配额，逐步调整 worker 数量、并发上限与批大小，记录接受率、队列等待、p95、工具超时、数据库连接和每成功任务费用。负载包含长短任务混合、单租户突发、慢消费者与429，不能只测平均延迟。\n\n故障演练主动终止 worker、重复投递消息、丢失外部响应、让租约过期后旧 worker 恢复，并在运行中滚动升级。验收关注任务不丢、确认不重、越权不发生、未知结果能对账；这些是业务保证，不能由“用了 async + Redis + MQ”自动推出。\n\n## 参考资料\n\n- [OpenAI Agents SDK — Sessions](https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-python\u002Fsessions\u002F)\n- [OpenAI Agents SDK — Human in the loop](https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-python\u002Fhuman_in_the_loop\u002F)\n- [Anthropic — Scaling Managed Agents](https:\u002F\u002Fwww.anthropic.com\u002Fengineering\u002Fmanaged-agents)\n- [FastAPI — Concurrency and async \u002F await](https:\u002F\u002Ffastapi.tiangolo.com\u002Fasync\u002F)\n- [FastAPI — Background Tasks](https:\u002F\u002Ffastapi.tiangolo.com\u002Ftutorial\u002Fbackground-tasks\u002F)\n- [AWS — Transactional outbox pattern](https:\u002F\u002Fdocs.aws.amazon.com\u002Fprescriptive-guidance\u002Flatest\u002Fcloud-design-patterns\u002Ftransactional-outbox.html)\n- [PostgreSQL — SELECT and locking clauses](https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-select.html)",[19,24,28,33,37,41,45],{"title":20,"url":21,"type":22,"note":23,"checked":7},"OpenAI Agents SDK — Sessions","https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-python\u002Fsessions\u002F","官方文档","SDK 对话历史与 session 接口；不同于所有业务动作的事务状态。",{"title":25,"url":26,"type":22,"note":27,"checked":7},"OpenAI Agents SDK — Human in the loop","https:\u002F\u002Fopenai.github.io\u002Fopenai-agents-python\u002Fhuman_in_the_loop\u002F","工具审批、暂停与运行状态恢复；外部业务授权仍需应用实施。",{"title":29,"url":30,"type":31,"note":32,"checked":7},"Anthropic — Scaling Managed Agents","https:\u002F\u002Fwww.anthropic.com\u002Fengineering\u002Fmanaged-agents","工程报告","session、harness、sandbox 解耦及恢复设计的公开报告；不定义所有外部工具的幂等语义。",{"title":34,"url":35,"type":22,"note":36,"checked":7},"FastAPI — Concurrency and async \u002F await","https:\u002F\u002Ffastapi.tiangolo.com\u002Fasync\u002F","异步等待、并发与并行的区别；进程数和下游容量仍需单独规划。",{"title":38,"url":39,"type":22,"note":40,"checked":7},"FastAPI — Background Tasks","https:\u002F\u002Ffastapi.tiangolo.com\u002Ftutorial\u002Fbackground-tasks\u002F","响应后的后台任务及重型任务建议；不是持久化任务队列的完整契约。",{"title":42,"url":43,"type":22,"note":44,"checked":7},"AWS — Transactional outbox pattern","https:\u002F\u002Fdocs.aws.amazon.com\u002Fprescriptive-guidance\u002Flatest\u002Fcloud-design-patterns\u002Ftransactional-outbox.html","事务 outbox 解决数据库与消息双写问题；消费者仍需处理重复消息。",{"title":46,"url":47,"type":22,"note":48,"checked":7},"PostgreSQL — SELECT and locking clauses","https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-select.html","行锁与 SKIP LOCKED 的语义和局限；任务租约与外部 fencing 由应用设计。",null,{"slug":51,"title":52,"date":7,"updated":7,"tags":53,"summary":57,"draft":14,"lang":15,"minutes":16},"agent-observability-delivery","Agent 怎么持续优化与上线：Trace、评测、SLO 和灰度交付的证据链",[9,54,55,56],"可观测性","评测","CI\u002FCD","对照 OpenAI tracing 与 harness 实践、Anthropic 长任务和评测方法，建立跨模型、工具、队列的观测链；解释 SLO、费用归因、Docker\u002FK8s 发布和可回滚的优化实验。",{"type":59,"children":60},"root",[61,69,74,81,102,113,124,130,158,163,183,260,273,279,299,304,309,315,326,347,356,365,370,376,517,522,527,533,546,565,578,588,594,675,687,693,698,703,715,721,726,731],{"type":62,"tag":63,"props":64,"children":65},"element","p",{},[66],{"type":67,"value":68},"text","一条 Agent 请求可能等待模型、检索、工具和审批。请求多起来以后，问题不仅是“能开多少协程”，还有任务会不会丢、写入会不会重复、旧 worker 会不会覆盖新结果，以及客户端断开后系统怎样继续。",{"type":62,"tag":63,"props":70,"children":71},{},[72],{"type":67,"value":73},"本文综合官方运行框架、FastAPI、PostgreSQL 与 outbox 资料。代码片段是用于解释语义的简化示例；完整生产实现还要接入鉴权、存储、调度、取消、监控和故障演练。",{"type":62,"tag":75,"props":76,"children":78},"h2",{"id":77},"两家厂商保存的状态与应用状态如何连接",[79],{"type":67,"value":80},"两家厂商保存的状态，与应用状态如何连接",{"type":62,"tag":63,"props":82,"children":83},{},[84,92,94,100],{"type":62,"tag":85,"props":86,"children":89},"a",{"href":21,"rel":87},[88],"nofollow",[90],{"type":67,"value":91},"OpenAI SDK Sessions",{"type":67,"value":93},"解决跨运行对话历史；",{"type":62,"tag":85,"props":95,"children":97},{"href":26,"rel":96},[88],[98],{"type":67,"value":99},"人工审批机制",{"type":67,"value":101},"支持暂停、保存运行状态并继续。这些能力帮助恢复模型交互，但某个工具是否已改变业务对象，仍需要业务系统确认。",{"type":62,"tag":63,"props":103,"children":104},{},[105,111],{"type":62,"tag":85,"props":106,"children":108},{"href":30,"rel":107},[88],[109],{"type":67,"value":110},"Claude Managed Agents",{"type":67,"value":112},"公开了 session、harness 和 sandbox 的解耦思路。对应用的启发是把可靠事件记录与可替换的执行进程分开；该工程报告并没有给所有第三方工具定义事务或幂等保证。",{"type":62,"tag":63,"props":114,"children":115},{},[116,122],{"type":62,"tag":117,"props":118,"children":119},"strong",{},[120],{"type":67,"value":121},"工程推导",{"type":67,"value":123},"：即使用厂商托管状态，应用也应保存业务任务 ID 到厂商 session \u002F run ID 的映射，以及已确认结果与验收状态。重复回调、超时后的查询和用户取消，都必须落到同一个业务任务上。",{"type":62,"tag":75,"props":125,"children":127},{"id":126},"async-io提高等待期间的利用率",[128],{"type":67,"value":129},"async I\u002FO：提高等待期间的利用率",{"type":62,"tag":63,"props":131,"children":132},{},[133,139,141,148,150,156],{"type":62,"tag":85,"props":134,"children":136},{"href":35,"rel":135},[88],[137],{"type":67,"value":138},"FastAPI 并发指南",{"type":67,"value":140},"解释了并发等待与并行计算的区别。等待模型 HTTP、异步数据库或网络工具时，",{"type":62,"tag":142,"props":143,"children":145},"code",{"className":144},[],[146],{"type":67,"value":147},"await",{"type":67,"value":149}," 可以释放事件循环；同步库放进 ",{"type":62,"tag":142,"props":151,"children":153},{"className":152},[],[154],{"type":67,"value":155},"async def",{"type":67,"value":157}," 并不会自动变异步，反而可能阻塞所有同进程请求。",{"type":62,"tag":63,"props":159,"children":160},{},[161],{"type":67,"value":162},"同步 I\u002FO 可以使用适当的线程池与超时；较重的 Python CPU 计算通常应使用独立进程或专门计算服务，具体还受运行时与原生库行为影响。PDF 解析、OCR、embedding 批处理与交互式流式响应可使用不同 worker 池，避免批任务占满交互资源。",{"type":62,"tag":63,"props":164,"children":165},{},[166,168,173,175,181],{"type":67,"value":167},"下面展示",{"type":62,"tag":117,"props":169,"children":170},{},[171],{"type":67,"value":172},"进程内",{"type":67,"value":174},"限制模型调用的语义，",{"type":62,"tag":142,"props":176,"children":178},{"className":177},[],[179],{"type":67,"value":180},"invoke_model",{"type":67,"value":182}," 是待接入的异步函数：",{"type":62,"tag":184,"props":185,"children":190},"pre",{"className":186,"code":187,"language":188,"meta":189,"style":189},"language-python shiki shiki-themes vitesse-light vitesse-light","model_slots = asyncio.Semaphore(16)  # 示例值，按压测和上游预算调整\n\nasync def bounded_call(request, remaining_seconds):\n    # 等待并发槽也消耗截止时间，不能只给 HTTP 调用计时。\n    async with asyncio.timeout(remaining_seconds):\n        async with model_slots:\n            return await invoke_model(request)\n","python","",[191],{"type":62,"tag":142,"props":192,"children":193},{"__ignoreMap":189},[194,205,215,224,233,242,251],{"type":62,"tag":195,"props":196,"children":199},"span",{"class":197,"line":198},"line",1,[200],{"type":62,"tag":195,"props":201,"children":202},{},[203],{"type":67,"value":204},"model_slots = asyncio.Semaphore(16)  # 示例值，按压测和上游预算调整\n",{"type":62,"tag":195,"props":206,"children":208},{"class":197,"line":207},2,[209],{"type":62,"tag":195,"props":210,"children":212},{"emptyLinePlaceholder":211},true,[213],{"type":67,"value":214},"\n",{"type":62,"tag":195,"props":216,"children":218},{"class":197,"line":217},3,[219],{"type":62,"tag":195,"props":220,"children":221},{},[222],{"type":67,"value":223},"async def bounded_call(request, remaining_seconds):\n",{"type":62,"tag":195,"props":225,"children":227},{"class":197,"line":226},4,[228],{"type":62,"tag":195,"props":229,"children":230},{},[231],{"type":67,"value":232},"    # 等待并发槽也消耗截止时间，不能只给 HTTP 调用计时。\n",{"type":62,"tag":195,"props":234,"children":236},{"class":197,"line":235},5,[237],{"type":62,"tag":195,"props":238,"children":239},{},[240],{"type":67,"value":241},"    async with asyncio.timeout(remaining_seconds):\n",{"type":62,"tag":195,"props":243,"children":245},{"class":197,"line":244},6,[246],{"type":62,"tag":195,"props":247,"children":248},{},[249],{"type":67,"value":250},"        async with model_slots:\n",{"type":62,"tag":195,"props":252,"children":254},{"class":197,"line":253},7,[255],{"type":62,"tag":195,"props":256,"children":257},{},[258],{"type":67,"value":259},"            return await invoke_model(request)\n",{"type":62,"tag":63,"props":261,"children":262},{},[263,265,271],{"type":67,"value":264},"这段代码不提供多进程或跨机器的总限流。启动四个进程后可能允许四倍调用；需要全局配额时，由入口或共享调度器分配预算，并防止所有实例同时重试。",{"type":62,"tag":142,"props":266,"children":268},{"className":267},[],[269],{"type":67,"value":270},"asyncio.timeout",{"type":67,"value":272}," 也无法中断阻塞事件循环的同步计算。",{"type":62,"tag":75,"props":274,"children":276},{"id":275},"容量协程数量不是吞吐承诺",[277],{"type":67,"value":278},"容量：协程数量不是吞吐承诺",{"type":62,"tag":63,"props":280,"children":281},{},[282,284,290,292,297],{"type":67,"value":283},"在稳态平均条件下，Little 定律给出 ",{"type":62,"tag":142,"props":285,"children":287},{"className":286},[],[288],{"type":67,"value":289},"在途任务数 ≈ 到达率 × 平均停留时间",{"type":67,"value":291},"。",{"type":62,"tag":117,"props":293,"children":294},{},[295],{"type":67,"value":296},"示例推算",{"type":67,"value":298},"：接受 20 个任务\u002F秒、平均停留 15 秒，则平均约有 300 个任务处于系统中；这不是 300 个模型调用都应同时执行，也不是 p95 容量保证。",{"type":62,"tag":63,"props":300,"children":301},{},[302],{"type":67,"value":303},"一个任务可能调用模型三轮、并发检索两次，或暂停等审批。容量规划分别估计：HTTP 连接、运行中任务、模型调用 RPM\u002FTPM、数据库连接、工具并发和队列积压。限制应覆盖用户、租户、模型和工具，防止一个慢租户耗尽公共资源。",{"type":62,"tag":63,"props":305,"children":306},{},[307],{"type":67,"value":308},"队列等待过长时应拒绝新任务或返回明确的繁忙状态；可以按优先级与公平性调度。扩 worker 前先看瓶颈：若上游 token 配额已满，扩容只会增加排队与限流。",{"type":62,"tag":75,"props":310,"children":312},{"id":311},"持久队列接收成功必须有恢复依据",[313],{"type":67,"value":314},"持久队列：接收成功必须有恢复依据",{"type":62,"tag":63,"props":316,"children":317},{},[318,324],{"type":62,"tag":85,"props":319,"children":321},{"href":39,"rel":320},[88],[322],{"type":67,"value":323},"FastAPI BackgroundTasks",{"type":67,"value":325},"适合响应后的进程内工作，重型工作可以使用专门队列。它本身不能替代需要持久化、重试与跨进程恢复的任务系统。",{"type":62,"tag":63,"props":327,"children":328},{},[329,331,337,339,345],{"type":67,"value":330},"接收长任务时，推荐在一个数据库事务中写入任务与待发布事件，再返回 ",{"type":62,"tag":142,"props":332,"children":334},{"className":333},[],[335],{"type":67,"value":336},"202 + task_id",{"type":67,"value":338},"。发布器把 outbox 事件送到消息队列，确认后标记发布；worker 依据任务 ID 去重消费。",{"type":62,"tag":85,"props":340,"children":342},{"href":43,"rel":341},[88],[343],{"type":67,"value":344},"AWS transactional outbox",{"type":67,"value":346},"解释了如何避免数据库提交与消息发送之间的双写漏洞，也指出重复消息仍需消费者处理。",{"type":62,"tag":63,"props":348,"children":349},{},[350],{"type":62,"tag":351,"props":352,"children":355},"img",{"alt":353,"src":354},"任务与外部副作用的恢复路径：同事务 outbox、可重投消息、租约 worker 与对账","\u002Fdiagrams\u002Fagent-concurrency-state.svg",[],{"type":62,"tag":63,"props":357,"children":358},{},[359],{"type":62,"tag":360,"props":361,"children":362},"em",{},[363],{"type":67,"value":364},"图 1 · 队列允许重投；任务接收、领取和业务动作分别有稳定身份。外部结果未知时先对账。",{"type":62,"tag":63,"props":366,"children":367},{},[368],{"type":67,"value":369},"接口幂等键按租户或用户范围唯一，并绑定规范化请求摘要。同一键同一内容返回原任务；同一键不同内容明确拒绝。先查“是否存在”再插入仍有竞争，应通过唯一约束或原子写入完成。",{"type":62,"tag":75,"props":371,"children":373},{"id":372},"最小数据模型哪些数据由谁负责",[374],{"type":67,"value":375},"最小数据模型：哪些数据由谁负责",{"type":62,"tag":377,"props":378,"children":379},"table",{},[380,404],{"type":62,"tag":381,"props":382,"children":383},"thead",{},[384],{"type":62,"tag":385,"props":386,"children":387},"tr",{},[388,394,399],{"type":62,"tag":389,"props":390,"children":391},"th",{},[392],{"type":67,"value":393},"实体",{"type":62,"tag":389,"props":395,"children":396},{},[397],{"type":67,"value":398},"关键字段示意",{"type":62,"tag":389,"props":400,"children":401},{},[402],{"type":67,"value":403},"职责与不变量",{"type":62,"tag":405,"props":406,"children":407},"tbody",{},[408,427,445,463,481,499],{"type":62,"tag":385,"props":409,"children":410},{},[411,417,422],{"type":62,"tag":412,"props":413,"children":414},"td",{},[415],{"type":67,"value":416},"tenant \u002F principal",{"type":62,"tag":412,"props":418,"children":419},{},[420],{"type":67,"value":421},"租户、身份、角色与授权关系",{"type":62,"tag":412,"props":423,"children":424},{},[425],{"type":67,"value":426},"服务端决定可访问范围",{"type":62,"tag":385,"props":428,"children":429},{},[430,435,440],{"type":62,"tag":412,"props":431,"children":432},{},[433],{"type":67,"value":434},"task",{"type":62,"tag":412,"props":436,"children":437},{},[438],{"type":67,"value":439},"业务 ID、请求摘要、状态、配置版本、deadline",{"type":62,"tag":412,"props":441,"children":442},{},[443],{"type":67,"value":444},"同一业务请求对应一个任务",{"type":62,"tag":385,"props":446,"children":447},{},[448,453,458],{"type":62,"tag":412,"props":449,"children":450},{},[451],{"type":67,"value":452},"attempt",{"type":62,"tag":412,"props":454,"children":455},{},[456],{"type":67,"value":457},"尝试 ID、worker、epoch、lease、进度",{"type":62,"tag":412,"props":459,"children":460},{},[461],{"type":67,"value":462},"同时只有当前有效持有者提交任务结果",{"type":62,"tag":385,"props":464,"children":465},{},[466,471,476],{"type":62,"tag":412,"props":467,"children":468},{},[469],{"type":67,"value":470},"operation",{"type":62,"tag":412,"props":472,"children":473},{},[474],{"type":67,"value":475},"稳定操作 ID、参数摘要、对象版本、结果",{"type":62,"tag":412,"props":477,"children":478},{},[479],{"type":67,"value":480},"重试不改变同一个逻辑动作的身份",{"type":62,"tag":385,"props":482,"children":483},{},[484,489,494],{"type":62,"tag":412,"props":485,"children":486},{},[487],{"type":67,"value":488},"event \u002F outbox",{"type":62,"tag":412,"props":490,"children":491},{},[492],{"type":67,"value":493},"事件 ID、任务 ID、序号、发布状态",{"type":62,"tag":412,"props":495,"children":496},{},[497],{"type":67,"value":498},"可重投、可追踪；排序规则明确",{"type":62,"tag":385,"props":500,"children":501},{},[502,507,512],{"type":62,"tag":412,"props":503,"children":504},{},[505],{"type":67,"value":506},"artifact \u002F source",{"type":62,"tag":412,"props":508,"children":509},{},[510],{"type":67,"value":511},"产物或来源 ID、版本、定位、权限、留存",{"type":62,"tag":412,"props":513,"children":514},{},[515],{"type":67,"value":516},"验收与引用可追溯，读取仍需授权",{"type":62,"tag":63,"props":518,"children":519},{},[520],{"type":67,"value":521},"MySQL \u002F PostgreSQL 可保存事务事实；Redis 可做缓存、短期配额或调度辅助；消息队列负责传输；向量库负责检索索引。没有必要强行使用每一种组件，也不要把可能淘汰的缓存当作任务成功的唯一记录。",{"type":62,"tag":63,"props":523,"children":524},{},[525],{"type":67,"value":526},"若业务确实有设备、监护关系或其他角色，在身份与授权模型上扩展关系；它们属于具体业务约束，不应硬编码进通用 Agent 循环。",{"type":62,"tag":75,"props":528,"children":530},{"id":529},"领取任务与-fencing租约过期不代表旧进程已停止",[531],{"type":67,"value":532},"领取任务与 fencing：租约过期不代表旧进程已停止",{"type":62,"tag":63,"props":534,"children":535},{},[536,538,544],{"type":67,"value":537},"worker 用短事务领取任务，记录租约到期时间和递增的执行代次 ",{"type":62,"tag":142,"props":539,"children":541},{"className":540},[],[542],{"type":67,"value":543},"epoch",{"type":67,"value":545},"，事务提交后再调用模型或工具。不能持有数据库行锁等待几分钟的外部请求。",{"type":62,"tag":63,"props":547,"children":548},{},[549,555,557,563],{"type":62,"tag":85,"props":550,"children":552},{"href":47,"rel":551},[88],[553],{"type":67,"value":554},"PostgreSQL SELECT 文档",{"type":67,"value":556},"说明 ",{"type":62,"tag":142,"props":558,"children":560},{"className":559},[],[561],{"type":67,"value":562},"FOR UPDATE SKIP LOCKED",{"type":67,"value":564}," 可用于多个消费者访问队列式表，但返回的是跳过锁定行的视图，不适合普通一致查询。它也不提供长期任务租约或重试策略，仍需要应用设计。",{"type":62,"tag":63,"props":566,"children":567},{},[568,570,576],{"type":67,"value":569},"任务更新可以使用条件写入：",{"type":62,"tag":142,"props":571,"children":573},{"className":572},[],[574],{"type":67,"value":575},"WHERE task_id = ? AND epoch = ? AND owner = ?",{"type":67,"value":577},"，并结合有效租约校验，只有当前执行者能提交状态。旧进程恢复后提交旧代次结果，应被拒绝。",{"type":62,"tag":63,"props":579,"children":580},{},[581,586],{"type":62,"tag":117,"props":582,"children":583},{},[584],{"type":67,"value":585},"局限在外部副作用",{"type":67,"value":587},"：数据库 fencing 只能挡住数据库所验证的写入。工具服务若不验证代次、稳定操作 ID 或幂等条件，旧 worker 仍可能执行外部动作。关键写入通过受控工具网关校验，或依赖上游唯一约束与查询对账，不能只加一个 Redis 锁就宣布“恰好执行一次”。",{"type":62,"tag":75,"props":589,"children":591},{"id":590},"三个崩溃窗口如何避免盲目重跑",[592],{"type":67,"value":593},"三个崩溃窗口：如何避免盲目重跑",{"type":62,"tag":377,"props":595,"children":596},{},[597,618],{"type":62,"tag":381,"props":598,"children":599},{},[600],{"type":62,"tag":385,"props":601,"children":602},{},[603,608,613],{"type":62,"tag":389,"props":604,"children":605},{},[606],{"type":67,"value":607},"故障发生处",{"type":62,"tag":389,"props":609,"children":610},{},[611],{"type":67,"value":612},"恢复方式",{"type":62,"tag":389,"props":614,"children":615},{},[616],{"type":67,"value":617},"应验证的结果",{"type":62,"tag":405,"props":619,"children":620},{},[621,639,657],{"type":62,"tag":385,"props":622,"children":623},{},[624,629,634],{"type":62,"tag":412,"props":625,"children":626},{},[627],{"type":67,"value":628},"数据库已提交，消息尚未发出",{"type":62,"tag":412,"props":630,"children":631},{},[632],{"type":67,"value":633},"outbox 发布器继续扫描",{"type":62,"tag":412,"props":635,"children":636},{},[637],{"type":67,"value":638},"已接收任务最终能被领取",{"type":62,"tag":385,"props":640,"children":641},{},[642,647,652],{"type":62,"tag":412,"props":643,"children":644},{},[645],{"type":67,"value":646},"消息已发出，发布确认未记录",{"type":62,"tag":412,"props":648,"children":649},{},[650],{"type":67,"value":651},"允许重投，消费侧按 ID 去重",{"type":62,"tag":412,"props":653,"children":654},{},[655],{"type":67,"value":656},"不创建第二个任务或重复确认",{"type":62,"tag":385,"props":658,"children":659},{},[660,665,670],{"type":62,"tag":412,"props":661,"children":662},{},[663],{"type":67,"value":664},"外部动作已成功，工具结果未写回",{"type":62,"tag":412,"props":666,"children":667},{},[668],{"type":67,"value":669},"查询稳定操作 ID，或复用幂等键对账",{"type":62,"tag":412,"props":671,"children":672},{},[673],{"type":67,"value":674},"不再次产生业务副作用",{"type":62,"tag":63,"props":676,"children":677},{},[678,680,685],{"type":67,"value":679},"尝试 ID 每次执行可以变化；",{"type":62,"tag":117,"props":681,"children":682},{},[683],{"type":67,"value":684},"逻辑操作 ID 跨重试保持稳定",{"type":67,"value":686},"。同一个操作若参数或目标版本改变，应被识别为新动作并重新授权，不能复用旧幂等键掩盖差异。未知结果没有查询能力时，暂停并升级处理比自动重试更可靠。",{"type":62,"tag":75,"props":688,"children":690},{"id":689},"流式接口慢客户端与取消",[691],{"type":67,"value":692},"流式接口、慢客户端与取消",{"type":62,"tag":63,"props":694,"children":695},{},[696],{"type":67,"value":697},"HTTP 接收确认、Agent 运行和事件流可以分开：客户端订阅任务事件，用事件序号或 SSE ID 恢复展示；服务器定义重放窗口，超出窗口时先读取任务快照再接续。确认、进度与模型文本是不同事件，不能把普通进度称为首 token。",{"type":62,"tag":63,"props":699,"children":700},{},[701],{"type":67,"value":702},"慢客户端会使缓冲增长。对可重放事件设置有限缓冲和明确断开策略；实时模型流若无法暂停或重放，定义中止与续接行为。不要因为展示端慢就无限占用内存，也不能静默丢掉用户需要的最终状态。",{"type":62,"tag":63,"props":704,"children":705},{},[706,708,714],{"type":67,"value":707},"取消是一项持久化请求：worker 在模型、工具和步骤边界检查，将取消尽量传播上游。已提交外部动作不能靠断开 SSE 撤销；还要处理“取消与完成同时发生”的状态竞争，依据已确认事实返回结果。多端协议与旧客户端兼容见",{"type":62,"tag":85,"props":709,"children":711},{"href":710},"\u002Farticles\u002Fdevice-cloud-api-sync",[712],{"type":67,"value":713},"端云同步篇",{"type":67,"value":291},{"type":62,"tag":75,"props":716,"children":718},{"id":717},"优化与验证用故障和负载测出边界",[719],{"type":67,"value":720},"优化与验证：用故障和负载测出边界",{"type":62,"tag":63,"props":722,"children":723},{},[724],{"type":67,"value":725},"固定模型、任务组合和上游配额，逐步调整 worker 数量、并发上限与批大小，记录接受率、队列等待、p95、工具超时、数据库连接和每成功任务费用。负载包含长短任务混合、单租户突发、慢消费者与429，不能只测平均延迟。",{"type":62,"tag":63,"props":727,"children":728},{},[729],{"type":67,"value":730},"故障演练主动终止 worker、重复投递消息、丢失外部响应、让租约过期后旧 worker 恢复，并在运行中滚动升级。验收关注任务不丢、确认不重、越权不发生、未知结果能对账；这些是业务保证，不能由“用了 async + Redis + MQ”自动推出。",{"type":62,"tag":75,"props":732,"children":734},{"id":733},"参考资料",[735],{"type":67,"value":733},[737,738,739,740,741,742,743,744,745,746],{"id":77,"depth":207,"text":80},{"id":126,"depth":207,"text":129},{"id":275,"depth":207,"text":278},{"id":311,"depth":207,"text":314},{"id":372,"depth":207,"text":375},{"id":529,"depth":207,"text":532},{"id":590,"depth":207,"text":593},{"id":689,"depth":207,"text":692},{"id":717,"depth":207,"text":720},{"id":733,"depth":207,"text":733},1792400815606]