浅谈Agent系统设计——中间件
围绕一个自动修复代码的 Agent 系统,介绍消息队列、工作流、数据库、缓存、模型网关、检索、存储、权限和可观测性工具的用途与取舍。
Contents目录
上一篇主要讨论了如何把 Agent 接入一个实际系统:用 Worker 消费任务,处理超时和重试,保存执行轨迹,以及校验最终输出。这一篇接着讨论:这些工作可以交给哪些现成的工具?
这里把“中间件”理解得宽一点,包括消息队列、数据库、工作流服务、模型网关等。它们负责排队、保存、恢复、检索和检查权限,让 Agent 能在一个实际系统里持续工作。模型能力相同的情况下,这些地方往往决定了系统是否好用、出了问题是否好查。
本文介绍的是工具的主要能力和选型思路,不追求把所有产品都列出来。关于未来影响的讨论,是基于任务变长、用户变多、模型变多等情况作出的判断,并不代表这些产品一定会朝某个方向发展。
先看一个具体场景
假设我们在做一个自动修复代码的系统。用户提交一个 Bug,系统准备代码仓库和相关文档,Agent 分析问题、修改代码、运行测试,最后生成补丁或 PR,也就是提交给别人审核的代码改动。
一次任务可能需要十几分钟;中途可能断网、模型超时,或者需要等人审核。用户还可能同时提交很多任务。如果只是启动一个进程,然后一直等它结束,后面要补的东西会越来越多。
可以先用下面这张表理解各类工具负责什么:
| 系统遇到的问题 | 对应的中间件 | 本文选择的工具 |
|---|---|---|
| 任务太多,怎样排队并分给 Worker? | 消息队列/后台任务/事件流 | RabbitMQ、BullMQ、Kafka |
| 一件事有很多步骤,中断后怎样接着做? | 持久化工作流 | Temporal |
| 任务做到哪了,重启后还记得吗? | 状态数据库 | PostgreSQL |
| 重复查询太慢,多个 Worker 怎样共享限额? | 缓存与快速状态 | Redis |
| 多个模型接口不同,怎样统一调用和管理费用? | 模型网关 | LiteLLM |
| Agent 怎样找到相关文档、代码说明和历史经验? | 检索基础设施 | pgvector、Qdrant、Elasticsearch |
| 补丁、测试报告和大文件保存在哪里? | 对象存储 | Amazon S3、Cloudflare R2 |
| 谁能发起任务,Agent 能替他做哪些操作? | 身份与策略管理 | Keycloak、OPA |
| 为什么慢、为什么失败、为什么花了这么多钱? | 可观测性 | OpenTelemetry、Langfuse |
这些工具不需要一次全部部署。选型时更有用的问题是:现在最费力、最容易出错的工作是什么,哪个工具能把这部分接过去?
消息队列/后台任务/事件流:RabbitMQ、BullMQ 和 Kafka
上一篇把系统抽象成生产者和消费者。消息队列就放在两者之间:生产者不断提交任务,Worker 有空时再领取任务。例如同时来了 100 个修复请求,而系统只能运行 10 个 Agent,剩下的请求就先排队。
这样可以让提交任务的服务尽快返回,也能限制同时运行的 Agent 数量。但队列只是让任务有地方等待,长期处理不过来时,仍然要扩容、限制新任务,或者告诉用户预计等待多久。
RabbitMQ:把待办任务交给 Worker
RabbitMQ 很适合“这里有一批事情,请找空闲 Worker 去做”的场景。在代码修复系统里,消息可以只包含任务编号、仓库版本和执行配置的位置。Worker 收到消息后,再去数据库和文件存储里取得详细内容。
它的优势是任务交接机制比较直接。配合合适的持久化配置、生产者确认和消费者手动确认,系统可以确认任务已经交到队列,以及 Worker 已经处理完成。Worker 断开连接时,尚未确认的消息可以重新交给其他 Worker。具体机制见 RabbitMQ 的确认与重投递文档。
代价是应用仍然要处理重复执行。例如 PR 已经创建成功,Worker 却在确认消息之前崩溃,任务就可能再次出现。这里需要记录执行归属和外部操作结果,重试时核对已有 PR,或者使用外部接口提供的防重复机制。一直失败的任务也不能无限重试,应设置次数上限,最后放到单独的失败队列里排查。
长任务还有一个容易忽略的问题:RabbitMQ 对消费者确认存在超时要求,要按实际任务时长设计交接方式。队列重新投递任务,也不代表原来的 Agent 已经停止,Worker 还要负责取消执行或隔离旧的执行结果。消费者文档说明了确认超时的行为。
如果系统主要就是提交任务、运行 Agent、收集结果,而且已有 RabbitMQ 或需要在不同语言的服务之间传递消息,我会倾向于选 RabbitMQ。以后 Worker 越来越多,或者不同任务需要不同机器,可以拆分队列来分配资源;如果流程开始包含很多步骤和长时间等待,就需要进一步考虑工作流工具。
BullMQ:方便接入应用的后台任务队列
BullMQ 是一套后台任务库,尤其适合使用 Node.js、TypeScript 的应用。下面以它常见的 Redis 方案为例:应用把修复请求放进 Redis,BullMQ 的 Worker 领取任务,运行 Agent,然后登记成功或失败。它把很多任务管理工作包装成可以直接在代码里调用的接口。BullMQ 官方介绍说明了这套工作方式。
它可以控制同时运行多少任务、延迟多久再执行,以及失败后重试几次、每次间隔多久。例如模型暂时不可用时,先等一会儿再尝试,而不是马上不断重发。任务完成后的结果、失败原因和处理进度,也可以通过接口记录和查询。重试文档介绍了重试次数与等待间隔的配置。
它的优势是接入成本比较低。如果项目已经有 Node.js 服务和 Redis,就可以较快地补上后台执行、重试和进度管理。BullMQ 的 Flow 还支持任务依赖:比如先让几个子任务分别检查代码、文档和测试,等它们完成后,再运行汇总任务。Flow 文档介绍了这种父子任务关系。
代价是队列可靠性取决于任务用 Redis 的配置。任务数据需要合适的持久化和备份,内存不足时也不能像普通缓存一样随便淘汰任务。最好与可随时清空的缓存分开管理;生产部署文档说明了这些要求。
长任务还需要注意 Worker 是否能持续告诉队列“我还在处理”。如果 Node.js Worker 被同步计算长时间堵住,来不及续上任务锁,队列可能认为它已经停止,把任务重新分配。因此重计算或测试进程应采用合适的隔离与异步执行方式;外部操作同样需要防止重复,任务重新分配也不能代替取消原来的 Agent。卡住任务的处理文档解释了这一点。
另外,BullMQ 限制任务领取的速度,不等于限制每一次模型调用的速度。一个 Agent 任务可能连续请求模型几十次,模型侧的限额仍然要由 Worker 或模型网关另外处理。它也不会自动恢复某个任务函数内部的执行位置,会话和文件检查点仍然需要应用保存。
如果团队主要使用 Node.js,任务以独立后台作业为主,我会优先考虑 BullMQ。RabbitMQ 更适合服务之间的消息交接,BullMQ 则更直接地提供应用需要的任务管理接口。以后系统从单个 Agent 扩展到几个相互依赖的任务,Flow 可以减少自己编写调度逻辑的工作;再发展到跨天等待、人工审批和复杂恢复时,就需要比较 Temporal,或者明确评估自己补充这些逻辑的成本。
Kafka:让多个服务读取同一批事件
Kafka 更适合记录“系统里发生了什么”。例如任务已创建、Agent 已开始、模型请求已完成、测试已失败、任务已结束。这些事件可以分别交给统计服务、费用服务和评估服务处理。Kafka 官方介绍把事件的写入、保存和后续处理作为它的核心能力。
它的优势是同一批记录可以被不同服务分别读取,并在保留期限内重新读取。例如后来增加一个费用分析服务,就可以利用已有的模型调用事件补做统计。相比把很多后续处理都塞进 Worker,事件流能让这些服务各自演进。
代价是要管理事件的保留时间、存储空间、消费进度和重复处理。Kafka 为了并行处理,会把记录分成多份;使用传统消费者组时,还需要理解这些分组与消费进度的关系。它当然也能分发任务,但十分钟的 Agent 任务是否可靠,不会仅仅因为换成 Kafka 就得到保证。事件回放也不应该直接变成再次创建 PR,需要区分统计、评估和真正的业务执行。
如果已经有多个服务需要独立处理同一批事件,或者团队本来就在维护 Kafka,选择它比较自然。以后 Agent 的运行数据可能被用于费用分析、离线评估和问题复盘,Kafka 的事件保存与回放会更有价值;只有几个 Worker 的系统,则未必值得承担这部分维护成本。
三者怎么选?
| 比较点 | RabbitMQ 的普通任务队列 | BullMQ 的后台任务 | Kafka 常见的事件流用法 |
|---|---|---|---|
| 主要关心什么 | 哪个 Worker 来完成这件事 | 怎样在应用里管理排队、进度和重试 | 哪些服务需要知道这件事发生了 |
| 处理后的记录 | 消息确认后通常从队列移除 | 已完成或失败的任务按配置保留或清理 | 按保留策略管理,可以再次读取 |
| 本文场景里的用途 | 分发代码修复任务 | 执行修复任务,并安排延迟、重试和任务依赖 | 分发任务进展、模型调用和测试结果事件 |
| 主要要补的应用逻辑 | 防重复执行、重试上限、任务取消 | 防重复执行、会话恢复、复杂流程状态 | 消费进度、重复处理、回放时的行为 |
这个区别说的是常见用法。RabbitMQ 的 Streams 也支持保留和回放记录,Kafka 的 Share Groups 则提供多个消费者分担记录、逐条确认等能力。以后两者的能力可能继续靠近,选型时更应该看实际工作方式、现有设施和团队经验。
持久化工作流:Temporal
当任务只有“启动 Agent,等待结果”时,队列通常够用。但如果流程变成下面这样,单靠队列就开始吃力了:
- 下载代码,准备执行环境。
- 让 Agent 生成补丁。
- 运行测试;失败时把结果交回 Agent 修改。
- 等待人工审核,可能要等几个小时。
- 审核通过后提交改动,否则返回修改。
Temporal 负责记住这个流程:哪些步骤已经完成,现在等待什么,哪一步可以重试,什么时候应该超时。它保存流程历史,让 Worker 重启后能够恢复流程进度。Temporal 的工作流文档介绍了这套执行方式。
它的优势是减少大量恢复逻辑。等待人工审核时,不需要让一个进程睡几个小时;机器重启后,也不必靠几张状态表猜测下一步该做什么。对于经常等待、重试、分支处理的任务,流程会更容易理解和维护。
代价是要学习它的编程约束,并维护服务,或者付费使用托管服务。Temporal 会根据历史记录重新运行流程控制代码来恢复状态,因此模型调用、读写文件和访问外部接口应放进独立的步骤函数,它把这些函数叫作 Activity。已经记录的步骤结果可以用于恢复流程,而不是每次恢复都重新问模型。工作流定义文档解释了这个约束。
不过,一个 Activity 内部运行了半小时,并不意味着崩溃后能自动从第 29 分钟接着做。系统仍然要保存会话、检查点和文件,恢复代码再利用这些内容。Activity 也可能被重试,创建 PR、发送消息等操作仍然需要防重复处理。Activity 文档明确区分了步骤重试和利用检查点继续执行。
我会在流程已经出现多个独立步骤、人工等待和复杂恢复逻辑时选 Temporal。消息队列主要解决任务交给谁,Temporal 进一步解决整件事接下来做什么。它自己有任务队列,因此已经由 Temporal 调度的步骤,不必再无理由地加一层 RabbitMQ。
如果以后 Agent 从几分钟的任务发展为跨天的工作,Temporal 这类工具的价值可能会上升:模型负责分析和决策,工作流负责把这些决策可靠地推进下去。它能减少重复劳动和进度丢失,但测试与审核仍然负责判断结果是否正确。
RabbitMQ、BullMQ 和 Temporal 怎么区分?
假设 Agent 已经生成补丁,现在要等人审核,第二天再运行测试。RabbitMQ 可以交付下一条任务消息,BullMQ 可以保存任务和依赖,但审核状态、何时推进下一步、怎样应对中断,仍然需要应用明确安排。Temporal 则把这种等待和步骤推进放进持久化流程里管理。
| 比较点 | RabbitMQ | BullMQ | Temporal |
|---|---|---|---|
| 主要提供什么 | 服务之间的消息交接 | 后台任务的排队、重试与依赖管理 | 多步骤流程的等待、推进与故障恢复 |
| 更适合的情况 | 多种服务通过消息协作,已有消息设施 | Node.js 应用需要方便地运行后台作业 | 长任务包含人工等待、分支和复杂恢复 |
| 中断后恢复什么 | 未确认的消息可以重新交付 | 未完成的任务可以重新安排,任务内部恢复由应用负责 | 根据已记录的流程历史与步骤结果恢复流程,步骤内部仍需检查点 |
| 主要维护成本 | 消息服务及应用的任务状态逻辑 | 队列存储、Worker 和需要自行补充的流程逻辑 | 工作流服务、步骤划分及流程代码升级规则 |
三者都可能重新执行某个操作,都需要处理重复创建 PR 等问题。选 BullMQ 不意味着必须再部署 RabbitMQ;选 Temporal 也不意味着前面两种队列都必须保留。先看系统需要的是消息交接、后台作业,还是可靠推进整段流程,再决定使用哪一个。
数据库:PostgreSQL 保存任务的正式状态
队列里的消息会被消费,Worker 的内存会随进程消失,因此任务需要一个长期记录。在这个场景里,PostgreSQL 可以保存任务属于谁、运行到哪一步、尝试了几次、使用哪个会话、生成了什么结果,以及谁批准了某次操作。
它的优势是能查询这些记录之间的关系,也能把相关更新放在同一次事务里。事务可以简单理解为“一组数据库修改要么一起成功,要么一起撤销”。例如把任务标记为完成,同时登记结果位置,可以一起提交,减少只更新一半的情况。PostgreSQL 的事务文档解释了这一点。
数据库还可以保存 Agent 的检查点。检查点就是某个阶段的状态快照,包含恢复时需要的信息。例如 LangGraph 提供 PostgreSQL 检查点后端,用于保存执行状态。LangGraph 的检查点文档列出了相关实现。但只保存一个会话编号,通常不足以恢复已经丢失的代码目录和执行环境。
它的代价是需要设计表结构、索引、备份和并发更新规则。例如两个 Worker 同时把任务从“待执行”改成“执行中”,不能只在各自内存里判断一次,而要让数据库检查并保证这次状态转换只有一个成功。完整对话、大日志和文件也不宜全部堆进任务表,否则日常状态查询会越来越重。
还有一个数据库和队列之间的缝隙:任务写入成功了,发送队列消息却失败了。常见做法是把任务和“待发送通知”一起写进同一次数据库事务,再由后台程序发送通知并记录进度。这样可以恢复漏发的通知;重复发送的情况仍然要处理。
我倾向于把 PostgreSQL 作为任务状态的主要依据,因为任务管理、权限查询和历史记录可以放在一起,后面也能按需增加 pgvector。已经有成熟 MySQL 设施的团队,也不必仅仅为了运行 Agent 更换数据库;这里选择 PostgreSQL,主要是看重它与本文其他工具的组合。
上一篇不建议自己用数据库拼消息队列,强调的是调度和重试逻辑的开发成本。用数据库保存任务状态很合理;对于小系统,采用成熟的数据库任务库也可能是合适的选择。需要避免的是低估了自己编写任务队列的工作量。
如果以后加入长期记忆、历史查询或任务复盘,PostgreSQL 中可查询的记录会更有用。但随着轨迹变大,也应逐步把大文件移到对象存储,让数据库主要保存状态、摘要和文件位置。
缓存:Redis 保存短期、频繁变化的数据
Redis 可以缓存重复的检索结果、文档摘要,保存短期执行状态,也可以帮助多个 Worker 共享请求限额。例如所有 Worker 每分钟一共最多发起多少次模型请求,不能让每个 Worker 各算各的。
它的优势是访问快,且提供计数和带过期时间的数据操作。限流时可以把“检查余额、扣减次数”放在一次原子操作或脚本中,避免两个 Worker 同时读到同一份余额后都认为自己可以继续。“原子”在这里就是这组操作不会被其他请求插进来打断。具体方案可以参考 Redis 的限流文档。
代价是缓存可能过期,也可能还没反映最新数据。例如仓库代码已经更新,缓存的检索结果却来自旧版本。因此缓存通常需要包含用户范围、仓库版本和查询条件,不能只拿问题文本当作缓存标识。否则既可能取到过时结果,也可能把一个用户的内容交给另一个用户。
Redis 支持持久化,但数据在故障时能保住多少,取决于具体配置。对于本文的分工,我会把可以重新计算的内容放在 Redis,把任务结果、审核记录等正式数据放在 PostgreSQL。关于快照和写入日志的取舍,见 Redis 持久化文档。
这里说的是 Redis 的缓存用途。如果用它保存 BullMQ 的队列任务,就要按任务存储来管理持久化、内存和清理规则,不能因为都是 Redis 就把它和可随时丢弃的缓存混在一起。
我会在重复查询已经影响速度,或者多个 Worker 需要共享计数时引入 Redis。如果只有一个 Worker,用进程内缓存也可能足够;如果只是偶尔查询一个任务状态,直接读 PostgreSQL 往往更简单。Redis 和 PostgreSQL 在这里分别承担快速访问和正式记录,两者可以配合使用。
以后多 Agent 同时工作时,共享限额和缓存可能更加有用。不过缓存的依赖也会变复杂:代码、文档、权限或模型配置变化,都可能让旧结果不再适用。届时要同时管理速度和结果的新鲜程度。
模型网关:LiteLLM 统一模型入口
当系统只调用一个模型时,直接使用它的 SDK 很方便。等到不同任务分别使用不同模型,多个团队又各有费用限额时,每个服务都自己接接口、保存密钥和处理切换,重复工作就会很多。
LiteLLM 的 Proxy Server 可以作为统一入口:Worker 向它发请求,由它按配置把请求发给相应的模型服务。它提供模型路由、失败后切换、请求限制和费用管理等能力。LiteLLM 网关文档和路由文档介绍了这些功能。
在代码修复系统里,可以让简单摘要使用便宜的模型,让复杂问题分析使用能力更强的模型,也可以限制某个团队能使用哪些模型。它的优势是模型接入和管理规则可以集中维护,Worker 不需要各自保存所有供应商的密钥。
代价是所有请求多经过一个服务,这个服务也要部署、监控和保证可用。更重要的是,接口统一不代表模型行为相同:工具调用、长上下文、输出格式和特有参数,都可能有差异。主模型失败后换一个模型,接口能成功返回,也不代表任务质量保持不变,所以切换策略需要用实际任务验证。
费用统计也要区分估算和最终账单。用量记录是否完整、价格配置是否更新、并发请求怎样预留费用,都会影响限额效果。LiteLLM 的预算功能还依赖相应的数据库与配置,部署时应核对具体行为。预算文档说明了这些条件。
如果多个服务共享模型资源,或者经常切换供应商,我会考虑 LiteLLM。只有一个应用时,也可以先用它的 SDK 统一调用;需要集中管密钥、限额和路由时,再部署网关服务。供应商原生 SDK 则更容易直接使用某家独有的能力。
如果以后一个任务会调用很多次模型,模型网关可能影响的不只是接入方便程度,还包括成本和质量:哪一步使用哪个模型、什么情况下可以切换,都可能成为系统优化的一部分。因此最好把实际使用的模型和切换原因保存下来,方便之后比较。
检索:pgvector、Qdrant 和 Elasticsearch
Agent 经常需要查项目文档、历史 Bug、代码规范和之前的修复经验。把所有资料一次塞进上下文里,既贵,也不容易找到重点。检索设施的工作就是先找出可能相关的少量资料,再交给 Agent 阅读。
这里有两种常见查法。关键词检索看具体词是否匹配,比如函数名和错误码;向量检索则先把文本转换成一组数字,再找表达意思相近的内容。两者结合,就是混合检索。例如“登录状态丢失”适合找意思相近的说明,SessionExpiredError 则需要准确匹配。
pgvector:在 PostgreSQL 里增加向量检索
pgvector 是 PostgreSQL 的扩展,可以在数据库里保存向量并查找相似内容。它也提供加速检索的索引;这类索引通常要在查找速度与是否找全相关结果之间取舍。项目文档说明了这些能力。
它的优势是可以把文档、所属项目、权限范围和向量放在同一个数据库里查询。对于已经使用 PostgreSQL 的系统,这样少维护一个独立服务,更新文档和更新关联记录也更容易协调。
代价是检索会和任务状态等业务查询争用数据库资源。文档变多、查询条件变复杂以后,需要认真检查索引和查询方式,尤其不能假设增加一个向量索引就能解决所有性能问题。关键词检索还需要结合 PostgreSQL 的文本搜索或其他方案设计。
如果实际数据和查询负载还在现有数据库能承担的范围内,我会优先考虑 pgvector。以后它可以让一个小系统逐步增加“查历史经验”的能力;当检索明显拖慢任务数据库时,再考虑分开部署,或者迁移到专门的检索服务。这里没有适合所有系统的固定文档数量门槛。
Qdrant:把向量检索单独做成一个服务
Qdrant 可以集中保存文档向量和附带信息,例如仓库、文档类型、更新时间,并按这些条件筛选相关资料。它也支持结合不同类型的向量做混合检索。Qdrant 概览和混合检索文档介绍了这些能力。
它的优势是检索可以独立扩容,不必和任务数据库挤在一起。团队也可以单独调整检索配置、评估结果,对于“找相关资料”已经成为主要工作负载的系统,这种分工比较清楚。
代价是又多了一套需要维护的数据。原始文档修改、删除或者撤销权限后,检索服务中的对应记录也要及时变化;两个系统之间的同步需要自己设计。部署一个向量数据库,也不会自动解决文档怎么切分、用什么模型转换向量和怎样评估检索质量的问题。
如果向量检索是系统的重要能力,并且希望它独立演进,我会考虑 Qdrant。以后多个 Agent 共享资料库时,它可能成为统一的资料检索入口;相应地,数据同步、权限过滤和更新速度也会变得更重要。
Elasticsearch:兼顾准确匹配和意思相近的检索
Elasticsearch 适合需要较丰富搜索能力的场景。在代码修复系统里,函数名、文件路径、错误码和完整报错都很重要,仅仅寻找“意思相近”的内容容易漏掉这些信息。它支持把全文检索与向量检索合并,得到一份综合排序的结果。混合检索文档介绍了这条路径。
它的优势是全文搜索能力丰富,也能对多个字段设置不同的重要程度,再结合语义相似度寻找资料。团队如果已经用它搜索日志或业务文档,沿用现有设施往往比重新维护一套更方便。
代价是要设计字段、文本拆分规则、索引和排序方式,维护集群也有成本。它与业务数据库之间同样需要同步;某些功能的可用范围还与部署方式和授权有关,落地前要核对。对于只需要简单相似度查询的小系统,这套配置可能偏重。
如果准确的关键词匹配与语义检索都很重要,或者已有 Elasticsearch 搜索系统,我会倾向于它。以后 Agent 需要查询越来越多代码、日志和文档时,混合检索可能更有价值,因为这些资料同时包含自然语言和必须精确匹配的标识符。
三者的主要差异
| 工具 | 我会优先考虑的情况 | 主要优势 | 主要代价 |
|---|---|---|---|
| pgvector | 已有 PostgreSQL,想先增加相关资料检索 | 少一套服务,方便结合业务数据查询 | 检索与业务共用资源,需要优化查询 |
| Qdrant | 向量检索是重要负载,需要独立维护和扩容 | 检索职责集中,支持按附带信息筛选 | 多一套数据,需要同步与维护 |
| Elasticsearch | 函数名、错误码等准确匹配和语义检索都重要 | 全文检索与混合检索能力丰富 | 索引、排序和部署配置较多 |
这不是一个按“高级程度”排列的列表。更可靠的办法是拿真实任务比较:能否找到正确资料,查询多快,权限过滤后是否仍然好用,以及团队维护起来是否轻松。
三个工具都需要注意同一件事:先限定 Agent 能读取的资料范围,再把检索结果交给模型。 数据库保存了权限字段,也要让每次查询真正使用这些条件;不能期待模型自行忽略它无权看到的内容。
存储:Amazon S3 和 Cloudflare R2
Agent 的输出经常包含文件:代码补丁、测试报告、截图、完整日志,或者恢复环境所需的文件包。把这些东西都保存在某个 Worker 的本地目录里,机器一换,结果可能就找不到了。
对象存储可以理解为供多个服务访问的文件仓库。数据库保存任务编号、文件位置和摘要,对象存储保存文件内容。它适合上传和下载完整文件;Agent 正在修改的代码目录,通常仍然放在本地工作目录或其他合适的文件系统里。
Amazon S3:和 AWS 上的系统配合
S3 可以保存补丁、报告和环境文件,并提供文件版本管理、访问控制和到期清理等能力。例如普通执行日志保留一段时间,最终补丁和重要报告保存更久。S3 官方文档介绍了相关功能。
它的优势是可以直接和 AWS 上的计算、权限和其他服务配合。已经在 AWS 运行 Worker 的团队,使用 S3 往往能减少额外的接入工作,也便于按不同用途管理文件。
代价是费用要一起看存储量、请求量、读取方式和数据传输,不能只比较每 GB 的存储价格。区域选择和权限配置也会影响 Worker 访问文件的效率。计费项目见 S3 价格说明。
如果系统主要运行在 AWS,或者需要它已有的文件管理能力,我会优先考虑 S3。以后积累的任务文件可能被用于复盘和比较不同模型,此时文件版本、保留规则和成本管理会更有用。
Cloudflare R2:考虑大量文件下载的场景
R2 也可以保存这些任务文件,并提供兼容 S3 的接口。它不收取出站带宽费用,也就是从 R2 向外传输文件的带宽费;存储、请求以及某些存储类型的读取仍然有费用。R2 价格文档列出了这些项目。
它的优势是当报告、截图或文件包被频繁下载时,费用结构可能比较有吸引力。已有基于 S3 接口的文件上传、下载代码,也可以评估迁移到 R2 的工作量。
代价是接口兼容有具体范围。S3 的某个高级能力不能因为名字里有“兼容”就直接假定可用,实际接入要按 R2 的 S3 API 兼容表核对。Worker 所在位置、访问延迟和网络条件,也应实测。
如果系统经常向用户提供大文件下载,或者 Worker 分布在不同平台,我会把 R2 放进比较范围。以后任务制品被更多人或服务反复读取时,它的出站费用特点可能更有影响;下载很少、主要在 AWS 内部访问的系统,则仍应比较整个方案的费用和接入便利程度。
两者怎么选?
| 比较点 | Amazon S3 | Cloudflare R2 |
|---|---|---|
| 值得优先考虑的情况 | 系统已经在 AWS,需要配合现有服务 | 文件经常向外下载,或已有 Cloudflare 设施 |
| 成本评估 | 综合存储、请求、读取与传输费用 | 出站带宽免费,仍需计算存储与操作费用 |
| 接入时要核对 | 区域、权限及所选存储类型 | S3 接口兼容范围与实际访问表现 |
无论选择哪一个,恢复文件最好都关联代码版本、依赖版本和生成时间。下载回一个文件包,不代表能自动恢复一个仍在运行的进程;环境重建与会话恢复仍然是 Worker 的职责。
权限/安全/风控:Keycloak 和 OPA
Agent 能调用工具以后,系统需要回答两个问题:谁发起了任务,以及这个任务能做哪些操作。在代码修复场景里,“可以读取某个仓库”和“可以向这个仓库提交改动”就是不同权限。
Keycloak:管理登录与身份
Keycloak 可以接管用户登录,提供统一身份,并管理用户、组和角色。用户登录以后,Agent 系统就能根据经过验证的身份,知道是谁提交了任务。它还可以对接已有身份系统,让用户不必为每个应用重复登录。Keycloak 官方介绍说明了这些能力。
它的优势是多个应用可以共用登录和身份管理,减少每个服务分别维护账号的工作。对于内部 Agent 平台,账号停用、组织变化和登录规则可以集中处理。
代价是身份服务本身也要维护,登录配置、密钥和账号数据都需要管理。更关键的是,Keycloak 给出的身份和角色需要应用验证并实际使用。用户有某个角色,不代表后台所有 Agent 都应该拿到该角色能够访问的全部凭据。
如果有多个应用需要统一登录,或者需要接入企业现有身份系统,我会考虑 Keycloak。已经有可用身份平台的团队则可以直接接入现有平台。以后更多用户通过 Agent 访问工具时,任务与发起人的身份关联会更重要,才能查清某次操作由谁委托。
OPA:根据规则判断某次操作是否允许
OPA 可以接收身份、目标资源和操作信息,再按规则给出决定。例如这次任务属于谁,要修改哪个仓库,操作是读取还是提交,是否已经获得人工批准。执行服务拿到决定后,再选择放行或拒绝。OPA 官方文档明确区分了权限决策和落实决策这两件事。
它的优势是权限规则可以集中表达和测试,而不用在每个工具服务里各写一套判断。比如“只能访问任务所属仓库”和“提交改动前需要审批”,可以作为多种工具共同遵守的规则。
代价是团队需要学习它的规则语言,维护规则和输入数据。规则写得对,输入却过时或来自不可信内容,决定也可能出错。OPA 返回“拒绝”之后,还必须由真正执行操作的服务阻止请求;它不会自己阻止一个绕过检查的命令执行。
如果权限规则已经跨越多个服务,或者经常按用户、仓库、操作和审核状态组合变化,我会考虑 OPA。规则很少时,在工具入口用清楚、经过验证的代码检查,也可能更容易维护。以后 Agent 能操作的系统越来越多,OPA 这类集中规则工具可能减少不同服务之间的权限分歧。
两者怎样配合?
本文让 Keycloak 主要负责身份,让 OPA 主要负责每次操作的规则判断。这是一种分工方式,Keycloak 本身也有授权能力,两个工具并不是必须成套部署。
| 工具 | 本文里的主要职责 | 仍然需要应用完成的事情 |
|---|---|---|
| Keycloak | 证明用户身份,提供组和角色信息 | 验证身份信息,把任务关联到正确用户 |
| OPA | 根据身份、资源和操作给出权限决定 | 提供可信输入,并在工具执行入口落实决定 |
对于需要长期运行的任务,最好在真正调用工具时再次检查相关权限。用户昨天有权限,今天可能已经被撤销。Agent 的工具凭据、文件范围和网络访问也应由执行环境限制,避免它绕过工具入口的检查。可以把“需要审批”的决定交给 Temporal 等工作流等待,批准后再验证具体操作。
可观测性:OpenTelemetry 和 Langfuse
上一篇把可观测性分为两部分:系统是否稳定运行,以及任务是否有效完成。下面两个工具分别帮助我们观察整个执行链路和模型调用,但都需要系统主动记录有用的信息。
OpenTelemetry:把一次任务经过的服务串起来
一次代码修复任务可能经过 API 服务、队列、Worker、检索服务、模型网关和测试服务。OpenTelemetry 可以帮助这些服务用统一方式产生并传递耗时、错误和执行链路信息。执行链路可以理解为“这次任务一路经过了哪些地方,每个地方发生了什么”。官方介绍说明了它的用途。
它的优势是能够把不同服务上的信息关联起来。例如任务花了 20 分钟,可以继续看有多少时间在排队、查询资料、等待模型和运行测试。使用通用的数据采集方式,也便于沿用已有监控设施或更换后端。
代价是需要在应用里加好记录点,并把关联信息随着队列消息和请求传下去。OpenTelemetry 本身主要负责采集和传输,不是一个直接提供全部存储、看板和报警的监控平台,还需要相应的后端。缺少模型调用内容和任务结果时,一条完整的耗时链路也很难说明 Agent 为什么修错了代码。
如果任务已经跨多个服务,或者团队已有通用监控系统,我会考虑 OpenTelemetry。以后 Worker 和工具服务增多时,它对定位性能瓶颈会更有用,尤其能帮助分辨任务在等待资源,还是在实际执行。
Langfuse:查看模型、工具调用和任务评估
Langfuse 更面向模型应用,可以关联输入、模型输出、工具与检索步骤、Token 用量、费用和评估分数。Token 可以粗略理解为模型处理文本时计数的小片段,是用量和计费的重要依据。Langfuse 的可观测性文档介绍了这些记录与分析能力。
在代码修复系统里,它可以帮助检查:Agent 当时读到了哪些资料,调用了什么工具,哪一步开始反复修改,同类任务换一个模型后用了多少资源。它的优势是这些信息围绕模型应用组织,比从普通文本日志中手动拼对话方便。
代价是需要正确接入调用链路,完整对话也会增加存储量。记录中可能包含用户代码、密钥或内部文档,需要先去掉不该保存的内容,再设置访问范围和保留时间。费用分析依赖用量与价格信息,评估分数也依赖评估方法,不能看到一个高分就认定修复正确。
我会在模型花费和任务质量已经成为主要问题时考虑 Langfuse。结果最好关联测试是否通过、审核是否通过等证据;这些证据也有局限,但比只观察对话长度更接近实际目标。以后切换模型、修改提示词和检索方式时,积累的任务轨迹可以帮助比较变化,也能提供待人工核对的失败样本。
两者的主要差异
| 比较点 | OpenTelemetry | Langfuse |
|---|---|---|
| 优先帮助回答 | 任务在哪个服务等待、变慢或出错? | 模型看了什么、做了什么、花费和效果如何? |
| 主要关注 | 跨服务的执行链路、耗时、错误与运行指标 | 模型输入输出、工具调用、用量与评估 |
| 接入后的工作 | 配置数据存储、查询、看板与报警后端 | 接好模型与工具记录,建立有效的评估证据 |
这是侧重点的区别,两者也可以配合。最需要先统一的是任务编号、每次执行尝试的编号,以及贯穿服务的关联信息。否则数据库里是一个任务,日志里是另一个编号,模型平台里又是第三个编号,信息再多也很难一起查。
把这些工具放在一起
对于本文的代码修复系统,可以按一次任务的经过理解它们怎样协作:
- 验证用户身份和仓库权限,在 PostgreSQL 创建任务记录,把任务交给 RabbitMQ 或 BullMQ。
- Worker 领取任务,取得文档和代码;需要相关资料时,查询 pgvector、Qdrant 或 Elasticsearch 中所选的一种方案。
- Worker 通过 LiteLLM 调用模型,Redis 按需提供缓存和共享限流。模型与工具的记录关联到同一个任务。
- Worker 保存补丁和测试报告到 S3 或 R2,在 PostgreSQL 登记状态与文件位置。提交改动前,由工具服务再次检查权限。
- 如果有复杂步骤和人工等待,改由 Temporal 管理流程进度;如果多个服务需要独立处理事件,再增加 Kafka。
OpenTelemetry 和 Langfuse 可以覆盖这些过程中的不同信息。实际部署时,不必完全照着这套组合;已有的数据库、身份平台和监控设施通常值得继续使用。
我的选择顺序会是:先让任务有可靠记录,能被领取,结果能保存,失败能查清,操作权限能落实。队列可以限制并发,初期未必需要 Redis;没有跨服务的长流程,未必需要 Temporal;只是读取当前仓库的文件,也未必需要向量数据库。
之后再根据具体问题增加工具:Node.js 应用需要后台任务和简单依赖管理时考虑 BullMQ,恢复逻辑越来越难维护时考虑 Temporal,多个服务都要读取事件时考虑 Kafka,模型接入和费用管理越来越分散时考虑 LiteLLM,检索成为主要负载时再拆出专门服务。选型的理由应该能落到这些实际变化上。
随着 Agent 任务变长、可操作的工具变多,恢复执行、追踪成本和检查权限可能会占更大比重。对一个可靠的系统来说,要能回答:这次任务做到哪了,为什么失败,再做一次会不会重复操作,结果保存在哪里,以及这一步究竟是谁允许的。中间件的价值,就体现在把这些问题处理得更清楚、更省力。