多 Agent 编排:同一运行时中的隔离与协作
多 Agent 编排:同一运行时中的隔离与协作
学习目标
理解 CopilotKit 如何在一个运行时中管理多个 Agent:
- Agent 注册表与隔离策略
- 线程管理与锁定机制
- 作用域工具 vs 全局工具
- 动态 Agent 工厂(多租户场景)
- ProxiedAgent 模式:前端透明访问远程 Agent
前置知识
本章涉及多 Agent 路由与 Agent 核心循环的通用原理,建议先阅读:
下文假设你已理解上述概念,直接聚焦 CopilotKit 的多 Agent 实现。
项目实践
Agent 注册表
AgentRegistry 是 CopilotKitCore 内部的 Agent 管理器:
注册表结构(伪代码):{ agents: Map<agentId, AbstractAgent>, subscribers: Map<agentId, AgentSubscriber[]>, register(agentId, agent) → 注册新 Agent get(agentId) → AbstractAgent, unregister(agentId) → 移除 Agent}每个 Agent 拥有独立的:
agentId:唯一标识description:Agent 描述(Agent 用此理解自身角色)threadId:当前会话的线程 IDmessages[]:消息历史state:自定义状态
线程隔离
每个 Agent 实例通过 threadId 隔离对话状态:
线程生命周期:1. 创建:首次 run/connect 时自动创建 threadId2. 运行:同一 threadId 的所有消息追加到同一历史3. 切换:切换 threadId 时,清除本地 messages/state4. 停止:stop 操作针对特定 threadId线程锁定机制(Intelligence 模式):
当 Agent 正在运行某个 threadId 时,该线程被锁定。其他 run 请求会收到 AGENT_THREAD_LOCKED 错误码:
错误码体系(CopilotKitCoreErrorCode):- AGENT_THREAD_LOCKED:线程被其他 run 占用- AGENT_RUN_FAILED_EVENT:协议级运行失败- AGENT_RUN_ERROR_EVENT:协议级运行错误锁定通过 Redis 实现(Intelligence 模式):
lockTtlSeconds:锁过期时间(默认 20s,最大 3600s)lockHeartbeatIntervalSeconds:心跳续约间隔(默认 15s,最大 3000s)lockKeyPrefix:自定义 Redis key 前缀
TTL 兜底:如果用户断开连接,锁最终会因 TTL 过期而自动释放,防止永久锁定。
作用域工具
工具可以通过 agentId 绑定到特定 Agent:
工具作用域模式:{ name: "getCalendar", agentId: "calendar-agent", // 仅 calendar-agent 可用 handler: () => { ... }}
{ name: "getWeather", // 无 agentId → 所有 Agent 可用 handler: () => { ... }}作用域工具的查找流程:
- 检查是否有匹配当前 agentId 的同名工具
- 若无,查找无 agentId 的全局工具
- 若仍无,触发 catch-all 或返回 “tool not found”
动态 Agent 工厂
AgentsFactory 支持根据请求上下文动态创建 Agent:
const runtime = new CopilotSseRuntime({ agents: ({ request }) => { const userId = request.headers.get("x-user-id"); const locale = request.headers.get("x-locale"); return { default: createAgentForUser(userId, locale) }; }});resolveAgents() 函数的解析逻辑(源码验证):
- 若
agents是函数 → 调用agents({ request })并 await 结果 - 若
agents是对象/Promise → 直接 await
典型应用场景:
- 多租户:每个租户独立的 Agent 配置
- A/B 测试:根据用户分组返回不同 Agent
- 灰度发布:部分用户使用新 Agent
ProxiedAgent 模式
前端通过 ProxiedCopilotRuntimeAgent 将远程 Agent 代理为本地对象:
前端代码透明使用:const agent = core.getAgent("support-agent");// agent 是 ProxiedCopilotRuntimeAgent 实例// 但前端代码只需知道它实现了 AbstractAgent 接口关键设计:
runtimeAgentIdvsagentId:本地注册用agentId,远程路由用runtimeAgentIdruntimeAgentId可在注册时覆盖(registerProxiedAgent({ runtimeAgentId: "xxx" }))- 若
runtimeAgentId和agentId都为空 → 抛出异常(防止产生/agent//run这样的畸形 URL)
前端 Agent 切换
React hook useAgent({ agentId }) 切换当前交互的 Agent:
const { messages, runAgent } = useAgent({ agentId: selectedAgentId });切换 Agent 时:
- 旧 Agent 的订阅取消
- 新 Agent 的消息历史加载
- 前端 UI 反映新 Agent 的状态
问题与规避
| 问题 | 规避策略 |
|---|---|
| 线程锁定冲突(并发 run) | 前端使用 isRunning 状态阻止重复 run;Intelligence 模式的 Redis 锁做服务端兜底 |
| Agent 工厂缓存爆炸(每次创建新实例) | 在工厂内部实现 LRU 缓存;限制同时存在的 Agent 实例数 |
runtimeAgentId 和 agentId 混淆 | 文档约定:agentId 是本地注册 key,runtimeAgentId 是远程路由 key |
| 切换 Agent 时消息闪烁(两个 run 的事件交错) | connectAgent 前先 detachActiveRun;桥接订阅确保消息实时同步 |
ProxiedAgent 的 clone() 不完整 | clone() 方法需要深拷贝 headers、messages、state、delegate(Intelligence 模式) |
设计取舍
平行多 Agent vs 嵌套多 Agent
CopilotKit 的多 Agent 是”平行”关系:
平行模式(CopilotKit):
- 每个 Agent 独立运行,前端选择与哪个 Agent 交互
- Agent 之间不直接通信
- 适合:不同领域的独立 Agent(如”客服 Agent” vs “技术 Agent”)
嵌套模式(LangGraph 多图):
- Agent 内部包含子 Agent
- 子 Agent 的结果合并到父 Agent
- 适合:复杂任务的子任务分解
两者不冲突:CopilotKit 的每个 Agent 本身可以是 LangGraph 多图。
静态注册 vs 动态工厂
| 维度 | 静态注册 | 动态工厂 |
|---|---|---|
| 适用场景 | 开发环境、少量固定 Agent | 生产环境、多租户 |
| 性能 | 无创建延迟 | 每次请求需创建/查找 |
| 灵活性 | 低(Agent 列表硬编码) | 高(根据请求动态创建) |
| 缓存 | 天然缓存(启动时创建) | 需要手动实现缓存 |
参考来源
packages/core/src/core/core.ts(AgentRegistry、Agent 管理)packages/core/src/agent.ts(ProxiedCopilotRuntimeAgent)packages/runtime/src/v2/runtime/core/runtime.ts(AgentsFactory、resolveAgents)packages/runtime/src/v2/runtime/runner/intelligence.ts(IntelligenceAgentRunner 线程锁)