Skip to content

第13章 给工具世界定个规矩

第6章我们讲了Function Calling,第12章讲了安全。但还有一个痛点没解决:你写的Agent接入的是OpenAI,工具定义是OpenAI的格式;另一个Agent用的是豆包,工具定义又是另一种格式。换个模型就得重写工具?MCP协议就是来解决这个问题的——让所有工具说同一种语言。而Go在这个故事里,有一张别人没有的牌。


13.1 为什么需要MCP?

13.1.1 "万国插座"问题

假设你写了三个Agent,用了三个不同的LLM:

AgentLLM工具定义方式
Agent AOpenAIOpenAI Function Calling 格式
Agent B豆包豆包函数调用格式
Agent CDeepSeekDeepSeek 工具模式

同一个工具 get_weather,你得写三遍定义。三套代码,三种格式,维护噩梦。

这就是MCP要解决的问题——一套工具定义,所有LLM都能用。


13.1.2 MCP是什么

MCP(Model Context Protocol) 是一个开放协议,由Anthropic在2024年底提出。它定义了LLM和外部工具之间的标准通信方式。

简单类比:

  • USB-C出现之前:每个手机有自己充电口,出门得带三根线
  • MCP出现之前:每个LLM有自己的工具调用格式,写工具得适配三套
  • 有了MCP之后:一套接口,所有LLM都能用

13.2 MCP Server / Client 协议规范

13.2.1 MCP的架构

MCP采用经典的C/S架构:

  • MCP Server:暴露工具。定义"我有哪些工具、每个工具要什么参数"
  • MCP Client:调用工具。Agent作为Client,通过标准协议调用Server暴露的工具

13.2.2 Eino的MCP支持

Eino内置了MCP的Client和Server能力:

go
import (
    "github.com/cloudwego/eino/components/tool/mcp"
)

// 方式一:作为 MCP Client——调用别人暴露的工具
func createMCPTools(ctx context.Context) ([]tool.InvokableTool, error) {
    // 连接一个外部 MCP Server
    mcpClient, err := mcp.NewClient(ctx, &mcp.ClientConfig{
        Transport: "stdio",  // 或 "sse"
        Command:   "python", // 如果MCP Server是Python写的
        Args:      []string{"-m", "weather_mcp_server"},
    })
    if err != nil {
        return nil, err
    }

    // 获取 Server 暴露的所有工具
    tools, err := mcpClient.ListTools(ctx)
    if err != nil {
        return nil, err
    }

    // 直接把这些工具注册给 Eino Agent
    return tools, nil
}

// 方式二:作为 MCP Server——把自己的工具暴露出去
func startMCPServer(ctx context.Context) error {
    mcpServer := mcp.NewServer("my-agent-tools", "1.0.0")

    // 注册工具
    mcpServer.AddTool(ctx, &mcp.ToolDefinition{
        Name:        "get_weather",
        Description: "查询指定城市的天气",
        InputSchema: mcp.Schema{
            Type: "object",
            Properties: map[string]mcp.Property{
                "city": {Type: "string", Description: "城市名"},
            },
            Required: []string{"city"},
        },
        Handler: handleWeatherQuery,
    })

    return mcpServer.Serve(ctx)
}

Eino帮你屏蔽了MCP协议的细节——你只需要定义工具和处理逻辑,框架负责协议的序列化、传输、错误处理。


13.3 Go实现MCP的天然优势:编译一次,到处运行

13.3.1 为什么Go特别适合MCP

MCP Server本质是一个独立运行的服务。部署MCP Server的痛点:

语言部署痛点
Python需要Python环境 + venv + 依赖安装
Node.js需要Node环境 + npm install
Go一个二进制文件,扔上去就跑
bash
# Go MCP Server 的部署
go build -o weather-server ./cmd/weather
scp weather-server user@server:/opt/mcp/
ssh user@server "/opt/mcp/weather-server"

# 整个部署过程不到10秒,不需要装任何依赖

Agent需要调用你的工具?给一个二进制文件就行。不需要Python环境、不需要Node版本、不需要系统库——这是Go做MCP最大的优势。


13.3.2 跨平台编译

bash
# 在 macOS 上编译 Linux 版
GOOS=linux GOARCH=amd64 go build -o weather-server-linux

# 编译 Windows 版
GOOS=windows GOARCH=amd64 go build -o weather-server.exe

# 编译 ARM 版(树莓派、Apple Silicon服务器)
GOOS=linux GOARCH=arm64 go build -o weather-server-arm

同一个代码库,三行命令出三个平台的二进制。Python做MCP Server,你得确保目标机器Python版本和依赖都对了。


13.4 gRPC工具链:让你的工具被任何语言调用

13.4.1 MCP + gRPC:更强的组合

MCP定义了"工具的标准接口",gRPC定义了"高效的通信方式"。两者组合在一起:

Go 写的 MCP Server(编译成二进制)
    ↓ 通过 gRPC 暴露
Java 微服务可以调用
Python 脚本可以调用
前端 Node.js 可以调用
go
// 用 gRPC 暴露 MCP 工具
import (
    "google.golang.org/grpc"
    mcp_pb "your-project/proto/mcp"
)

type MCPServer struct {
    mcp_pb.UnimplementedMCPServer
    tools map[string]ToolHandler
}

func (s *MCPServer) CallTool(ctx context.Context, 
    req *mcp_pb.ToolCallRequest) (*mcp_pb.ToolCallResponse, error) {
    
    handler, ok := s.tools[req.ToolName]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "工具 %s 不存在", req.ToolName)
    }

    result, err := handler.Execute(ctx, req.Params)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "工具执行失败: %v", err)
    }

    return &mcp_pb.ToolCallResponse{Result: result}, nil
}

// 启动 gRPC 服务
func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    mcp_pb.RegisterMCPServer(s, &MCPServer{tools: loadTools()})
    s.Serve(lis)
}

Go 写 Proto → protoc 生成各语言客户端 → 任何语言都能调你的工具。这是 Go 在工具链生态里的杀手锏。


13.4.2 流式工具调用

gRPC 支持四种调用模式。Agent场景里最有用的是服务端流式——工具边执行边返回结果:

go
func (s *MCPServer) StreamTool(req *mcp_pb.ToolCallRequest, 
    stream mcp_pb.MCP_StreamToolServer) error {
    
    // 工具执行过程中,分批返回结果
    results := executeLongRunningTool(ctx, req.ToolName, req.Params)
    
    for _, chunk := range results {
        if err := stream.Send(&mcp_pb.ToolCallChunk{
            Content: chunk,
            Progress: float32(chunk.Index) / float32(len(results)),
        }); err != nil {
            return err
        }
    }
    return nil
}

13.5 MCP不是替代,是互补

13.5.1 MCP和Function Calling的关系

很多人以为MCP要替代Function Calling——不对。

Function CallingMCP
解决的问题LLM怎么"知道"该调哪个工具工具怎么被"定义"和"发现"
层级LLM内部能力LLM外部协议
关系MCP包装的工具最终还是通过Function Calling让LLM调用

可以理解为:Function Calling是"怎么用",MCP是"怎么接"。它们不是竞争关系,是上下游。


13.5.2 MCP的生态展望

MCP还年轻,但方向很明确——标准化工具接口。目前:

  • Anthropic(提出者)、OpenAI 都在跟进
  • Eino 已内置 MCP Client/Server 支持
  • Go 编译二进制 + gRPC 流式,让 Go 在 MCP 生态里有独特优势

13.6 本章小结

这一章讲清楚了工具世界的"统一接口":

  1. MCP是什么:工具定义的"USB-C"。一套接口,所有LLM通用。
  2. Eino的MCP支持:内置Client(调别人工具)和Server(暴露自己的工具)。
  3. Go的独特优势:编译一个二进制,不需要Python环境、不需要npm install。跨平台编译,三行命令出三个平台。
  4. gRPC强化:Proto定义 + 代码生成,任何语言都能调你的Go工具。流式返回让工具执行过程可见。
  5. MCP + Function Calling是协作关系:MCP管"怎么接",Function Calling管"怎么用"。

✅ 知识点检查

学完这一章,试试回答这几个问题:

  • [ ] MCP解决了什么痛点?把它比作USB-C是什么意思?
  • [ ] Go做MCP Server比Python有什么优势?
  • [ ] gRPC在MCP生态里扮演什么角色?
  • [ ] MCP和Function Calling是什么关系?

📚 延伸阅读


🎯 下一章预告

第14章,Agent的边界——

"知道边界在哪,比知道怎么做更难。Agent能做什么、不能做什么?做错了谁负责?"