本文将为读者深入介绍 Eino 与 MCP(Model Context Protocol)的核心概念,并通过实际案例展示如何部署和调用大型语言模型(LLM)。无论是本地部署还是远程调用 LLM,我们都将提供详细的操作指南。
通过一个实用的旅游规划项目,我们将展示如何利用 LLM 技术创建一个能够生成详细酒店住宿方案的智能助手。这个项目不仅演示了 LLM 的强大功能,也为读者提供了一个可复制的模板,以便在自己的应用中实现类似功能。
在本教程中,您将学习如何:
- 理解 Eino 与 MCP 的基础架构和工作原理
- 选择并配置适合您需求的 LLM 模型
- 构建一个能够提供详细酒店住宿信息的交互式系统
- 优化模型响应以提升用户体验
无论您是 AI 开发新手还是有经验的开发者,本指南都将帮助您快速掌握这些强大工具的使用方法,并应用到实际项目中。
Eino 整体架构
Eino作为字节跳动开源的基于Golang的大模型应用框架,它结合了开源社区中多个优秀的LLM应用开发框架,例如Langchain和LlamaIndex, 提供了一个可扩展性,可靠性和有效性都很强的符合Go语言编程的LLM应用开发框架。
官方文档中有着很详细的实例去介绍Eino的各个组件,这里就不做过多阐述了,详细内容可以参考
Eino: 概述
Eino主框架:
EinoDev 插件:
- 可视化拖拽编排AI应用,并生成代码。插件安装 + 使用指南(Goland)
Eino 代码实例:
- 完整的代码实例:实例代码仓库
MCP(Model Context Protocol)
MCP 是由Anthropic推出的用于LLM应用和外部数据源或工具之间通信的标准协议。可以把MCP当作AI应用程序的USB-C接口,规范了应用程序应该按照什么样的格式为LLM提供上下文。
工作原理
MCP协议采用了一种创新的三层架构设计,将LLM与资源间的通信分为客户端、服务器和资源三个核心组件。
客户端负责向MCP服务器发送请求,而服务器则充当中介,将这些请求精确转发至相应资源。这种分层架构优化了访问控制机制,确保资源只能被授权用户访问,从而提高了整体系统安全性。
MCP协议的工作流程如下:
- 连接建立:客户端向服务器发起连接请求,建立稳定的通信通道
- 请求传输:客户端根据特定需求构造请求消息并发送至服务器
- 请求处理:服务器接收并解析请求,执行相应操作(如数据库查询或文件读取)
- 结果返回:服务器将处理结果封装为响应消息并回传给客户端
- 连接终止:任务完成后,客户端可主动关闭连接,或由服务器在超时后自动断开
本质上遵循Client-Server架构,其中包含以下几个核心概念:
- MCP 主机(MCP Hosts):发起请求的 LLM 应用程序。
- MCP 客户端(MCP Clients):在主机程序内部,与 MCP server 保持 1:1 的连接。
- MCP 服务器(MCP Servers):为 MCP client 提供上下文、工具和 prompt 信息。
- 本地资源(Local Resources):本地计算机中可供 MCP server 安全访问的资源(例如文件、数据库)。
- 远程资源(Remote Resources):MCP server 可以连接到的远程资源(例如通过 API)。
MCP客户端
MCP客户端就像是一个信使,它代表应用程序向服务器发送请求。它的工作包括:
- 准备和发送请求
- 建立与服务器的连接
- 接收服务器返回的结果
- 处理可能出现的错误
简单来说,客户端就是"提问方",它向服务器提出需求并等待回应。
MCP服务器
MCP服务器则像是一个管理员,它接收客户端的请求并负责:
- 检查请求是否有权限访问资源
- 将请求转发给正确的资源
- 收集资源的回应
- 将结果发回给客户端
服务器是"中间人",它确保只有合适的请求才能获取资源,并帮助管理资源的使用。
互动过程
- 客户端向服务器说"你好"并提供身份信息
- 服务器检查身份并回应"你好"
- 客户端发送特定请求,如"我需要某个数据"
- 服务器检查权限,找到相应资源
- 服务器获取数据并发送回客户端
- 完成后,双方结束对话
这种设计就像一个安保严密的图书馆:你(客户端)不能直接拿书(资源),必须通过图书管理员(服务器),他会检查你的借书证,然后帮你找到并提供你需要的书。
目前Claude Desktop 和Cursor都支持了MCP server的接入能力,而且他们也同时作为MCP client提供给用户比较好的调用体验,但对于这两者的上手都需要一定的时间成本,本文将教会大家如何在本地快速搭建一个MCP client。
随着MCP被广泛的认可和使用,有越来越多的MCP server被开源供大家使用,其中不乏一些基础能力,如对filesystem的交互,与git工具的继承等。eg: 官方的MCP Server列表
项目实战
介绍完Eino 以及 MCP,我们便可以开始今天的Demo。在开始之前,需要做以下的准备
环境准备
1. Go 安装
这里以Mac开发环境为例,如果本地有Golang环境则可以跳过该步骤:
brew install go
go version
2. node.js 安装
brew install node
3. 本地部署deepseek model
- 准备Ollama环境。 在Ollama官网上下载Ollama, 选择对应的操作系统下载: Download Ollama on macOS
- 下载完成后,双击打开,选择移动到Application中
- 打开终端Terminal,输入ollama, 如果显示如下内容表示Ollama已经安装完成了。
- 安装模型, 在ollama官网中,选择你想要的模型: Ollama Search. 这里deepseek-r1为例
- 选择适合的model后,本地执行
ollama run deepseek-r1:7b
如果出现上述的对话界面则表示安装成功
4. 使用火山方舟大模型
由于本地的存储和内存空间有限,本地运行的模型大部分的表现都很一般,类似于deepseek就有含有不同参数的板板,deepseek-r1:7b只有4GB 但是 deepseek-r1:671b 却有404GB。我们可以尝试去接入完整的model来获得更好的使用体验。
这里采用的是字节跳动的火山模型平台:火山方舟大模型服务平台-火山引擎
注册/登陆后可以快速上手,对于新用户,主流的模型都有50万免费token:
1. 配置API Key
2. 选择合适的Model 并获取Model ID
项目
完整代码地址:https://github.com/Gilvt-WTJ/LLM
1. 本地新建一个文件目录,并执行
mkdir eino_mcp & cd eino_mcp
go mod init github.com/mcp_eino/demo
2. 获取相关依赖
go get github.com/cloudwego/eino-ext/components/ollama
go get github.com/mark3labs/mcp-go/client
go get github.com/cloudwego/eino-ext/components/tool/mcp
go get github.com/cloudwego/eino/callbacks
go get github.com/cloudwego/eino-ext/components/model/ark
3. 创建文件目录如下
.
├── agent
│ ├── react_agent.go -> 主逻辑
├── go.mod
├── go.sum
├── hotel_plan.txt -> 存放最终结果
├── main.go
├── mcp_tool
│ └── mcp_tool.go -> 不同MCP server转化成的eino tools
├── model
│ ├── ark.go -> 从方舟平台拿到的模型
│ └── ollama.go -> 通过ollama拿到的本地模型
└── utils
└── utils.go -> 用于记录LLM model思考的中间过程
4. 获取LLM model
这里有两种形式去拿到该应用的基础模型:本地模型和远程方舟平台
本地获取
import (
"context"
"github.com/cloudwego/eino-ext/components/model/ollama"
"github.com/cloudwego/eino/components/model"
"log"
)
func GetDeepseekModelFromOllama(ctx context.Context) model.ChatModel {
chatModel, err := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{
BaseURL: "http://localhost:11434",
Model: "deepseek-r1:7b",
})
if err != nil {
log.Fatalf("create deepseek-r1 model failed: %v", err)
}
return chatModel
}
远程获取
需要用户的API key, model region以及Model name,这个参考上面的教程.
import (
"context"
"github.com/cloudwego/eino-ext/components/model/ark"
)
func GetDoubaoChatModel(ctx context.Context) *ark.ChatModel {
model, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{
APIKey: "API-Key", // How to get API Key: https://www.volcengine.com/docs/82379/1399008#b00dee71
Region: "cn-beijing",
Model: "doubao-1.5-pro-256k-250115", // Model List: https://www.volcengine.com/docs/82379/1330310
})
if err != nil {
panic(err)
}
return model
}
5. 调用不同的MCP server
因为任务目标是分析用户意图,从Airbnb那里拿到结果,并把结果写进本地文件中。所以在此过程中,需要能够和Airbnb端进行交互以及和本地文件系统进行交互的能力,因此我们从MCP 官方server中选择了: mcp-server-airbnb and filesystem
import (
"context"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
"time"
)
func GetFileSystemMcpCli(ctx context.Context, filePath string) (*client.StdioMCPClient, error) {
client, err := client.NewStdioMCPClient(
"npx",
[]string{},
"-y",
"@modelcontextprotocol/server-filesystem",
filePath,
)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "filesystem-client",
Version: "1.0.0",
}
_, err = client.Initialize(ctx, initRequest)
if err != nil {
return nil, err
}
return client, nil
}
func GetAirbnbMCPCli(ctx context.Context) (*client.StdioMCPClient, error) {
client, err := client.NewStdioMCPClient(
"npx",
[]string{},
"-y",
"@openbnb/mcp-server-airbnb",
"--ignore-robots-txt",
)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "airbnb-client",
Version: "1.0.0",
}
_, err = client.Initialize(ctx, initRequest)
if err != nil {
return nil, err
}
return client, nil
}
6. 转换MCP server中的能力成为eino框架中的tools
// 获取MCP Client
airbnbCli, err := mcp_tool.GetAirbnbMCPCli(ctx)
if err != nil {
fmt.Printf("failed to get mcp_tool cli: %v", err)
return nil
}
// 转换成 tool
abnbTools, err := eino_mcp.GetTools(ctx, &eino_mcp.Config{
Cli: airbnbCli,
ToolNameList: []string{"airbnb_search", "airbnb_listing_details"},
})
if err != nil {
fmt.Printf("failed to list tools: %v", err)
return nil
}
// agent config
agentConfig := &react.AgentConfig{
Model: model.GetDoubaoChatModel(ctx),
MaxStep: 12,
}
// Assign tool
agentConfig.ToolsConfig.Tools = abnbTools
// Create an agent
agent, err := react.NewAgent(ctx, agentConfig)
if err != nil {
fmt.Printf("failed to new agent: %v", err)
return nil
}
7. 写用户输入和系统Prompt
这个阶段可以用传统的stream流对话的方式进行,也可以用传统的Stdio方式进行,我们这里拿后者作为事例。
在main.go 中加入以下代码,并运行main.go即可得到完整结果。
比较重要的是 systemPrompt 以及 userInput. 通常用户可以自定义这两者去实现想要的结果。
例如,userInput 现在是:
我在2025年5月1日到5月7日要去Las Vegas游玩,一共有四个人包括三个大人,一个小孩子,每天的预算是500美元,前三天住一个酒店,后四天住一个酒店。你可以帮我找到合适的住宿么并把结果输出到 hotel_plan.txt中"
如果我现在想要改变时间成为2025-4-1~2025-4-4 有十个人,结果记录在travel_plan.csv中,那么结果也会是截然不同。
import (
"context"
"fmt"
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/schema"
"github.com/mcp_eino/demo/agent"
"github.com/mcp_eino/demo/utils"
"log"
)
func main() {
ctx := context.Background()
reactAgent := agent.GetDemoReactAgent(ctx)
if reactAgent == nil {
panic("failed to get react agent")
}
systemPrompt := `
# 酒店定制专家系统
## 角色
你是一位酒店定制专家,帮助用户根据其需求找到最合适的酒店。拟定每日住宿计划并把结果写到本地文件中。
## 职责
1. 分析用户的酒店需求
2. 提供最多3种完整旅行方案
3. 将推荐结果保存到文件中
## 信息收集
询问用户以下信息:
- 目的地
- 旅行日期(开始和结束日期)
- 预算范围
- 旅客人数
## 推荐方法
1. 分析用户需求
2. 提供最多3种旅行方案
3. 每个方案必须包含以下内容:
- 方案名称/主题
- 每天的详细住宿安排(日期+酒店)
- 总体预算估算
## 每个酒店信息应包括:
- 酒店名称
- 位置
- 星级
- 每晚价格
- 推荐房型
- 2-3项主要设施
- 简短描述(不超过2句话)
## 输出格式
1. **需求摘要**:用户旅行需求概要
2. **方案列表**:最多3种旅行方案,每种方案包含每天的住宿安排
3. **预订建议**:1-2条重要注意事项
## 文件创建
将完整推荐内容保存到本地文件中,格式整洁易读。
## 响应风格
- 专业简洁
- 内容完整但不冗长
- 确保信息准确
`
callbacks.InitCallbackHandlers([]callbacks.Handler{&utils.LoggerCallbacks{}})
userInput := "我在2025年5月1日到5月7日要去Las Vegas游玩,一共有四个人包括三个大人,一个小孩子,每天的预算是500美元,前三天住一个酒店,后四天住一个酒店。你可以帮我找到合适的住宿么并把结果输出到 hotel_plan.txt中"
msg, err := reactAgent.Generate(ctx, []*schema.Message{
{
Role: schema.User,
Content: userInput,
},
{
Role: schema.System,
Content: systemPrompt,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Content)
}
结果如此