步骤并行化
在 Agent 系统中并行化步骤时,错误隔离是关键设计。我们在从 Prompt 到 Harness:企业级 Agent 工程的完整演进之路中讨论了两种错误隔离机制,针对两种不同的并发混乱:
错误隔离是关键设计:
Promise.allSettled保证一个子任务失败不中断其他子任务,失败结果标记__error传递给下游步骤由 LLM 自行判断处理。Context 并发写入安全通过浅拷贝 sharedContext 确保只读前序数据。SSE 事件乱序问题通过taskId分组渲染解决。
下面我们来简单介绍一下原文提到的两种隔离机制,以及它们对应的并发混乱问题。
1. Context 并发写入
假设我们有两个步骤并行执行,并且共享同一个 sharedContext:
sharedContext = { userInput: "分析销售数据", previousResult: "……"}如果两个并行步骤都直接修改它的话:
步骤 A:写入 sharedContext.result = A步骤 B:写入 sharedContext.result = B显然,由于 A,B 的执行顺序不固定,所以可能出现:
- A 覆盖 B;
- B 覆盖 A;
- 一个步骤读到了另一个步骤写到一半的数据。
这就是 Context 的并发写入问题。
1.1 解决方案:软拷贝
文中的解决方式是软拷贝。也就是说,在启动每个并行步骤前,我们给它一份独立的浅拷贝:
const contextA = { ...sharedContext };const contextB = { ...sharedContext };于是:
步骤 A 只读取 contextA,并把结果单独返回步骤 B 只读取 contextB,并把结果单独返回可见,二者都不直接修改原始的 sharedContext,而是等它们都完成后,再由调度器统一合并结果:
const [resultA, resultB] = await Promise.all([ runStepA(contextA), runStepB(contextB)]);
sharedContext.stepAResult = resultA;sharedContext.stepBResult = resultB;浅拷贝的局限性在于,它只复制最外层。
换句话说,如果 Context 里有嵌套对象,比如:
const sharedContext = { user: { name: "Alice" }};
const contextA = { ...sharedContext };那么 contextA.user 和 sharedContext.user 仍指向同一个对象,不同的步骤依然会修改共享数据。
因此,这种方案真正安全的前提是:
在原文中,浅拷贝恰恰是配合只读约束,防止并行步骤直接争抢同一个顶层 Context.
2. SSE 事件乱序问题
同样,假设两个 Agent 任务同时执行:
任务 A:生成报告任务 B:分析代码它们产生的 SSE 事件就可能交错到达前端:
A:开始搜索B:开始读取代码B:读取完成A:搜索完成B:开始分析如果前端把所有事件直接放进同一个列表,用户看到的就是两个任务混在一起,容易误以为事件顺序错乱。
2.1 解决方案:分组
原文中的解决办法是:每条 SSE 事件都携带所属任务的 ID,按照 taskID 进行分组:
{ "taskId": "task-A", "type": "search_started", "message": "开始搜索"}
{ "taskId": "task-B", "type": "code_read", "message": "读取代码完成"}这样,前端收到事件后,不是全部追加到同一个列表,而是根据 taskId 分组:
eventsByTask[event.taskId].push(event);最终就会分别显示:
任务 A1. 开始搜索2. 搜索完成
任务 B1. 开始读取代码2. 读取完成3. 开始分析这样,即使不同任务的事件在网络中交错到达,界面也不会把它们混在一起。
需要注意的是,taskID 并不能解决同一任务内部的乱序,因为它们的 taskID 完全相同。此时,我们还需要给事件增加递增序号:
{ "taskId": "task-A", "sequence": 3, "message": "工具完成"}然后前端就按下面的规则处理:
先按 taskId 分组再按 sequence 排序文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!





