函数调用 Function Calling#
Function Calling 是一种将大模型与外部工具和 API 相连的关键功能,大模型能够将用户的自然语言智能地转化为对特定工具或 API 的调用,从而高效满足各种场景需求,如动态信息查询、任务自动化等
工具调用的一般步骤:
应用程序将用户问题和tools列表一起发送给大模型,tools列表表明模型可以调用的工具
LLM 对用户意图进行分析,决定是否需要使用工具以及使用哪些工具
a. 无需工具则生成回答响应给应用程序
b. 需要调用工具输出工具名和参数信息响应给应用程序
应用程序解析模型响应
- 有工具调用,则调用工具并将调用结果和之前的消息记录一并发给模型,继续处理
- 无工具调用,继续处理程序逻辑或直接给用户
循环上面步骤,达到结束条件则会话完成
火山引擎文档中有一张图多轮工具调用的逻辑图,可以辅助理解 多轮工具调用
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提供两个端点:
- SSE 终端节点,供客户端建立连接并从服务器接收消息
- 一个常规的 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 应用开发框架
关键特性
提供给大模型的工具定义
接口
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.BaseTool
interface,因此需要从MCP拿到tool并实现BaseTool,然后提供请求大模型时作为请求体传入即可。
Eino中MCP实现直接使用了开源方案 github.com/mark3labs/mcp-go/client
从MCP Server获取工具列表:
- 创建MCP Client
- 初始化连接到MCP Server
- 获取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#