正好我们团队也在开发一个 Java 生态的 FEL 框架,因其主打简洁流畅的任务编排能力,获得了一定关注。FEL 框架不追求炫酷的图结构,而是专注于让开发者用最自然的方式定义 AI 工作流。
我们和 Eino 团队瞄准的都是提升 AI 应用的开发效率与可维护性,但我们的 FEL 框架和 Eino 框架走的是两条截然不同的路。
所以就写了一个对比文章,从代码示例出发,介绍一下 FEL 框架与 Eino 不同的编排设计哲学,同时也很想听听大家的意见和建议。
一、简单场景:入门流程演示
让我们先来看一个最基础的对话流程:
┌─────┐ ┌────────┐ ┌───────┐ ┌─────┐
│start│───→│ prompt │───→│ model │───→│ end │
└─────┘ └────────┘ └───────┘ └─────┘
这几乎是所有 LLM 应用的起点。
✅ Eino 的做法:链式 or 图式
Eino 提供了三种编排方式:Chain、Graph、Workflow。对于这个简单场景,推荐使用 Chain:
chain, _ := NewChain[map[string]any, *Message]().
AppendChatTemplate(prompt).
AppendChatModel(model).
Compile(ctx)
chain.Invoke(ctx, map[string]any{"query": "what's your name?"})
干净利落,链式调用清晰表达了执行顺序。
如果使用 Graph 方式,可以获得更高自由度,但是代码会变得复杂:
graph := NewGraph[map[string]any, *schema.Message]()
_ = graph.AddChatTemplateNode("node_template", chatTpl)
_ = graph.AddChatModelNode("node_model", chatModel)
_ = graph.AddEdge(START, "node_template")
_ = graph.AddEdge("node_template", "node_model")
_ = graph.AddEdge("node_model", END)
compiledGraph, err := graph.Compile(ctx)
if err != nil {
return err
}
compiledGraph.Invoke(ctx, map[string]any{"query":"what's your name?"})
多了节点命名、显式连边等操作——冗余感上升的代价,换来了更高的自由度。
✅ FEL 的做法:贴近描述业务的写法
FEL 坚持“大道至简”,只提供一种 Fluent API 风格的流程定义,即使是新手,也能快速搭建基础流程:
AiProcessFlow flow = AiFlows.create()
.prompt(Prompts.human("question: {query}"))
.generate(model)
.reduce(() -> "", (acc, chunk) -> acc += chunk.text())
.close();
flow.converse().offer(Tip.from("query", "what's your name?"));
流程定义没有图、节点 ID 或 start/end 标记,整个调用链像一条自然的语言流水线,更贴近业务描述。
在复杂场景下,FEL 的这种设计优势更加明显:即便流程包含多步生成、多模型协作和流式处理,代码仍然保持简洁,开发者可以更专注于业务逻辑。
这种设计的背后,是一种以开发者体验为中心的理念:只需要专注开发业务本身,就能写出可靠的 AI 流程。
二、进阶挑战:优雅的条件分支
真实世界的应用从来不是一条直线。加入条件判断后,可以进一步校验编排能力。
我们扩展一下需求:
┌─────┐ ┌────────┐ ┌───────┐ ┌──────────┐
│start│───→│ prompt │───→│ model │───→│need log? │
└─────┘ └────────┘ └───────┘ └─────┬────┘
│
┌────────┼────────┐
│Yes │No │
▼ ▼ │
┌────────┐ ┌─────┐ │
│log │──→│ end │◄──┘
└────────┘ └─────┘
在模型输出后,判断是否需要记录日志。如果需要,则调用日志函数;否则直接返回。
Eino:两种路径选择,同一套实现逻辑
无论是 Chain 还是 Graph ,Eino 都依赖“条件函数返回目标节点名”的机制来实现跳转。
Chain 写法:
branchCond := func(ctx context.Context, input *schema.Message) (string, error) {
if isNeedLog(input) {
return "log", nil
}
return "else", nil
}
log := compose.InvokableLambda(func(ctx context.Context, input *schema.Message) (*schema.Message, error) {
log(input)
return input, nil
})
elseBranch := compose.InvokableLambda(func(ctx context.Context, input *schema.Message) (string, error) {
return input, nil
})
chain, _ := NewChain[map[string]any, *schema.Message]().
AppendChatTemplate(prompt).
AppendChatModel(model).
AppendBranch(compose.NewChainBranch(branchCond).AddLambda("log", log).AddLambda("else", elseBranch))
Compile(ctx)
chain.Invoke(ctx, map[string]any{"query": "what's your name?"})
Graph 写法:
branchCond := func(ctx context.Context, input *schema.Message) (string, error) {
if isNeedLog(input) {
return "node_log", nil
}
return compose.END, nil
}
graph := NewGraph[map[string]any, *schema.Message]()
_ = graph.AddChatTemplateNode("node_template", chatTpl)
_ = graph.AddChatModelNode("node_model", chatModel)
_ = graph.AddLambdaNode("node_log", log)
_ = graph.AddEdge(START, "node_template")
_ = graph.AddEdge("node_template", "node_model")
_ = graph.AddBranch("node_model", branchCond)
_ = graph.AddEdge("node_log", END)
compiledGraph, err := graph.Compile(ctx)
if err != nil {
return err
}
compiledGraph.Invoke(ctx, map[string]any{"query": "what's your name?"})
可以看到,虽然整体表现方式不同,但是条件分支的核心逻辑一致:通过字符串匹配决定流向。
优点是灵活性高,支持任意拓扑;缺点也很明显——字符串硬编码易出错,调试困难。一旦拼错节点名,运行时才会报错。
FEL:条件即表达式,无需跳转
FEL 的处理方式更像是函数式编程中的 match 或 when 表达式:
AiProcessFlow flow = AiFlows.create()
.prompt(Prompts.human("question: {query}"))
.generate(model)
.reduce(() -> "", (acc, chunk) -> acc += chunk.text())
.conditions()
.when(this::isNeedLog, this::log)
.others(input -> input)
.close();
flow.converse().offer(Tip.from("query", "what's your name?"));
关键在于 conditions 这个 DSL 关键字,它把分支逻辑封装成声明式语句,完全避免了“跳转”概念。分支动作也是函数式接口,易于测试和复用。整体语法延续了之前的流畅风格,无割裂感。
你可以把它理解为:“在这个环节,根据某些规则做选择,然后继续往下走”,而不是“我要跳到哪个节点去”。
三、设计哲学的碰撞:图 vs 流
看到这里,你会发现 Eino 和 FEL 的差异远不止语法糖那么简单。它们代表了两种截然不同的设计哲学:
[td]维度[/td]
[td]Eino ( Coze )[/td]
[td]FEL[/td]
抽象层级
图结构优先,强调可视化与拓扑控制
流程优先,强调语义表达与可读性
学习成本
需要理解节点、边、分支、循环等图概念
只需掌握链式调用与函数组合
类型检查
提供上下游类型对齐,部分类型在运行期执行 Compile 方法时类型检查
链式调用,天然的上下游类型推导和衔接,能够在编译时期识别类型错误,更安全
四、思考:我们需要什么样的 AI 编排?
对于开发者而言,AI 编排工具的终极理想无疑是:越简单越好用。我们渴望的是能快速落地、易于维护的解决方案,而不是陷入复杂的架构设计中。
但现实往往需要权衡——简洁的 API 背后,是否牺牲了应对复杂场景的能力?强大的图模型,又是否会抬高使用门槛,让日常开发变得笨重?
Eino 框架走了一条更偏“能力先行”的道路。它直接暴露图结构与节点控制,以原生支持循环、分支、动态跳转等复杂拓扑,为构建智能体、自动化流程等高级场景提供了坚实基础。
而我们开发的 FEL 框架选择了“简洁至上”的路径,它能通过流畅的 Fluent API 抽象掉底层细节,让开发者专注业务逻辑本身,显著提升了常规任务的开发效率。
我们认为,真正的成熟框架,不能只停留在“简单”或“强大”的单一体验上。我们还需要它在状态管理、流式处理、循环递归、错误恢复、可观测性等方面都交出令人信服的答卷。
或许,在不同的业务条件之下,Eino 和 FEL 这样不同的写法,可以分别适合不同的场景吧?
我们想多听听大家的意见,希望能够得到更多的反馈,让项目更好的向前演进。
我们的项目地址是: https://github.com/ModelEngine-Group/fit-framework
如果大家能够给我们提提意见,我们是非常开心的,会促使我们有更强的动力向前。
如果过程中有一些问题,欢迎给我们 Github 的项目提 Issue 。
如果有意愿或者喜欢,或者只是给我们鼓励一下,希望能给我们 github 项目点个小星星,真的感谢大家~