以最小demo讲解Eino中的graph编排

代码全部来源于木乔的github,很强的大佬,建议大家可以去B站学习他的视频课。

本文仅以一个不含model节点的graph来展示Eino中的编排效果,仅适合刚开始了解Eino graph的同学

我对graph的理解

我作为一个曾经在本科时期自学了三年的前端开发,认为graph与前端开发的直觉是完全相反的,例如在开发一个Vue组件时,脑子里已经可以对组件有一个大概的认识,后续只要一步步趋近于它就可以,而agent中的graph是极其庞大的,与前端组件是完全不同的量级,故在开发agent个人项目时,需要系统地梳理逻辑。

Graph编排

Eino的graph概念与langgraph相同,甚至编码顺序几乎也相同:

  1. 注册图
  2. 编写节点
  3. 将节点加入图
  4. 连接节点
  5. 编译图
  6. 运行

故熟悉langgraph的同学很快就能看懂Eino的代码

先了解一下Eino中的部分节点概念,以下仅列出示例中用到的内容:

markdown 复制代码
Lambda:最普通的节点,也是最灵活的节点,用于写自己的业务逻辑
ChatModel:模型节点------但是此次没用到
branch:分支(路由),实现if-else的效果

整体流程代码

这里只放出一个最简单的Eino graph编排,无model参与,核心在于看懂Eino作为go语言框架的类型对齐------即节点的输出应与下一个节点的输入类型

实在懒得画图,故用文字拼接一下

markdown 复制代码
、                      /------→ 喵   ------→   End(图出口)
  输入→ 数字映射  ------→ branch分支→ 哈   ------→   End(图出口)
(图入口)               \------→ 没有人类了!------→ End(图出口)
、                        \------→ End(图出口)

这里直接放出完整代码,可以直接拿来跑一下: 测试时输入1 2 3其中一个,观察一下输出结果

go 复制代码
package main

import (
    "context"
    "fmt"

    "github.com/cloudwego/eino/compose"
    "github.com/joho/godotenv"
)

func main() {
    err := godotenv.Load(".env")
    if err != nil {
        panic(err)
    }
    //创建上下文
    ctx := context.Background()

    //注册图
    g := compose.NewGraph[string, string]()

    //编写节点
    lambda0 := compose.InvokableLambda(func(ctx context.Context,
        input string) (output string, err error) {
        if input == "1" {
            return "毫猫", nil
        } else if input == "2" {
            return "耄耋", nil
        } else if input == "3" {
            return "device", nil
        }
        return "", nil
    })
    lambda1 := compose.InvokableLambda(func(ctx context.Context,
        input string) (output string, err error) {
        return "喵", nil
    })
    lambda2 := compose.InvokableLambda(func(ctx context.Context,
        input string) (output string, err error) {
        return "哈!", nil
    })
    lambda3 := compose.InvokableLambda(func(ctx context.Context,
        input string) (output string, err error) {
        return "没有人类了!", nil
    })
    //加入节点
    err = g.AddLambdaNode("lambda0", lambda0)
    if err != nil {
        panic(err)
    }
    err = g.AddLambdaNode("lambda1", lambda1)
    if err != nil {
        panic(err)
    }
    err = g.AddLambdaNode("lambda2", lambda2)
    if err != nil {
        panic(err)
    }
    err = g.AddLambdaNode("lambda3", lambda3)
    if err != nil {
        panic(err)
    }
    //加入分支
    //AddBranch需要传入startNode与分支逻辑
    err = g.AddBranch("lambda0", compose.NewGraphBranch(func(ctx context.Context,
        in string) (endNode string, err error) {
        if in == "毫猫" {
            return "lambda1", nil //这里返回的是节点的名称
        } else if in == "耄耋" {
            return "lambda2", nil
        } else if in == "device" {
            return "lambda3", nil
        }
        // 否则,返回 compose.END,表示流程结束
        return compose.END, nil
    }, map[string]bool{ //这里还需要传入一个map,表示哪些节点是可以使用的分支节点
        "lambda1": true, "lambda2": true, "lambda3": true, compose.END: true,
    }))

    //连接节点
    err = g.AddEdge(compose.START, "lambda0")
    if err != nil {
        panic(err)
    }
    //由于lambda0与branch的连接逻辑已经在上面AddBranch中实现,故这里不必再实现
    err = g.AddEdge("lambda1", compose.END)
    if err != nil {
        panic(err)
    }
    err = g.AddEdge("lambda2", compose.END)
    if err != nil {
        panic(err)
    }
    err = g.AddEdge("lambda3", compose.END)
    if err != nil {
        panic(err)
    }

    //编译运行
    r, err := g.Compile(ctx)
    if err != nil {
        panic(err)
    }

    //运行
    answer, err := r.Invoke(ctx, "2")
    if err != nil {
        panic(err)
    }
    fmt.Println(answer)
}

注册图

go 复制代码
g := compose.NewGraph[string, string]()

可以看到该graph其实十分简单,在最开始时需要注册图,泛型中传入输入类型与输出类型

编写节点

go 复制代码
//编写节点
lambda0 := compose.InvokableLambda(func(ctx context.Context,
    input string) (output string, err error) {
    if input == "1" {
        return "毫猫", nil
    } else if input == "2" {
        return "耄耋", nil
    } else if input == "3" {
        return "device", nil
    }
    return "", nil
})
lambda1 := compose.InvokableLambda(func(ctx context.Context,
    input string) (output string, err error) {
    return "喵", nil
})
lambda2 := compose.InvokableLambda(func(ctx context.Context,
    input string) (output string, err error) {
    return "哈!", nil
})
lambda3 := compose.InvokableLambda(func(ctx context.Context,
    input string) (output string, err error) {
    return "没有人类了!", nil
})

这也就是为什么我最开始会说:编写graph需要提前宏观布局,若是想到哪写到哪,整个编排逻辑会变得乱糟糟,若我直接将"编写节点"单独拿出来,只会让读者一脸懵------为什么这里要这样写,为什么那里要这样做。。。

在我们最开始用文字构建的graph中,我们一共需要四个节点:

  1. 数字映射
  2. 豪猫节点
  3. 耄耋节点
  4. device节点

故在节点编写出他们各自的逻辑

节点注册

go 复制代码
//加入节点
err = g.AddLambdaNode("lambda0", lambda0)
if err != nil {
    panic(err)
}
err = g.AddLambdaNode("lambda1", lambda1)
if err != nil {
    panic(err)
}
err = g.AddLambdaNode("lambda2", lambda2)
if err != nil {
    panic(err)
}
err = g.AddLambdaNode("lambda3", lambda3)
if err != nil {
    panic(err)
}

g.AddLambdaNode(节点id,节点逻辑函数)可将节点依次注册进图中,但此时图中仅有节点(Node)而没有边(Edge)

节点连接

branch

go 复制代码
//加入分支
//AddBranch需要传入startNode与分支逻辑
err = g.AddBranch("lambda0", compose.NewGraphBranch(func(ctx context.Context,
    in string) (endNode string, err error) {
    if in == "毫猫" {
        return "lambda1", nil //这里返回的是节点的名称
    } else if in == "耄耋" {
        return "lambda2", nil
    } else if in == "device" {
        return "lambda3", nil
    }
    // 否则,返回 compose.END,表示流程结束
    return compose.END, nil
}, map[string]bool{ //这里还需要传入一个map,表示哪些节点是可以使用的分支节点
    "lambda1": true, "lambda2": true, "lambda3": true, compose.END: true,
}))

branch分支类似于if-else,根据输入的不同,将路径通过return 节点名路由至该去的目的地

注意AddBranch需要填入3个参数,参数一为接入节点,即"上一个节点是谁"。参数二为一个路由函数,根据上一个节点的输入,return对应的输出。参数三为一个map,表示待选的下游节点

branch连接后,将其余节点连接:

go 复制代码
//连接节点
err = g.AddEdge(compose.START, "lambda0")
if err != nil {
    panic(err)
}
//由于lambda0与branch的连接逻辑已经在上面AddBranch中实现,故这里不必再实现
err = g.AddEdge("lambda1", compose.END)
if err != nil {
    panic(err)
}
err = g.AddEdge("lambda2", compose.END)
if err != nil {
    panic(err)
}
err = g.AddEdge("lambda3", compose.END)
if err != nil {
    panic(err)
}

与langgraph完全一致的逻辑,compose.START表示图起点,compose.END表示图终点,g.AddEdge使用节点名将各节点连接起来

编译与运行

go 复制代码
//编译运行
r, err := g.Compile(ctx)
if err != nil {
    panic(err)
}

//运行
answer, err := r.Invoke(ctx, "2")
if err != nil {
    panic(err)
}
fmt.Println(answer)

编译与运行极为简单,仅需g.Compile(ctx)即可,编译后可使用g.invokeg.stream运行直接输出或流式输出

相关推荐
神秘的猪头1 小时前
Gin 项目为什么要分 Handler、Service、Repository?顺便讲透接口、依赖注入与指针
后端·go
mldong1 小时前
AI Agent 不能自己签字:用 Python 工作流引擎给 AI 加一道人类审批闸门
后端·python·agent
ShallWeL2 小时前
【Agent工程】(15)—— 评测集与回归门禁
人工智能·agent·工作流
七夜zippoe2 小时前
从 Prompt Engineering 到 Agent Engineering:开发范式的代际跃迁
android·ai·prompt·agent·engineering
七夜zippoe2 小时前
AI Agent 的三位一体架构:模型(大脑)+ 工具(双手)+ 记忆(海马体)的深度解析
人工智能·ai·架构·agent·三位一体
梦想的颜色14 小时前
【AI速览】2026年 9月 GPT‑6 Astra 深度解析:Agent 时代的前沿旗舰,能力、成本、落地痛点与选型判断
人工智能·openai·agent·astra·vibecoding·大模型测评·gpt6
米小虾15 小时前
4-bit 量化"几乎无损"?把它放进 Agent 循环里再试一次
人工智能·agent