DeepSeek-R1(以下简称 DeepSeek)以其优秀的复杂问题推理能力和规划能力脱颖而出,然而其原生函数调用(Function Call)功能的缺失,无法让大模型去选择不同的工具和程序,以获取对应的信息,使其难以完成以下关键动作:
-
实时数据获取(天气/票务/交通)
-
外部服务交互(地图API/支付接口)
-
复杂任务拆解执行(多步骤自动化)
这就导致它的应用场景受到限制,大多只能用于简单的对话式问答。有没有一个解决办法,能实现让 DeepSeek 做 Function Call?
答案是肯定的,我们提出
"计划——执行”多智能体的协同范式
:
由 DeepSeek 负责“指挥”,由擅长 Function Call 的其他大模型去听指挥进行函数调用。这需要利用“计划——执行”多智能体范式,由“计划”智能体负责推理和生成计划,由“执行”智能体负责执行计划:
“计划——执行”多智能体范式的三大优势:
-
专业的“智能体”干专业的事情
:比如 DeepSeek 负责推理和计划,豆包大模型负责 Function Call。
-
“智能体”层面的单一职责原则
:每个智能体的职责是明确的,解耦的,方便 Prompt 调优和评测。
-
在提供解决问题整体方案的同时,保持灵活性
:符合人类解决问题的通用模式。
要实现 “计划 —— 执行” 多智能体,我们必须要解决几个问题:多模型、多工具集成,复杂流程编排,上下文管理以及中间步骤追踪。Eino(
文档
https://cloudwego.io/zh/docs/eino/
,
GitHub 项目页
https://github.com/cloudwego/eino
) 框架通过提供开箱即用的模型组件实现和工具执行器、面向抽象接口的灵活流程编排能力、完备的全局状态管理以及回调机制,确保了上述问题的有效解决。
接下来,文章将直观的解释“计划 —— 执行”多智能体范式,介绍如何借助 Eino 框架来实现基于 DeepSeek 的‘计划 —— 执行’多智能体,最后通过一个有趣且靠谱的主题乐园行程规划助手的实战案例,带大家从 0 到 1 搭建一个完整的应用。
基本的 ReAct 单智能体,是由一个 Agent 既负责计划拆解,也负责 Function Call:
-
对 LLM 的要求高
:既要擅长推理规划,也要擅长做 Function Call。
-
LLM 的 prompt 复杂
:既要能正确规划,又要正确的做 Function Call,还要能输出正确的结果。
-
没有计划
:每次 Function Call 之后,LLM 需要重新推理,没有整体的可靠计划。
解决的思路,首先是把单个的 LLM 节点拆分成两个,一个负责“计划”,一个负责“执行”:
这样就解决了上面的问题 3,Planner 会给出完整计划,Executor 依据这个完整计划来依次执行。
部分解决了问题 1、2,Planner 只需要擅长推理规划,Executor 则需要擅长做 Function Call 和总结,各自的 prompt 都是原先的一个子集。
但同时带来一个新的问题:
-
缺少纠错能力
:最开始的计划,在执行后,是否真的符合预期、能够解决问题?
继续优化多智能体结构,在 Executor 后面增加一个 LLM 节点,负责“反思和调整计划”:
这样就彻底解决了上面列出的问题,Executor 只需要按计划执行 Function Call,Reviser 负责反思和总结。
这就是“计划——执行”多智能体:通过将任务解决过程拆解为负责计划的 Planner 和 Reviser,以及负责执行的 Executor,实现了智能体的单一职责以及任务的有效计划与反思,同时也能够充分发挥 DeepSeek 这种推理模型的长项、规避其短板(Function Call)。
基于 Eino 框架实现“计划——
执
行”多智能体
-
能够快速简单的集成 DeepSeek、豆包等各种大模型。
-
-
能够快速实现流程编排,把多个智能体以及工具按设计的流程串联起来,并能随时快速调整。
-
能够及时的输出各智能体的执行过程,包括 DeepSeek 的推理过程。
-
Eino 是字节跳动开源的基于 Golang 的大模型应用开发框架,已在豆包、抖音、扣子等多个业务线广泛使用。我们选择
Eino 作为框架
来进行全码开发,因为:
-
Eino 可以用几行代码完成对各种大模型的调用,包括 DeepSeek。
-
Eino 可以用几行代码快速把一个本地 Function 封装成 Tool,且有开箱即用的 Tool 执行器。
-
Eino 的流程编排能力可靠且灵活:分支判断,循环,运行时参数配置等。
-
Eino 的数据流处理能力为大模型应用场景而设计,可配合完整的回调机制实时输出中间结果。
-
Eino 可以通过在图编排时配置和读写全局状态来实现有效的上下文管理和传递。
Eino 的详细信息参见:
文档
https://cloudwego.io/zh/docs/eino/
GitHub 项目页
https://github.com/cloudwego/eino
我们通过实现一个主题乐园行程规划助手,来探索如何用 Eino 实现基于 DeepSeek 的“计划——执行”多智能体。这个多智能体的功能是根据用户的游园需求,规划出具体、符合要求、可操作的行程安排。完整代码仓库地址:
https://github.com/cloudwego/eino-examples/tree/main/flow/agent/multiagent/plan_execute
package plan_execute
import (
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
type Config struct {
PlannerModel model.ChatModel
PlannerSystemPrompt string
ExecutorModel model.ChatModel
ToolsConfig compose.ToolsNodeConfig
ExecutorSystemPrompt string
ReviserModel model.ChatModel
ReviserSystemPrompt string
MaxStep int
}
type PlanExecuteMultiAgent struct {
runnable compose.Runnable[[]*schema.Message, *schema.Message]
}
Eino 的流程编排有“节点(Node)”、“边(Edge)”和“分支(Branch)”组成,数据流转时要求严格的类型对齐。完整的数据流转图如下:
上图中,Planner,Executor,Reviser 都是输入为[]*Message,输出为*Message的 ChatModel 节点,Branch1 判断 Executor 是否完成了本轮次所有的 Function Call,Branch2 判断 Reviser 是否输出了最终答案,各个 ToList 节点负责连接两个 ChatModel,将输出的 *Message 转化为 []*Message,从而满足类型校验要求。
我们实现一个 NewMultiAgent 方法来实现上述编排逻辑:
func NewMultiAgent(ctx context.Context, config *Config) (*PlanExecuteMultiAgent, error) {
var (
toolInfos []*schema.ToolInfo
toolsNode *compose.ToolsNode
err error
plannerPrompt = config.PlannerSystemPrompt
executorPrompt = config.ExecutorSystemPrompt
reviserPrompt = config.ReviserSystemPrompt
maxStep = config.MaxStep
)
if len(plannerPrompt) == 0 {
plannerPrompt = defaultPlannerPrompt
}
if len(executorPrompt) == 0 {
executorPrompt = defaultExecutorPrompt
}
if len(reviserPrompt) == 0 {
reviserPrompt = defaultReviserPrompt
}
if maxStep == 0 {
maxStep = defaultMaxStep
}
if toolInfos, err = genToolInfos(ctx, config.ToolsConfig); err != nil {
return nil
, err
}
if err = config.ExecutorModel.BindTools(toolInfos); err != nil {
return nil, err
}
if toolsNode, err = compose.NewToolNode(ctx, &config.ToolsConfig); err != nil {
return nil, err
}
graph := compose.NewGraph[[]*schema.Message, *schema.Message]()
executorPostBranchCondition := func(_ context.Context, msg *schema.Message) (endNode string, err error) {
if len(msg.ToolCalls) == 0 {
return nodeKeyExecutorToList, nil
}
return nodeKeyTools, nil
}
reviserPostBranchCondition := func(_ context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) {
defer sr.Close()
var content string
for {
msg, err := sr.Recv()
if err != nil {
if err == io.EOF {
return nodeKeyReviserToList, nil
}
return "", err
}
content += msg.Content
if strings.Contains(content, "最终答案") {
return compose.END, nil
}
if len(content) > 20 {
return nodeKeyReviserToList, nil
}
}
}
_ = graph.AddChatModelNode(nodeKeyPlanner, config.PlannerModel, compose.WithNodeName(nodeKeyPlanner))
_ = graph.AddChatModelNode(nodeKeyExecutor, config.ExecutorModel, compose.WithNodeName(nodeKeyExecutor))
_ = graph.AddChatModelNode(nodeKeyReviser, config.ReviserModel, compose.WithNodeName(nodeKeyReviser))
_ = graph.AddToolsNode(nodeKeyTools, toolsNode)
_ = graph.AddLambdaNode(nodeKeyPlannerToList, compose.ToList[*schema.Message]())
_ = graph.AddLambdaNode(nodeKeyExecutorToList, compose.ToList[*schema.Message]())
_ = graph.AddLambdaNode(nodeKeyReviserToList, compose.ToList[*schema.Message]())
_ = graph.AddEdge(compose.START, nodeKeyPlanner)
_ = graph.AddEdge(nodeKeyPlanner, nodeKeyPlannerToList)
_ = graph.AddEdge(nodeKeyPlannerToList, nodeKeyExecutor)
_ = graph.AddBranch(nodeKeyExecutor, compose.NewStreamGraphBranch(executorPostBranchCondition, map[string]bool{
nodeKeyTools: true,
nodeKeyExecutorToList: true,
}))
_ = graph.AddEdge(nodeKeyTools, nodeKeyExecutor)
_ = graph.AddEdge(nodeKeyExecutorToList, nodeKeyReviser)
_ = graph.AddBranch(nodeKeyReviser, compose.NewStreamGraphBranch(reviserPostBranchCondition, map[string]bool{
nodeKeyReviserToList: true,
compose.END: true,
}))
_ = graph.AddEdge(nodeKeyReviserToList, nodeKeyExecutor)
runnable, err := graph.Compile(ctx, compose.WithNodeTriggerMode(compose.AnyPredecessor), compose.WithMaxRunSteps(maxStep))
if err != nil {
return nil, err
}
return &PlanExecuteMultiAgent{
runnable: runnable,
}, nil
}
-
query_theme_park_opening_hour: 查询乐园 A 的整体营业时间
-
query_park_ticket_price: 查询乐园 A 的门票价格
-
list_locations: 列出乐园 A 中的所有区域,每个游乐设施都归属于一个区域
-
query_location_adjacency_info: 查询乐园 A 中的一个区域到其他相邻区域的步行时间,以分钟为单位
-
query_attraction_queue_time: 查询游乐设施的排队时间,以分钟为单位
-
query_attraction_info: 查询游乐设施的具体信息
-
query_performance_info: 查询演出的具体信息
-
query_restaurant_info: 查询餐厅的具体信息
-
validate_performance_time_table: 校验安排的表演场次是否符合事实
-
arrange_performances: 根据选中的表演名称,自动根据表演的时间表排程
-
validate_plan_items: 根据一个一日日程安排提案,校验各个计划项内部及之间是否自洽
type ActivityType string
const