DeepSeek + Function Call:基于 Eino 的“计划——执行”多智能体范式实战
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 搭建一个完整的应用。
“计划——执行”多智能体
对 LLM 的要求高:既要擅长推理规划,也要擅长做 Function Call。 LLM 的 prompt 复杂:既要能正确规划,又要正确的做 Function Call,还要能输出正确的结果。 没有计划:每次 Function Call 之后,LLM 需要重新推理,没有整体的可靠计划。
缺少纠错能力:最开始的计划,在执行后,是否真的符合预期、能够解决问题?
基于 Eino 框架实现“计划——
执行”多智能体
能够快速简单的集成 DeepSeek、豆包等各种大模型。 能够快速简单的集成和执行各种 Tool。 能够快速实现流程编排,把多个智能体以及工具按设计的流程串联起来,并能随时快速调整。 能够及时的输出各智能体的执行过程,包括 DeepSeek 的推理过程。 能够有效的管理和传递上下文。
Eino 可以用几行代码完成对各种大模型的调用,包括 DeepSeek。 Eino 可以用几行代码快速把一个本地 Function 封装成 Tool,且有开箱即用的 Tool 执行器。 Eino 的流程编排能力可靠且灵活:分支判断,循环,运行时参数配置等。 Eino 的数据流处理能力为大模型应用场景而设计,可配合完整的回调机制实时输出中间结果。 Eino 可以通过在图编排时配置和读写全局状态来实现有效的上下文管理和传递。
实战:主题乐园行程规划助手
定义多智能体
package plan_executeimport ("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 promptExecutorModel model.ChatModel // executor 智能体使用的大模型ToolsConfig compose.ToolsNodeConfig // executor 智能体使用的工具执行器配置ExecutorSystemPrompt string // executor 智能体的 system promptReviserModel model.ChatModel // reviser 智能体使用的大模型ReviserSystemPrompt string // reviser 智能体的 system promptMaxStep int // 多智能体的最大执行步骤数,避免无限循环}// PlanExecuteMultiAgent “计划——执行”多智能体.type PlanExecuteMultiAgent struct {// 图编排后的可执行体,输入是 Message 数组,输出是单条 Messagerunnable compose.Runnable[[]*schema.Message, *schema.Message]}
多智能体编排逻辑
// NewMultiAgent 根据配置编排一个“计划——执行”多智能体.func NewMultiAgent(ctx context.Context, config *Config) (*PlanExecuteMultiAgent, error) {var (toolInfos []*schema.ToolInfotoolsNode *compose.ToolsNodeerr errorplannerPrompt = config.PlannerSystemPromptexecutorPrompt = config.ExecutorSystemPromptreviserPrompt = config.ReviserSystemPromptmaxStep = 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 后的分支判断用的条件函数。该函数的输出是运行时选中的 NodeKeyexecutorPostBranchCondition := 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 stringfor {msg, err := sr.Recv()if err != nil {if err == io.EOF {return nodeKeyReviserToList, nil}return "", err}content += msg.Contentif 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 实现
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 stringconst (ActivityTypeAttraction ActivityType = "attraction"ActivityTypePerformance ActivityType = "performance"ActivityTypeRestaurant ActivityType = "restaurant"ActivityTypeOther ActivityType = "other")// Activity 主题乐园中的一个项目,可以是游乐设施、表演或餐厅.type Activity struct {Name string `json:"name"`Desc string `json:"desc"`Type ActivityType `json:"type"`Location string `json:"location" jsonschema:"description:项目所属的区域"`MinHeight int `json:"min_height,omitempty" jsonschema:"description:参加游乐设施需要的最小身高,单位是厘米。如果为空,则没有身高要求"`Duration int `json:"duration,omitempty" jsonschema:"description:一个项目参加一次需要的时间,注意不包括排队的时间。如果为空,则缺少具体的时间信息"`TimeTable []string `json:"time_table,omitempty" jsonschema:"description:一个演出的时间表。如果为空,则使用 OpenTime 和 CloseTime 来表示这个项目的运营时间范围"`OpenTime string `json:"open_time,omitempty" jsonschema:"description:一个项目开始运营的时间"`CloseTime string `json:"close_time,omitempty" jsonschema:"description:一个项目结束运营的时间"`RequireBooking bool `json:"require_booking,omitempty" jsonschema:"description:一个餐厅是否需要提前预约"`HasPriorityAccess bool `json:"has_priority_access,omitempty" jsonschema:"description:一个项目是否有高速票服务"`PriorityAccessCost int `json:"priority_access_cost,omitempty" jsonschema:"description:一个项目如果有高速票服务,则一个人的高速票需要花多少钱"`QueueTime int `json:"queue_time,omitempty" jsonschema:"description:一个项目常规需要的排队时间,单位是分钟。如果为空,则这个项目一般不需要排队"`}
注意大多数字段中都有 jsonschema:"description:xxx" 的 go struct tag。Eino 框架可抽取这个信息以及其他的 tag 给到大模型。
// GetAttractionInfo 获取游乐设施信息.func GetAttractionInfo(_ context.Context, in *ListAttractionRequest) (out *ListAttractionResponse, err error) {if len(in.Name) > 0 && in.Name != "all" {for _, a := range attractions {if a.Name == in.Name {return &ListAttractionResponse{Attractions: []Activity{a,},}, nil}}}if len(in.Location) > 0 {locationAttractions := make([]Activity, 0)for _, a := range attractions {if a.Location == in.Location {locationAttractions = append(locationAttractions, a)return &ListAttractionResponse{Attractions: locationAttractions,}, nil}}}return &ListAttractionResponse{Attractions: attractions,}, nil}
func GetTools(ctx context.Context) (tools []tool.BaseTool, err error) { queryTimeTool, err := utils.InferTool("query_theme_park_opening_hour", "查询乐园 A 的整体营业时间", GetParkHour) if err != nil { return nil, err } tools = append(tools, queryTimeTool) // 以下省略多个 Tool return}上下文管理
上下文管理
用户输入的任务。 之前执行的智能体(包括自身)的输出。 之前执行的智能体的 Function Call,以及对应的结果。 自身的 System Prompt。
// state 以多智能体一次运行为 scope 的全局状态,用于记录上下文type state struct {messages []*schema.Message}func NewMultiAgent(ctx context.Context, config *Config) (*PlanExecuteMultiAgent, error) {// ... 省略 N 行 ...// 创建一个待编排的 graph,规定整体的输入输出类型,配置全局状态的初始化方法graph := compose.NewGraph[[]*schema.Message, *schema.Message](compose.WithGenLocalState(func(ctx context.Context) *state {return &state{}}))// 在大模型执行之前,向全局状态中保存上下文,并组装本次的上下文modelPreHandle := func(systemPrompt string, isDeepSeek bool) compose.StatePreHandler[[]*schema.Message, *state] {return func(ctx context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) {for _, msg := range input {state.messages = append(state.messages, msg)}if isDeepSeek {return append([]*schema.Message{schema.SystemMessage(systemPrompt)}, convertMessagesForDeepSeek(state.messages)...), nil}return append([]*schema.Message{schema.SystemMessage(systemPrompt)}, state.messages...), nil}}// ... 省略 N 行 ...// 添加 Planner 节点,同时添加 StatePreHandler 读写上下文_ = graph.AddChatModelNode(nodeKeyPlanner, config.PlannerModel, compose.WithStatePreHandler(modelPreHandle(plannerPrompt, true)), compose.WithNodeName(nodeKeyPlanner))// 添加 Executor 节点,同时添加 StatePreHandler 读写上下文_ = graph.AddChatModelNode(nodeKeyExecutor, config.ExecutorModel, compose.WithStatePreHandler(modelPreHandle(executorPrompt, false)), compose.WithNodeName(nodeKeyExecutor))// 添加 Reviser 节点,同时添加 StatePreHandler 读写上下文_ = graph.AddChatModelNode(nodeKeyReviser, config.ReviserModel, compose.WithStatePreHandler(modelPreHandle(reviserPrompt, true)), compose.WithNodeName(nodeKeyReviser))// 添加 Tool 执行器节点,同时添加 StatePreHandler 读写上下文_ = graph.AddToolsNode(nodeKeyTools, toolsNode, compose.WithStatePreHandler(func(ctx context.Context, in *schema.Message, state *state) (*schema.Message, error) {state.messages = append(state.messages, in)return in, nil}))// ... 省略 N 行 ...}
main 函数:多智能体执行
实例化 DeepSeek 和豆包的模型,并放到多智能体的配置中。 获取 Tool 列表。 依据配置编排和初始化多智能体。 将多智能体的各中间步骤及时输出。
func main() {ctx := context.Background()deepSeekModel, err := deepseek.NewChatModel(ctx, &deepseek.ChatModelConfig{Model: os.Getenv("DEEPSEEK_MODEL_ID"),APIKey: os.Getenv("DEEPSEEK_API_KEY"),BaseURL: os.Getenv("DEEPSEEK_BASE_URL"),})if err != nil {log.Fatalf("new DeepSeek model failed: %v", err)}arkModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{APIKey: os.Getenv("ARK_API_KEY"),Model: os.Getenv("ARK_MODEL_ID"),})if err != nil {log.Fatalf("new Ark model failed: %v", err)}toolsConfig, err := tools.GetTools(ctx)if err != nil {log.Fatalf("get tools config failed: %v", err)}// 创建多智能体的配置,system prompt 都用默认值config := &Config{// planner 在调试时大部分场景不需要真的去生成,可以用 mock 输出替代PlannerModel: &debug.ChatModelDebugDecorator{Model: deepSeekModel,},ExecutorModel: arkModel,ToolsConfig: compose.ToolsNodeConfig{Tools: toolsConfig},ReviserModel: &debug.ChatModelDebugDecorator{Model: deepSeekModel,},}planExecuteAgent, err := NewMultiAgent(ctx, config)if err != nil {log.Fatalf("new plan execute multi agent failed: %v", err)}printer := newIntermediateOutputPrinter() // 创建一个中间结果打印器printer.printStream() // 开始异步输出到 consolehandler := printer.toCallbackHandler() // 转化为 Eino 框架的 callback handler// 以流式方式调用多智能体,实际的 OutputStream 不再需要关注,因为所有输出都由 intermediateOutputPrinter 处理了_, err = planExecuteAgent.Stream(ctx, []*schema.Message{schema.UserMessage("我们一家三口去乐园玩,孩子身高 120 cm,园内预算 2000 元,最爱的是各种表演,游乐设施比较偏爱刺激项目,希望能在一天内尽可能多体验不同的活动,帮忙规划一个行程。我们会在园区开门之后立刻入园,在园区关闭之后再离开。")},agent.WithComposeOptions(compose.WithCallbacks(handler)), // 将中间结果打印的 callback handler 注入进来// 给 planner 指定 mock 输出//agent.WithComposeOptions(compose.WithChatModelOption(debug.WithDebugOutput(schema.AssistantMessage(debug.PlannerOutput, nil))).DesignateNode(nodeKeyPlanner)),// 给 reviser 指定 mock 输出//agent.WithComposeOptions(compose.WithChatModelOption(debug.WithDebugOutput(schema.AssistantMessage("最终答案", nil))).DesignateNode(nodeKeyReviser)),)if err != nil {log.Fatalf("stream error: %v", err)}printer.wait() // 等待所有输出都处理完再结束}
流式输出中间过程
调试与优化
实际效果
高速票精准投放:仅在“抱抱熊飞天赛车”午间时段使用(性价比最高),其他项目利用排队低峰期 表演全覆盖:新增“复仇者小分队培训行动”,保留所有核心表演场次 动线优化:区域集中化(未来世界→玩具的故事→幻想世界→奇幻园林→入口大街) 预算控制:通过调整午餐为快餐(蓝莓熊餐盒人均50元)可使总费用降至2000元以内 刺激项目最大化:包含3个过山车类项目(抱抱熊/矿山车/飞吧地平线)+ 2个黑暗骑乘(家勒比海贼/派斯音速)
相关链接
核心仓库:https://github.com/cloudwego/eino 扩展仓库,含本文用到的 DeepSeek 和豆包模型实现:https://github.com/cloudwego/eino-ext 示例仓库,含本文内的主题乐园行程规划助手:https://github.com/cloudwego/eino-examples