Open WebUI+MCP搭建个人AI智能体

这里以制作一个电动车充电分析的智能体为例子,让智能体能实现帮我记录、查询、分析我的小电驴的充电和里程记录。

一、部署Open WebUI

Open WebUI 是一个为大型语言模型(LLM)设计的,可以本地化部署免费 开源语言模型交互平台。它支持各种 LLM 运行器,如 Ollama 和 OpenAI 兼容的 API,并内置了 RAG 推理引擎,是一个强大的 AI 部署解决方案。

支持Python 函数调用 支持本地 RAG 支持MCP 等等

它还有很多其他的功能,可以自己探索,还是很强大的,这里我们只用它MCP、函数调用的能力。

Docker部署

shell 复制代码
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

具体看官方文档哈 Open WebUI

如果你的服务器上安装了1Panel面板,在它的AI模块菜单下可以一键安装Ollama和OpenWebUI,实属懒人必备,当然我也是这样做的

他支持 ​Ollama​ 本地模型和连接 OpenAI 兼容接口,这样也兼容硅基流动的接口,启动成功后注册进入管理员界面,可以在管理员面板中配置

我是用的服务器是阿里云买的个人服务器 2核2G3M ,安装了Olama尝试不是了几个模型,无奈1.5b的模型都部署不上,也尝试下载了0.5b的模型,对话效果只能评价为 不一定会说人话,还是选择了硅基流动的api,上面有一些免费使用的模型,当然参数量大部分都在7B左右,我们要做的就是使用免费的模型搭建可用的智能体。

这里我启用了一些在硅基流动上免费的模型,这样就可以实现对话了

二、部署MCPO

OpenWebUI支持的OpenAPI工具服务器,可以通过OpenAPI规范作为标准协议给LLM代理提供外部工具集成,目的是已用、学习成本小;在官方文档上解释了为什么没有直接使用MCP,MCP工具通过标准输入输出(Studio)进行通信,在本地计算机可以实现,在云上部署无法实现(当然也可以使用SSE);

MCPO是一个MCP的代理服务器,可以通过标准 REST/OpenAPI API使用通过MCP实现的工具服务器,可以实现调用Restful接口方式来使用MCP提供的工具方法。

安装MCPO

shell 复制代码
docker run -p 8000:8000 ghcr.io/open-webui/mcpo:main --api-key "top-secret" --config /path/to/config.json

在配置config.json中可以配置需要代理的MCP工具服务

json 复制代码
{
  "mcpServers": {
    "memory": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-memory"]
    },
    "time": {
      "command": "uvx",
      "args": ["mcp-server-time", "--local-timezone=America/New_York"]
    },
    "mcp_sse": {
      "url": "http://127.0.0.1:8001/sse"
    } // SSE MCP Server
  }
}

这里附上mcpo的github地址,这里哈,具体细节可以自己看看

这里是我尝试代理的几个MCP工具服务,可以直接通过swagger调用工具接口,确实也方便调试了,个人感觉也挺好用的

也可以进入具体代理的MCP中测试具体的方法接口

在Open WebUI中可以配置MCPO的链接地址,可以在对话的工具找到配置的MCP,选用这样就可以调用MCP的了

三、实现MCP工具

前置步骤我们都已经做完了,有了对话界面,AI可以调用MCP工具,接下来就编写一个MCP实现我们的自己的智能体了。

结合我自己的需求,前段时间新购入了一个绿源电动车,每次充电回记录一下充电时间和充电时候的里程,想看看多久之后电池会有衰减,衰减到多大程度(感觉自己也挺无聊);这样想让AI帮我记录充电时的里程,比如对模型发送文字"今天在小区后面的充电桩充电了,充电时里程是100公里,帮我记录",智能体会帮我记录下来,通过记录的数据帮我分析电池衰减情况;这样我们实现一个对充电记录表的CURD就可以了,剩下的交给AI去分析;

我们选择MySQL数据库存储数据,这是表设计

sql 复制代码
CREATE TABLE `lvyuan_electric_vehicle_log` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`charge_date` datetime NOT NULL COMMENT '充电日期时间',
	`total_mileage` decimal(10,2) NOT NULL COMMENT '当前总里程(km)',
	`charge_location` varchar(255) DEFAULT NULL COMMENT '充电地点',
	`battery_percent` int(11) DEFAULT NULL COMMENT '充电前电池百分比(%)',
	`charger_info` varchar(100) DEFAULT NULL COMMENT '充电桩信息',
	`notes` text COMMENT '备注信息',
	PRIMARY KEY (`id`),
	KEY `idx_charge_date` (`charge_date`)
) ENGINE=InnoDB COMMENT='绿源电动车全周期里程记录表';

快速实现一个MCP,我选择使用Python来实现,代码量小,MPCO代理服务器也是使用Python编写,可以通过uv命令启动,支持比较好,代码如下

python 复制代码
import asyncio
import os

import pymysql
from mcp.server import Server
from mcp.types import Resource, ResourceTemplate, Tool, TextContent
from pydantic import AnyUrl
import json


def get_db_config():
    config = {
        "host": os.getenv("ADB_MYSQL_HOST", "localhost"),
        "port": int(os.getenv("ADB_MYSQL_PORT", 3306)),
        "user": os.getenv("ADB_MYSQL_USER"),
        "password": os.getenv("ADB_MYSQL_PASSWORD"),
        "database": os.getenv("ADB_MYSQL_DATABASE"),
    }

    if not all([config["user"], config["password"], config["database"]]):
        raise ValueError("Missing required database configuration")

    return config

def get_lvyuan_mileage_record_list(self, start_date: str | None = None, end_date: str | None = None, limit: int | None = None) -> list[TextContent]:
    """
    获取绿源电动车充电里程记录
    :param start_date: 开始日期(可选)
    :param end_date: 结束日期(可选)
    :param limit: 返回记录数量(可选)
    :return: 包含绿源电动车充电里程记录的字符串
    """
    config = get_db_config()
    conn = pymysql.connect(**config)
    conn.autocommit(True)
    cursor = conn.cursor()

    try:
        sql = "SELECT * FROM lvyuan_electric_vehicle_log"
        params = []
        
        if start_date or end_date:
            conditions = []
            if start_date:
                conditions.append("charge_date >= %s")
                params.append(start_date)
            if end_date:
                conditions.append("charge_date <= %s")
                params.append(end_date)
            sql += " WHERE " + " AND ".join(conditions)
        
        sql += " ORDER BY charge_date DESC"
        
        if limit is not None:
            sql += " LIMIT %s"
            params.append(limit)
        
        # 打印执行SQL
        print("执行查询SQL:" + sql)
        cursor.execute(sql, tuple(params) if params else None)

        columns = [desc[0] for desc in cursor.description]
        rows = cursor.fetchall()
        if rows:
            result = [",".join(map(str, row)) for row in rows]
            return [TextContent(type="text", text="\n".join([",".join(columns)] + result))]
        else:
            return [TextContent(type="text", text="未找到任何绿源电动车充电里程记录。")]
    except Exception as e:
        return [TextContent(type="text", text=f"获取绿源电动车充电里程记录时出错: {str(e)}")]
    finally:
        if cursor:
            cursor.close()
        if conn.open:
            conn.close()


def add_lvyuan_mileage_record(charge_date: str, total_mileage: float, charge_location: str, battery_percent: int, charger_info: str, notes: str) -> list[TextContent]:
    """
    新增一条绿源电动车充电里程记录
    :param charge_date: 充电日期时间
    :param total_mileage: 当前总里程(km)
    :param charge_location: 充电地点
    :param battery_percent: 充电前电池百分比(%)
    :param charger_info: 充电桩信息
    :param notes: 备注信息
    :return: 操作结果
    """
    config = get_db_config()
    conn = pymysql.connect(**config)
    conn.autocommit(True)
    cursor = conn.cursor()
    try:
        sql = """
            INSERT INTO lvyuan_electric_vehicle_log (charge_date, total_mileage, charge_location, battery_percent, charger_info, notes)
            VALUES (%s, %s, %s, %s, %s, %s)
        """
        cursor.execute(sql, (charge_date, total_mileage, charge_location, battery_percent, charger_info, notes))
        return [TextContent(type="text", text="新增记录成功。")]
    except Exception as e:
        return [TextContent(type="text", text=f"新增记录失败: {str(e)}")]
    finally:
        if cursor:
            cursor.close()
        if conn.open:
            conn.close()


def update_lvyuan_mileage_record(record_id: int, charge_date: str | None = None, total_mileage: float | None = None, charge_location: str | None = None, battery_percent: int | None = None, charger_info: str | None = None, notes: str | None = None) -> list[TextContent]:
    """
    修改指定ID的绿源电动车充电里程记录
    :param record_id: 记录ID(必填)
    :param charge_date: 充电日期时间(可选)
    :param total_mileage: 当前总里程(km)(可选)
    :param charge_location: 充电地点(可选)
    :param battery_percent: 充电前电池百分比(%)(可选)
    :param charger_info: 充电桩信息(可选)
    :param notes: 备注信息(可选)
    :return: 操作结果
    """
    if record_id is None:
        return [TextContent(type="text", text="记录ID不能为空。")]
    fields = []
    values = []
    if charge_date is not None:
        fields.append("charge_date=%s")
        values.append(charge_date)
    if total_mileage is not None:
        fields.append("total_mileage=%s")
        values.append(total_mileage)
    if charge_location is not None:
        fields.append("charge_location=%s")
        values.append(charge_location)
    if battery_percent is not None:
        fields.append("battery_percent=%s")
        values.append(battery_percent)
    if charger_info is not None:
        fields.append("charger_info=%s")
        values.append(charger_info)
    if notes is not None:
        fields.append("notes=%s")
        values.append(notes)

    if not fields:
        return [TextContent(type="text", text="请至少提供一个需要修改的字段。")]

    sql = f"UPDATE lvyuan_electric_vehicle_log SET {', '.join(fields)} WHERE id=%s"
    values.append(record_id)

    config = get_db_config()
    conn = pymysql.connect(**config)
    conn.autocommit(True)
    cursor = conn.cursor()
    try:
        cursor.execute(sql, tuple(values))
        return [TextContent(type="text", text="修改记录成功。")] 
    except Exception as e:
        return [TextContent(type="text", text=f"修改记录失败: {str(e)}")]
    finally:
        if cursor:
            cursor.close()
        if conn.open:
            conn.close()


def delete_lvyuan_mileage_record(record_id: int) -> list[TextContent]:
    """
    删除指定ID的绿源电动车充电里程记录
    :param record_id: 记录ID
    :return: 操作结果
    """
    config = get_db_config()
    conn = pymysql.connect(**config)
    conn.autocommit(True)
    cursor = conn.cursor()
    try:
        sql = "DELETE FROM lvyuan_electric_vehicle_log WHERE id=%s"
        cursor.execute(sql, (record_id,))
        return [TextContent(type="text", text="删除记录成功。")]
    except Exception as e:
        return [TextContent(type="text", text=f"删除记录失败: {str(e)}")]
    finally:
        if cursor:
            cursor.close()
        if conn.open:
            conn.close()


def get_latest_lvyuan_mileage_record() -> list[TextContent]:
    """
    查询最新一条绿源电动车充电里程记录
    :return: 最新记录
    """
    config = get_db_config()
    conn = pymysql.connect(**config)
    conn.autocommit(True)
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT * FROM lvyuan_electric_vehicle_log ORDER BY id DESC LIMIT 1;")
        columns = [desc[0] for desc in cursor.description]
        row = cursor.fetchone()
        if row:
            result = ",".join(map(str, row))
            return [TextContent(type="text", text="\n".join([",".join(columns), result]))]
        else:
            return [TextContent(type="text", text="未找到最新绿源电动车充电里程记录。")] 
    except Exception as e:
        return [TextContent(type="text", text=f"查询最新记录时出错: {str(e)}")]
    finally:
        if cursor:
            cursor.close()
        if conn.open:
            conn.close()


app = Server(
    name="lvyuan_mileage_server",
    version="1.0.0"
)

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_lvyuan_mileage_record_list",
            description="获取绿源电动车充电里程记录,可指定时间范围",
            inputSchema={
                "type": "object",
                "properties": {
                    "start_date": {
                        "type": "string",
                        "description": "开始日期(可选),格式为YYYY-MM-DD HH:MM:SS"
                    },
                    "end_date": {
                        "type": "string",
                        "description": "结束日期(可选),格式为YYYY-MM-DD HH:MM:SS"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "返回记录数量(可选)"
                    }
                },
                "required": []
            },
        ),
        Tool(
            name="add_lvyuan_mileage_record",
            description="新增一条绿源电动车充电里程记录",
            inputSchema={
                "type": "object",
                "properties": {
                    "charge_date": {
                        "type": "string",
                        "description": "充电日期时间"
                    },
                    "total_mileage": {
                        "type": "number",
                        "description": "当前总里程(km)"
                    },
                    "charge_location": {
                        "type": "string",
                        "description": "充电地点"
                    },
                    "battery_percent": {
                        "type": "integer",
                        "description": "充电前电池百分比(%)"
                    },
                    "charger_info": {
                        "type": "string",
                        "description": "充电桩信息"
                    },
                    "notes": {
                        "type": "string",
                        "description": "备注信息"
                    }
                },
                "required": ["charge_date", "total_mileage", "charge_location", "battery_percent", "charger_info", "notes"]
            },
        ),
        Tool(
            name="update_lvyuan_mileage_record",
            description="修改指定ID的绿源电动车充电里程记录",
            inputSchema={
                "type": "object",
                "properties": {
                    "record_id": {
                        "type": "integer",
                        "description": "记录ID"
                    },
                    "charge_date": {
                        "type": "string",
                        "description": "充电日期时间"
                    },
                    "total_mileage": {
                        "type": "number",
                        "description": "当前总里程(km)"
                    },
                    "charge_location": {
                        "type": "string",
                        "description": "充电地点"
                    },
                    "battery_percent": {
                        "type": "integer",
                        "description": "充电前电池百分比(%)"
                    },
                    "charger_info": {
                        "type": "string",
                        "description": "充电桩信息"
                    },
                    "notes": {
                        "type": "string",
                        "description": "备注信息"
                    }
                },
                "required": ["record_id"]
            },
        ),
        Tool(
            name="delete_lvyuan_mileage_record",
            description="删除指定ID的绿源电动车充电里程记录",
            inputSchema={
                "type": "object",
                "properties": {
                    "record_id": {
                        "type": "integer",
                        "description": "记录ID"
                    }
                },
                "required": ["record_id"]
            },
        ),
        Tool(
            name="get_latest_lvyuan_mileage_record",
            description="查询最新一条绿源电动车充电里程记录",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": []
            },
        ),
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "get_lvyuan_mileage_record_list":
        return get_lvyuan_mileage_record_list(arguments)
    elif name == "add_lvyuan_mileage_record":
        return add_lvyuan_mileage_record(**arguments)
    elif name == "update_lvyuan_mileage_record":
        return update_lvyuan_mileage_record(**arguments)
    elif name == "delete_lvyuan_mileage_record":
        return delete_lvyuan_mileage_record(**arguments)
    elif name == "get_latest_lvyuan_mileage_record":
        return get_latest_lvyuan_mileage_record(**arguments)
    else:
        raise ValueError(f"Unknown tool: {name}")
        
async def main():
    from mcp.server.stdio import stdio_server

    async with stdio_server() as (read_stream, write_stream):
        try:
            await app.run(
                read_stream,
                write_stream,
                app.create_initialization_options()
            )
        except Exception as e:
            raise


if __name__ == "__main__":
    asyncio.run(main())

部署到服务器上,通过MCPO进行代理,将地址添加到Open WebUI中,完成。

四、在Open WebUI中新建智能体

这里我们要做的只是新建一个智能体,选择模型,配置提示词,选择需要调用的工具;模型一定要选择一个支持函数调用的模型,这里我们就选择Qwen/Qwen2.5-7B-lnstruct,主打免费,提示词我们也借助AI帮我们生成一个;

实验一下效果,通过对话实现了数据保存和查询,感觉不错

让他查询所有记录,分析每次充电后的行驶里程,分析是否有续航减少的情况,可以看到OpenWebUI可以直接展示HTML格式的代码,Nice!

这样我的智能体就搭建完成了,在尝试过程中,问一些复杂的分析时会出现模型的输出的Token不足无法输出完整、有些问题无法正确理解用户的需求等问题,简单的分析和数据录入、查询还是可以做到的。

相关推荐
啊哈哈哈哈哈啊哈哈13 分钟前
R4打卡——tensorflow实现火灾预测
人工智能·python·tensorflow
闻道☞18 分钟前
RAGFlowwindows本地pycharm运行
python·pycharm·ragflow
默凉31 分钟前
注意力机制(np计算示例)单头和多头
python
咸其自取1 小时前
Flask(3): 在Linux系统上部署项目
python·ubuntu
未来之窗软件服务1 小时前
数字人,磁盘不够No space left on device,修改python 执行环境-云GPU算力—未来之窗超算中心
linux·开发语言·python·数字人
python_chai2 小时前
Python多进程并发编程:深入理解Lock与Semaphore的实战应用与避坑指南
开发语言·python·高并发·多进程··信号量
Cheng_08292 小时前
llamafactory的包安装
python·深度学习
咸其自取2 小时前
Flask(1): 在windows系统上部署项目1
python·flask
CopyLower2 小时前
**Microsoft Certified Professional(MCP)** 认证考试
python·microsoft·flask
赵谨言3 小时前
基于Python的推荐算法的电影推荐系统的设计
经验分享·python·毕业设计