OpenDots 管任务编排、上层交互仍可交给 CopilotKit,接口要对上

文章导读
OpenDots 负责把一句话拆成可执行的任务、调度步骤、收敛执行状态;CopilotKit 负责界面上的对话、消息渲染和用户操作。两边不重叠的部分是:任务树、步骤状态、执行结果归 OpenDots;消息历史、会话上下文、用户输入归 CopilotKit。真正容易出问题的是接口层:如果两边对 session 标识、任务标识、结果字段各有一套命名,界面就会显示“请求发出去了但状态不动”。判断标准很简
📋 目录
  1. 一 画出一次交互的字段流向
  2. 二 确定消息与会话状态由哪一层持有
  3. 三 在编排层暴露的调用入口上做一次联调
  4. 四 两边字段名不一致时的对齐做法
  5. 五 失败时先看哪一层的返回
A A

OpenDots 负责把一句话拆成可执行的任务、调度步骤、收敛执行状态;CopilotKit 负责界面上的对话、消息渲染和用户操作。两边不重叠的部分是:任务树、步骤状态、执行结果归 OpenDots;消息历史、会话上下文、用户输入归 CopilotKit。真正容易出问题的是接口层:如果两边对 session 标识、任务标识、结果字段各有一套命名,界面就会显示“请求发出去了但状态不动”。判断标准很简单——同一次交互能否在两边日志里用同一个 id 串起来,串不起来就先别加功能。

如果前端已经用 CopilotKit,加入 OpenDots 后建议只让它接管编排:任务拆分、步骤调度、执行状态与结果。会话与消息历史仍由交互层持有,OpenDots 侧保存 session_id 作为外键即可,不另存一份对话。对接先做三件事:确定请求、任务、结果三类字段的流向;给每个字段定唯一持有方;用一次真实请求在调用入口上联调。字段名不一致时先写映射表再改代码,改名后必须重放同一条请求复验。适用边界是单前端、单编排服务的接入场景,多端共用会话时需要另行确认 id 生成规则。

画出一次交互的字段流向

先别改代码,先在纸上确认一次交互经过哪几个节点。典型链路是四个节点:浏览器界面 → CopilotKit 的 runtime/后端接入点 → OpenDots 的调用入口 → OpenDots 的调度与执行器。职责建议按下表切开,切完之后再谈字段。

职责建议归属说明
用户输入、消息渲染、历史滚动CopilotKit 一侧界面直接消费,放编排层会多一次往返
任务拆分、步骤顺序、重试与超时OpenDots属于编排逻辑,交互层不应判断
会话 id 的生成交互层生成,编排层只保存两边都生成就会出现两套 id
任务状态(pending/running/done/failed)OpenDots唯一真值来源
最终回复文本OpenDots 产出,CopilotKit 渲染交互层只做展示与格式化

三类数据在层间的走向可以这样记:

  • 请求:界面把用户这一轮的 message、session_id、必要的 client_meta 交给 CopilotKit 接入点;接入点补齐或校验 session_id 后,转成 OpenDots 调用入口要求的入参。
  • 任务:OpenDots 收到请求后生成 task_id,把 message 拆成步骤,返回 task_id 和初始状态;此后状态只在 OpenDots 内部变化,交互层通过查询或事件拿快照。
  • 结果:任务结束时 OpenDots 返回结果对象(状态、输出文本、错误码),交互层把输出文本渲染成一条消息,把状态更新到对应的 task_id 上。

画完这张流向图,如果发现某个字段同时出现在两边并且都可能被写,那就是后面状态分叉的源头,先标出来。

确定消息与会话状态由哪一层持有

最省事的做法不是“两边同步”,而是选一个持有方,另一边只读。选择依据有三条:谁直接渲染谁持有、谁需要事务谁持有、谁能独立重启谁持有。按这三条,消息历史与对话上下文建议由 CopilotKit 一侧持有,因为界面要直接渲染并且需要分页加载;任务与执行状态由 OpenDots 持有,因为重试、超时、步骤顺序只有编排层清楚。

OpenDots 管任务编排、上层交互仍可交给 CopilotKit,接口要对上

落到约定上:交互层保存 session_id 和消息列表,把 session_id 作为外键传给 OpenDots;OpenDots 只保存 session_id 与 task_id 的关联,不留一份完整对话副本。反过来,交互层对 task 状态只读,不做乐观改写,界面上的“执行中”以 OpenDots 返回的状态为准,超时未更新时显示为未知而不是自行猜测成功。

验证方式很直接:重启其中一侧,看另一侧是否还能凭 id 恢复。如果重启 OpenDots 后界面上的历史消息还在、任务状态变成未知并可重新查询,说明持有方划分是成立的。

在编排层暴露的调用入口上做一次联调

不要等前端页面全做完再联调。用一个最简的 HTTP 调用先把入口打通,字段全部占位,能返回 task_id 就算通过。下面是一个通用骨架,路径与字段名需要按实际 OpenDots 入口替换:

curl -X POST http://localhost:PORT/opendots/tasks \
  -H 'Content-Type: application/json' \
  -d '{
    "session_id": "sess_demo_001",
    "task_id": null,
    "message": "把这份数据整理成三段摘要",
    "client_meta": {"source": "copilotkit", "locale": "zh-CN"},
    "timeout_ms": 30000
  }'

预期的返回骨架,字段同样是占位:

{
  "task_id": "task_xxx",
  "status": "pending",
  "session_id": "sess_demo_001",
  "result": null,
  "error": null
}

联调时盯三个点:返回里必须带回同一个 session_id,便于两边日志对齐;task_id 必须非空,否则后续状态查不到;status 的取值集合要提前固定,交互层只认这几个值。用这条 curl 跑通之后,再把 CopilotKit 接入点的一侧替换成真实请求,改动面就只剩字段映射。

OpenDots 管任务编排、上层交互仍可交给 CopilotKit,接口要对上

两边字段名不一致时的对齐做法

对接最常见的卡点就是同一个东西三个名字:界面叫 conversationId,接入点叫 session_id,编排层叫 sessionId。别急着全局替换,先写一张映射表放在接入层代码旁边,作为唯一改名入口。

语义交互层字段编排层字段转换位置
会话标识conversationIdsession_id接入层请求构造
本轮用户输入message.contentmessage接入层请求构造
任务标识runIdtask_id接入层响应解析
执行状态runStatusstatus接入层响应解析
结果文本outputresult.text接入层响应解析

映射表要写清三列之外的两件事:空值怎么表示(null 还是缺字段),以及枚举值的对应关系(比如 success 对 done)。改名后的复验步骤建议固定成四步:第一步,重放联调时那条 curl,确认返回未变;第二步,在前端发同一句输入,比对两边日志里的 session_id 与 task_id 是否一致;第三步,检查界面渲染的文本是否等于编排层 result.text 的原文;第四步,构造一次空结果,确认空值约定被两边同样处理。四步都过再提交,不要跳过第三步,文本被二次加工的问题只能在这一步暴露。

失败时先看哪一层的返回

排查要有确定的起点,按返回来源分成三类,动作各不相同。

  • 交互层返回的失败:表现为界面报错、请求未到达编排层,或接入点返回 4xx。下一步动作是看浏览器网络面板和接入点日志,确认请求有没有发出、header 与序列化是否正常,先不动 OpenDots。
  • 编排层返回的失败:拿到 task_id 但 status 变成 failed,或返回体里 error 非空。下一步动作是去 OpenDots 侧按 task_id 查任务详情与步骤日志,定位是拆分阶段失败还是某一步执行失败,同时确认超时时间是否过短。
  • 回传解析阶段的失败:接口返回 200,但界面显示空白或一直停在执行中。这类多半是字段名或枚举没对上,下一步动作是对照映射表逐项核对响应字段,尤其是 status 的取值和 result 的嵌套路径。

三类失败的区分标志是有没有 task_id:没有 task_id 基本在前两层之间,有 task_id 就往编排层和回传解析上找。把这条判断写进排查记录,后续接手的人不用每次从头猜。