持久化AI代理2026:租约执行器实战

·阅读约14分钟·Evergreen Tools Team
Durable Agents

💡 工具推荐调试代理状态 JSON 时,试试 Evergreen Tools 的 JSON验证工具Diff检查工具,全部免费!

2026年,AI代理开始做真正的工作:花二十分钟研究一个问题、等十分钟构建、请求用户审批、或数小时后等外部任务完成。如果仍然把一个代理当作长驻进程来写——一个worker接收请求、进入代理循环、调用模型和工具、等待结果、最终返回答案——资源会被白白浪费,一次部署或崩溃就会摧毁已经完成的工作。Ju Lin 在 2026年8月26日的文章《Agents Should Be Durable, Not Long-Lived》中提出了一个更优雅的模型:把「代理运行」与「执行它的进程」分离。本文用完整的代码示例讲透这套模式。

1. 为什么长驻进程模式会失败

传统实现中,一个代理等于一个长期运行的进程。状态存在内存里,worker 必须全程存活。真实工作流里代理经常要等待:20分钟的研究、10分钟的构建、人工审批、外部回调。让一个 worker 为整个运行过程保持存活,既浪费资源,又让失败代价高昂——部署、崩溃、机器重启都可能摧毁已经完成的进度。这正是 2026 年生产级代理系统转向「持久化运行」的核心原因。

// The old way: one agent = one long-lived process
// Problems: a 20-min research task holds a worker hostage,
// a deploy or crash destroys in-flight work.
class LongLivedAgent {
  constructor(private request: Request) {
    this.messages = []; // state lives in RAM
  }

  async run() {
    const research = await this.research(20 * 60_000); // blocks
    const build = await this.waitForCI(10 * 60_000);   // blocks
    const approval = await this.askHuman();            // blocks
    return this.finalize(research, build, approval);
  }
}
Agent Architecture

2. 核心模式:持久状态 + 租约执行

正确的抽象不是短命代理,而是「带租约执行器的持久化代理」。代理运行本身是持久的:状态、消息、工具结果、预算、当前位置都存储在 worker 之外。worker 是临时的:它租约一个可运行的代理,在短时间内执行有用的工作,checkpoint 新状态,然后消失。另一个 worker 可以从 checkpoint 继续。这并不意味着每次模型调用都要新开进程——一个 worker 可以持有 30 到 60 秒的租约,在有进展时执行多个代理步骤。

// Durable run: state lives outside the worker.
// A worker "leases" a runnable agent for a short window,
// checkpoints progress, then disappears.
interface AgentState {
  runId: string;
  messages: Message[];
  toolResults: ToolResult[];
  budgetSpent: number;
  position: string; // where in the plan we are
  waitingOn: null | { type: "ci" | "human" | "timer"; ref: string };
}

async function executeLease(state: AgentState, leaseSeconds = 60) {
  const deadline = Date.now() + leaseSeconds * 1000;
  while (Date.now() < deadline) {
    const step = await nextStep(state);           // one agent step
    state = await checkpoint(state);              // persist after every step
    if (step.kind === "wait") return { state, status: "waiting" };
  }
  return { state, status: "lease-expired" };      // another worker continues
}

3. 状态必须廉价可重建

持久化模式有一个关键约束:状态必须便宜。好消息是,现代数据库让 checkpoint 变得异常简单——每次执行一个步骤就写入一次。用 libSQL/Turso 这样的边缘数据库,run_id 作为主键做 upsert 即可。坏消息是,浏览器、shell 和沙箱这类有状态的服务可能需要独立的长期服务,而流式输出也必须与当前持有运行的 worker 解耦。这些都是基础设施问题,而大规模 Web 系统早就解决了类似问题——没有人会给每个用户分配一个常驻服务器进程。

// Checkpoint storage: cheap to write, cheap to rebuild.
import { Database } from "@libsql/client";

const db = new Database({ url: process.env.TURSO_URL! });

export async function checkpoint(state: AgentState) {
  await db.execute({
    sql: `INSERT INTO agent_runs (run_id, state_json, updated_at)
         VALUES (?, ?, ?)
         ON CONFLICT(run_id) DO UPDATE SET state_json = excluded.state_json`,
    args: [state.runId, JSON.stringify(state), Date.now()],
  });
}

export async function loadRun(runId: string): Promise<AgentState> {
  const row = await db.execute({
    sql: "SELECT state_json FROM agent_runs WHERE run_id = ?",
    args: [runId],
  });
  return JSON.parse(row.rows[0].state_json as string);
}

4. 等待边界是设计的核心

最重要的边界是「等待」。如果代理需要等五分钟的 CI,它不应该睡五分钟。它应该记录「正在等待 CI」然后退出;当 CI 完成——或定时器触发——运行再次变为可运行。同样的模式适用于速率限制、人工审批、定时任务、外部回调和代理之间的通信。这套「等待即退出」的语义是租约模式能扩展到百万级并发运行的关键。

// The important boundary is WAITING.
// If an agent needs 5 minutes of CI, it must not sleep 5 minutes.
// It records "waiting on ci" and exits. A timer or webhook re-activates it.
const RUNNABLE_QUEUE = "agent:runnable";

async function waitForCI(state: AgentState, buildId: string): Promise<AgentState> {
  await checkpoint({ ...state, waitingOn: { type: "ci", ref: buildId } });
  await redis.lpush(RUNNABLE_QUEUE, state.runId); // parked, not running
  return state; // worker exits now
}

// CI webhook: when the build finishes, make the run runnable again.
app.post("/webhooks/ci/:buildId", async (req) => {
  const runId = await redis.get(`ci:${req.params.buildId}`);
  await redis.rpush(RUNNABLE_QUEUE, runId);
});
Scale

5. 幂等与副作用安全

租约执行意味着重试是常态,所以副作用绝不能执行两次。实践中用幂等键包裹每个外部副作用:先检查 Redis 里是否已有结果,有则直接返回,没有则执行并写入。再加上调度器从可运行队列里取出 run_id、分配 worker、执行租约、释放 worker 的循环,一套支撑百万级代理运行的系统就成型了。

// One million active runs, far fewer processes.
// Most agents are waiting; only runnable ones get compute.
async function scheduler() {
  while (true) {
    const runId = await redis.blpop(RUNNABLE_QUEUE, 0);
    const state = await loadRun(runId);
    const worker = acquireWorker();               // short lease
    try {
      const { state: next } = await executeLease(state, 60);
      await checkpoint(next);
    } finally {
      releaseWorker(worker);
    }
  }
}

// Idempotency: side effects must not run twice after a retry.
export async function withIdempotency<T>(key: string, fn: () => Promise<T>): Promise<T> {
  const done = await redis.get(`effect:${key}`);
  if (done) return JSON.parse(done);
  const result = await fn();
  await redis.set(`effect:${key}`, JSON.stringify(result), { EX: 86_400 });
  return result;
}

6. 何时开始迁移

如果你的代理只是秒级响应的简单查询,长驻进程完全够用。但一旦代理开始等待构建、请求审批、或跨小时运行,就该迁移到持久化模式。迁移路径:先把状态抽到数据库,再加 checkpoint,然后引入租约和等待边界。每一步都是增量收益,最后一步是让系统具备真正的横向扩展能力。

📌 常见问题 FAQ

持久化代理和长驻进程的根本区别是什么?

长驻进程把状态放在 worker 内存里,worker 必须全程存活;持久化代理把状态(消息、工具结果、预算、位置)存在外部存储,worker 只持有短租约执行有进展的步骤,然后消失。

每次模型调用都要新开进程吗?

不需要。那会造成不必要的调度和状态重建开销。一个 worker 可以持有 30-60 秒租约,在有进展时连续执行多个代理步骤。关键边界是等待:一旦需要等待,就记录状态并退出。

如何保证重试后副作用不重复执行?

用幂等键。每个外部副作用(发邮件、调用 API、写数据库)都先查 Redis 中是否已有对应 key 的结果,有则直接返回,没有则执行并写入。这样租约重试就完全安全。

流式输出怎么处理?

流式输出必须独立于当前持有运行的 worker。通常把流接到一个持久化的发布-订阅通道(如 Redis Streams),任何 worker 接管运行后都能继续向客户端推送。

这个模式适合所有代理吗?

秒级响应的简单代理用长驻进程即可。一旦代理开始等待构建、请求人工审批或跨小时运行,就值得迁移。迁移可以增量进行:先抽状态,再加 checkpoint,最后引入租约。