package eino import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/flow/agent/react" "github.com/cloudwego/eino/schema" "github.com/sundynix/sundynix-dispatcher/internal/dsl" "github.com/sundynix/sundynix-dispatcher/internal/harness" ) // 多智能体协同(Eino × Anthropic 融合,详见仓库 MULTI_AGENT.md): // orchestrator = react.Agent(机器=Eino),编排认知按 Anthropic orchestrator-worker 配方; // 专家 = 包成工具的子 agent(agent-as-tool),lead 给每个专家写定制简报(brief)后并行派发、综合。 // leadOrchestratorPrompt 是协调者(lead)的编排提示词 —— Anthropic 配方:先分解、按复杂度定额(别过度派发)、 // 给每个专家写明确简报、并行派发、收齐后综合消解冲突,不直接拼接专家原文。 const leadOrchestratorPrompt = `你是多智能体协调者(lead)。你手下有若干专家,每个专家是你可以调用的一个工具。 工作方式: 1. 先想清楚任务能否拆分、需要哪几个专家、各花多少功夫——简单任务用 1 个专家甚至自己直接答,复杂任务才并行派多个,不要过度派发。 2. 调用专家时,在 brief 参数里给它写一份明确的简报:要它完成的子任务、期望的输出形式、边界与约束;不要把原问题原样丢给它。 3. 可以在一轮里并行调用多个专家。收齐各专家的结论后,由你综合成一段连贯、完整的最终答复,消解专家之间的冲突,必要时点明哪个结论来自哪个专家。 不要把专家的原始返回直接拼接给用户——要消化、综合。` // specialistCondensedSuffix 追加到每个专家的系统提示词:要求精炼返回,省 orchestrator 综合时的上下文。 const specialistCondensedSuffix = "\n\n返回要求:只给结论与关键依据要点,简洁,不要堆砌原始材料或冗长背景。" // specialistSpec 是协调者节点配置里的一个子智能体(专家)定义。 type specialistSpec struct { Name string Use string // 用途/擅长,作为工具描述供 lead 判断何时调 System string // 专家人设 Tools []string // 该专家可用的 MCP 工具名子集(空=纯对话专家) } // parseSpecialists 从协调者节点 config 的 agents 列表解析出专家定义。 func parseSpecialists(cfg map[string]any) []specialistSpec { raw, _ := cfg["agents"].([]any) var out []specialistSpec for _, item := range raw { m, ok := item.(map[string]any) if !ok { continue } s := specialistSpec{Name: cstr(m, "name"), Use: cstr(m, "use"), System: cstr(m, "system")} if s.Name == "" { continue } if tools, ok := m["tools"].([]any); ok { for _, t := range tools { if ts, ok := t.(string); ok && strings.TrimSpace(ts) != "" { s.Tools = append(s.Tools, strings.TrimSpace(ts)) } } } out = append(out, s) } return out } // specialistTool 把一个专家(react.Agent 或 ChatModel)包成 Eino InvokableTool(agent-as-tool): // 入参 brief = lead 写给它的定制简报;返回专家的精炼结论。每次派发落一条 agent 轨迹。 type specialistTool struct { info *schema.ToolInfo name string run func(ctx context.Context, brief string) (string, error) tr *execTracer } func (s *specialistTool) Info(context.Context) (*schema.ToolInfo, error) { return s.info, nil } // InvokableRun 跑一次专家派发:取 lead 写的 brief → 执行子 agent → 返回精炼结论给 orchestrator。 // 专家失败作为"观察"返回(不中断协调);专家 I/O 计入共享预算(Anthropic 的成本克制由 Budget 兜底)。 func (s *specialistTool) InvokableRun(ctx context.Context, argsJSON string, _ ...tool.Option) (string, error) { var args struct { Brief string `json:"brief"` } if argsJSON != "" { _ = json.Unmarshal([]byte(argsJSON), &args) } brief := strings.TrimSpace(args.Brief) if brief == "" { brief = "(协调者未给简报,请按你的职责处理当前任务)" } if bud := harness.BudgetFrom(ctx); bud != nil { bud.AddPrompt(brief) } end := s.tr.span("agent:"+s.name, "agent", "派发专家 "+s.name) out, err := s.run(ctx, brief) if err != nil { end("专家执行失败", err) return "专家 " + s.name + " 执行失败:" + err.Error(), nil } if bud := harness.BudgetFrom(ctx); bud != nil { bud.AddComplete(out) } end("简报 "+truncate(brief, 100)+" → "+truncate(out, 160), nil) return out, nil } // buildSpecialists 据配置建专家工具集。带工具→react.Agent(过滤后的 MCP 工具子集);不带→纯 ChatModel。 // 无可用模型的专家跳过。专家是内联叶子(非图节点),天然不能再是 coordinator → 杜绝套娃递归。 func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistSpec, b *board, taskID string, tr *execTracer) []tool.BaseTool { allTools := o.agentTools(b, taskID, tr) // 全量 MCP 工具,下面按 spec.Tools 过滤 byName := map[string]tool.BaseTool{} for _, t := range allTools { if info, err := t.Info(ctx); err == nil { byName[info.Name] = t } } var out []tool.BaseTool for _, spec := range specs { sys := firstNonEmpty(spec.System, defaultAgentSystem) + specialistCondensedSuffix run := o.specialistRunner(ctx, spec, sys, byName) if run == nil { tr.info("coordinator", "system", "专家跳过", "无可用模型:"+spec.Name) continue } out = append(out, &specialistTool{ name: spec.Name, tr: tr, run: run, info: &schema.ToolInfo{ Name: spec.Name, Desc: firstNonEmpty(spec.Use, "专家 "+spec.Name), ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ "brief": {Type: schema.String, Desc: "给该专家的明确简报:子任务、期望输出、边界约束", Required: true}, }), }, }) } return out } // specialistRunner 构造专家执行闭包:带工具且模型支持函数调用→react.Agent.Generate;否则→ChatModel.Generate。 // 无可用模型返回 nil(该专家被跳过)。 func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec, sys string, byName map[string]tool.BaseTool) func(context.Context, string) (string, error) { var tools []tool.BaseTool for _, tn := range spec.Tools { if t, ok := byName[tn]; ok { tools = append(tools, t) } } if len(tools) > 0 { if tcm := o.pool.ToolCallingModel(); tcm != nil { ag, err := react.NewAgent(ctx, &react.AgentConfig{ ToolCallingModel: tcm, ToolsConfig: compose.ToolsNodeConfig{Tools: tools}, MaxStep: reactMaxStep(), StreamToolCallChecker: streamHasToolCall, }) if err == nil { return func(c context.Context, brief string) (string, error) { msg, gerr := ag.Generate(c, []*schema.Message{schema.SystemMessage(sys), schema.UserMessage(brief)}) if gerr != nil { return "", gerr } return msg.Content, nil } } } // 工具型专家但模型不支持函数调用 → 退纯对话(下方) } cm := o.pool.ChatModel() if cm == nil { return nil } return func(c context.Context, brief string) (string, error) { msg, gerr := cm.Generate(c, []*schema.Message{schema.SystemMessage(sys), schema.UserMessage(brief)}) if gerr != nil { return "", gerr } return msg.Content, nil } } // runCoordinator 执行多智能体协调节点:orchestrator = ReAct(专家=agent-as-tool),lead 提示词按 // Anthropic 配方(分解→定制简报→并行派发→综合)。无 ToolCallingModel / 0 可用专家 → 降级单 agent。 func (o *Orchestrator) runCoordinator(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) { specs := parseSpecialists(n.Config) tcm := o.pool.ToolCallingModel() if tcm == nil || len(specs) == 0 { tr.info(node, "system", "协调者降级", "模型不支持函数调用或未配置专家,退回普通对话") o.runAgent(ctx, taskID, b, system, tr, node) return } specialists := o.buildSpecialists(ctx, specs, b, taskID, tr) if len(specialists) == 0 { tr.info(node, "system", "协调者降级", "无可用专家,退回普通对话") o.runAgent(ctx, taskID, b, system, tr, node) return } ag, err := react.NewAgent(ctx, &react.AgentConfig{ ToolCallingModel: tcm, ToolsConfig: compose.ToolsNodeConfig{Tools: specialists}, MaxStep: reactMaxStep(), StreamToolCallChecker: streamHasToolCall, }) if err != nil { tr.emit(node, "model", "error", "构建协调者", err.Error(), 0) o.runAgent(ctx, taskID, b, system, tr, node) return } // lead 提示词 = Anthropic 编排配方 + 节点自定义任务背景。 leadSys := leadOrchestratorPrompt if s := strings.TrimSpace(system); s != "" && s != defaultAgentSystem { leadSys += "\n\n任务背景:" + s } rc := &RunCtx{ UserID: b.uid, SessionID: b.sid, System: leadSys, Query: b.query, History: b.history, ToolOut: append(append([]string{}, b.toolOut...), b.refs...), Upstream: append([]string{}, b.agentOut...), // 前序协作 agent 产出 → 接力 } msgs, _ := buildMessages(ctx, rc) if bud := harness.BudgetFrom(ctx); bud != nil { for _, m := range msgs { bud.AddPrompt(m.Content) } if bud.Exceeded() { if b.fatalErr == nil { b.fatalErr = errBudget } tr.emit(node, "system", "error", "token 预算", "已达单任务预算上限,中止", 0) return } } tr.emit(node, "model", "start", "多智能体协调", fmt.Sprintf("%d 个专家可派发", len(specialists)), 0) t0 := time.Now() sr, err := ag.Stream(ctx, msgs) if err != nil { tr.emit(node, "model", "error", "多智能体协调", err.Error(), time.Since(t0).Milliseconds()) return } defer sr.Close() o.streamAgentReply(ctx, taskID, b, sr, tr, node, "多智能体协调", t0) }