SearchOS 拆任务看编排定义、合并结果看后处理环节

文章导读
拆任务这一步改的是编排定义,合并结果这一步改的是后处理代码,两处混着改是 SearchOS 里最常见的返工来源。判断标准很直接:一处描述“谁去做什么、什么时候停”,另一处处理“已经拿到的一堆结果怎么拼成一个输出”。改之前先确认要动的文件属于哪一侧,改完用子任务列表和输出结构分别验证,不要把合并顺序写在编排配置里,也不要通过调整角色分工去凑输出字段的顺序。
📋 目录
  1. 一 先在目录里把编排定义和后处理代码分开认出来
  2. 二 在编排定义里改一处分工,看任务分配结果变不变
  3. 三 在后处理里改合并顺序,看输出结构变不变
  4. 四 给两处各留一个可观测点
  5. 五 改动前留一份原始文件,做前后对比记录
A A

拆任务这一步改的是编排定义,合并结果这一步改的是后处理代码,两处混着改是 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 这类词,内容在遍历一个已存在的结果列表并决定字段怎么拼,属于后处理。

r>
判断维度编排定义后处理
读入什么用户查询与全局配置各角色返回的结果列表
输出什么子任务清单与角色分配最终响应结构与字段
改动影响面拆分粒度、并发角色数字段顺序、去重与引用归并
典型误改把合并顺序写进配置在合并代码里改角色数量

确认归属之后再动手。如果一份文件同时出现角色声明和结果合并逻辑,说明职责已经混在一起,先不要改行为,只记录这个现状,后续再考虑拆分。

在编排定义里改一处分工,看任务分配结果变不变

一次只改一个最小项,改动才可归因。常用的三个最小项是角色数量、单个角色的工具范围、终止条件;同时改两项以上,子任务列表发生变化时无法判断是哪一项引起的。

SearchOS 拆任务看编排定义、合并结果看后处理环节
# 示意结构,字段名以仓库实际为准
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 目录里的各角色结果,只重跑后处理这一段,避免拆分侧的随机性干扰判断。结构变化的具体表现有三类:顶层字段顺序变化、同名字段取值来源变化、引用条目顺序与重复项数量变化。三类都要记录,只对比条数容易漏掉覆盖问题。

SearchOS 拆任务看编排定义、合并结果看后处理环节

给两处各留一个可观测点

拆分和合并各留一处日志锚点,出问题时能立刻判断断在哪一侧。编排侧打角色分配记录,后处理侧打汇总开始与结束记录,带上 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;三者放在一起才能复现对比。

  1. 备份文件:改动前复制原文件到带时间戳的目录,或用版本控制提交一次干净状态。
  2. 记录字段:run_id、改动文件名、改动的最小项、子任务条数、角色分布、输出顶层字段列表、引用条目数。
  3. 复跑对比:用同一条查询重跑,逐项对照上面的字段,差异写入记录。

记录字段不必多,但要能回答两个问题:这次改动影响的是拆分还是合并,出问题时应该回滚哪一个文件。