同一任务的调度与续跑对比
同一任务的调度与续跑对比
目标
延续《同一任务的请求与回复落盘对比》,继续回答一个更底层的问题:
为什么同一个长任务,在 OpenCode 里会落成一串 message + part,而在 Claude Code 里更像一串 tool_use + tool_result?
根因不在“存储格式”本身,而在:
- 调度层怎么组织一次任务
- 工具执行后怎么决定继续还是停止
- 系统把哪一层对象视为“主对象”
样本任务
仍然用同一个样本:
- 主题:
Kiro 反代方案研究 - OpenCode 真实会话:
ses_32971ea5fffe2xliqQDUy4vj2q - Claude Code 参考实现:
claude-code-rev
先说结论
同一个任务在两边看起来不一样,是因为两边的“主线对象”不同:
OpenCode的主线对象是session messageClaude Code的主线对象是query turn
这会直接决定:
- 何时生成新的 assistant 记录
- 工具执行结果挂到哪里
- 停止和续跑是由谁裁决
OpenCode:session-first
OpenCode 更像“会话编排器”。
关键代码:
- /Users/util6/fork-code/opencode/packages/opencode/src/session/prompt.ts
- /Users/util6/fork-code/opencode/packages/opencode/src/session/processor.ts
- /Users/util6/fork-code/opencode/packages/opencode/src/session/message-v2.ts
它的心智模型是:
- 用户消息进入 session
- 系统创建 assistant message
- 本轮流式输出被拆成多个
part processor处理完后只返回三种结果continuecompactstop
prompt.loop()再决定是否进入下一轮
所以在 OpenCode 里,一次长任务的自然结果就是:
- 一串 user message
- 一串 assistant message
- 每条 assistant message 再拆成 reasoning/tool/text parts
也就是说,它先假定“会话记录”是主对象,然后把工具、推理、文本都挂到会话对象下面。
为什么会出现很多 assistant message
因为 OpenCode 不是把整条长任务压成一个超级大 turn。
它更像:
- 这一轮创建一个 assistant message
- 往里面写 reasoning / tool / status / text
- 本轮结束
- 如果还要继续,再进入下一轮,通常又会生成新的 assistant message
所以你在落盘里看到的是:
继续- 一条 assistant 进度回复
任务进度如何- 一条 assistant 进度回复
我如何查看还在运行中的任务?- 一条 assistant 解释回复
- 最后才是一条综合方案回复
这不是“被切碎了”,而是它本来就把任务推进建模成多条会话消息。
Claude Code:query-first
Claude Code 更像“统一 query loop”。
关键代码:
- /Users/util6/fork-code/claude-code-rev/src/QueryEngine.ts
- /Users/util6/fork-code/claude-code-rev/src/query.ts
它的心智模型是:
- 用户消息进入 transcript
query.ts启动一轮 query loop- 流式拿 assistant 输出
- 如果发现
tool_use,立即进入工具执行 - 工具结果塞回消息流
- 再决定是否续下一轮
所以它更关心的是:
- 这轮用了什么工具
- 工具返回了什么
- 是否还需要 follow-up
而不是像 OpenCode 那样优先关心:
- 这轮 assistant message 里有哪些 parts
为什么 transcript 看起来像执行日志
因为它的主对象不是“assistant message 容器”,而是“query loop 中的事件”。
在这套设计里,最关键的事实是:
- 用户消息要写 transcript
- 工具调用要写 transcript
- 工具结果要写 transcript
- 最终成功结果通过
result/success向上层返回
所以你从本地 transcript 读到的就更像:
user
tool_use
tool_result
tool_use
tool_result
tool_use
tool_result
...而不是:
assistant reasoning
assistant tool
assistant text同一个任务,续跑机制为什么不同
这才是两个系统差别最大的地方。
OpenCode 的续跑
OpenCode 的续跑决定非常收敛。
processor 在一轮结束后,主要返回:
continuecompactstop
然后外层 prompt.loop() 处理。
这意味着它的停止判断很像一个小型状态机出口:
- 这轮没被阻塞
- 没报错
- 还有后续工具或续跑理由
- 那就继续
否则就停。
所以 OpenCode 的续跑更像:
每一轮处理完,再做一次明确裁决。
Claude Code 的续跑
Claude Code 的续跑嵌在 query loop 里面。
决定是否继续的关键变量之一是:
needsFollowUp
只要本轮 assistant 输出里出现 tool_use,系统就会认为这一轮还没结束。
但就算没有新的 tool_use,它也不一定立刻停,还会继续判断:
- prompt-too-long 恢复
- max_output_tokens 恢复
- stop hook
- token budget continuation
所以 Claude Code 的续跑更像:
系统会尽量把这一条请求自己推进到不能再推进为止。
为什么这会影响落盘样子
因为“续跑”影响“落盘单位”。
对 OpenCode 来说
续跑边界在 processor -> prompt.loop() 之间。
所以每一轮比较容易沉淀成一个新的 assistant message。
结果就是:
- 会话消息很多
- 每条消息内部 parts 很丰富
- 用户看数据库时更像在看聊天记录
对 Claude Code 来说
续跑边界就在 query loop 内部。
系统不急着把每一步都包装成独立 assistant 消息,而是优先推进:
- tool_use
- tool_result
- 下一轮 query
结果就是:
- transcript 更像流水账
- 事件密度高
- 用户看日志时更像在看 runtime trace
放回到 Kiro 这个样本里
OpenCode 里的真实推进方式
同一个任务在 OpenCode 里大致被推进成下面这样:
- 用户提出研究需求
- assistant 写 reasoning
- assistant 启多个后台 task
- assistant 读本地文件、grep、glob
- 系统插入 background complete reminder
- 用户发
继续 - assistant 给阶段性进度答复
- 用户继续问进度和任务状态
- assistant 再给多次阶段性答复
- 所有后台任务完成后,assistant 产出最终方案
这里你能看到:
- 用户交互被当成任务的一部分
- 系统提醒被当成消息的一部分
- assistant 的中间答复被完整保留
所以落盘自然像“多次对话请求”。
Claude Code 里的对应推进方式
如果同一个任务发生在 Claude Code,更合理的主线会是:
- 用户发一次研究需求
- 系统在同一个 query loop 里连续读本地文件
- 连续发外部搜索和抓取
- 连续消费 tool_result
- 如果还需继续,就直接下一轮 follow-up
- 最后给上层一个
result/success
这里的重点不是“中间给了用户几次解释”,而是“这条 query 做完了哪些动作”。
所以它更像“一个大任务的一串执行事件”。
用户体验上的直接后果
OpenCode
优点:
- 中间状态更显性
- 用户更容易看到“当前任务走到了哪一步”
- 更容易形成多轮协作感
代价:
- 数据库里会有很多消息和 part
- 想提取“一次完整回复正文”时,反而要自己拼接
Claude Code
优点:
- 执行链很连续
- query loop 自推进能力更强
- 更适合恢复和重放运行轨迹
代价:
- transcript 不像聊天稿
- 用户很难直接从本地 transcript 里看见最终答复全文
最终结论
同一个长任务之所以会在两边长成不同的落盘形态,不是因为一边“更完整”、另一边“更简化”,而是因为它们把不同层级当成了系统主对象:
OpenCode:主对象是 session messageClaude Code:主对象是 query turn
因此:
OpenCode更像把协作过程写进会话Claude Code更像把执行过程写进日志
再压成一句:
OpenCode 先建会话,再让工具服务会话;Claude Code 先推进 query,再让 transcript 记录推进过程。