feat(agent): 多智能体协同 v1 —— coordinator 节点(Eino×Anthropic 融合)

LLM 自主在 agent 间路由/委派:orchestrator=ReAct(Eino 出机器),编排认知按 Anthropic
orchestrator-worker 配方(出脑子)。专家=包成工具的子 agent(agent-as-tool),lead 给
每个专家写定制简报(brief)后并行派发、综合。方案见 MULTI_AGENT.md。

为什么 agent-as-tool 而非 Eino host:host 的 specialist 拿原始输入(preHandler
return state.msgs),传不了 lead 写的定制简报,而定制简报正是 Anthropic 多智能体的
精髓。agent-as-tool 让 orchestrator 自己 emit 工具调用、参数 brief 即简报。
= OpenAI agent.as_tool() / Anthropic 研究系统的 orchestrator-worker。

- coordinator.go: specialistTool(react.Agent/ChatModel 包成 InvokableTool,入参 brief,
  精炼返回) + parseSpecialists/buildSpecialists(带工具→react,不带→ChatModel,MCP 工具
  按 spec.tools 过滤) + runCoordinator(lead 提示词=Anthropic 配方) + leadOrchestratorPrompt。
- 双路接入 execDSLNode(compose)+ runGraph(graph.go)的 case coordinator。
- 护栏:禁套娃(专家是内联叶子)/ MaxStep / 专家 I/O 计入共享 Budget / 降级(无
  ToolCallingModel 或 0 专家 → runAgent)。
- streamAgentReply:抽出 runReactAgent 与 runCoordinator 共用的流式回流尾段。
- 复用即得:evaluator-optimizer=harness 低分纠偏;成本天花板=预算护栏;上下文隔离=
  专家独立 react.Agent;观测=每次派发落 agent 轨迹。

测试:parseSpecialists / agent-as-tool 包装(brief 透传+精炼返回+失败作观察) / 降级。
live 验证(真 deepseek):两专家**并行派发**、lead 给各自写**不同定制简报**、最终
**综合**(非拼接)成稿,评测 1.00。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-29 15:37:23 +08:00
parent 49d19d9b59
commit 66caeef35c
6 changed files with 426 additions and 2 deletions
@@ -225,7 +225,12 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar
return
}
defer sr.Close()
o.streamAgentReply(ctx, taskID, b, sr, tr, node, "ReAct 智能体", t0)
}
// streamAgentReply 把一个 agent / 协调者的流式输出回流到 sink:跨分片脱敏 + 计输出 token +
// 落产出(供下游接力) + 轨迹收尾。runReactAgent 与 runCoordinator 共用,杜绝两路尾段漂移。
func (o *Orchestrator) streamAgentReply(ctx context.Context, taskID string, b *board, sr *schema.StreamReader[*schema.Message], tr *execTracer, node, label string, t0 time.Time) {
chunks := 0
var produced strings.Builder // 本节点产出(供下游 agent 接力)
red := harness.NewStreamRedactor() // 输出护栏:跨分片脱敏,杜绝密钥被切断而漏检
@@ -243,7 +248,7 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar
break
}
if rerr != nil {
tr.emit(node, "model", "error", "ReAct 智能体", rerr.Error(), time.Since(t0).Milliseconds())
tr.emit(node, "model", "error", label, rerr.Error(), time.Since(t0).Milliseconds())
return
}
if chunk.Content == "" {
@@ -256,6 +261,6 @@ func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *boar
bud.AddComplete(produced.String()) // 成本护栏:计入输出 token
}
o.recordAgentOutput(b, produced.String())
tr.emit(node, "model", "end", "ReAct 智能体",
tr.emit(node, "model", "end", label,
fmt.Sprintf("%d 段输出 / %d 字", chunks, len([]rune(produced.String()))), time.Since(t0).Milliseconds())
}