17. Skill Orchestration & Concurrency Architecture
本文档记录了系统中关于 Skill 管理机制的演进思路,以及为支撑 Innovator 等 Orchestrator 高效执行多目标任务而设计的“并发子代理 (Concurrent Sub-Agent)”架构。我们正式废除了旧有的 SQLite 数据库绑定方案,确立了 Local File-System First(纯文件系统挂载) 原则。
1. Local File-System First 架构 (移除 SQLite / data/skills 依赖)
在旧版本中,Skill 系统的可用状态、元数据及其层级关系被同时维护在 data/skills/ 和本地 skills.sqlite 数据库里。这种双源结构经常导致:
- 文件存在但由于数据库未记录,导致技能不可用 (幽灵 Bug)。
- 修改本地文件(如重写提示词)后热重载失效。
1.1 核心重构与 rootdir/skills/ 绝对源头
目前的纯无状态化架构中,取消了所有的 DB 查询(弃用 server/services/skill-db 参与核心运行时计算),直接对原生的目录系统进行实时挂载扫描:
- 系统核心组件
skill-config-loader.js会在每次拉取架构配置时,直接遍历项目根目录下的skills/(即path.resolve(__dirname, '../../../skills'))。 - 目录存在即技能存在;内部包含
SKILL.md的文件夹将被全自动解析并作为可用 Agent 注入。如果该文件含有layer: orchestrator或类似的大脑定义,将自动提升为 Orchestrator。 - 这一改动完全去除了状态机依赖并发地保持了 Claude CLI 等底层协议提倡的原始文件驱动属性。
1.2 注入点
当我们首次向 Innovator 等代理生成 system prompt 时,系统扫描出的 fullSystemConfig.agents 配置会实时序列化后填充到提示词中的 {{AVAILABLE_AGENTS}} 占位符内,让主代理在没有任何 DB 信息的前提下也能了解当前所有安装的可调用技能池。
2. 并发子编排引擎 (Concurrent Orchestration Engine)
在研究深度报告 (Deep Research) 或网页浏览时,Innovator 通常需要发起对多个独立话题甚至工具的并行调用,以压缩墙上时间(Wall-clock time)。为了支撑该目标,系统对基础解析器(parser.js)以及对话驱动路由(agent-chat.js)做出了彻底改造。
2.1 突破串行化拦截
传统的设计为了维持“因果链 (Causal Chain)”,使用了 _applySerialConstraint 强制截取 Agent 产生的最后一个 [CALL: xxx] 数据块。现在通过开放 allowParallel: true 选项,系统可以从一次输出流中正则匹配出所有的调度请求:
[CALL: meta_research]
分析 AI 的未来发展趋势
[/CALL]
[CALL: meta_research]
调查自动驾驶汽车的采用率
[/CALL]
以上将返回完整的 Calls Array。
2.2 Promise.all 协程调度
在 /api/agent/:agentId/chat 核心路由中:
- 将串行(
for (const call of calls))阻塞迭代替换为了await Promise.all()。 - 这允许系统在一轮思考响应内,瞬间启动多条
AgentManager.callAgent()协程链路。每一条链路都会实例化独立的底层 Claude 进程并发执行。
2.3 任务追踪标识 (Unique Call ID)
为了对这些并发运行的环境进行隔离和精准回显追踪,路由在启动任务前会为每一个 Sub-Agent 事件分配一个唯一标识,例如:
const callId = "sub-" + randomUUID().substring(0, 6)
向前端的 SSE 事件流中(如 event: 'chunk')挂载该 ID,保证前端不论收到多混乱的异步流交错,都能正确路由进独立的终端。
3. 前端多流终端并发 UI (Multi-Terminal UI)
由于传统的单窗口 msg-assistant 已经无法呈现多个异步进程在彼此互不相关的流速下喷吐的状态,为了匹配上述后端能力,并达成震撼的多线程交互体验,在 Vue 侧 (AgentChat.vue) 引入了“多窗口动态并行卡片流 (Parallel Windows)”:
3.1 状态驱动器 (subAgents Ref)
使用字典格式响应式地保存所有的子任务对象:
const subAgents = ref({
'sub-1a2b3c': { agent: 'meta_research', status: 'running', content: '...' },
'sub-9x8y7z': { agent: 'meta_digest', status: 'success', content: '...' }
});
在收听 sub_agent 事件时,通过 callId 作为 Key 进行实时内容拼接和状态更新,甚至做到独立容器的自动 scrollTop = scrollHeight 下推。
3.2 动态卡片墙 (Grid Flexbox)
在聊天气泡大外框的末端,通过 CSS Grid 自适应布局 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) 渲染:
- 当一个 Agent 并发出了三项分支,这三项任务会像控制台界面一般,在横向平整排列。
- 每一个子容器都拥用专属的 Terminal 等宽字体控制台、带有彩色指示灯(运行中蓝色辉光、完成绿勾、失败红叉)。
- 所有并行终端运行出各自的结果后(或者任意一条发生中断),后台等待在
Promise.all的阻塞点会统一收束收集它们的汇总返回内容(包裹着[Sub Agent 返回结果]),随后统一塞入主 Agent 的反馈下文继续下一轮迭代。