跳转至

Eino 动态创建 subagent 的最小 Demo

Source: 用户提供的可运行 demo(cloudwego/eino) Collected: 2026-09-20 Published: Unknown

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strings"
    "time"

    "github.com/cloudwego/eino-ext/components/model/openai"
    "github.com/cloudwego/eino/adk"
    "github.com/cloudwego/eino/components/model"
    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/components/tool/utils"
    "github.com/cloudwego/eino/compose"
    "github.com/cloudwego/eino/schema"
)

// 演示「动态创建 subagent」:
// 先预定义若干 agent 角色,运行时按角色 new 出 ChatModelAgent,再用 adk.NewAgentTool 包成工具。
// 对父 agent(supervisor)来说它们只是普通 tool,只有被调用时才会跑各自完整的子 agent loop。

// ---------- agent-as-tool:动态创建 subagent ----------

// agentRole 是一个预先设计好的 agent 角色:职责、指令、可用工具。
type agentRole struct {
    name, desc, instruction string
    tools                   []tool.BaseTool
}

func newSubAgentTool(ctx context.Context, cm model.ToolCallingChatModel, r agentRole) (tool.BaseTool, error) {
    // 运行时按角色动态实例化出一个子 agent
    sub, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        r.name,
        Description: r.desc,
        Instruction: r.instruction,
        Model:       cm,
        ToolsConfig: adk.ToolsConfig{ToolsNodeConfig: compose.ToolsNodeConfig{Tools: r.tools}},
    })
    if err != nil {
        return nil, err
    }
    fmt.Printf("  [+] 动态创建 subagent: %s\n", r.name)

    // 关键一步:把子 agent 包成 tool(tool 名 = agent.Name)。调用它会跑一个完整的子 agent loop。
    return adk.NewAgentTool(ctx, sub), nil
}

// ---------- main ----------

type weatherInput struct {
    City string `json:"city" jsonschema_description:"City name, e.g. Beijing"`
}

type calcInput struct {
    Expression string `json:"expression" jsonschema_description:"Arithmetic expression, e.g. 1+2*3"`
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
    defer cancel()

    cm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        APIKey:  os.Getenv("OPENAI_API_KEY"),
        Model:   os.Getenv("OPENAI_MODEL"),
        BaseURL: os.Getenv("OPENAI_BASE_URL"),
    })
    if err != nil {
        log.Fatal(err)
    }

    weather, err := utils.InferTool("get_weather", "Get the current weather of a city",
        func(_ context.Context, in *weatherInput) (string, error) {
            return fmt.Sprintf("%s: sunny, 25C", in.City), nil
        })
    if err != nil {
        log.Fatal(err)
    }

    calc, err := utils.InferTool("calculate", "Evaluate a simple arithmetic expression",
        func(_ context.Context, _ *calcInput) (string, error) { return "42", nil })
    if err != nil {
        log.Fatal(err)
    }

    // 预定义角色(这里硬编码),运行时再按角色动态实例化成 subagent 工具。
    roles := []agentRole{
        {"weather_agent", "An agent that answers weather questions", "You are a weather expert. Use get_weather.", []tool.BaseTool{weather}},
        {"calculator_agent", "An agent that evaluates arithmetic", "You are a math expert. Use calculate.", []tool.BaseTool{calc}},
    }

    tools := make([]tool.BaseTool, 0, len(roles))
    for _, r := range roles {
        t, err := newSubAgentTool(ctx, cm, r)
        if err != nil {
            log.Fatal(err)
        }
        tools = append(tools, t)
    }

    supervisor, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
        Name:        "supervisor",
        Description: "Delegates tasks to subagents",
        Instruction: "You are a supervisor. Delegate each request to the right tool: " +
            "weather_agent / calculator_agent, then summarize the result.",
        Model: cm,
        ToolsConfig: adk.ToolsConfig{
            ToolsNodeConfig:    compose.ToolsNodeConfig{Tools: tools},
            EmitInternalEvents: true, // 让子 agent 内部事件透传到顶层
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println()

    runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: supervisor, EnableStreaming: true})
    iter := runner.Query(ctx, "Tell me the weather in Beijing, then calculate 6*7.")
    for {
        event, ok := iter.Next()
        if !ok {
            break
        }
        if event.Err != nil {
            log.Fatal(event.Err)
        }
        printEvent(event)
    }
}

// printEvent 打印一条事件。用 RunPath 表示调用链(如 supervisor > weather_agent),
// 因为父 flowAgent 会覆盖 AgentName,但 RunPath 保留来源。
func printEvent(event *adk.AgentEvent) {
    if event.Output == nil || event.Output.MessageOutput == nil {
        return
    }
    mv := event.Output.MessageOutput

    msg := mv.Message
    if msg == nil && mv.MessageStream != nil {
        var err error
        if msg, err = schema.ConcatMessageStream(mv.MessageStream); err != nil {
            log.Printf("流式输出错误: %v", err)
            return
        }
    }
    if msg == nil {
        return
    }

    label := event.AgentName
    if len(event.RunPath) > 0 {
        steps := make([]string, 0, len(event.RunPath))
        for _, s := range event.RunPath {
            steps = append(steps, s.String())
        }
        label = strings.Join(steps, " > ")
    }

    for _, tc := range msg.ToolCalls {
        fmt.Printf("[%s] -> 调用工具 %s 参数: %s\n", label, tc.Function.Name, tc.Function.Arguments)
    }
    if msg.Content != "" {
        prefix := ""
        if msg.Role == schema.Tool {
            prefix = "工具返回: "
        }
        fmt.Printf("[%s] %s%s\n", label, prefix, msg.Content)
    }
}