目录

Base Agent - API 形态实现

至此我们对 API 形态的 Base Agent 实现做一个总结。最后我们会讨论 Agent 的 Serverless 改造。

1. Agent 执行流程

API 形态:

  1. UI: 实现 doNext,负责驱动 AgentLoop 的执行,实现多步执行的语义,每一次 AgentLoop 发起一次对 API 的 Post 请求,并更新 UI 侧状态。
  2. API Server: 使用请求的 SessionID 重建 AgentLoop,执行一轮循环,并根据 AgentLoop 执行结果更新 Session

对比 TUI: TUI 初始化 AgentLoop,提供 doNext 方法驱动 AgentLoop 执行。

2. UI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
export const useAgent = (initialSessionId?: string) => {
  const messages = useAgentStore((s) => s.messages);
  const addMessages = useAgentStore((s) => s.addMessages);
  const setMessages = useAgentStore((s) => s.setMessages);

  const unprocessedToolCalls = useAgentStore((s) => s.unprocessedToolCalls);
  const setUnprocessedToolCalls = useAgentStore(
    (s) => s.setUnprocessedToolCalls,
  );

  const currentActor = useAgentStore((s) => s.currentActor);
  const setCurrentActor = useAgentStore((s) => s.setCurrentActor);

  const copilotRequests = useAgentStore((s) => s.copilotRequests);
  const setCopilotRequests = useAgentStore((s) => s.setCopilotRequests);

  const doNext = async (
    params:
      | { type: "userInput"; input: string }
      | { type: "copilot"; responses: CopilotResponse[] },
  ) => {
    setCurrentActor("agent");

    let round = 1;

    while (runningRef.current) {
      try {
        const body = {
          sessionId: sessionIdRef.current,
        };
        if (round === 1) {
          Object.assign(body, {
            userInput: params.type === "userInput" ? params.input : undefined,
            copilotResponses:
              params.type === "copilot" ? params.responses : undefined,
          });
        }

        const { sessionId, agentResponse } = await fetch("/api/next", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(body),
          signal: abortController.current?.signal,
        }).then(
          (res) =>
            res.json() as Promise<{
              sessionId: string;
              agentResponse: AgentResponse;
            }>,
        );

        round++;

        sessionIdRef.current = sessionId;
        // 根据响应维护 UI 状态

        if (
          agentResponse.copilotRequests &&
          agentResponse.copilotRequests.length > 0
        ) {
          setCopilotRequests(agentResponse.copilotRequests);
          break;
        } else {
          setCurrentActor(agentResponse.actor);

          addMessages(agentResponse.messages);

          setUnprocessedToolCalls(agentResponse.unprocessedToolCalls);

          if (agentResponse.actor === "user") {
            break;
          }
        }
      } catch (error) {
        const isAbortError =
          error instanceof Error && error.name === "AbortError";
        if (!isAbortError) {
          console.error("Error in agent loop:", error);
        }
        runningRef.current = false;
        setCurrentActor("user");
        break;
      }
    }
  };

2.1 Web UI 的 doNext 逻辑

同样维护 while 循环,循环会持续执行:只要当前 actor 仍然是 agent,就继续执行处理。

  • TUI 的 doNext:本地持有 agentLoop 对象,直接调用对象的 next() 函数。
  • Web UI 的 doNext:核心逻辑是发起一次 fetch 网络请求。

2.2 请求参数构造

  1. sessionId:Web UI 特有参数。后端接口会返回 sessionId,客户端保存该引用,每次请求都携带。
  2. 判断是否为本轮循环的第一轮(不等于对话的第一轮,每次触发对话都会开启一轮新循环):
    • 如果是本轮循环第一轮:把用户输入 userInput,或者用户的 copilotResponse 放进请求参数,对应三类请求里的 user input、copilot response 两种场景。
    • 如果不是本轮循环第一轮(客户端驱动多次调用,例如连续工具调用):不再携带 userInput / copilotResponse,避免服务端收到重复消息;请求只携带 sessionId,对应第三种 none 的场景。

2.3 处理接口返回结果

  1. 如果返回类型为 copilot(接口返回的是 copilot request):保存这份 copilot request,交由 useAgent hook 维护 UI 状态。
  2. 如果是普通正常响应:维护当前 actor 消息、待处理 tool call。

2.4 消息存入 store

  • TUI:每次调用 getMessage 获取全量消息,使用 setMessage 整体覆盖更新 store。
  • Web UI:接口返回增量消息,调用 addMessage 追加写入 store。

2.5 循环退出条件

  • actor 变为 user,跳出循环。
  • 收到 copilot 请求,跳出循环。

2.6 核心差异总结

  • TUI 的 doNext:本地函数调用完成业务逻辑。
  • Web UI 的 doNext:通过网络API请求完成业务逻辑,配套做了参数、消息更新逻辑改造。

3. API Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

export async function POST(req: Request) {
  try {
    const {
      copilotResponses,
      sessionId,
      userInput,
    }: {
      sessionId?: string;
      userInput?: string;
      copilotResponses?: CopilotResponse[];
    } = await req.json();

    const { env } = getCloudflareContext();
    const storage = new D1SessionStorage(env.DB);
    const sessionManager = new SessionManager(storage);

    // 1. 从请求的 sessionId 查询上一次的对话消息
    const session = sessionId
      ? await sessionManager.getSession(sessionId)
      : await sessionManager.createSession();

    if (!session) {
      return new Response(`Session "${sessionId}" not found`, { status: 404 });
    }
    // 2. 重建 AgentLoop 状态
    const agentLoop = new AgentLoop(
      {
        abortSignal: req.signal,
      },
      session.messages,
    );

    if (userInput) {
      const userMessages: UserModelMessage[] = [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: userInput,
            },
          ],
        },
      ];
    //   维护 Session 状态
      // 1. 数据库的持久化操作
      await sessionManager.addMessages(session.id, userMessages);
      // 2. 内存里的状态更新。
      // 所以虽然有两个 message 的存储过程,但是因为不是两个持久化的需求,所以就不要处理事务的一致性的问题了
      await agentLoop.userInput(userMessages);
    }

    if (copilotResponses) {
      await sessionManager.addCopilotResponses(session.id, copilotResponses);
      await agentLoop.addCopilotResponses(copilotResponses);
    }

    const res = await agentLoop.next();

    await sessionManager.addMessages(session.id, res.messages);

    return new Response(
      JSON.stringify({
        sessionId: session.id,
        agentResponse: res,
      }),
      { status: 200 },
    );
  } catch (error) {
    return new Response(
      JSON.stringify({
        error: error instanceof Error ? error.message : "Unknown error",
      }),
      { status: 500 },
    );
  }
}

4. Agent Serverless 改造

我们通过架构调整,让 Web UI + API Server 形态的 Agent 支持完整的 Serverless 部署。总体来看,Agent 类应用非常适配 Serverless 基础设施,核心就是利用 Serverless 的核心特性:无流量缩容到 0、有流量自动弹性扩容,以此实现更低成本、更高弹性的运行模式。

通过将 Agent 依赖的能力进行拆分,可以让 Agent 本体做到纯无状态计算

4.1 模型能力解耦

模型服务天然就是独立部署的,Agent 通过 API 远程调用模型。 模型算力、推理扩容、资源开销完全与 Agent 运行时解耦,不占用 Agent 实例生命周期。

4.2 会话存储解耦

Web/API 形态的 Agent 需要持久化多轮对话、会话状态。 我们将 Session 存储能力从 Agent 业务逻辑中完全抽离,独立部署、独立适配存储后端。

为配合无状态 Agent 计算层,需要对 Session 层做了可插拔存储改造。目标是让 Session 存储后端可灵活替换、多场景适配

实现也并不复杂,我们只要定义好 Session 的接口,提供不同的具体实现即可。

最终实现:计算层完全无状态,状态层可插拔、可扩展、多环境自适应

4.3 核心计算无状态化

剥离模型调用、持久化存储之后,Agent 仅剩纯计算逻辑:

  • Agent Loop 单次步骤执行逻辑
  • 消息处理、解析、流转

这部分完全无状态,天然支持横向扩容,满足 Serverless 运行要求。

4.4 核心难点:执行时长限制

所有主流 Serverless 平台(AWS Lambda、Cloudflare Workers 等)都有单次执行时长上限,原因是: Serverless 的低成本、缩容到 0 的能力,依赖「每一次请求都是短生命周期」。因为如果服务长期存在,就会变成一个常驻副本。对于云平台就没有成本优势了。

多数开源 Agent 框架采用一次请求跑完完整任务用户输入 → 多轮循环、工具调用、推理 → 最终出结果

问题非常明显:简单 RAG 任务尚可,但复杂任务(Deep Research、多步骤复杂工作流)可能运行数十分钟,完全无法适配 Serverless 超时限制。

4.5 Agent Loop 单步执行模式

我们改造了 Agent 的运行模型:Agent 的每一步 Loop 执行,对应一次独立的 Serverless 请求。

特点:

  1. 单次执行粒度极小,不会触发平台超时限制
  2. 执行完即销毁,无需常驻进程等待处理、或者维护内存里的状态
  3. 所有状态外置 Session 存储,永不丢失
  4. 客户端可基于上一步状态,随时发起,让计算的负载继续运行

通过这种分步执行的设计,长耗时 Agent 任务可以被拆分为大量短耗时请求,完美适配 Serverless 运行模型。

4.6 Session 的抽象设计

不希望框架强绑定某一种存储实现(文件、Postgres、Cloudflare D1/R2/KV等),要同时兼容 API‑Server、本地 CLI 等多种客户端形态,因此做存储层抽象改造。

核心目标

  1. 存储后端可插拔替换:同一套上层 Agent 业务逻辑,可以切换不同存储实现(D1、R2、KV、文件、Postgres)。
  2. 多客户端适配:API Server 使用数据库,本地 CLI 可以直接使用文件存储。
  3. Agent 业务语义底层存储实现 解耦,上层业务不感知底层是数据库/对象存储/文件。

将存储分为三层分层:

1
2
3
4
5
6
7
SessionManager (Agent业务语义层:addMessage/addCopilotMessage)
        ↓ 调用
SessionStorageInterface (中立CRUD:create/get/update/list/delete)
        ↓ 实现
D1SessionStorage / FileSessionStorage / KVSessionStorage …
        ↓ 使用 Drizzle ORM(数据库场景)
底层存储:Cloudflare D1 / 文件 / KV / R2 / Postgres

两个抽象的分工:

  1. SessionStorageInterface:解决「Agent业务 ↔ 存储」的语义鸿沟,面向所有存储类型(数据库、对象存储、文件)。
  2. Drizzle ORM:只解决「不同数据库之间」的语法、驱动差异。

1. Session 数据模型

扩展 Session 类型,除 id、messages、copilot messages 之外,新增创建时间、最后更新时间等元数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export type Session = {
  id: string;
  messages: Context["messages"];
  copilotResponses: Context["copilotResponses"];
  createdAt: Date;
  updatedAt: Date;
};

export type SessionListItem = {
  id: string;
  messageCount: number;
  copilotResponseCount: number;
  createdAt: Date;
  updatedAt: Date;
};

2. SessionStorage Interface

隔离 Agent 业务语义与底层存储能力,定义一套与业务无关的通用 CRUD 接口:create / get / update / list / delete

  • 上层 SessionManager:承载 Agent 业务语义,对外提供 createSessiongetSessionaddMessageaddCopilotMessage 这类面向 Agent 的业务方法。
    • 内部并不直接操作数据库/文件,而是调用下层 SessionStorageInterface 的通用 CRUD。
    • 完成业务校验、消息拼接、元数据更新等逻辑。
  • 下层各类存储驱动实现:实现 SessionStorageInterface,把通用 CRUD 翻译成对应存储的原生操作(SQL、文件读写、对象存储API)。

关键点:数据库/文件本身没有 addCopilotMessage 这种 Agent 业务接口;通过中间这层接口做语义转译,上层业务不需要感知底层存储差异。

3. 数据库差异化抹平:Drizzle ORM

即使同为数据库,SQLite(D1)、Postgres、MySQL 之间依然存在语法、驱动差异,引入 Drizzle ORM 进一步屏蔽数据库差异:

  1. 定义统一 Table Schema,D1 基于 SQLite,切换到 Postgres 时代码改动很小。
  2. Schema 变更可通过 Drizzle CLI 自动生成 migration 迁移 SQL 文件,管理表结构迭代。