商业产业 Hacker News Agent 2026-07-29T16:59 正文 ok 2570 words

Agentic 应用的基础设施模式

英文标题:🔶 Infrastructure Patterns for Agentic Applications

大多数团队会先把 Agent 包装进一个 HTTP 路由,但这种方式只适合演示,难以应对生产环境中的长时运行、状态持久化和非确定性执行。文章提出三类核心模式:通过队列将 Agent 运行与请求生命周期解耦、用幂等性与补偿机制处理重试和部分失败、用工作流引擎编排复杂的分支与恢复过程。对于需要并行子任务的多 Agent 系统,工作流负责 fan-out/fan-in 与失败策略,队列负责分发执行,并需通过并发限制和退避机制控制共享资源争用。文章还以 Render Workflows 为例,说明托管工作流平台如何将队列、Worker、重试、隔离和可观测性整合为 Agent 的执行基础设施。
打开原文相关 3 · 机会 0
media37.6 KB

中文

大多数团队构建 Agent 的方式,与构建任何其他 Web 功能如出一辙:将模型封装在路由处理程序中,解析请求,然后等待响应。

这对于演示没有问题,但它也会成为生产环境中第一个出问题的环节。

Agent 的运行时间长、具有状态且是非确定性的。它们会调用工具、等待外部 API、分支为子任务、遭遇速率限制,并在多步骤序列中途崩溃。如果你的 Agent 绑定在单个 HTTP 请求上,那么你就把应用的可靠性与一个非确定性循环的实际运行时长耦合在了一起。

要超越演示阶段,就意味着要打破这种耦合。本文将介绍团队用于将脆弱的 Agent 脚本转变为可扩展、具备韧性的生产系统的三种核心模式。

Agent 的核心是一个循环:接收目标、决定下一步做什么、调用模型或工具、观察结果、更新状态、重复直到完成。这个循环具备一些特性,使其不适合简单直接的 Web 架构。

Agent 是长时运行的。 普通 API 请求应当快速完成,而 Agent 往往不会;有时完成一次任务需要数小时,甚至数天。

Agent 是有状态的。 一次运行并不是一次函数调用,而是由目标、计划、工具调用及其输出、重试、错误、决策和最终输出组成。如果进程崩溃,你需要知道此前已经发生了什么。否则,你要么丢失进度,要么盲目地重新运行任务。两者都不理想,尤其对于长时运行的 Agent 而言。

Agent 是非确定性的。 传统工作流代码会这样说:

Agent 代码会这样说:

模型会在运行时选择下一步动作,这使恢复、重放和调试变得更加困难。

一次运行可能执行三步,也可能执行三十步。它可能分叉到多个工具、等待人工参与、产生大型中间产物,或提前停止。基础设施必须通过超时、预算、检查点、审批以及明确的终止条件,为这种不可预测性设定边界。

综合来看,这些特性意味着 Agent 需要的不只是被应用代码包裹的模型调用。它们需要一种能够将运行与请求解耦的架构,以及能够保存进度、记录决策、控制副作用,并将产物与模型上下文分开管理的基础设施。

第一个生产模式,是停止在 Web 请求内部运行整个 Agent。请求并不是适合 Agent 运行的生命周期。它应创建一条持久化运行记录、将工作加入队列,然后立即返回。

这为应用提供了一个简单的边界:

API 将任务加入队列。队列持久化存储任务。Worker 执行任务。数据库记录进度。

客户端会立即获得一个运行 ID。Agent 在其他地方运行。用户可以查看状态、订阅更新,或在运行完成时接收回调。

队列是创建工作的一方与执行工作的一方之间的持久化缓冲区。当运行时间长于普通请求、任务大多相互独立,并且你需要扩展 Worker、进行重试或吸收突发流量时,这是默认的起点。

陷阱: 队列知道某个任务存在,但不知道这个任务属于哪一个逻辑流程。一个后台任务没有问题;但二十个存在依赖关系的步骤、三次重试、两个分支和一次人工审批暂停,并不是队列能替你管理的事情。你必须自行构建这部分逻辑。正如后文将看到的,这正是工作流引擎发挥作用的地方。

可靠性:安全重试与部分失败

将工作移入队列解决了生命周期问题,但并没有解决执行持久化的问题。

大多数生产队列都是至少一次(at-least-once)语义:一个任务可能运行不止一次。这是一种为了避免丢失工作而作出的有意权衡,你的代码必须能够承受它。一旦 Agent 能够调用工具、写入记录、发送消息或配置资源,重试行为就成为应用正确性模型的一部分。

有两项纪律最为重要。幂等性回答的是:当单个步骤运行两次时会发生什么。补偿回答的是:当一次运行在一系列步骤中途停止时会发生什么。重试使前者成为必需条件,永久性失败则使后者成为生产环境中的必要能力。会接触远程服务的 Agent 两者都需要。

糟糕的 Worker 代码假设步骤之间不会发生崩溃:

更好的代码会创建一个幂等性边界:

对于 Agent 而言,每一次具有副作用的工具调用都需要采用同样的处理方式:在调用工具前检查是否已有完成记录、向上插入或更新一条“运行中”记录、调用工具,然后将其标记为完成。

除非被重试的操作是安全的,否则重试并不是一种恢复策略。

设想一个 Agent 完成了五个步骤中的三个:

  1. 它对信用卡扣款;
  2. 配置一项资源;
  3. 并发送确认邮件。

然后第四步发生永久性失败。前三步都不能通过数据库事务回滚,因为扣款并不会因删除一行数据库记录而撤销;资金已经发生流转。从第一步重新启动运行,会再次扣款并再次发送邮件,重新制造幂等性本应避免的重复副作用问题。

分布式系统对此有一个标准答案:Saga 模式. 对于 Agent 可以执行的每一个具有副作用的操作,都要定义一个能够逆转该效果的补偿操作。扣款通过退款逆转。邮件通过发送更正消息逆转。当一次运行永久失败时,编排器会按逆序遍历已完成的步骤,并执行它们的补偿操作。

补偿操作必须像正向操作一样具备幂等性。补偿本身也可能失败;退款 API 不可用的概率并不比扣款 API 不可用低。补偿链需要有限次数的重试、为无法完成的补偿设置死信路径,以及一个人工介入的兜底机制。没有这些机制,失败的运行最终可能处于“撤销了一半”的状态,而非“完成了一半”,这并没有更好。

补偿并不总是处理部分失败的正确方式。如果五个并行子订单中四个成功、一个失败,完成那四个并放弃第五个通常是更好的结果。这避免了撤销已经成功的工作。只有在部分成功不可接受时,才值得构建补偿机制,而不应将其作为对任何失败的默认响应。

幂等性处理的是一个步骤运行两次的情况。补偿处理的是一次运行中途停止的情况。无论运行由简单 Worker 还是工作流引擎执行,两者都不可或缺。

队列能够分发工作,但不会记住一次运行的结构。它知道某个任务存在,却不知道第三步依赖第二步、某项人工审批正在等待、两个分支需要在综合之前汇合,或者最终步骤失败时应执行补偿。

一旦最难的问题不再是 这个任务应该在哪里运行?,而变成 鉴于已经发生的事情,下一步应该做什么?,你就已经从队列处理进入了编排领域。

工作流引擎提供了这一编排层。它保存一次运行的_历史_:哪些步骤已开始、已完成、已失败、已重试、已超时,或正在等待外部输入。在工作流中,崩溃并不意味着从头开始。

每个工作流系统都会将代码划分为同样的两个角色:决定下一步要做什么的协调器(coordinator),以及负责完成实际工作的步骤(steps)——例如模型调用、工具执行和记录写入。不同引擎的名称各不相同,但责任划分才是关键概念:决策位于一层,副作用位于另一层。

协调器有一条严格规则:在相同的历史条件下,它必须始终作出相同的决策。这源于恢复机制的工作方式。崩溃之后,引擎会从头重新执行决策逻辑来重建一次运行,并为已经完成的工作替换为已记录的结果。只有代码每次都作出相同决策,重放才能回到同一个位置。因此,任何第二次执行时可能给出不同结果的事物——模型调用、当前时间、随机数、外部服务调用——都应放在步骤中;其结果会在首次运行时记录下来,此后始终从历史中读取。即使只是协调器中的一次随意时钟读取,也会让恢复后的运行悄然偏离自身历史。持久化执行能够告诉你一次运行崩溃时处于什么位置,但它并不能消除幂等性的需求,因为步骤仍可能运行多次。这也是工作流引擎天然适合承载补偿逻辑的原因:它已经跟踪了哪些步骤已完成,而这正是 Saga 逆向遍历所需的记录。

扩展工作流:fan-out、fan-in 与资源争用

当一次运行发生分支时,工作流会变得尤其有用。Fan-out 是常见形态:主导 Agent(lead agent)将一个目标拆分为相互独立的子任务,将每项任务发送给 Worker 或子 Agent,然后综合结果。

这带来了并行性和新的上下文窗口,对于研究、文档分析及其他广度优先型工作非常有用。但难点不在于分发 Worker,而在于协调 fan-in:如果一个分支失败会怎样、可以容忍多少失败、是否应综合部分结果,以及如何避免无限等待。队列可以运行 Worker,工作流则决定下一步做什么。

当子任务相互独立、主要是只读操作且可并行化时,fan-out 值得采用。当子任务紧密耦合,或者需要共享持续变化的状态时,它的效果会很差。增加 Agent 也会增加成本和协调开销,因此“应该使用多少个 Agent”这个问题,比启动它们的技术细节更重要。

Fan-out 也会引入资源争用。一个系统可以在三个并行子 Agent 时正常工作,却在十一 个时失败,即使供应商的总容量十分充足。未经协调的调用可能撞上同一个速率限制、耗尽同一个连接池,或在同一时刻进行重试。应将供应商、数据库和模型 API 视为共享资源:限制并发度、在需要时使用令牌桶,并在共享资源施加限制时进行退避。

在大规模场景下,工作流负责协调过程,队列负责分发执行。二者并非竞争关系;它们运行在不同层级。

这里有两个细节至关重要。Promise.allSettled 能避免一个失败分支污染整个 fan-in;而成功阈值会明确部分失败策略:由工作流而非队列决定,是否可以基于已返回的结果继续运行。

这是高规模 Agentic 系统的成熟模式:持久化流程状态、多个 Worker 池、受速率限制的分发,以及可审计性。其代价是真实存在的运维复杂性。你现在拥有 API 平面、编排平面和执行平面。只有当工作负载确有需求时,这一切才值得。

上述模式与平台无关。你可以使用任意队列、任意工作流引擎和任意 Worker 集群来组装它们。问题在于,其中有多少组装工作是你愿意自己拥有和运维的。

在 Render 上,答案几乎是完全不需要。你的 Web 服务就是 API;Render Workflows负责其余一切。你将任务定义为普通 TypeScript 或 Python 函数,平台则提供队列、Worker、重试、隔离以及按运行维度的可观测性。无需配置队列,无需维持常驻的 Worker 集群,也无需运维编排器集群。

以一个具体 Agent 为例:给定一个类似于 “总结我们的前三大竞争对手如何为其企业级套餐定价” 的目标,它会进行规划、搜索、阅读并综合出一份简报。将其写成工作流代码后,它与本文开头的循环完全一致——协调器负责决策,步骤负责行动:

有三点值得注意:

这就是完整的执行平面。 没有队列客户端、没有消息 Schema、没有轮询循环,也没有 Worker 注册。三个 task() 包装器取代了第一种模式中的 API → Queue → Worker 管道。当 runAgent 调用 decide(...) 或 executeAction(...) 时,这并不是本地函数调用——由于这些函数被 task() 包装,每次调用都会在各自按需启动的实例上发起一个串联的任务运行,并拥有各自的重试策略和计算配置。Promise.all 就是 fan-out;平台会处理分发、隔离和重试。

这个循环就是 Agent。 协调器并不执行固定计划。每次循环都会基于此前已发生的一切重新作出决策——因此,动作既可以跨迭代串行执行,也可以在单次迭代内并行执行;而当模型判定任务已完成时,运行即会终止(受任务超时限制)。

一次 Agent 运行是一棵任务运行树。 runAgent 的运行是根节点;每一个 decide 和 executeAction 运行都从它串联而出。Render 会追踪整棵树——状态、重试、每次运行的日志——因此,“Agent 做了什么?”是仪表板能够回答的问题。如果你的应用需要保存自己的记录,请存储根运行的 ID,并从那里开始追踪。

从你的 Web 服务或任何其他位置启动一次 Agent 运行,只需要一次 SDK 调用:

startTask 就是入队操作。平台会持久化存储工作、分发它,并公开其状态——第一种模式中的整个边界,都在一次调用中完成。(如果调用方希望阻塞直到完成,则应使用 runTask;对于 API 请求边界而言,startTask 才是正确选择。)

Workflows 是重心,但当一次运行需要更多能力时,平台的其余部分也可以组合进来。调用 startTask 的 Cron Job 可以提供定时 Agent。Key Value可以在并行运行争用共享速率限制时提供令牌桶。Postgres用于保存大型产物,使任务传递 ID 而不是负载——必须在一次运行之外持续存在的状态应放入存储;sta

English / 原文

Most teams start building agents the same way they build any other web feature: wrap the model in a route handler, parse the request, and wait for the response.

This is fine for a demo. It's also the first thing to break in production.

Agents are long-running, stateful, and non-deterministic. They call tools, wait for external APIs, branch into subtasks, hit rate limits, and crash halfway through multi-step sequences. If your agent is tied to a single HTTP request, you've coupled your application's reliability to the wall-clock time of a non-deterministic loop.

Moving past the demo means breaking that coupling. This post walks through the three core patterns teams use to turn fragile agent scripts into scalable, resilient production systems.

At the core of an agent is a loop: receive a goal, decide what to do next, call a model or tool, observe the result, update state, repeat until done. That loop has a few properties that make it hostile to naive web architectures.

Agents are long-running. A normal API request should finish quickly. Agents often don't, sometimes taking hours or even days to complete.

Agents are stateful. A run isn't one function call. It's a goal, a plan, tool calls and outputs, retries, errors, decisions, and a final output. If the process crashes, you need to know what already happened. If not, you either lose progress or rerun work blindly. Neither is great, especially for long-running agents.

Agents are non-deterministic. Traditional workflow code says:

Agent code says:

The model chooses the next action at runtime, which makes recovery, replay, and debugging much harder.

A run may take three steps or thirty. It may fan out across several tools, wait for a human, produce large intermediate artifacts, or stop early. Infrastructure has to set boundaries around that unpredictability with timeouts, budgets, checkpoints, approvals, and explicit termination conditions.

Taken together, these properties mean agents need more than model calls wrapped in application code. They need architecture that decouples runs from requests, and infrastructure that preserves progress, records decisions, controls side effects, and manages artifacts separately from model context.

The first production pattern is to stop running the whole agent inside the web request. A request is the wrong lifetime for an agent run. It should create a durable run record, enqueue the work, and return immediately.

That gives the application a simple boundary:

The API enqueues the job. The queue stores the job durably. The worker executes the job. The database records progress.

The client gets a run ID immediately. The agent runs somewhere else. The user checks status, subscribes to updates, or receives a callback when the run completes.

A queue is a durable buffer between the thing that creates work and the thing that performs it. This is the default starting point when runs are longer than a normal request, tasks are mostly independent, and you need worker scaling, retries, or burst absorption.

The trap: a queue knows a job exists, but it doesn't know the logical process that job belongs to. One background job is fine. Twenty dependent steps, three retries, two branches, and a human approval pause is not something a queue manages for you. You'll build that logic yourself. As we'll see later, that's where workflow engines come in.

Reliability: safe retries and partial failure

Moving work to a queue solves the lifetime problem. It does not solve for durability of execution.

Most production queues are at-least-once: a job may run more than once. That's a deliberate tradeoff to avoid losing work, and your code has to survive it. Once an agent can call tools, write records, send messages, or provisions resources, retry behavior becomes part of the application’s correctness model.

Two disciplines matter most. Idempotency answers what happens when a single step runs twice. Compensation answers what happens when a run stops partway through a sequence of steps. Retries make the first a requirement. Permanent failures make the second necessary for production. Agents that touch remote services needs both.

Bad worker code assumes no crash between steps:

Better code creates an idempotency boundary:

For agents, every side-effecting tool call needs the same treatment: check for a completed record before calling the tool, upsert a "running" record, call the tool, mark it complete.

Retry is not a recovery strategy unless the retried operation is safe.

Consider an agent that completes three of five steps:

  1. it charges a card,
  2. provisions a resource,
  3. and sends a confirmation email.

Then step four then fails permanently. None of the first three steps can be rolled back with a database transaction, since a charge is not undone by deleting a database row; the money has already moved. Restarting the run from step one would recharge the card and resend the email, recreating the exact duplicate-side-effect problem idempotency was meant to prevent.

Distributed systems have a standard answer to this: the saga pattern. For every side-effecting action an agent can take, define a compensating action that reverses the effect. A charge is reversed with a refund. An email is reversed with a correction message. When a run fails permanently, the orchestrator walks the completed steps in reverse order and runs their compensations.

Compensations must be idempotent in the same way forward actions are. Compensations can also fail on their own; a refund API can be unavailable just as easily as a charge API. A compensation chain needs a bounded retry, a dead-letter path for compensations that will not complete, and a manual escape hatch. Without these, a failed run can end up half-undone instead of half-done, which is not an improvement.

Compensation is not always the right response to a partial failure. If four of five parallel sub-orders succeeded and one failed, completing the four and dropping the fifth is often the better outcome. This avoids unwinding work that succeeded. Compensation is worth building when partial success is unacceptable, not as a default response to any failure.

Idempotency covers a step that runs twice. Compensation covers a run that stops halfway through. Both are required whether the run is executed by a simple worker or by a workflow engine.

A queue distributes work, but it does not remember the shape of a run. It knows that a job exists. It does not know that step three depends on step two, that a human approval is pending, that two branches need to join before synthesis, or that compensation should run if the final step fails.

Once the hard part is no longer where should this job run? but what should happen next, given what already happened? , you have moved from queueing into orchestration.

A workflow engine provides that orchestration layer. It stores the history of a run: which steps started, completed, failed, retried, timed out, or waited for external input. In a workflow, a crash doesn't mean starting over.

Every workflow system divides the code into the same two roles: a coordinator that decides what happens next, and steps that do the actual work — model calls, tool executions, record writes. The names vary from engine to engine, but the division of responsibility is the concept that matters: decisions live in one layer, effects live in the other.

The coordinator has one hard rule: given the same history, it must always make the same decisions. This is because of how recovery works. After a crash, the engine rebuilds a run by re-executing the decision logic from the top, substituting recorded results for work that already completed. Replay only lands in the same place if the code decides the same way every time. So anything that could answer differently on a second pass — a model call, the current time, a random number, a call to an external service — belongs in a step, where the result is recorded the first time it runs and read back from history ever after. A stray clock read inside the coordinator, and a resumed run silently diverges from its own history. Durable execution tells you where a run was when it crashed. It does not remove the need for idempotency, because steps can still run more than once. This is why a workflow engine is the natural home for compensation logic: it already tracks which steps completed, which is the record a saga needs to walk backward.

Scaling workflows: fan-out, fan-in, and contention

Workflows become especially useful when a run branches. Fan-out is the common shape: a lead agent breaks a goal into independent subtasks, sends each to a worker or subagent, and then synthesizes the results.

This buys parallelism and fresh context windows, which is useful for research, document analysis, and other breadth-first work. But the hard part is not dispatching the workers. It is coordinating the fan-in: what happens if one branch fails, how many failures are tolerable, whether to synthesize partial results, and how to avoid waiting forever. A queue can run the workers. A workflow decides what happens next.

Fan-out is worth using when subtasks are independent, mostly read-only, and parallelizable. It works poorly when subtasks are tightly coupled or need to share state that keeps changing. Adding agents also adds cost and coordination overhead, so the question of how many agents to use matters more than the mechanics of spawning them.

Fan-out also introduces contention. A system can work with three parallel subagents and fail at eleven, even when there is plenty of aggregate provider capacity. Uncoordinated calls can collide on the same rate limit, exhaust the same connection pool, or retry at the same time. Treat providers, databases, and model APIs as shared resources: cap concurrency, use token buckets where needed, and back off when the shared resource pushes back.

At scale, a workflow coordinates the process and a queue distributes execution. They are not competitors; they operate at different layers.

Two details carry the weight here. Promise.allSettled keeps one failed branch from poisoning the entire fan-in, and the success threshold makes the partial-failure policy explicit: the workflow, not the queue, decides whether the run can proceed with what came back.

This is the mature pattern for high-scale agentic systems: durable process state, multiple worker pools, rate-limited dispatch, audibility. The cost is real operational complexity. You now have an API plane, an orchestration plane, and an execution plane. This is worth it when the workload demands it.

The patterns above are platform-agnostic. You can assemble them from any queue, any workflow engine, and any worker fleet. The question is how much of that assembly you want to own and operate.

On Render, the answer is almost none of it. Your web service is the API; Render Workflows is everything else. You define tasks as plain TypeScript or Python functions, and the platform provides the queue, the workers, the retries, the isolation, and per-run observability. There is no queue to provision, no worker fleet to keep warm, and no orchestrator cluster to operate.

Take a concrete agent: given a goal like “summarize how our top three competitors price their enterprise tiers,” it plans, searches, reads, and synthesizes a brief. Written as workflow code, it is exactly the loop from the start of this post — a coordinator that decides, and steps that act:

Three things to notice:

This is the entire execution plane. There is no queue client, no message schema, no polling loop, no worker registration. Three task() wrappers replace the API → Queue → Worker plumbing from the first pattern. When runAgent calls decide(...) or executeAction(...), that is not a local function call — because the functions are wrapped in task(), each call starts a chained task run on its own on-demand instance, with its own retry policy and its own compute profile. Promise.all is the fan-out; the platform handles dispatch, isolation, and retry.

The loop is the agent. The coordinator doesn’t execute a fixed plan. Each pass through the loop decides again, given everything that has already happened — so actions can run in sequence across iterations, in parallel within one, and the run terminates when the model decides it is done (bounded by the task’s timeout).

One agent run is one tree of task runs. The run of runAgent is the root; every decide and executeAction run chains off it. Render tracks the whole tree — status, retries, logs per run — so “what did the agent do?” is a question the dashboard can answer. If your application needs its own record, store the root run’s ID and go from there.

Starting an agent run, from your web service or anywhere else, is one SDK call:

startTask is the enqueue. The platform stores the work durably, dispatches it, and exposes its status — the entire boundary from the first pattern, in one call. (If the caller wants to block until completion, runTask does that instead; for an API request boundary, startTask is the right fit.)

Workflows is the center of gravity, but the rest of the platform composes in when a run needs more. A Cron Job calling startTask gives you scheduled agents. Key Value gives you token buckets when parallel runs contend on a shared rate limit. Postgres holds large artifacts, so tasks pass IDs instead of payloads — state that must outlive a run goes in a store; state that must not leak between runs dies with the container. None of this is required to get started; each is one primitive, added when the workload demands it.

Workflows removes queue and worker plumbing. Today, it does not remove application-level correctness work: idempotency, compensation, artifact storage, trace propagation, approval boundaries, cost controls, and rate limits are still yours to build with the patterns in this post.

That boundary is moving, though. Much of this list is exactly where Workflows is headed: stronger durability guarantees, first-class idempotency, and human-in-the-loop primitives like native approval gates are all on the roadmap. The patterns in this post will stay the same, but expect more of them to ship as platform features instead of application code you maintain.

Workflows are now in Beta, with current limitations around supported languages, scheduling, Blueprint support, and HIPAA-compatible hosts.

What no platform can remove is the design burden of orchestration. You still have to decide where state lives, where retries are safe, where approval happens, and how a run is traced end to end. The platform's job is to make those decisions cheap to implement, not to make them for you.

The next generation of agentic applications will be built like distributed systems, whether the underlying infrastructure is hand-rolled or managed.

Remember that a queue asks: where should this work run? A workflow asks: what should happen next, given what has already happened? An agent asks: what should I do next to accomplish the goal?

Render Workflows can make the execution layer simpler. It can start task runs, queue them, retry them, and show their status. But the application still owns the semantics of the run: what counts as progress, which side effects are safe to retry, where artifacts live, when a human must approve, and how partial failure is repaired.

That is the real boundary. Infrastructure moves work. Workflows preserve progress. Agents choose actions. Reliable agentic applications come from keeping those responsibilities separate, even when the platform makes them easy to deploy together.

Render Workflows are in Beta. If you want to run these patterns without building the infrastructure behind them, start with the Workflows docs.