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

一、部署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不足无法输出完整、有些问题无法正确理解用户的需求等问题,简单的分析和数据录入、查询还是可以做到的。