很多人第一次看到 AnyPal 的“子会话”时,会自然地把它理解成换了一个名字的 Subagent。实际上,这不是一次术语更新,而是一次执行模型的重构:我们把原先藏在一次工具调用内部的临时 Worker,升级成了具有身份、状态、历史、权限边界和恢复能力的正式会话。
这篇文章想讲清楚一个并不轻松的决定:我们为什么曾经选择 Subagent,为什么后来放弃把它作为并行协作的主抽象,以及今天的子会话机制解决了什么、又付出了什么代价。

我们为什么最初选择 Subagent
AnyPal 早期的设计与当时主流 Coding Agent 产品的思路相近:主 Agent 充当 Coordinator,通过 run_subagent 启动隔离 Worker。Worker 不继承父对话历史,只接收一段结构化任务;它可以拥有专门角色,例如 Explore、Review、Test,也可以在自己的上下文窗口里连续调用工具。完成后,它向父 Agent 返回一条汇总结果。
这个模型非常有吸引力。首先,它能隔离上下文。搜索日志、测试输出和大量代码片段不会挤占主会话窗口。其次,它天然适合角色专精:搜索子代理只拿只读搜索工具,审查子代理只关注风险,测试子代理只负责验证。第三,它对模型来说很简单——run_subagent 只是一个工具,调用方式与读取文件、执行命令没有本质区别。
我们甚至进一步实现了并行调用、最多五层嵌套、独立沙箱、生命周期事件和嵌套进度卡片。专用的 SearchSubagent 还可以在内部组合精确搜索、语义搜索与 GraphRAG,再把去重、水合后的结果交还主模型。单看一次任务的完成效率,这套方案并不失败。
真正的问题:Subagent 是“调用”,不是“协作者”
问题出现在任务变长、客户端变多、用户开始要求可恢复和可干预之后。
旧模型的 Worker 本质上附属于父会话中的一次工具调用。它有内部 ID 和运行事件,却不是用户会话列表中的一等实体。父 Agent 通常只能等待最终摘要;即使 UI 展示了活动流,用户看到的仍是一张嵌套卡片,而不是一个可以进入、阅读、继续和单独停止的工作空间。
这会产生几个结构性缺陷。
第一,监督信息被压缩得太早。Worker 做过哪些工具调用、遇到哪些失败、为什么改变方案,往往被压成一条 summary。摘要适合消费,却不适合审计。当结果可疑时,Coordinator 缺少原始会话证据。
第二,生命周期绑得太紧。父轮结束、用户点击 Stop、页面关闭或宿主重启时,究竟应该停止哪一层 Worker,很容易出现语义歧义。我们曾专门把 Subagent 生命周期转发到父 Session 的运行日志,只为让 Session Editor 看见它;这恰好说明 Subagent 自己不是日志、状态和恢复的天然归属。
第三,中途协作能力弱。传统 Subagent 更像“发包—等待—收结果”。如果子任务方向偏了,最自然的操作不是进入它的上下文进行 steer,而是终止后重跑,或者让父 Agent 在结果返回后补救。对十秒钟的搜索这可以接受,对持续数分钟、修改多个文件的工程任务就代价高昂。
第四,嵌套带来治理复杂度。竞品常见的 Subagent 方案也有相似取舍:把代理封装成工具,接入快、上下文干净、专用角色效果好;但越强调嵌套和自治,越难回答“谁拥有这个任务”“它是否真的完成”“停止父任务会不会误杀独立工作”“重启以后到哪里继续”。这不是某一家产品实现得好或坏,而是 tool-call 型 Subagent 的抽象边界决定的。
第五,它会破坏主会话的 prefix cache 连续性。大模型服务商的 prompt cache 或 KV cache 依赖请求前缀保持稳定:system prompt、工具 schema、历史消息的顺序和字节只要发生变化,后续部分就难以继续命中。旧 Subagent 会新建独立的 Session Stub 和 Orchestrator,使用自己的 system prompt、角色指令和工具集合,在同一模型端点上发起一串与主会话前缀完全不同的请求。主会话等待期间,这些请求会占用缓存容量和有效期;子代理结束后,大段 summary、完整 assistant 文本或工具结果又被回灌到父历史,下一次主请求的可缓存前缀随之增长和分叉。结果是主会话原本连续、稳定的缓存热度容易下降,输入 token 成本和首 token 延迟上升。
这里需要严谨地说:Provider API 并不保证“每启动一次 Subagent 就清空缓存”,不同厂商的缓存淘汰策略也不相同。但从架构上看,临时 Worker 在同一端点制造大量不同前缀,并在结束后向父上下文回灌大结果,确实显著增加了缓存未命中、过期和容量竞争的概率。对于一次短搜索,这个成本可能不明显;当多个 Subagent 并发、各自经历多轮工具调用时,它就会从实现细节变成真实的延迟与费用问题。
第六,Subagent 的工具面天然受限。旧实现不是把父会话当前拥有的能力完整复制给 Worker,而是在创建 Subagent 时按角色传入一组固定工具,再由静态 Tool Planner 暴露。例如 Search 角色可能只拿到 grep_search,Web 角色拿到少量浏览器工具,而通用 Review 在某些路径下甚至没有工具。这种白名单有利于安全,却让能力扩展变得僵硬:用户刚创建的自定义工具、临时启用的 MCP Server、通过 Virtual Toolset 动态展开的工具,以及父 Session 获得的会话级授权,都很难自动、准确地注入 Subagent。
要补齐它,就必须额外复制工具发现、权限校验、MCP 生命周期、宿主上下文和执行桥接;复制少了,Worker 看得到却调用不了,复制多了,又可能越过父会话原有的安全边界。最终每增加一种 Subagent 角色,都要维护一套“它究竟能用什么”的特殊接线。这与 AnyPal 允许用户自由扩展工具和 MCP 能力的方向并不一致。
这张图最关键的不是“并行”,而是所有权:Worker 被工具调用包裹,最终仍要折叠回父消息。
我们的转折点:让并行任务拥有正式身份
AnyPal 后来建立了统一的 SessionGroup 模型:用户眼中的一个会话,底层是一个始终存在的 Coder,加上零到多个可动态加入的 Adversary。这个决策最初是为了消除 Pair 与 SessionGroup 双轨、统一 Electron 与 VS Code 的行为,却也为子会话提供了成熟的基础设施:会话身份、消息历史、运行状态、工作区归属、权限、持久化和事件广播都已经存在。
于是我们做了一个关键选择:并行工作不再创建藏在工具内部的临时代理,而是通过 start_session 批量创建真正的子 SessionGroup。每个子组记录 parentGroupId,拥有自己的 Coder Session、消息流、模型、任务规模、工具授权和可选隔离工作树。父会话是 Coordinator,但子会话不是父调用栈上的函数帧。
这个变化让“并行”从一种角色变成一种能力。AnyPal 不再要求用户切换到名为 Multi-Tasks 的特殊 Agent;开启 Parallel 后,Harness 向符合安全条件的会话动态授予 start_session、get_session、list_subsessions、inject_to_subsession 和 stop_subsession。Agent、Engineering Lead,乃至未来其他产品角色,都可以复用同一套能力,而不必复制一套编排引擎。
更重要的是,子会话继续走正式 Session 的工具发现与授权路径。它可以根据自身角色、任务规模和安全策略获得会话级工具,也能承接用户自定义工具、MCP Server 与按需展开的 Virtual Toolset,而不需要为每一种 Worker 角色手写一套静态工具清单。能力仍然受控,但控制点从“Subagent 构造函数里的固定数组”回到了统一的 Session admission 与 tool grant 机制。
子会话如何改变协作闭环
现在,Coordinator 必须先把任务拆成两个以上真正独立的交付物。单项任务会自动回退到当前会话,避免“为了并行而并行”;存在共享可变文件、顺序依赖或相互等待的工作,也不应被放进同一批次。

子会话启动后,Monitor 持续跟踪 pending、running、retrying、completed、failed 和 stopped。父会话可以廉价查询整体状态,也可以通过 get_session 分页读取子会话最近的消息、工具调用和最终回答。它不再只能相信一句 summary,而可以像真正的技术负责人一样检查证据。
如果子会话跑偏,Coordinator 可使用 inject_to_subsession 发送 steer,在下一次 Agent Loop 边界抢占当前计划;不紧急的建议则进入 queue。若子会话因重启或重试耗尽而停止,同一机制可以唤醒它继续,而不是丢弃已有上下文重新执行。
我们还增加了确定性的 progress gate。一个子会话即使声称 completed,只要没有实质工具调用、只返回意图性文字或缺少真实进展,就会被标为 unverified。Monitor 会自动重新注入继续指令;超过预算后,再把判断权交回父 Coordinator。换句话说,“模型说完成了”不再等于“系统确认交付了”。
与 Subagent 及竞品范式的取舍对比
| 维度 | 工具调用型 Subagent(含常见竞品方案) | AnyPal 子会话 |
|---|---|---|
| 接入成本 | 低;新增一个工具和 Worker Loop 即可 | 高;需要完整会话、存储、监控和权限体系 |
| 上下文隔离 | 好;默认不继承父历史 | 好;独立消息历史,并有明确父子关系 |
| 短任务延迟 | 更低,适合搜索、分类、一次性审查 | 创建和持久化正式会话有额外开销 |
| Prefix cache | 独立 Worker 前缀和结果回灌容易打断主会话缓存热度 | 会话边界明确,可分别维护稳定前缀与独立缓存统计 |
| 工具扩展 | 常由角色静态白名单决定,自定义工具与 MCP 注入困难 | 复用 Session 工具发现、动态授权与 Virtual Toolset |
| 可观察性 | 通常依赖嵌套卡片与最终摘要 | 可读取状态、消息、工具历史和验证结论 |
| 中途干预 | 较弱,常见方式是等待或取消重跑 | 支持 steer、queue、恢复与单独停止 |
| 重启恢复 | 取决于是否额外持久化 Worker | SessionGroup 元数据与子会话状态可恢复 |
| 生命周期 | 依附父轮或工具调用,嵌套语义复杂 | 子会话独立运行;停止边界按 group 明确授权 |
| 并行治理 | 容易递归扩张,需要深度和预算限制 | 只允许独立批次,子会话默认不能再发起 child wave |
| 审计与监管 | 摘要优先,原始证据容易被折叠 | 父会话可分页检查 transcript,并用进度门禁复核 |
| 资源成本 | 相对轻量 | 更高,需要 SQLite 目录、惰性恢复和热缓存控制 |
我们并不认为子会话在所有场景都优于 Subagent。对于一次性的只读搜索、结果可被立即消费且不需要人工进入的任务,专用 Subagent 仍然非常高效。事实上,AnyPal 代码中仍保留部分 Subagent 基础设施,用于嵌套专用执行和兼容历史路径。我们放弃的不是“让另一个模型帮忙”,而是把通用并行工程任务继续建立在临时工具调用之上。
子会话的代价也是真实的。正式会话会增加元数据规模;当 workspace 累积到数千个 SessionGroup 时,整块 JSON 重写曾造成主进程卡顿。为此我们又把元数据迁移到 workspace 级 SQLite,采用单行增量更新、按需 hydrate 和有限热缓存。更强的产品语义,必然要求更严肃的存储工程。
不盲目效仿竞品,用自己的方式解决自己的问题
早期 AnyPal 引入 Subagent,确实参考了当时主流 Coding Agent 的 Coordinator/Worker 路线。参考优秀产品没有问题,问题在于把“竞品已经采用”误当成“我们的最终答案”。竞品面对的是它自己的云端基础设施、模型供应链、封闭工具目录和产品交互;AnyPal 面对的则是 Electron、Web 与服务端多表面运行,用户自选模型端点,本地工作区,插件化工具,以及开放的 MCP 生态。约束不同,最优抽象自然可能不同。
因此,我们没有继续沿着竞品路线给 Subagent 叠加更多补丁:再做一套 Worker 持久化、再造一套工具注入、再增加一套恢复协议,最后得到一个越来越像 Session、却仍藏在工具卡片里的“准会话”。我们选择回到问题本身:用户需要的究竟是一个替主模型跑完短任务的函数,还是一个可进入、可监督、可恢复、可扩展的协作者?对于通用并行工程,答案是后者。
这也是 AnyPal 的产品立场:我们会研究竞品,理解它们解决了什么、为何这样设计;但不会用 UI 名称或功能清单驱动架构,更不会为了看起来“对齐”而复制不适合自身约束的机制。真正的对齐对象应当是用户问题,而不是竞品实现。子会话不是为了标新立异,而是我们在自己的多端运行、开放工具和长期任务场景中,独立推导出的工程答案。
最后的判断:协作需要地址,而不只需要返回值
回头看,Subagent 回答的是:“怎样把一段工作交给另一个模型,并拿回结果?”子会话回答的是另一个问题:“怎样让多个可独立运行、可监督、可恢复的智能体,在同一产品里长期协作?”
前一个问题更像函数调用,后一个问题更像分布式系统。函数调用追求输入、执行和返回值;分布式协作则必须面对身份、所有权、状态、消息、重试、取消、恢复、权限、审计和一致性。
AnyPal 从 Subagent 转向子会话,并不是因为 Subagent 没有价值,也不是为了制造一个不同于竞品的新名词。真正的原因是:当 Agent 从“替用户完成一次任务”走向“组织多个长期运行的任务”时,临时 Worker 已经不足以承载产品承诺。一个真正的协作者必须有地址,可以被找到;有历史,可以被审查;有状态,可以被恢复;有边界,可以被停止;也有证据,证明它不是只说了“我会做”,而是真的做完了。

597

被折叠的 条评论
为什么被折叠?



