这次我们来看一个非常贴合实际踩坑需求的方向:把普通 subagent 工作流做成持久化(persistent)和可追踪(trackable)的形态。现在做 Agent 应用,主流方案已经变成“主代理拆任务 + 多个子代理并行执行 + 结果汇总”,看起来简单,真正跑到生产环境就难受了:进程一断、超时一响、某条子任务失败,前面的状态全部丢失;想复盘某个子代理当时收到了什么提示词、输出了什么结果,只能靠手动打日志;想统计每个子代理的耗时和消耗,基本等于没有。这个项目标题本身就是答案,它要解决的就是这一层核心问题:让普通子代理工作流不再是一次性脚本,而是能存、能查、能恢复、能对比的工程化流程。
这篇文章会先拆解 subagent 工作流的持久化与追踪到底指什么,再给一套通用的部署、验证、接口调用和排查流程。如果你正在写多代理编排代码,或者准备把一个临时 Agent 流程改造成可维护的服务,直接按这个思路去对照,能省掉很多返工。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目定位 | 子代理工作流持久化与执行追踪层,核心是给普通 subagent 流程增加状态存储、中断恢复和过程回放能力 |
| 核心功能 | 工作流状态持久化、子代理输入输出记录、执行事件追踪、失败重试、任务恢复、批量执行 |
| 硬件要求 | 取决于底层模型在哪里运行。使用远程大模型 API 时,本地只需要普通 CPU 环境;使用本地模型时按模型显存需求评估 |
| 支持平台 | 通常以 Python 环境为主,建议 Linux / macOS / Windows WSL 下测试 |
| 启动方式 | 命令行启动、API 服务启动、监控面板(具体以项目实现为准) |
| 是否支持 API | 一般会提供查询任务状态和提交任务的接口,需要按实际项目确认路径和参数 |
| 是否支持批量任务 | 支持多子任务并发或队列编排,建议从串行批量开始验证 |
| 数据存储 | 本地 SQLite 或 PostgreSQL,具体取决于实现 |
| 适合场景 | Agent 调试、多代理协作流程、长耗时自动化任务、批量内容生产、流程复盘 |
从材料来看,这个方向的核心不是再做一个新的 Agent 框架,而是给已有的“普通子代理工作流”补上基础设施。先有流程,再谈保存和追踪。
2. 问题背景:普通子代理工作流为什么需要持久化与追踪
先描述一个典型的 subagent 工作流。主代理接收一个复杂目标,把它拆成多个子任务,每个子任务交给独立的子代理去执行,子代理可能调用工具、查询资料、调用模型生成结果,最后把结果返回给主代理汇总。这种结构在调研类任务、报告生成、代码生成、资料整理场景里非常常见。
但普通实现有几个典型问题:
- 进程一旦中断,所有内存中的上下文全部丢失。网络超时、API 报错、服务器重启,都可能让整个工作流重新跑一遍。
- 子代理执行过程中没有中间快照。如果某个子代理跑了 10 分钟才失败,只能从头再来。
- 没有统一的执行轨迹。每个子代理的输入提示词、输出内容、耗时、调用次数、token 消耗、失败原因都散落在不同的日志位置。
- 并行子任务之间状态难以对齐。A 子代理已经完成,B 子代理还在重试,主代理无法准确知道整体进度。
- 无法做回归对比。调整了某个子代理的提示词或模型参数后,没办法对比前后两次执行结果。
持久化和可追踪就是针对这些问题补的短板。持久化解决的是“状态别丢”,子代理执行过程中的任务状态、中间结果、上下文记录都落到存储里,崩溃后可以从最近一个稳定点重新拉起。可追踪解决的是“过程可查”,每次执行都有唯一 ID,每个子代理都有状态变更事件,每个结果都有对应的输入记录,随时可以回答“这个结果是怎么得出来的”。
这里也提醒一下,标题里的 persistent 指的是工作流状态持久化,不是拓扑数据分析里的 persistent homology(持久同调),两者只是英文撞词,搜索资料时别混在一起。
3. 适用场景与使用边界
这个方向适合这几类人:
- 正在写多代理编排代码的开发者,想把流程从脚本升级成可维护的服务。
- 需要长时间运行 Agent 任务的人,比如批量生成报告、批量整理资料、定时调研。
- 需要分析 Agent 效果的人,想看清每个子代理的输入输出,方便优化提示词。
- 做 Agent 平台或内部工具的人,需要给用户提供可查询的执行记录。
不适合的场景也很清楚:如果只是单次调用模型、一次性问答,不需要为它引入状态存储和追踪层,成本大于收益。
使用边界需要重点说。subagent 工作流可能涉及敏感数据,比如用户隐私、内部文档、业务数据。做持久化时,数据会落盘,所以必须做好脱敏和访问控制。日志和追踪记录里如果包含完整提示词或模型输出,也要评估泄露风险。涉及人脸、声音、版权素材时,必须先确认授权。调用大模型 API 时,密钥不能写进代码和配置文件,应通过环境变量或密钥管理服务注入。
4. 环境准备与前置条件
下面是一套通用的本地验证环境,按项目实际要求调整。
- 操作系统:推荐 Ubuntu 22.04 或 Windows WSL2,macOS 也可以。
- Python 版本:建议 3.10 或 3.11,部分依赖对 3.12 的兼容需要实测。
- Python 依赖:至少需要 pydantic、SQLAlchemy 或 sqlite3、大模型官方 SDK 或 OpenAI 兼容 SDK。
- 数据库:建议先使用 SQLite,零配置,验证通过后再考虑 PostgreSQL。
- 大模型 API:需要准备一个可用的 API Key,推荐使用 OpenAI 兼容接口,方便切换本地模型服务。
- 磁盘空间:代码体积不大,但执行记录会持续增长,预留 10GB 以上比较稳妥。
- 端口:如果要启动 API 服务,提前确认端口没有被占用。
检查命令:
python --version pip --version sqlite3 --version如果本机没有虚拟环境,建议先建一个:
python -m venv venv source venv/bin/activate # Linux / macOS # 或 venv\Scripts\activate # Windows然后安装依赖,下面是一个最小模板,实际包名按项目替换:
pip install pydantic sqlalchemy openai httpx5. 安装部署与启动方式
这个项目大概率是以 Python 包或服务形式存在。部署分为两步:先确认入口脚本,再确认配置。
第一步,准备环境变量。把大模型 API Key 写入环境变量,不要写进代码:
export LLM_API_KEY="sk-your-key" export LLM_BASE_URL="https://api.example.com/v1"第二步,准备配置文件。一个典型的配置包含模型名称、温度、最大并发数、数据库路径、子代理超时时间:
model: name: "your-model-name" temperature: 0.2 max_tokens: 2048 workflow: default_timeout: 300 max_retries: 2 concurrency: 4 storage: database_url: "sqlite:///./subagent_flow.db" tracking: save_input: true save_output: true save_events: true第三步,进入项目目录,启动服务。如果是命令行方式:
python -m subagent_flow run --config config.yaml如果项目提供 API 服务:
python -m subagent_flow server --host 127.0.0.1 --port 8765注意,以上命令是通用模板,具体模块名和参数以实际项目 README 为准。启动后建议先检查两个东西:数据库文件是否生成,日志是否正常输出。
6. 核心设计:持久化与追踪的数据模型
要做持久化和追踪,先要有一套稳定的数据模型。这里给出一个通用设计,任何 subagent 工作流都可以按这个思路落库。
实体可以拆成四层:
- workflow:整个任务的根记录,包含任务名称、输入参数、整体状态、开始时间、结束时间。
- task:一个 workflow 下的具体任务,可能对应主代理拆出的一个子任务,包含对应子代理类型、模型、提示词、状态。
- agent_run:一个 task 的某次执行实例,同一个 task 失败重试会有多个 run,便于对比。
- event:细粒度的执行事件,包含每个关键步骤的时间戳、日志、中间结果。
状态机可以设计为:
pending -> running -> success \-> retrying -> running \-> failed -> waiting_retry每次子代理执行都要记录这些字段:
| 字段 | 说明 |
|---|---|
| run_id | 执行实例唯一 ID |
| task_id | 任务 ID |
| parent_run_id | 父代理执行 ID,用于追踪调用链 |
| subtask_type | 子代理类型 |
| status | 当前状态 |
| input_data | 子代理输入,需要脱敏 |
| output_data | 子代理输出,需要脱敏 |
| error_message | 失败原因 |
| started_at | 开始时间 |
| finished_at | 结束时间 |
| duration_ms | 耗时 |
| llm_calls | 本轮调用模型次数 |
| total_tokens | token 总量 |
用 JSON 表达一次子代理执行记录:
{ "run_id": "run_20250101_001", "task_id": "task_research_001", "parent_run_id": "run_20250101_000", "subtask_type": "web_search", "status": "success", "input_data": { "query": "subagent workflow best practice", "timeout": 60 }, "output_data": { "summary": "...", "sources": ["..."], "confidence": 0.85 }, "error_message": null, "started_at": "2025-01-01T10:00:00Z", "finished_at": "2025-01-01T10:01:30Z", "duration_ms": 90000, "llm_calls": 3, "total_tokens": 5200 }这套模型的价值在于:主代理和子代理之间的嵌套关系可以通过 parent_run_id 串起来,任何人拿到一个顶层 workflow ID,就能查到整棵执行树。
7. 功能测试与效果验证
有了部署和数据模型,接下来就要逐项验证。不要一上来就接复杂业务,先把核心链路跑通。
7.1 基础执行与状态落库
测试目标:确认一个普通 subagent 工作流执行结束后,状态能正确写入数据库。
测试方式:运行一个只有两三个子代理的简单流程,比如“主代理拆两个子任务,一个做摘要,一个做关键词提取”。
确认点:
- 数据库中出现 workflow、task、agent_run、event 记录。
- workflow 状态为 success。
- 每个子代理都有独立的 run_id。
- 输入输出都按配置保存。
如果没有落库,先检查 storage 配置是否生效,再检查数据库文件路径是否写错。
7.2 中断恢复测试
这是持久化最关键的验证点。
测试方式:启动一个包含多个子代理的工作流,在中间某个子代理运行时手动杀掉进程,然后重新启动项目,看是否支持从最近一个已完成状态恢复。
确认点:
- 已完成的任务不会重新执行。
- 未完成的任务会进入 waiting_retry 或 retrying 状态。
- 重新启动后主流程不会从零开始。
如果项目不支持自动恢复,那么至少要确认它支持“手动指定断点继续执行”,否则持久化价值会打折扣。
7.3 追踪日志与回放测试
测试目标:确认每次子代理执行的过程可以被回看。
测试方式:执行一次包含错误的重试流程,人为让某个子代理调用不存在的工具或返回错误格式,然后查看追踪记录。
确认点:
- 失败任务的 error_message 是否完整。
- 是否有重试事件。
- 重试前后输入输出是否分开保存,方便对比。
- 能否根据 workflow_id 找到整棵调用链。
这一步直接决定你后续调 Agent 的效率。
7.4 参数对比与回归测试
测试目标:确认修改提示词或模型参数后,能对比不同版本的效果。
测试方式:同一个任务跑两次,一次 temperature 0.1,一次 0.8,然后通过 run_id 对比两次结果。
确认点:
- 两次执行记录是否独立。
- 是否保留各自输入输出。
- 能否稳定复现“同一任务不同配置”的对比视图。
没有这个能力,优化提示词就只能靠感觉。
7.5 批量任务与并发测试
测试目标:验证多子任务并行时状态追踪是否准确。
测试方式:准备 10 个任务文本,启动并发执行,观察状态变化。
确认点:
- 并发数是否受配置控制。
- 每个任务的状态是否独立。
- 日志中能否区分不同 run_id。
- 批量过程中单条失败不影响其他任务。
批量任务最容易出现的问题是日志串写、状态覆盖,这一步要重点看 run_id 是否隔离。
8. 接口 API 调用示例
如果项目提供 API 服务,通常会暴露三类接口:提交工作流、查询执行状态、获取执行结果。下面是通用调用模板,接口地址和参数需要按实际项目替换。
提交任务:
import requests url = "http://127.0.0.1:8765/api/workflows" payload = { "name": "research_flow", "input": { "topic": "subagent workflow persistence" } } response = requests.post(url, json=payload, timeout=30) print(response.status_code) print(response.json())查询状态:
import requests url = "http://127.0.0.1:8765/api/workflows/workflow_001/status" response = requests.get(url, timeout=30) print(response.json())获取某个子代理的执行记录:
import requests url = "http://127.0.0.1:8765/api/agent_runs/run_20250101_001" response = requests.get(url, timeout=30) data = response.json() print(data["status"]) print(data["input_data"]) print(data["output_data"])批量重跑失败任务:
import requests url = "http://127.0.0.1:8765/api/workflows/workflow_001/retry_failed" payload = {"include_waiting": True} response = requests.post(url, json=payload, timeout=60) print(response.json())调用接口时要注意:如果任务耗时长,请求要设置合理的超时时间,最好是提交后轮询状态,而不是同步等待结果。
9. 资源占用与性能观察
这个方向的资源占用分三块看。
第一块是本地服务自身开销。如果只做状态编排、追踪、数据库写入,不加载本地模型,那么 CPU 和内存占用很低,普通开发机和服务器都能跑。
第二块是模型调用开销。使用远程 API 时,消耗体现在 token 费用和请求延迟上;使用本地模型时,显存占用取决于模型规格。这里不能给一个统一数字,要看模型版本和量化方式,设备上通过 NVIDIA-SMI 或任务管理器观察即可:
nvidia-smi第三块是存储增长。持久化意味着所有执行记录都落盘,长时间跑会导致数据库体积膨胀。观察方式:
sqlite3 subagent_flow.db "SELECT COUNT(*) FROM agent_run;" sqlite3 subagent_flow.db "SELECT SUM(length(input_data) + length(output_data)) FROM agent_run;"如果发现数据增长过快,优先怀疑是否保存了过大的中间结果。建议对 output_data 做截断或摘要化保存,完整结果放在对象存储里,只保留路径引用。
另外注意并发数对性能的影响。并发太高会导致 API 限流、本地模型显存溢出,需要根据实际任务调节 concurrency 参数。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动后数据库表没有生成 | 数据库路径配置错误或启动目录不对 | 检查启动日志,确认 SQLite 文件位置 | 配置绝对路径,删除异常文件后重启 |
| 子代理执行失败但追踪记录为空 | 没有保存 event,或保存逻辑在异常分支前被中断 | 查看 event 表是否有记录,检查代码是否在 try 块里提前返回 | 在 finally 块中落库状态和错误 |
| 工作流崩溃后无法恢复 | 未实现断点保存,或只在最后保存状态 | 查看 workflow 状态是否为 running 且没有 checkpoint | 增加中间 checkpoint,保存已完成任务列表 |
| 重试后结果覆盖了第一次结果 | 没有区分 run_id,重试复用了同一条记录 | 检查 agent_run 表主键逻辑 | 每次执行一律生成新 run_id |
| API 查询超时 | 查询任务过于复杂或同步等待长任务 | 检查接口超时设置,确认任务状态接口是否返回完整结果 | 改为轮询状态接口,减少同步阻塞 |
| 并发任务状态互相覆盖 | 公共内存字典在并发下被多线程写 | 查看日志中 run_id 是否混乱 | 使用数据库作为唯一状态源,内存只做缓存 |
| 数据库文件越来越大 | 保存了过多中间日志和完整输出 | 检查数据量分布 | 对日志设置保留周期,输出改为摘要保存 |
| 密钥泄露风险 | 配置文件里写了 API Key 或提交到了仓库 | 检查 git 记录和环境变量 | 改用环境变量,轮换密钥 |
11. 最佳实践与使用建议
这类系统重在工程化,建议从一开始就按下面的规则来做。
每次执行都分配全局唯一 ID。不管是 workflow 还是 agent_run,都要有唯一标识,并且让主代理和子代理的 ID 通过 parent_run_id 关联起来。
关键步骤做快照。子代理调用工具前、模型调用前后、返回结果前,至少要保存一次事件或状态。快照不是所有日志都存,而是存能重建执行上下文的最小信息。
敏感信息脱敏。输入输出中的密钥、手机号、身份证、内部文档内容,在落库前做脱敏或替换。日志和追踪记录建议加访问权限控制。
配置和代码分离。模型名、并发数、超时时间、数据库地址全部放配置文件,不要写死。
失败重试要有上限。每个子代理设置最大重试次数,超过后进入 failed 状态,避免死循环消耗 token。
先小参数验证。第一次实验用 2 到 3 个子代理、并发数 1、超时时间 60 秒,跑通后再逐步放大。
批量任务加日志和进度统计。每秒或每个任务结束后打印 run_id、状态、耗时,方便定位卡点。
对外接口限制访问范围。如果启动 API 服务,不要直接绑定 0.0.0.0 对外网开放,先绑定 127.0.0.1,必要时加认证。
涉及人脸、声音、版权素材时必须确认授权。如果子代理会生成图片、声音、视频,需要在流程入口加入授权检查,避免在不知情的情况下处理侵权内容。
12. 总结与下一步
这个方向最值得尝试的点,是把容易失控的 subagent 工作流变成可观测、可恢复、可对比的工程系统。你别指望一个框架能解决所有 Agent 问题,但状态落库、执行追踪、失败重试这几件事,是任何长耗时多代理流程都绕不过去的。
第一次验证时,优先测三件事:中断恢复、重试记录、调用链追踪。这三项是整个方案的核心价值。最容易踩的坑是刚开始没区分 run_id,导致重试覆盖首次结果,后面复盘时数据直接报废。
后续可以扩展的方向包括:加入指标统计面板,把每个子代理的耗时、token、成功率按天汇总;接入 PostgreSQL,支撑多人协作和更大规模批量;给主代理增加动态决策能力,根据子代理的中间结果实时调整任务拆解策略。
如果你手头正好有跑不通或复盘困难的多代理流程,按这篇文章的思路去改造,会比重新写一个编排框架更务实。