调用 DeepSeek API,并通过 MCP (Model Context Protocol) 协议接入 mcp-clickhouse,从而让 DeepSeek 能够查询 ClickHouse 数据库来回答用户问题。
- stdio 模式
前置准备
在运行代码之前,你需要确保环境中有以下依赖:
-
安装 Python 库 :
bashpip install mcp openai -
安装
uv(因为你的配置中使用了uv来运行 mcp-clickhouse):bashpip install uv -
获取 DeepSeek API Key:你需要一个有效的 DeepSeek API 密钥。
Python 代码 (main.py)
请将代码中的 <YOUR_DEEPSEEK_API_KEY> 替换为你的实际 Key,并确保 <clickhouse-host> 等配置已填入正确的值。
python
import asyncio
import os
import json
import sys
from typing import List, Dict, Any, Optional
# 导入 MCP 相关库
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import mcp.types as types
# 导入 OpenAI SDK (DeepSeek 兼容 OpenAI 格式)
from openai import AsyncOpenAI
# ================= 配置部分 =================
# DeepSeek API 配置
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "<YOUR_DEEPSEEK_API_KEY>")
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
MODEL_NAME = "deepseek-chat" # 或者 "deepseek-reasoner" (R1)
# MCP ClickHouse Server 配置 (来自你的 JSON)
MCP_CLICKHOUSE_CONFIG = {
"command": "uv",
"args": [
"run",
"--with",
"mcp-clickhouse",
"--python",
"3.10",
"mcp-clickhouse"
],
"env": {
"CLICKHOUSE_HOST": "<clickhouse-host>",
"CLICKHOUSE_PORT": "<clickhouse-port>",
"CLICKHOUSE_USER": "<clickhouse-user>",
"CLICKHOUSE_PASSWORD": "<clickhouse-password>",
"CLICKHOUSE_ROLE": "<clickhouse-role>",
"CLICKHOUSE_SECURE": "true",
"CLICKHOUSE_VERIFY": "true",
"CLICKHOUSE_CONNECT_TIMEOUT": "30",
"CLICKHOUSE_SEND_RECEIVE_TIMEOUT": "30"
}
}
# ================= 工具函数 =================
def convert_mcp_tool_to_openai(mcp_tool: types.Tool) -> Dict[str, Any]:
"""
将 MCP 工具定义转换为 OpenAI/DeepSeek 支持的 Function Calling 格式。
"""
return {
"type": "function",
"function": {
"name": mcp_tool.name,
"description": mcp_tool.description,
"parameters": mcp_tool.inputSchema
}
}
# ================= 主逻辑 =================
async def run_chat_loop():
# 1. 初始化 DeepSeek 客户端
client = AsyncOpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)
# 2. 准备 MCP Server 参数
server_params = StdioServerParameters(
command=MCP_CLICKHOUSE_CONFIG["command"],
args=MCP_CLICKHOUSE_CONFIG["args"],
env={**os.environ, **MCP_CLICKHOUSE_CONFIG["env"]} # 合并当前环境变量
)
print(f"正在启动 MCP Server: {MCP_CLICKHOUSE_CONFIG['command']}...")
# 3. 连接 MCP Server 并开始对话循环
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# 初始化连接
await session.initialize()
# 获取 MCP Server 提供的工具列表
mcp_tools_list = await session.list_tools()
openai_tools = [convert_mcp_tool_to_openai(tool) for tool in mcp_tools_list.tools]
print(f"\n已连接 MCP Server,加载了 {len(openai_tools)} 个工具: {[t['function']['name'] for t in openai_tools]}")
print("-" * 50)
print("你可以开始询问关于 ClickHouse 的问题了 (输入 'quit' 退出)。")
messages = [
{"role": "system", "content": "你是一个智能助手,可以利用工具查询 ClickHouse 数据库来回答用户问题。"}
]
while True:
user_input = input("\n用户: ")
if user_input.lower() in ["quit", "exit"]:
break
messages.append({"role": "user", "content": user_input})
# 第一轮调用:发送用户问题 + 工具定义给 DeepSeek
try:
response = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
tools=openai_tools,
)
except Exception as e:
print(f"API 调用错误: {e}")
continue
assistant_msg = response.choices[0].message
messages.append(assistant_msg)
# 检查 DeepSeek 是否想调用工具
if assistant_msg.tool_calls:
print(f"\n[思考] 模型决定调用工具...")
for tool_call in assistant_msg.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f" -> 调用工具: {tool_name}, 参数: {tool_args}")
# 执行 MCP 工具
try:
mcp_result = await session.call_tool(tool_name, arguments=tool_args)
# 获取文本结果 (ClickHouse MCP 通常返回文本或 JSON 文本)
tool_output = ""
if mcp_result.content:
for content in mcp_result.content:
if content.type == "text":
tool_output += content.text
print(f" <- 工具返回结果 (前100字符): {tool_output[:100]}...")
# 将工具结果添加回对话历史
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_output
})
except Exception as e:
error_msg = f"工具执行出错: {str(e)}"
print(f" ! {error_msg}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": error_msg
})
# 第二轮调用:将工具结果发送回 DeepSeek 获取最终回答
final_response = await client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
# 这里依然传入 tools,以防模型需要继续多步调用
tools=openai_tools,
)
final_content = final_response.choices[0].message.content
print(f"\nDeepSeek: {final_content}")
messages.append(final_response.choices[0].message)
else:
# 如果不需要调用工具,直接输出结果
print(f"\nDeepSeek: {assistant_msg.content}")
if __name__ == "__main__":
# Windows 下通常需要这个策略
if sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(run_chat_loop())
代码逻辑解释
-
MCP 连接 (
stdio_client):- 代码使用
uv run ...命令启动mcp-clickhouse的子进程。 - 通过标准输入/输出 (stdio) 与该进程建立通信通道。
CLICKHOUSE_HOST等敏感信息通过环境变量 (env) 传递给子进程,保证安全性。
- 代码使用
-
工具发现与转换 (
convert_mcp_tool_to_openai):session.list_tools()从 mcp-clickhouse 获取可用的工具(例如执行 SQL 查询的工具)。- 由于 MCP 的工具定义格式与 OpenAI/DeepSeek 的 Function Calling 格式略有不同(主要在于外层结构),代码中定义了一个转换函数将其适配。
-
对话循环 (Chat Loop):
- User Input: 接收你的问题。
- First API Call: 将问题和转换后的工具列表发送给 DeepSeek。
- Tool Execution : 如果 DeepSeek 返回
tool_calls(例如它生成了一个 SQL 语句来查询数据),Python 代码会拦截这个请求,通过session.call_tool在本地的 mcp-clickhouse 服务中执行该 SQL。 - Second API Call : 将 ClickHouse 返回的查询结果(数据)作为
tool类型的消息发回给 DeepSeek。 - Final Answer: DeepSeek 根据查询结果生成最终的自然语言回答。
运行示例
假设你问:"查询 users 表里有多少行数据?"
- DeepSeek 分析意图,返回
tool_calls,调用query_sql工具,参数为SELECT count() FROM users。 - Python 脚本通过 MCP 执行该 SQL。
- ClickHouse 返回
105。 - Python 脚本将
105发给 DeepSeek。 - DeepSeek 回答:"users 表里共有 105 行数据。"