Agent Loop 不等于上线:Run API 与「两本账、八张表」
本地跑通 Agent Loop 后用 FastAPI 包一层就能上线?拆解 Harness 工程的三个目标与七个模块,重点讲 Run API 的资源建模和数据库的两本账、八张表。
TL;DR
Agent Loop 在本地跑通时,很容易产生一种错觉:接下来只要用 FastAPI 包一层就可以上线了。 但真实场景会立刻拷问你——HTTP 超时怎么办?worker 崩溃后是从头再跑还是断点恢复?用户连点三次退款按钮会不会真退三次? 这篇沿着一次点击进入系统的路径,拆 Harness 工程的前两个模块: Run API 的设计哲学是「请求归请求,执行归执行」,把一次执行建模为拥有 run_id 的长期资源; 数据库则要记两类性质不同的事实——覆盖式的主状态回答「现在在哪」,追加式的事件流回答「如何走到这里」, 再由八张表把步骤、模型、工具、恢复、成本与审计串成一条完整证据链。
目录
在前面的学习中,我们已经拆过 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、七个核心模块
为了支撑上述目标,可以把这一整套后端服务拆解为七大模块:
- Run API:请求入口与资源建模
- 状态与事件储存(Database):两本账与八张表,系统的唯一事实源
- 队列与 Worker:长任务的异步派发与并发控制
- 状态与状态机转移:严格约束任务的生命周期演进
- Agent Loop 运行时:真实的模型与工具交互逻辑
- 工具运行时与安全:带副作用工具的鉴权、重试与防重控制
- 实时进度面板(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 个标准的接口:
- 创建 Run(POST /runs):毫秒级响应,创建执行任务并立即返回
run_id,状态标记为 queued。 - 查询当前状态(GET /runs/{id}):用户刷新页面后,通过
run_id获取任务当前是 running 还是 succeeded。 - 订阅实时事件(GET /runs/{id}/events):建立 SSE 长连接,后端向前端推送每一步的微观进度(如
model.started、tool.completed),实现良好的可观察性。 - 取消请求(POST /runs/{id}/cancel):允许用户中止卡住的或错误的执行。需要注意的是,如果是运行中的取消,API 层只做「标记请求」,真正的停止由 Worker 在安全点完成,避免在写入数据库中途强行阻断。
- 恢复执行(POST /runs/{id}/resume):针对需要人工干预(比如 waiting for user)的场景。例如大额退款挂起,等待主管审核通过后,调用此接口让 Worker 从 Checkpoint 断点续跑。
- 完整轨迹追踪(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、拆解八张表:工程化事实的立体切面
这八张表的设计,让每类数据只回答一个问题:
agent_runs(主状态表):系统的绝对中心。保存run_id、当前 status、多租户org_id、幂等键以及 input/output。其他所有表都通过run_id外键挂载在它之下。run_events(事件日志表):不可变的追加式流水。核心字段是sequence(单调递增序号)。当网络抖动前端断开,重连时只需带上 Last-Event-ID,后端即可通过sequence > N瞬间补发丢失的事件帧。agent_steps(步骤表):将漫长的执行划分为可观测的阶段(如查询订单、决策退款、撰写回复)。相比于密集的 Event,Step 提供了更清晰的宏观执行视角。model_calls(模型调用表):每一次对 LLM 的请求都独立落库。不仅记录完整的 prompt 和 completion 上下文,更重要的是记录prompt_tokens、completion_tokens以及latency_ms,为延迟分析和模型评估提供原始数据。tool_calls(工具调用表):管理副作用的最关键表格。记录参数args_json与返回结果result_json。有副作用的工具(如退款 API)在此表必须记录idempotency_key,以保证下游外部系统不会因为 Worker 重试而发生重复扣款。checkpoints(恢复点表):崩溃自愈的存档点。在工具调用完成或进入挂起状态时,将当前上下文写入state_json。一旦当前 Worker 进程意外崩溃死亡,调度器会将任务派发给新 Worker,新 Worker 直接读取最近的 Checkpoint 即可进行断点续传,而不必从头重新请求大模型。usage_records(用量账单表):成本聚合视角。用于统计某个租户、某个时间段内总共消耗了多少 Token 与真金白银。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,把控制权交还前端。同步等待在真实业务中完全不可行。#
判断 · 把握较大
幂等设计是「网络抖动导致重复退款」的唯一解法。对 org_id 和 idempotency_key 建唯一联合约束,相同键加相同 Input 直接返回原 run_id,相同键但不同 Input 返回 409。重复请求本身不是错误,是分布式系统的常态。#
判断 · 把握较大
数据库要记两类性质不同的事实。主状态是覆盖式的,始终只有一行在更新,回答「当前在哪一步」;事件流是追加式的,单调递增绝不修改历史,回答「我们是如何走到这一步的」,也是断线补发和事后复盘的依据。#
把握较大
run_events 的 sequence 单调递增序号是断线重连的关键。前端重连时只需带上 Last-Event-ID,后端通过 sequence > N 即可瞬间补发丢失的事件帧。#
把握较大
Chatbot 的 messages 记录「人和 AI 说了什么」,Agent Harness 还必须记录「系统做了什么,以及它是如何做到的」。只存一句「已为您退款 39.9 元」,你不知道它查了什么、调了几次模型、退款是否真的成功、消耗了多少 Token、谁批准了这次操作。#
判断 · 把握较大
引用本文
晨旭,《Agent Loop 不等于上线:Run API 与「两本账、八张表」》,晨光里的AI,2026-07-27
[晨旭:《Agent Loop 不等于上线:Run API 与「两本账、八张表」》](https://chenxu.xin/writing/agent-harness-run-api)带进你的 AI 继续追问
这篇文章有一份干净的 Markdown 原文,可以直接交给任何模型读,不用复制粘贴。