# Agent Loop 不等于上线：Run API 与「两本账、八张表」

> 作者：晨旭｜发布：2026-07-27｜系列：动手落地AI
> 来源：https://chenxu.xin/writing/agent-harness-run-api

本地跑通 Agent Loop 后用 FastAPI 包一层就能上线？拆解 Harness 工程的三个目标与七个模块，重点讲 Run API 的资源建模和数据库的两本账、八张表。

---

在前面的学习中，我们已经拆过 LLM、工具调用，也理解了 Agent Loop：

> 用户提出目标 → 模型思考 → 调用工具 → 拿到结果 → 再思考，直到完成任务。

当这个循环在本地顺利跑通时，很容易产生一种错觉：Agent 已经做好了，接下来只要用 FastAPI 包一层，就可以上线了。

在我深入学习 Harness 工程以及接触线上项目之前，我也是这么认为的。但实际上远非如此简单。

想象一个场景：如果你的 Agent 是一个负责处理客服工单的 AI 员工，它的任务链可能长达几分钟甚至几个小时。在这个过程中：

- 如果 HTTP 请求超时了怎么办？
- 如果 worker 崩溃了，重启后它是从头再跑一遍，还是能从断点恢复？
- 如果用户网络卡顿，连续点了三次退款按钮，你的系统是不是会给用户退三次钱呢？

面对这些关乎生产安全的拷问，纯粹的 LLM 和 Agent Loop 是无法回答的。这时我们就需要一套「马具」，也就是我们熟知的 **Harness Engineering**。

除去 LLM 本身和核心的 Agent Loop（prompt、模型调用策略等），剩下的所有为了让 Agent 在真实业务环境中稳定运行的后端服务工程，就是这篇文章以及之后几篇文章要深度拆解的内容。本文分为三个部分：

- Agent Loop 之外的 Harness Engineering
- Run API 设计：请求归请求，执行归执行
- 数据库设计：两本账与八张表

（文章中所有图片归深圳途明智启科技有限公司版权所有。）

## Harness 工程：三个目标与七个核心模块

做一个拥有真实用户的 AI 应用，最基本的要求就是服务要稳定。但这在工程上是很模糊的表述，具体要怎么实现呢？

在 Harness 工程中，需要将其转化为可量化的 SLO（服务级目标），例如延迟必须小于多少、成功率大于多少、队列滞后小于多少秒。总体可以把整个 Harness 工程的设计围绕着三个核心目标和七个核心模块展开。

### 1、三个核心目标

**跑得稳（稳定与可控）**：管理一次不确定性、长生命周期、有副作用、可恢复的执行过程。任何一个节点崩溃，系统都能自我治愈并从断点恢复，同时绝对保证写操作（如退款）的安全，实现高并发与高可用。

**可监控（可追溯与审计）**：AI Native 时代，API 调用皆是成本。系统必须精确监控 Token 消耗、工具调用耗时，并保证每一步执行都落库可查，提供清晰的事件流（SSE）追踪和事后审计能力。

**可进化（Self-Evolve）**：系统运行时留下的完整 Trace 数据，是数据飞轮的重要来源。基于这些真实的运行轨迹，通过数据清洗、评估和人工标注，形成闭环的微调和强化学习，驱动模型与系统的自我迭代。

### 2、七个核心模块

为了支撑上述目标，可以把这一整套后端服务拆解为七大模块：

1. **Run API**：请求入口与资源建模
2. **状态与事件储存（Database）**：两本账与八张表，系统的唯一事实源
3. **队列与 Worker**：长任务的异步派发与并发控制
4. **状态与状态机转移**：严格约束任务的生命周期演进
5. **Agent Loop 运行时**：真实的模型与工具交互逻辑
6. **工具运行时与安全**：带副作用工具的鉴权、重试与防重控制
7. **实时进度面板（SSE 事件流）**：给前端用户的实时反馈



这七个模块看起来很多，但把图从左向右看就会发现：一切都从用户发起请求开始。

那么，用户点击「让 AI 处理」后，后端究竟应该创建什么？

答案不是一条必须保持连接的 HTTP 请求，而是一项拥有独立身份、能够长期存在的工作：**Run**。

那一次点击是如何进入整套 Harness 系统？它为什么不能只是一条 HTTP 请求？当 HTTP 已经结束，Run 又依靠什么继续活着？

顺着这三个问题，就来到了本文的重点。

## Run API 设计：请求归请求，执行归执行

在做 demo 阶段，我们通常让前端发请求，后端阻塞等待，直到 Agent 跑完整个流程再返回结果。但是这种做法在真实业务中是完全不可行的。

一个真实 Agent 任务（例如处理工单）涉及多次模型调用、工具查询、甚至等待人工确认，耗时可能长达数分钟。同步等待会导致网关超时，且一旦网络断开，执行进度就立刻消失了；更甚者，前端报错重试，后端其实已经完成了具有副作用的操作（如退款），最终造成了生产事故：双重退款。

因此，Run API 的设计哲学是：**请求归请求，执行归执行**。我们将一次大模型的执行，建模为一个长期存在的系统资源——Run（用 `run_id` 来追踪）。

### 1、餐厅点餐模型：API 与 Worker 的异步协作

这里可以用「餐厅点餐」来类比这个解耦的过程：

**前台收单（Run API）**：API 的作用不是去把任务跑完，而是建立契约。当前端发起任务，API 层进行身份校验、参数校验，然后在数据库中登记一次「排队中」的工单事务，并立刻（毫秒级）给前端返回一个取餐号（`run_id`）。

**后厨排队制作（Queue & Worker）**：后台的 Worker 就像厨师，他们从队列里接单（拿取 `run_id` 对应的任务），一步步执行大模型的思考与工具调用。

**随时看进度（SSE 订阅）**：前端拿着取餐号，可以随时查看到底是「已下锅」还是「装盘中」，甚至可以主动退单（Cancel）。

Run API 的职责不是把任务当场就做完，而是创建一份可被长期管理的工作，并把控制权交还给前端。

### 2、支撑解耦的六个核心 API 入口

围绕 Run 这个资源，后端需要提供 6 个标准的接口：

1. **创建 Run（POST /runs）**：毫秒级响应，创建执行任务并立即返回 `run_id`，状态标记为 queued。
2. **查询当前状态（GET /runs/{id}）**：用户刷新页面后，通过 `run_id` 获取任务当前是 running 还是 succeeded。
3. **订阅实时事件（GET /runs/{id}/events）**：建立 SSE 长连接，后端向前端推送每一步的微观进度（如 `model.started`、`tool.completed`），实现良好的可观察性。
4. **取消请求（POST /runs/{id}/cancel）**：允许用户中止卡住的或错误的执行。需要注意的是，如果是运行中的取消，API 层只做「标记请求」，真正的停止由 Worker 在安全点完成，避免在写入数据库中途强行阻断。
5. **恢复执行（POST /runs/{id}/resume）**：针对需要人工干预（比如 waiting for user）的场景。例如大额退款挂起，等待主管审核通过后，调用此接口让 Worker 从 Checkpoint 断点续跑。
6. **完整轨迹追踪（GET /runs/{id}/trace）**：事后复盘与审计，拉取整个任务周期的模型交互、工具调用明细。

这六个接口合在一起，建立了一份长期任务契约：POST 赋予它身份，GET、events 和 trace 负责观察，cancel 与 resume 负责控制。

### 3、生产级 API 的硬性要求：幂等与多租户隔离

在实现这六个接口时，有两个重要的约束。

**其一是多租户隔离**：在所有的读写操作中，必须绑定 `org_id`。查询不仅要在业务逻辑里判断，还要下沉到 SQL 语句中（`WHERE id=:run_id AND org_id=:org_id`），防止跨用户的数据越权。

**其二是幂等设计**：这个是解决「网络抖动导致重复退款」的唯一解法。

每次创建 Run 时，前端必须生成并携带一个基于业务实体的唯一键（如 `ticket:T-9527:handle:v1`）。在数据库层面，对 `org_id` 和 `idempotency_key` 建立唯一联合约束：

- 当相同的幂等键、相同的 Input 再次请求时，后端不报错，也不新建任务，而是直接返回第一次成功创建的 HTTP 200 及原本的 `run_id`
- 如果相同的幂等键却传入了不同的 Input，后端应立刻拦截并返回 HTTP 409（IDEMPOTENCY_CONFLICT）

**重复的请求本身不是错误，是分布式系统的常态，但幂等机制是保障系统不发生不可逆副作用的底线。**

到这里，Run API 解决了两个问题：它让任务拥有稳定身份，也让前端获得了观察和控制任务的入口。但一个更根本的问题随之出现：

> `POST /runs` 已经返回，HTTP 连接已经结束，`run_id` 如何能在几分钟后被查询、在 Worker 崩溃后被恢复呢？

要解决这个问题，Run 就不能只活在某个 Web 进程或 Worker 的内存里，它必须被持久化到数据库中。

## 数据库设计：两本账与八张表

既然 Run 是一项长期存在的工作，数据库就不能只保存最终回答。

假设 messages 表中只有一句：「已为您退款 39.9 元。」我们依然不知道 Agent 先查了什么、调用了几次模型、退款工具是否真的成功、Worker 是否中途崩溃、消耗了多少 Token，也不知道是谁批准了这次敏感操作。

Chatbot 的 messages 主要记录「人和 AI 说了什么」；**Agent Harness 还必须记录「系统做了什么，以及它是如何做到的」。**

为了回答这些问题，数据库首先需要记录两类性质不同的事实，也就是「两本账」。

### 1、两本账：状态（现在在哪）vs 事件（怎么走到这里）

**第一本账：主状态（Status）**。它是覆盖式的，记录一次执行当下的最终形态。从 queued 变为 running，再变为 succeeded，数据库里始终只有一行记录在更新，回答的是「当前在哪一步」。

**第二本账：事件流（Events）**。它是追加式的，记录了执行过程中的一切变迁。`run.created`、`tool.started`、`model.completed`，这些事件像日志一样单调递增，绝不修改历史。它回答的是「我们是如何走到这一步的」，是前端断线补发和系统复盘的核心依据。

有了这两个记账原则，下一步才是决定：一次 Run 中的步骤、模型调用、工具副作用、恢复现场、成本和责任，分别应该记到哪里。

### 2、拆解八张表：工程化事实的立体切面

这八张表的设计，让每类数据只回答一个问题：

1. **`agent_runs`（主状态表）**：系统的绝对中心。保存 `run_id`、当前 status、多租户 `org_id`、幂等键以及 input/output。其他所有表都通过 `run_id` 外键挂载在它之下。
2. **`run_events`（事件日志表）**：不可变的追加式流水。核心字段是 `sequence`（单调递增序号）。当网络抖动前端断开，重连时只需带上 Last-Event-ID，后端即可通过 `sequence > N` 瞬间补发丢失的事件帧。
3. **`agent_steps`（步骤表）**：将漫长的执行划分为可观测的阶段（如查询订单、决策退款、撰写回复）。相比于密集的 Event，Step 提供了更清晰的宏观执行视角。
4. **`model_calls`（模型调用表）**：每一次对 LLM 的请求都独立落库。不仅记录完整的 prompt 和 completion 上下文，更重要的是记录 `prompt_tokens`、`completion_tokens` 以及 `latency_ms`，为延迟分析和模型评估提供原始数据。
5. **`tool_calls`（工具调用表）**：管理副作用的最关键表格。记录参数 `args_json` 与返回结果 `result_json`。有副作用的工具（如退款 API）在此表必须记录 `idempotency_key`，以保证下游外部系统不会因为 Worker 重试而发生重复扣款。
6. **`checkpoints`（恢复点表）**：崩溃自愈的存档点。在工具调用完成或进入挂起状态时，将当前上下文写入 `state_json`。一旦当前 Worker 进程意外崩溃死亡，调度器会将任务派发给新 Worker，新 Worker 直接读取最近的 Checkpoint 即可进行断点续传，而不必从头重新请求大模型。
7. **`usage_records`（用量账单表）**：成本聚合视角。用于统计某个租户、某个时间段内总共消耗了多少 Token 与真金白银。
8. **`audit_logs`（审计日志表）**：安全合规的底线。并非所有事件都是审计日志，诸如 `refund_order` 等涉及资金、权限的高危操作，必须单独记入审计表，回答「谁在什么时间、触发了什么敏感动作」。



至此，同一个 `run_id` 终于把状态、轨迹、调用、恢复、成本与审计串成了一条完整证据链。

### 3、两本账必须从 Run 诞生时就保持一致

数据库设计的最后一个关键，不是多建一张表，而是保证相关事实不会出现「半成功」。

处理 `POST /runs` 时，系统至少要同时写入：

- `agent_runs` 中的 queued 主记录
- `run_events` 中的第一条 `run.created` 事件

这两个写入描述的是同一个业务事实，因此必须放在同一个事务中：要么一起提交，要么一起回滚。

所以，事务不是数据库章节末尾的附加知识，而是连接 Run API 和两本账的最后一环：`POST /runs` 只有在 Run 身份与第一条历史同时落库后，才算真正创建成功。

## 最后

现在再回头看整条链路：

- 用户发起请求后，Run API 没有让 HTTP 一直等待，而是创建一个拥有 `run_id` 的长期资源
- 数据库用状态账回答「现在在哪」，用事件账保存「如何走到这里」
- 八张表再把步骤、模型、工具、恢复、成本与审计补充完整

它们共同完成了一次关键转变：**后端不再只响应一次请求，而是在持续管理一份可查询、可取消、可恢复、可观察的工作。**

当然，Run 现在只是被创建并可靠地保存了，它仍然停留在 queued 状态。接下来，是谁把任务投递出去？Worker 如何安全领取并推进状态机？投递失败后又怎样重试和兜底？

下一篇文章，我会继续沿着同一条 Run 的生命线，拆解异步队列、Dispatcher 调度以及 Worker 运行时。

## 核心结论

- Run API 的设计哲学是「请求归请求，执行归执行」。API 的作用不是把任务跑完，而是建立契约：校验后在数据库登记一张工单，毫秒级返回 run_id，把控制权交还前端。同步等待在真实业务中完全不可行。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/agent-harness-run-api#request-and-execution-must-decouple
- 幂等设计是「网络抖动导致重复退款」的唯一解法。对 org_id 和 idempotency_key 建唯一联合约束，相同键加相同 Input 直接返回原 run_id，相同键但不同 Input 返回 409。重复请求本身不是错误，是分布式系统的常态。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/agent-harness-run-api#idempotency-is-the-only-answer-to-double-refund
- 数据库要记两类性质不同的事实。主状态是覆盖式的，始终只有一行在更新，回答「当前在哪一步」；事件流是追加式的，单调递增绝不修改历史，回答「我们是如何走到这一步的」，也是断线补发和事后复盘的依据。（把握较大）
  永久链接：https://chenxu.xin/writing/agent-harness-run-api#two-ledgers-status-and-events
- run_events 的 sequence 单调递增序号是断线重连的关键。前端重连时只需带上 Last-Event-ID，后端通过 sequence > N 即可瞬间补发丢失的事件帧。（把握较大）
  永久链接：https://chenxu.xin/writing/agent-harness-run-api#sequence-enables-reconnect-replay
- Chatbot 的 messages 记录「人和 AI 说了什么」，Agent Harness 还必须记录「系统做了什么，以及它是如何做到的」。只存一句「已为您退款 39.9 元」，你不知道它查了什么、调了几次模型、退款是否真的成功、消耗了多少 Token、谁批准了这次操作。（判断 · 把握较大）
  永久链接：https://chenxu.xin/writing/agent-harness-run-api#agent-db-records-how-not-just-what

---

本文出自晨光里的AI（https://chenxu.xin），作者晨旭。
引用时请保留来源链接。文中标注「判断」「假设」的部分是作者的个人看法，不是事实。
