☰
subagent工作流持久化与可追踪性工程实践
2026/10/19 15:17:25 网站建设 项目流程

最近在 Hacker News 上看到一个项目的标题:Make ordinary subagent workflows persistent and trackable。这个标题很短,但踩中的是一个真实存在的工程痛点:subagent 工作流要进入生产环境,单次跑通远远不够,还得能持久化、能追踪、能恢复。

上周我接手了一个内容分析任务,主 agent 把任务拆成调研、数据提取、事实核查和成稿四个子任务,分别交给不同的 subagent 执行。第一轮跑下来,结果看起来不错。可到了复盘阶段,我发现自己回答不了三个问题:报告里的某个数字来自哪次工具调用?事实核查子代理当时看到的上下文是什么?如果明天再跑一次,这个结果还能复现吗?

答案全都是否。这些信息只存在于一次性的内存上下文和密密麻麻的终端输出里,进程结束,一切归零。这件事让我重新理解了 subagent 工作流的短板:真正卡住你的往往不是模型不够聪明,而是缺少后端工程里的两个基本能力——持久化和可追踪性。

1. 看起来是函数调用,跑起来是黑盒调度

1.1 subagent 的本质:把大任务拆给有独立上下文的执行单元

理解 subagent 工作流之前,先看它的基本形态。一个主 agent 扮演调度者,把包含多步骤的复杂任务拆解成子任务,然后分派给多个 subagent。每个 subagent 拥有独立的上下文窗口、独立的工具集和独立的 prompt,执行完把结果交回给主 agent,由主 agent 汇总成最终输出。

这种架构之所以流行,是因为它解决了单 agent 模式的几个硬限制。

第一,上下文窗口是有限的。真实业务任务经常需要同时处理多个文档、多个数据源和多个约束条件,塞进一个上下文容易爆。拆分给 subagent 后,每个 subagent 只需要关注自己的局部上下文,压力小得多。第二,任务分工更明确。主 agent 负责“判断和协调”,子 agent 负责“执行和产出”,职责边界比一个 agent 从头做到尾要清楚。第三,可以并行执行。多个 subagent 同时跑,理论上能缩短整体耗时。

所以你会看到,几乎所有主流 agent 框架都在往这个方向走:主 agent 派活,subagent 干活。外部看起来,subagent 调用就像一次函数调用,传一个任务描述进去,拿一个结果出来。

1.2 但它和普通函数调用有一个本质区别

问题就出在“看起来像函数调用”这个地方。

普通函数调用是可复现的:相同输入、相同环境、相同状态下,输出基本一致,出了 bug 可以通过复现来定位。但 subagent 调用不是这样。它内部至少包含三个阶段:接收任务并解析、决定调用哪些工具、基于工具结果生成最终输出。这三个阶段都由模型采样驱动,存在随机性。同一个任务,同一套 prompt,跑两次结果可能不完全一样。

更重要的是,subagent 内部执行路径是动态分叉的。它可能在执行到一半时决定先读文件 A,再调外部 API,再根据结果修改自己的思路。这些中间步骤通常不会暴露给上层。最终拿到的只有一个输出对象,中间发生了什么,外部很难直接看到。

我用一个类比来解释:普通函数调用像是把每一步都写在代码里,按顺序执行;subagent 调用更像是把需求交给一个临时团队,团队内部自己商量着干,最后交回一份报告。报告质量不错,但中间过程依赖团队自身运转,外部没有观测手段。

这里就出现了一个工程上的判断:demo 阶段可以接受黑盒,因为结果不对重跑一次就行;但进入生产之后,重跑有成本,审计有要求,协作有边界,黑盒会变成最大的风险。

1.3 黑盒带来的三个直接后果

  • 不可追溯。输出异常时,不知道该查哪一环。是任务拆解错了?是某个 subagent 的输入不完整?还是工具调用返回了脏数据?没有记录就等于没有证据。
  • 不可恢复。进程中断、上下文超限、网络抖动,都会导致整个工作流从头再来。重跑一次不仅慢,还可能因为模型随机性得到完全不同的结果。
  • 不可优化。没有历史数据,就不知道每个子任务花了多少 token、用了多少时间、失败率有多高。调优停留在拍脑袋层面,而不是基于数据做迭代。

不要因为“有输出”就认为流程可靠。很多 agent 工作流不是不报错,而是报错了也说不清是哪一环出的问题。

这三大问题,正好对应了“persistent”和“trackable”这两个关键词。持久化解决恢复和复现,可追踪解决调试和优化。它们是同一枚硬币的两面。

2. 持久化与可追踪,到底在解决什么

2.1 对调试:从“猜”变成“查”

传统后端工程里,排查线上问题靠的是日志、错误堆栈和链路追踪。一个请求经过了网关、服务 A、服务 B、数据库,每一层都留下记录,出了问题沿着调用链一层层定位。这就是“可观测性”。

subagent 工作流其实是一个分布式系统:主 agent 是入口,subagent 是微服务,工具调用是外部依赖。但它被包装成了一个优雅的接口,导致很多人忽略了可观测性建设。没有轨迹时,排查问题只能靠“猜”:重新跑一遍看能不能复现,换个模型试试,改一下 prompt 再试。这种做法偶尔有效,但本质是在和随机性赌博。

有了轨迹之后,排查逻辑完全不同。每个 subagent 的输入快照、输出快照、工具调用记录、耗时、状态变更都被保存下来。问题出现时,直接沿着轨迹定位到具体环节,看是哪一次工具调用返回了异常,看是哪个 subagent 的输出不符合预期,看是主 agent 的任务拆解有问题还是传参有遗漏。

这个转变看起来只是多了一些日志,实际改变的是一种工作方式:从依赖模型直觉,变成依赖工程证据。

2.2 对运行:长任务变成可断点续跑的状态机

很多 subagent 工作流不是一次性跑几十秒就结束的。真实场景里,一个流程可能要处理几十个文件、调用几十次外部服务、分多个阶段运行。任务越复杂,运行时间越长,中途失败的概率就越高。

失败原因多种多样,上下文超限、外部服务超时、网络波动、进程被杀、机器重启。如果没有持久化,任何一次失败都意味着从头再来。多个子任务已经跑完的结果全部作废,token 浪费是小事,时间成本和机会成本才是大问题。

持久化的价值在于把“一次性运行”变成“有状态工作流”。每个 subagent 子任务都有明确状态:待执行、执行中、已完成、失败、重试中、暂停。每个已完成的任务保留结果和上下文快照。进程重启后,不是重新开始,而是从最近的成功状态继续往后跑。

这就像一套多工序流水线,每道工序完成时都记录进度。中间断了一次电,恢复后不需要把已经做好的工序推倒重来,只需要从断电处接着往下走。

2.3 对演进:轨迹是调优工作流的核心数据资产

这一条被很多人忽略,但我认为长期价值最大。

一个 subagent 工作流跑了几百次之后,会留下大量执行轨迹。这些轨迹记录着每个子任务的耗时、token 消耗、成功率、工具调用频率、结果质量。基于这些数据,可以做三类调优:

  • 发现瓶颈子任务。哪个 subagent 耗时最长?哪个环节总是失败?哪个工具调用最不稳定?
  • 对比不同配置。改了 prompt、换了模型、调整了任务拆解策略之后,结果是不是真的变好了?有历史轨迹才有对比基准。
  • 沉淀最佳实践。哪些子任务的拆解方式更稳健?哪些输入格式更容易让 subagent 理解?这些经验可以从历史轨迹里总结出来。

没有持久化和可追踪,就没有这些数据。没有数据,调优就永远只能靠个例体验和样本印象,无法形成稳定的方法论。

维度没有持久化/可追踪有持久化/可追踪
调试复现难,靠重试和猜测按轨迹定位具体环节
恢复进程结束后全部重跑断点续跑,保留已完成结果
优化缺少历史数据,凭感觉调整基于耗时、成功率、token 数据迭代
审计无法回答“为什么是这个结果”每次输出都有完整调用链支撑
协作别人很难接手你的 agent 任务轨迹可直接交接、复查、复盘

3. 让 subagent 工作流落地的三层记录模型

如果要自己实现一套持久化和可追踪方案,不用一上来就设计成大型平台。我比较推荐拆成三层来做:事件记录层、轨迹关联层、状态恢复层。

3.1 事件记录层:把每个生命周期节点变成结构化数据

第一层最基础,也最容易被做过头。

很多人在实现时最容易犯的错误是想把中间过程的所有内容都保存下来,包括模型的每一句话、每一个 token、每一次改写。结果就是存储成本爆炸,真正要用的时候又不知道看哪里。

我的建议是先定义关键事件集合,只记录对排查和审计有价值的信息。典型事件包括:

事件类型含义关键字段
agent_startedsubagent 开始执行任务描述、输入快照、父 span
agent_messagesubagent 输出中间消息消息内容、时间戳
tool_calledsubagent 准备调用工具工具名、参数快照
tool_finished工具调用返回返回快照、状态、耗时
agent_finishedsubagent 正常完成输出快照、token 统计
agent_failedsubagent 执行失败错误信息、失败阶段
task_paused任务被暂停暂停原因
task_resumed任务从快照恢复恢复位置、原因

记录结构要统一。一个最低限度的 AgentEvent 数据类大概是这样:

# 示意结构:字段请结合你自己的 agent 运行框架调整 @dataclass class AgentEvent: event_id: str trace_id: str span_id: str parent_span_id: str | None event_type: str # agent_started / tool_called / ... agent_id: str task_id: str timestamp: str status: str # running / completed / failed / retrying payload: dict # 输入快照、输出快照、错误信息等

这里的关键不是字段多少,而是结构稳定。事件一旦写入,最好作为追加日志保存,不做修改。后续排查和统计分析都依赖这条稳定的事件流。

3.2 轨迹关联层:用 trace_id 和 span_id 串起调用树

只有一堆事件日志还不够,还要能表达事件之间的关系。

一个 subagent 工作流的调用链是树状结构:主 agent 是根节点,它派出的第一个 subagent 是子节点,这个 subagent 调用工具时又会产生更底层的节点。要重建这棵树,只需要两个字段:trace_id 和 parent_span_id。

trace_id 表示一次完整的任务运行,从主 agent 接收请求到最终输出,所有事件共享同一个 trace_id。span_id 表示一次具体执行单元,比如“调研子代理的这一次运行”,或者“事实核查子代理调用搜索工具的这一次调用”。parent_span_id 指向调用方的 span,这样就能串出完整调用树。

一次典型的 JSON 事件可以长这样:

{ "event_id": "evt_00123", "trace_id": "trace_20250215_003" "span_id": "span_research_0003", "parent_span_id": "span_root_0001", "event_type": "agent_finished", "agent_id": "subagent_research", "task_id": "task_research_20250215", "timestamp": "2025-02-15T14:32:11.000Z", "status": "completed", "payload": { "input_snapshot": { "task": "提取近三年营收数据", "source": "财务报告第10页到第15页" }, "output_snapshot": { "2023": "3.2亿", "2022": null, "2021": "2.8亿" }, "token_usage": { "prompt_tokens": 8200, "completion_tokens": 1200 } } }

有了这个结构,哪怕只有一个日志文件,也可以在排查时重建“谁调用了谁、每次调用传了什么、返回了什么”的完整路径。

3.3 状态恢复层:快照和状态机让任务断点续跑

有事件和轨迹,只解决了“追踪”的一半。另一半“持久化”要解决的是状态恢复。

每个 subagent 子任务都应该有一个状态机:

pending -> running -> completed running -> failed -> retrying -> running running -> paused -> resumed -> running

状态机实现起来不复杂,难点在于恢复时如何重建上下文。恢复一个子任务,不只是把 prompt 再喂一遍。如果它在执行过程中已经调过工具、积累了中间结论,那么恢复时必须带上这些信息,否则它不知道自己之前做过什么。

常用做法是保存“上下文快照”。在关键节点,把 subagent 的输入、已完成步骤的结果、当前待办列表序列化保存。进程重启后,读取快照,从最近一个暂停点继续执行。

这里有一个重要提醒:恢复重跑时要注意工具的副作用。比如一个 subagent 之前已经发送了一封邮件,恢复时如果重新执行到“发送邮件”这一步,很可能会重复发送。所以,有副作用的工具调用必须记录“已完成”状态,恢复时不能盲目重放。

不要急着把中间过程所有产出都存下来。先记录“谁、调用谁、传了什么、返回了什么、成功还是失败”这五类信息,就已经解决了大部分追溯问题。

3.4 存储选型:从 JSONL 到数据库

具体存储方案取决于规模和团队约束。

  • 单机学习和验证阶段,直接用 JSONL 文件追加写入就够了。结构简单,可读性强,配合脚本就能分析。
  • 小团队内部使用,SQLite 是不错的选择。事件表和状态表分开,查询效率高,部署成本低。
  • 生产环境和多 agent 高并发场景,建议用 PostgreSQL 或 MySQL,事件表按 trace_id 建索引,状态表单独维护。有条件的话,事件流系统或对象存储也是可选项。

不管选哪种,有一个原则:事件追加写,状态单独更新。事件是不可变记录,用于追溯和分析;状态是可变记录,用于恢复和调度。两者合在一起容易造成锁竞争和性能瓶颈。

4. 落地顺序:先跑通一条轨迹,再补恢复能力

很多人拿到这类方案会想一步到位,直接上一个完整平台。我的建议是反过来,先跑通一条最小可用的轨迹,再逐步补恢复能力。顺序大概如下。

4.1 第一步:给每次 subagent 调用打上结构化日志

在最外层包装 subagent 调用,不管用什么框架,都统一经由一个函数发起。这样可以在调用前后自动记录事件。

def run_subagent(scope, agent_id, task, inputs): trace_id = scope["trace_id"] span_id = generate_span_id() record_event( trace_id=trace_id, span_id=span_id, event_type="agent_started", agent_id=agent_id, status="running", payload={"task": task, "inputs": inputs} ) try: result = agent.run(task, inputs) record_event( trace_id=trace_id, span_id=span_id, event_type="agent_finished", agent_id=agent_id, status="completed", payload={"outputs": result} ) return result except Exception as exc: record_event( trace_id=trace_id, span_id=span_id, event_type="agent_failed", agent_id=agent_id, status="failed", payload={"error": str(exc)} ) raise

这一步只做一件事:让每次调用都有记录。先不追求断点续跑,也不追求复杂分析。能看到“谁在什么时候执行了什么任务、成功还是失败”,就已经比纯终端输出前进了一大步。

4.2 第二步:用 trace_id 把调用链串成树

单条日志只能回答“发生了什么”,串成树才能回答“为什么发生”。

具体做法是在主 agent 发起整个工作流时生成一个 trace_id,每次分派 subagent 时生成一个 span_id,同时把当前 span 作为子事件的 parent_span_id。这样,所有事件最终能还原成一棵调用树。

实现上不需要很复杂。用一个上下文对象往下传,里面包含 trace_id、当前 span_id、任务信息。所有后续调用从上下文里读这些字段,写入事件即可。

4.3 第三步:实现状态机与断点续跑

轨迹跑通之后,再补状态恢复能力。

每个子任务在数据库里维护一条状态记录,字段包括 task_id、agent_id、status、input_snapshot、output_snapshot、error_info、attempt_count。每次启动工作流之前,先扫描未完成任务;对已完成的任务直接复用结果;对失败和暂停的任务,根据快照和状态决定从哪个位置继续。

这一步最花时间,但不是因为技术复杂,而是因为需要结合你实际的 agent 框架来确定“什么是可恢复的最小上下文”。一个通用的做法是:保存上次成功事件之前的输入输出快照,恢复时从该事件之后重新执行。

4.4 第四步:增加基础分析能力

有了事件表和状态表,分析就水到渠成。最简单的分析可以先从三个维度开始:

  • 失败率。按 agent_id 分组,统计每个 subagent 的失败次数和失败原因。
  • 耗时。统计每次运行的各阶段耗时,找出瓶颈。
  • token 消耗。结合事件里的 token 字段,看哪个子任务最贵。

有能力的话,再做一个小页面或者脚本,输入 trace_id 就能展示整棵调用树。这一步会极大提升排查效率。

4.5 自己实现还是使用现成框架

这个问题的判断标准很简单:你使用的 agent 框架如果已经内置了 tracing 和 checkpointer,优先用,但必须确认它是持久化到外部存储还是只存在内存里。只存在内存里,对断点续跑没有帮助。

如果框架没有内置能力,自己实现一套三层模型并不复杂。核心代码量不大,关键是事件结构和状态机设计要稳定。不要在一开始就追求漂亮的 UI、完整的权限控制、多环境支持,这些可以后面再补。

5. 排查链路:从“重新跑一遍”到“按轨迹定位”

5.1 一个真实得不能再真实的例子

假设你跑了一个金融数据处理工作流。主 agent 把一个任务分派给“数据提取” subagent,要求它从一份财报里提取近三年的营收数据。最终报告里,2021 和 2023 的数据都有,2022 的数据显示为空。

没有轨迹时,你大概率会做三件事:重新跑一遍整个工作流;修改数据提取的 prompt,强调“一定要提取 2022 年”;换一个更贵的模型再试。运气好可能成功,但你始终不知道根因是什么。

有轨迹之后,排查路径会清晰很多。第一步查看“数据提取” subagent 的输入快照,发现主 agent 传给它的源文件范围是“报告第1页到第20页”,而 2022 年财务数据恰好不在这个范围内。这不是模型能力问题,是任务拆解或者传参问题。

如果没有这个输入快照,你可能永远在模型层打转。

5.2 有轨迹后的五步排查法

排查步骤查看内容对应的追踪数据
1. 看现象是报错、缺数据、卡住还是结果不一致事件状态、错误信息
2. 看输入subagent 收到的任务描述和上下文是否完整输入快照
3. 看工具调用它读取了哪些文件、调了哪些外部服务tool_called / tool_finished 事件
4. 看环境和参数上下文长度、超时、并发、token 限制运行配置和资源记录
5. 看边界框架版本、嵌套层级、模型支持能力执行环境信息

这套排查顺序有一个核心逻辑:先确认外部输入没有问题,再看内部执行过程,最后才怀疑模型能力。大多数 agent 结果异常,根因在输入和工具,不在模型。如果模型本身有问题,通常表现为所有子任务都变差,而不是某一个环节单独出错。

5.3 没有轨迹时的降级方案

如果你手上的工作流确实还没有任何轨迹系统,但问题已经发生,这时候只能做有限操作。

第一,保留所有终端输出作为临时证据,至少要记录主 agent 的最终输出和每个 subagent 的一小段摘要。第二,在复现时打乱输入变量,逐项确认是任务描述、上下文、工具还是模型的问题。第三,从这次排查开始,给相关调用加上最基础的事件日志。降级方案不能替代体系化建设,但可以帮你积累第一手证据。

排查顺序不要乱。先看输入快照,再看工具调用记录,最后才考虑模型和参数问题。大多数 agent 结果异常,根因在输入和工具,不在模型。

6. 不是所有工作流都需要这套方案

持久化和可追踪听起来很好,但引入它是有成本的。存储要钱,写日志要改代码,状态机要维护,事件结构要保证兼容。如果所有 subagent 工作流都上全套方案,很可能是过度设计。

6.1 适合它的场景

  • 多步长任务。一个流程要跑几分钟甚至更久,失败重跑成本高,非常适合持久化和断点续跑。
  • 团队共享工作流。多个开发者、多个业务方共同使用同一个 agent 流程,必须有统一的执行记录,否则出了问题无法分清责任。
  • 审计和结果回溯。面向客户、面向合规、面向汇报的 agent 输出,需要有完整的调用链来支撑“为什么是这个结果”。
  • 不可复现的外部依赖。工作流里调用了外部 API,而外部服务返回结果不可复现,这时候记录输入输出快照非常重要,否则只能依赖记忆。

6.2 暂时不需要它的场景

  • 一次性 demo。给同事展示一下能力,跑通就行,额外加日志是浪费。
  • 单轮轻量调用。没有多 agent 协作,没有工具调用,调用关系一目了然,可追踪价值不大。
  • 架构还在快速变化的原型阶段。今天换这个框架,明天改那个拆解策略,上持久化只会拖慢迭代速度。
  • 对延迟极其敏感的场景。每次调用都同步写数据库会拖慢响应,除非用异步日志,否则不建议强上。
场景类型是否需要持久化追踪原因
多步长任务强烈建议失败重跑成本高
团队共享工作流建议需要审计、复盘、交接
一次性 demo不需要增加复杂度,收益低
单轮轻量调用不需要没有足够的中间状态

6.3 关于成本,三个很实际的提醒

第一,存储成本要控制。不要保存每个中间 token,只保存结构化事件、输入快照摘要和输出结果。对于大文件内容,存引用和哈希,不存全文。

第二,事件结构要向前兼容。后续增加字段时,尽量使用可选字段,不要把旧事件结构破坏掉。否则历史轨迹会变成一堆无法解析的垃圾数据。

第三,持久化不能替代 agent 设计。如果任务拆解本身不合理,或者工具选择错误,记录只会让你更快发现问题,不会让问题自动消失。别指望靠日志系统解决能力问题。

7. 让每一次 subagent 调用都留下脚印

7.1 回到主判断

subagent 工作流真正的分水岭,不是某个模型版本升级,而是从“不可复现的实验”走向“可审计的工程系统”。这个转变依赖的恰恰是标题里的两个词:persistent 和 trackable。

持久

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询