代码全部来源于木乔的github,很强的大佬,建议大家可以去B站学习他的视频课。
本文仅以一个不含model节点的graph来展示Eino中的编排效果,仅适合刚开始了解Eino graph的同学
我对graph的理解
我作为一个曾经在本科时期自学了三年的前端开发,认为graph与前端开发的直觉是完全相反的,例如在开发一个Vue组件时,脑子里已经可以对组件有一个大概的认识,后续只要一步步趋近于它就可以,而agent中的graph是极其庞大的,与前端组件是完全不同的量级,故在开发agent个人项目时,需要系统地梳理逻辑。
Graph编排
Eino的graph概念与langgraph相同,甚至编码顺序几乎也相同:
- 注册图
- 编写节点
- 将节点加入图
- 连接节点
- 编译图
- 运行
故熟悉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中,我们一共需要四个节点:
- 数字映射
- 豪猫节点
- 耄耋节点
- 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.invoke或g.stream运行直接输出或流式输出