拆任务这一步改的是编排定义,合并结果这一步改的是后处理代码,两处混着改是 SearchOS 里最常见的返工来源。判断标准很直接:一处描述“谁去做什么、什么时候停”,另一处处理“已经拿到的一堆结果怎么拼成一个输出”。改之前先确认要动的文件属于哪一侧,改完用子任务列表和输出结构分别验证,不要把合并顺序写在编排配置里,也不要通过调整角色分工去凑输出字段的顺序。
适用场景是 SearchOS 类检索编排系统需要调整任务拆分粒度或结果合并结构时。操作动作是先按“描述角色与工具”还是“处理结果列表”把文件分成编排定义与后处理两类,再各自改一处最小项。验证方式是同一份输入前后对比子任务列表,同一份中间产物前后对比输出字段结构。风险边界在于字段名和文件位置因仓库而异,改动前必须备份原始文件,回滚依赖备份而非记忆。
先在目录里把编排定义和后处理代码分开认出来
两类文件经常放在相近的目录层级,命名也不总是带 orchestrator 或 postprocess 字样。可以先按下面的特征扫一遍仓库,把候选文件列出来再逐个确认,而不是凭目录名直接下判断。
searchos/
├── config/
│ └── pipeline.yaml # 常见编排定义落点:角色、工具、终止条件
├── orchestrator/
│ ├── planner.py # 生成子任务、分配角色
│ └── roles/
├── postprocess/
│ ├── merge.py # 合并、去重、排序、拼字段
│ └── render.py
└── runs/
└── <run_id>/
├── subtasks.jsonl # 拆分产物
└── merged.json # 合并产物
判断依据看文件在描述什么。出现 role、agent、tool、step、stop_when、max_rounds 这类词,内容在声明“有哪些角色、每个角色能用哪些工具、什么条件下结束”,属于编排定义。出现 results、items、merge、dedupe、sort、sources、citations 这类词,内容在遍历一个已存在的结果列表并决定字段怎么拼,属于后处理。
| 判断维度 | 编排定义 | 后处理 |
|---|---|---|
| 读入什么 | 用户查询与全局配置 | 各角色返回的结果列表 |
| 输出什么 | 子任务清单与角色分配 | 最终响应结构与字段 |
| 改动影响面 | 拆分粒度、并发角色数 | 字段顺序、去重与引用归并 |
| 典型误改 | 把合并顺序写进配置 | 在合并代码里改角色数量 |
确认归属之后再动手。如果一份文件同时出现角色声明和结果合并逻辑,说明职责已经混在一起,先不要改行为,只记录这个现状,后续再考虑拆分。
在编排定义里改一处分工,看任务分配结果变不变
一次只改一个最小项,改动才可归因。常用的三个最小项是角色数量、单个角色的工具范围、终止条件;同时改两项以上,子任务列表发生变化时无法判断是哪一项引起的。
# 示意结构,字段名以仓库实际为准
orchestrator:
roles:
- name: retriever
tools: [search, fetch]
- name: verifier
tools: [fetch] # 只缩小这一处工具范围
stop_when: no_new_sources
验证方式是跑同一条查询两遍,分别保存 runs/<run_id>/subtasks.jsonl,对比子任务条数、每个子任务的角色名、绑定工具列表。如果子任务列表完全没变,说明改的位置只是说明性配置或未被加载;先确认配置文件路径确实被入口读取,再怀疑字段名。字段名拼错通常不会报错,只会静默走默认值,这一点需要结合环境确认。
风险边界:缩小工具范围可能让某个角色拿不到必要输入,表现为子任务存在但结果为空。这种情况属于拆分侧问题,不要转去后处理排查。
在后处理里改合并顺序,看输出结构变不变
合并顺序直接决定输出字段的排列与覆盖关系。多个角色都返回同名字段时,后写入的会覆盖先写入的;引用列表按角色分批 append 还是统一去重,最终 citations 的顺序也不同。
# 示意骨架,函数名与字段名以仓库实际为准
def merge(results, order=("retriever", "verifier")):
merged = {"items": [], "citations": []}
for name in order:
part = results.get(name) or []
merged["items"].extend(part)
merged["citations"].extend(collect_citations(part))
return dedupe(merged)
验证必须使用同一份中间产物,也就是固定一个 run 目录里的各角色结果,只重跑后处理这一段,避免拆分侧的随机性干扰判断。结构变化的具体表现有三类:顶层字段顺序变化、同名字段取值来源变化、引用条目顺序与重复项数量变化。三类都要记录,只对比条数容易漏掉覆盖问题。
给两处各留一个可观测点
拆分和合并各留一处日志锚点,出问题时能立刻判断断在哪一侧。编排侧打角色分配记录,后处理侧打汇总开始与结束记录,带上 run_id 和条目数。
grep -E "role_assign|merge_start|merge_end" runs/<run_id>/*.log
正常顺序应该是先出现若干条 role_assign,再出现一条 merge_start,最后一条 merge_end。只有 role_assign 没有 merge_end,问题在后处理;一条 role_assign 都没有,问题在编排定义没被加载或查询没进入拆分。没有日志能力时,用中间产物代替:subtasks.jsonl 是否存在、条数是否为零,merged.json 是否生成、字段是否齐全,同样能区分断点位置。
改动前留一份原始文件,做前后对比记录
回滚依赖备份而不是记忆。需要保留的是被改动的编排配置文件、后处理源码文件,以及改动前那次运行的 subtasks.jsonl 与 merged.json;三者放在一起才能复现对比。
- 备份文件:改动前复制原文件到带时间戳的目录,或用版本控制提交一次干净状态。
- 记录字段:run_id、改动文件名、改动的最小项、子任务条数、角色分布、输出顶层字段列表、引用条目数。
- 复跑对比:用同一条查询重跑,逐项对照上面的字段,差异写入记录。
记录字段不必多,但要能回答两个问题:这次改动影响的是拆分还是合并,出问题时应该回滚哪一个文件。