专栏名称: 字节跳动技术团队
字节跳动的技术实践分享
目录
相关文章推荐
51好读  ›  专栏  ›  字节跳动技术团队

DeepSeek + Function Call:基于 Eino 的“计划——执行”多智能体范式实战

字节跳动技术团队  · 公众号  ·  · 2025-03-18 18:00

正文

图片

DeepSeek-R1(以下简称 DeepSeek)以其优秀的复杂问题推理能力和规划能力脱颖而出,然而其原生函数调用(Function Call)功能的缺失,无法让大模型去选择不同的工具和程序,以获取对应的信息,使其难以完成以下关键动作:

  • 实时数据获取(天气/票务/交通)

  • 外部服务交互(地图API/支付接口)

  • 复杂任务拆解执行(多步骤自动化)

这就导致它的应用场景受到限制,大多只能用于简单的对话式问答。有没有一个解决办法,能实现让 DeepSeek 做 Function Call?

答案是肯定的,我们提出 "计划——执行”多智能体的协同范式

由 DeepSeek 负责“指挥”,由擅长 Function Call 的其他大模型去听指挥进行函数调用。这需要利用“计划——执行”多智能体范式,由“计划”智能体负责推理和生成计划,由“执行”智能体负责执行计划:

图片

“计划——执行”多智能体范式的三大优势:

  1. 专业的“智能体”干专业的事情 :比如 DeepSeek 负责推理和计划,豆包大模型负责 Function Call。

  2. “智能体”层面的单一职责原则 :每个智能体的职责是明确的,解耦的,方便 Prompt 调优和评测。

  3. 在提供解决问题整体方案的同时,保持灵活性 :符合人类解决问题的通用模式。

要实现 “计划 —— 执行” 多智能体,我们必须要解决几个问题:多模型、多工具集成,复杂流程编排,上下文管理以及中间步骤追踪。Eino( 文档 https://cloudwego.io/zh/docs/eino/ GitHub 项目页 https://github.com/cloudwego/eino ) 框架通过提供开箱即用的模型组件实现和工具执行器、面向抽象接口的灵活流程编排能力、完备的全局状态管理以及回调机制,确保了上述问题的有效解决。

接下来,文章将直观的解释“计划 —— 执行”多智能体范式,介绍如何借助 Eino 框架来实现基于 DeepSeek 的‘计划 —— 执行’多智能体,最后通过一个有趣且靠谱的主题乐园行程规划助手的实战案例,带大家从 0 到 1 搭建一个完整的应用。


“计划——执行”多智能体

基本的 ReAct 单智能体,是由一个 Agent 既负责计划拆解,也负责 Function Call:

图片

可能存在的问题有三个:
  1. 对 LLM 的要求高 :既要擅长推理规划,也要擅长做 Function Call。
  2. LLM 的 prompt 复杂 :既要能正确规划,又要正确的做 Function Call,还要能输出正确的结果。
  3. 没有计划 :每次 Function Call 之后,LLM 需要重新推理,没有整体的可靠计划。
解决的思路,首先是把单个的 LLM 节点拆分成两个,一个负责“计划”,一个负责“执行”:

图片

这样就解决了上面的问题 3,Planner 会给出完整计划,Executor 依据这个完整计划来依次执行。 部分解决了问题 1、2,Planner 只需要擅长推理规划,Executor 则需要擅长做 Function Call 和总结,各自的 prompt 都是原先的一个子集。 但同时带来一个新的问题:
  1. 缺少纠错能力 :最开始的计划,在执行后,是否真的符合预期、能够解决问题?
继续优化多智能体结构,在 Executor 后面增加一个 LLM 节点,负责“反思和调整计划”:

图片

这样就彻底解决了上面列出的问题,Executor 只需要按计划执行 Function Call,Reviser 负责反思和总结。
这就是“计划——执行”多智能体:通过将任务解决过程拆解为负责计划的 Planner 和 Reviser,以及负责执行的 Executor,实现了智能体的单一职责以及任务的有效计划与反思,同时也能够充分发挥 DeepSeek 这种推理模型的长项、规避其短板(Function Call)。

基于 Eino 框架实现“计划——

行”多智能体


实现一个“计划——执行”多智能体,需要:
  • 能够快速简单的集成 DeepSeek、豆包等各种大模型。
  • 能够快速简单的集成和执行各种 Tool。
  • 能够快速实现流程编排,把多个智能体以及工具按设计的流程串联起来,并能随时快速调整。
  • 能够及时的输出各智能体的执行过程,包括 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")
// Config “计划——执行”多智能体的配置.type Config struct { PlannerModel model.ChatModel // planner 智能体使用的大模型 PlannerSystemPrompt string // planner 智能体的 system prompt
ExecutorModel model.ChatModel // executor 智能体使用的大模型 ToolsConfig compose.ToolsNodeConfig // executor 智能体使用的工具执行器配置 ExecutorSystemPrompt string // executor 智能体的 system prompt
ReviserModel model.ChatModel // reviser 智能体使用的大模型 ReviserSystemPrompt string // reviser 智能体的 system prompt
MaxStep int // 多智能体的最大执行步骤数,避免无限循环}
// PlanExecuteMultiAgent “计划——执行”多智能体.type PlanExecuteMultiAgent struct { // 图编排后的可执行体,输入是 Message 数组,输出是单条 Message 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 方法来实现上述编排逻辑:
// 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 } // 为 Executor 配置工具 if err = config.ExecutorModel.BindTools(toolInfos); err != nil { return nil, err } // 初始化 Tool 执行器节点,传入可执行的工具 if toolsNode, err = compose.NewToolNode(ctx, &config.ToolsConfig); err != nil { return nil, err } // 创建一个待编排的 graph,规定整体的输入输出类型 graph := compose.NewGraph[[]*schema.Message, *schema.Message]() // 定义 Executor 后的分支判断用的条件函数。该函数的输出是运行时选中的 NodeKey executorPostBranchCondition := func(_ context.Context, msg *schema.Message) (endNode string, err error) { if len(msg.ToolCalls) == 0 { return nodeKeyExecutorToList, nil } return nodeKeyTools, nil } // 定义 Reviser 后的分支判断用的条件函数。 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 } } } // 添加 Planner 节点 _ = graph.AddChatModelNode(nodeKeyPlanner, config.PlannerModel, compose.WithNodeName(nodeKeyPlanner)) // 添加 Executor 节点 _ = graph.AddChatModelNode(nodeKeyExecutor, config.ExecutorModel, compose.WithNodeName(nodeKeyExecutor)) // 添加 Reviser 节点 _ = graph.AddChatModelNode(nodeKeyReviser, config.ReviserModel, compose.WithNodeName(nodeKeyReviser)) // 添加 Tool 执行器节点 _ = graph.AddToolsNode(nodeKeyTools, toolsNode) // 添加三个 ToList 转换节点 _ = 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) // 编译 graph,将节点、边、分支转化为面向运行时的结构。由于 graph 中存在环,使用 AnyPredecessor 模式,同时设置运行时最大步数。 runnable, err := graph.Compile(ctx, compose.WithNodeTriggerMode(compose.AnyPredecessor), compose.WithMaxRunSteps(maxStep)) if err != nil { return nil, err } return &PlanExecuteMultiAgent{ runnable: runnable, }, nil}


Tool 实现


我们的主题乐园行程规划助手,需要用到下列工具:
  • 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






请到「今天看啥」查看全文


推荐文章
河北卫视  ·  【自查】千万别再这么用手机了!
7 年前
蒲公英Ouryao  ·  12批次中药饮片不合格被通告
7 年前
城乡建设一PPP  ·  孙洁:PPP与政府购买服务辨析
7 年前
来自星星  ·  7月19日十二星座运势分析
7 年前