函数调用 Function Calling

Function Calling 是一种将大模型与外部工具和 API 相连的关键功能,大模型能够将用户的自然语言智能地转化为对特定工具或 API 的调用,从而高效满足各种场景需求,如动态信息查询、任务自动化等

工具调用的一般步骤:

  1. 应用程序将用户问题和tools列表一起发送给大模型,tools列表表明模型可以调用的工具

  2. LLM 对用户意图进行分析,决定是否需要使用工具以及使用哪些工具

    a. 无需工具则生成回答响应给应用程序

    b. 需要调用工具输出工具名和参数信息响应给应用程序

  3. 应用程序解析模型响应

    1. 有工具调用,则调用工具并将调用结果和之前的消息记录一并发给模型,继续处理
    2. 无工具调用,继续处理程序逻辑或直接给用户
  4. 循环上面步骤,达到结束条件则会话完成

火山引擎文档中有一张图多轮工具调用的逻辑图,可以辅助理解 多轮工具调用

MCP

官网 https://modelcontextprotocol.io/

MCP(Model Context Protocol)即模型上下文协议,与 function calling(函数调用)都是实现大语言模型与外部系统交互的关键技术概念

MCP 主要负责规范化函数的具体执行过程,为 AI 模型和外部数据源或工具之间建立统一的通信接口。

二者的关系表现为 function calling 是 MCP 生态下的一种具体功能实现形式。function calling 为 MCP 提供了函数调用的指令来源,而 MCP 则为 function calling 生成的指令提供了标准化的执行框架,确保这些指令能够在不同的外部系统中可靠地执行。

MCP也可以简单理解为function的共享,因此MCP开源社区在最近几个月都非常活跃。

MCP遵循CS架构(Client-Server),几个核心概念:

  • 主机(Host):通常是发起连接的 LLM 应用程序,如 Claude Desktop、IDE 插件等,负责管理客户端实例和安全策略
  • 客户端(Client):位于主机内,是主机与服务器之间的桥梁,与服务器建立 1:1 会话,处理协议协商和消息路由等
  • 服务器(Server):是独立运行的轻量级服务,通过标准化接口提供特定功能,如文件系统访问、数据库查询等

核心架构这块参考官方文档 https://modelcontextprotocol.io/docs/concepts/architecture

传输机制

MCP的client-server间传输层协议当前有两种,都使用JSON-RPC2.0作为消息交换格式:

Stdio

  • 进程间通信
  • 适用于命令行等同服务器通信

Client将Server作为子进程启动,Server从其标准输入 (stdin) 读取 JSON-RPC 消息,并将消息发送到其标准输出 (stdout)。Server可以将 UTF-8 字符串写入其标准错误 (stderr) 以进行日志记录,Client可以捕获、转发或忽略此日志记录。Server不能向其 stdout 写入任何不是有效 MCP 消息的内容,Client不能向Server的 stdin 写入任何不是有效 MCP 消息的内容。

HTTP with SSE

  • Server –> Client,基于HTTP+SSE实现的事件推送
  • Client –> Server,POST请求

Server提供两个端点:

  1. SSE 终端节点,供客户端建立连接并从服务器接收消息
  2. 一个常规的 HTTP POST 端点,供客户端向服务器发送消息

SSE协议

https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html

SSE(Server-Sent Events),也称为服务器推送事件,是一种基于HTTP的单向通信协议,用于实现 服务器向客户端的单向实时通信。其核心原理是通过 长连接 保持客户端与服务端的通信通道,服务端主动向客户端推送数据(事件流)

在MCP中,除了遵循SSE协议外,MCP Server还需要实现处理client发来的请求,例如下面的这些method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const (
	// Initiates connection and negotiates protocol capabilities.
	// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
	MethodInitialize MCPMethod = "initialize"

	// Verifies connection liveness between client and server.
	// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/ping/
	MethodPing MCPMethod = "ping"

	// Lists all available server resources.
	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
	MethodResourcesList MCPMethod = "resources/list"

  // 省略省略 ......

	// Lists all available executable tools.
	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
	MethodToolsList MCPMethod = "tools/list"

	// Invokes a specific tool with provided parameters.
	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
	MethodToolsCall MCPMethod = "tools/call"
)

消息交换模式

MCP 使用 JSON-RPC 对消息进行编码。JSON-RPC 消息必须采用 UTF-8 编码。

  • Request-Response:客户端或服务器发送请求,另一方响应
  • Notifications:任何一方发送单向消息

Eino

基于 Golang 的 AI 应用开发框架

关键特性

Eino tool

提供给大模型的工具定义

接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 工具基础信息,工具名称、使用说明和参数定义
type BaseTool interface {
	Info(ctx context.Context) (*schema.ToolInfo, error)
}

// 可调用的工具接口,同步执行工具
type InvokableTool interface {
	BaseTool
	InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
}

// 支持流式输出的工具接口
type StreamableTool interface {
	BaseTool
	StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*schema.StreamReader[string], error)
}

工具信息对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type ToolInfo struct {
    // 工具的唯一名称,用于清晰地表达其用途
    Name string
    // 用于告诉模型如何/何时/为什么使用这个工具
    // 可以在描述中包含少量示例
    Desc string
    // 工具接受的参数定义
    // 可以通过两种方式描述:
    // 1. 使用 ParameterInfo:schema.NewParamsOneOfByParams(params)
    // 2. 使用 OpenAPIV3:schema.NewParamsOneOfByOpenAPIV3(openAPIV3)
    *ParamsOneOf
}

Eino中如何调用工具?

Eino编排中有分支的概念,分支根据上游输出在其下游节点中动态选择其中一个,条件结果中大模型意图需要拿到工具调用结果,则执行对应的工具调用,并将结果拼接到会话记录一并发送给模型。

一个简单的编排如下:

1
2
3
4
5
6
graph TD
    U(用户) --> A[大模型]
    A --> B{分支判断}
    B -->|需要调用工具| C[工具调用]
    C[工具调用] --> A
    B -->|不需要工具| D(结束)

简单分支示例:

1
2
3
4
5
6
7
8
9
func trBranch() *compose.GraphBranch {
	branchCondition := func(ctx context.Context, input *schema.Message) (string, error) {
		if len(input.ToolCalls) > 0 {
			return trToolKey, nil
		}
		return compose.END, nil
	}
	return compose.NewGraphBranch(branchCondition, map[string]bool{compose.END: true, trToolKey: true})
}

这里的条件判断有流式和非流式的输入参数,输入参数一般是由上游大模型输出的,不同大模型输出内容略有差异,需要自行调整

Eino中的MCP

如何将MCP Server提供的tool给Eino使用?Eino中tool被统一包装成tool.BaseToolinterface,因此需要从MCP拿到tool并实现BaseTool,然后提供请求大模型时作为请求体传入即可。

Eino中MCP实现直接使用了开源方案 github.com/mark3labs/mcp-go/client

从MCP Server获取工具列表:

  1. 创建MCP Client
  2. 初始化连接到MCP Server
  3. 获取MCP提供的tools并转换为Eino的实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func GetMCPTool(ctx context.Context, c config.McpConfig) []tool.BaseTool {
	var cli *client.Client
	var err error
	switch c.Type {
	case "stdio":
		cli, err = client.NewStdioMCPClient(c.Command, c.Env, c.Args...)
		if err != nil {
			logger.Fatal("创建mcp client失败", "err", err, "name", c.Name, "baseUrl", c.BaseUrl)
		}
	case "sse":
		cli, err = client.NewSSEMCPClient(c.BaseUrl)
		if err != nil {
			logger.Fatal("创建mcp client失败", "err", err, "name", c.Name, "baseUrl", c.BaseUrl)
		}
		err = cli.Start(ctx)
		if err != nil {
			logger.Fatal("启动mcp client失败", "err", err, "name", c.Name, "baseUrl", c.BaseUrl)
		}
	}

	initRequest := mcp.InitializeRequest{}
	initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
	initRequest.Params.ClientInfo = mcp.Implementation{
		Name:    "m-client",
		Version: "1.0.0",
	}

	_, err = cli.Initialize(ctx, initRequest)
	if err != nil {
		logger.Fatal("mcp协商协议失败", "err", err, "name", c.Name, "baseUrl", c.BaseUrl)
	}

	tools, err := mcpp.GetTools(ctx, &mcpp.Config{Cli: cli, ToolNameList: c.ToolNameList})
	if err != nil {
		logger.Fatal("获取mcp工具失败", "err", err, "name", c.Name, "baseUrl", c.BaseUrl)
	}

	return tools
}

mcpp.GetTools实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func GetTools(ctx context.Context, conf *Config) ([]tool.BaseTool, error) {
	listResults, err := conf.Cli.ListTools(ctx, mcp.ListToolsRequest{})
	if err != nil {
		return nil, fmt.Errorf("list mcp tools fail: %w", err)
	}

	nameSet := make(map[string]struct{})
	for _, name := range conf.ToolNameList {
		nameSet[name] = struct{}{}
	}

	ret := make([]tool.BaseTool, 0, len(listResults.Tools))
	for _, t := range listResults.Tools {
		if len(conf.ToolNameList) > 0 {
			if _, ok := nameSet[t.Name]; !ok {
				continue
			}
		}

		marshaledInputSchema, err := sonic.Marshal(t.InputSchema)
		if err != nil {
			return nil, fmt.Errorf("conv mcp tool input schema fail(marshal): %w, tool name: %s", err, t.Name)
		}
		inputSchema := &openapi3.Schema{}
		err = sonic.Unmarshal(marshaledInputSchema, inputSchema)
		if err != nil {
			return nil, fmt.Errorf("conv mcp tool input schema fail(unmarshal): %w, tool name: %s", err, t.Name)
		}

		ret = append(ret, &toolHelper{
			cli: conf.Cli,
			info: &schema.ToolInfo{
				Name:        t.Name,
				Desc:        t.Description,
				ParamsOneOf: schema.NewParamsOneOfByOpenAPIV3(inputSchema),
			},
		})
	}

	return ret, nil
}

MCPClient.ListTools(ctx, mcp.ListToolsRequest{})实现:

发起JSONRPC请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 省略较多中间处理方法
// ....


// sendRequest sends a JSON-RPC request to the server and waits for a response.
// Returns the raw JSON response message or an error if the request fails.
func (c *Client) sendRequest(
	ctx context.Context,
	method string,
	params any,
) (*json.RawMessage, error) {
	if !c.initialized && method != "initialize" {
		return nil, fmt.Errorf("client not initialized")
	}

	id := c.requestID.Add(1)

	request := transport.JSONRPCRequest{
		JSONRPC: mcp.JSONRPC_VERSION,
		ID:      mcp.NewRequestId(id),
		Method:  method,
		Params:  params,
	}

	response, err := c.transport.SendRequest(ctx, request)
	if err != nil {
		return nil, fmt.Errorf("transport error: %w", err)
	}

	if response.Error != nil {
		return nil, errors.New(response.Error.Message)
	}

	return &response.Result, nil
}

从MCP Server拿到可用工具列表后,给模型绑定工具,就可以在请求模型时自动带入工具列表参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func BindTools(chatModel model.ToolCallingChatModel, tools ...tool.BaseTool) model.ToolCallingChatModel {
	toolInfos := make([]*schema.ToolInfo, 0, len(tools))
	for _, t := range tools {
		ti, err := t.Info(context.Background())
		if err != nil {
			logger.Fatal("获取tool info失败", "err", err)
		}
		toolInfos = append(toolInfos, ti)
	}
	model, err := chatModel.WithTools(toolInfos)
	if err != nil {
		logger.Fatal("绑定tool失败", "err", err)
	}
	return model
}

Eino发起MCP的工具调用

遵循MCP协议,Eino内部由MCP Client发起POST请求到MCP Server的tools/call端点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

type toolHelper struct {
	cli  client.MCPClient
	info *schema.ToolInfo
}

func (m *toolHelper) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return m.info, nil
}

func (m *toolHelper) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	arg := make(map[string]any)
	err := sonic.Unmarshal([]byte(argumentsInJSON), &arg)
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal mcp tool input to map[string]any, input: %s, error: %w", argumentsInJSON, err)
	}
	result, err := m.cli.CallTool(ctx, mcp.CallToolRequest{
		Request: mcp.Request{
			Method: "tools/call",
		},
		Params: struct {
			Name      string    `json:"name"`
			Arguments any       `json:"arguments,omitempty"`
			Meta      *mcp.Meta `json:"_meta,omitempty"`
		}{
			Name:      m.info.Name,
			Arguments: arg,
		},
	})
	if err != nil {
		return "", fmt.Errorf("failed to call mcp tool: %w", err)
	}

	marshaledResult, err := sonic.MarshalString(result)
	if err != nil {
		return "", fmt.Errorf("failed to marshal mcp tool result: %w", err)
	}
	if result.IsError {
		return "", fmt.Errorf("failed to call mcp tool, mcp server return error: %s", marshaledResult)
	}
	return marshaledResult, nil
}

实现MCP Server