本地部署deepseek大模型后使用c# winform调用(可离线)

news/2025/2/26 16:39:32

        介于最近deepseek的大火,我就在想能不能用winform也玩一玩本地部署,于是经过查阅资料,然后了解到ollama部署deepseek,最后用ollama sharp NUGet包来实现winform调用ollama 部署的deepseek。

        本项目使用Vs2022和.net 8.0开发,ollama sharp 使用的是最新版本。也可以使用.net farmwork 4.7.2开发,但是ollama sharp 没办法使用最新的,只能使用3.几的版本,3点几的版本有问题,因为ollama sharp提供的交互方法不是异步的,这就会导致,大模型如果回复你一个很长的的问题的时候,就会突然中断,最后我就彻底放弃了,发现最新版本的ollama sharp的交互方法是异步的,最后抱着试一试的心态,果然成功了,让写个4000字的论文框架,基本上回答时间在2分钟左右也不会中断,(2分钟是因为我的内存有点少,显卡还行吧)。效果还是很不错的,本人使用的deepseek r1 14b的大模型,4060的显卡,16G的内存,回复速度还是很快的,内存基本上跑80%左右。显卡40%上下浮动。

展示图

下载ollama

地址:奥拉马

下载Windows版本然后进行安装就好了,安装完成以后,我们可以在系统环境变量里面添加这两个

第二个是利用ollama下载的大模型的位置,C盘不够的可以加这个变量,如果C盘够多可以忽略,最好设置完以后重启一下电脑再安装ollama,安装好以后可以打开cmd 如图所示:如果是这样,说明你已经安装成功了,

利用ollama安装deepseek r1 14b

这里我们还是打开ollama网站,打开

如果说内存在32G可以选择32b的体验一下,应该会比14b更好用些,最后点击箭头所指的地方复制下来打开cmd,直接ctrl+c复制然后回车他就会自动下载,这里有个小技巧:他下载会越来越慢,我们可以按一下ctrl+c,再按一下键盘的上方向键他就会接着下载,这个时候慢慢就快起来了。

下载完成后我们新打开一个cmd输入ollama list这个可以查看我们已经下载下来的大模型

补充一点:还可以使用ollama rm 大模型的Name进行删除

Ollama Sharp

awaescher/OllamaSharp:在 .NET 中使用 Ollama API 的最简单方法 

上面的是链接地址,这是github里面的一个开源项目,使用之前可以看看他的介绍以及使用方法,知其然,知其所以然。

winform 连接大模型

我们打开我们的vs2022。创建新工程,一定要选择后面不带括号.netfarmwork的,才会用到8,.0框架

我们进去以后先添加nuget包,找到依赖项,右键管理NUGET包,打开以后搜索ollama sharp

这里我已经安装过了

等待安装成功以后,我们打开我们窗体的设计器,在左侧的工具箱添加一下的控件

listbox主要用来展示安装的大模型

richtextbox主要用来展示用户输入的文字和deepseek回复的文字

textBox读取用户输入的文字

一个发送按钮一个取消思考按钮

附上源代码:

using OllamaSharp.Models;
using OllamaSharp;
using System.Text.RegularExpressions;

namespace WinFormsApp1
{
    public partial class Form1 : Form
    {
        private Uri uri;
        private OllamaApiClient ollama;
        private List<Model> models;
        private bool connect;
        static ManualResetEvent resetEvent = new ManualResetEvent(false);
        private CancellationTokenSource cancellationTokenSource;
        int step = 0;
        private bool mIsCancel = false;
        public Form1()
        {
            InitializeComponent();
        }

        private async void Form1_Load(object sender, EventArgs e)
        {
            richTextBox1.AppendText("稍等,我正在加载模型。。。。。" + Environment.NewLine);
            uri = new Uri("http://localhost:11434");
            ollama = new OllamaApiClient(uri);
            connect = await ollama.IsRunningAsync();
            models = (await ollama.ListLocalModelsAsync()).ToList();
            mSelectItem = 0;
            LoadModles();
            step = 1;
            richTextBox1.AppendText("请在上方选择你要使用的模型,单击即可" + Environment.NewLine);
        }


        /// <summary>
        /// 流程交互
        /// </summary>
        public void WorkFololw()
        {
            Task.Run(() =>
            {
                while (true)
                {
                    Thread.Sleep(200);
                    string cleanText = "";
                    if (textBox2.Text != "")
                    {
                        cleanText = textBox2.Text;
                    }

                    switch (step)
                    {
                        case 1:
                            Thread.Sleep(100);

                            if (models.Count == 0)
                            {
                                return;
                            }
                            
                            ollama.SelectedModel = models.ToArray()[mSelectItem].Name; // 选择模型名称
                            Log("我已经准备好了小帅哥快来玩呀!", 0, Color.Black);
                            step = 2;

                            break;
                        case 2:
                            if (cleanText.Contains("\r\n"))
                            {
                                var prompt = textBox2.Text; // 从文本框读取提示词
                                Log(Environment.NewLine + "用户哥:" + textBox2.Text.TrimEnd('\r', '\n') + Environment.NewLine, 0, Color.Blue);
                                var keepChatting = true;
                                var chat = new Chat(ollama, prompt);

                                Invoke(new Action(() =>
                                {
                                    button2.Visible = true;
                                    richTextBox1.AppendText("deepSeek-R1>:" + Environment.NewLine);
                                }));
                                BeginSiKao(keepChatting, chat, "");
                                step = 3;
                            }
                            break;
                        case 3:
                            if (cleanText.Contains("\r\n"))
                                step = 2;
                            break;

                    }
                }
            });
        }

        /// <summary>
        /// 开始思考
        /// </summary>
        /// <param name="keepChatting"></param>
        /// <param name="chat"></param>
        public async void BeginSiKao(bool keepChatting, Chat chat, string mImageMsg)
        {
            //开始聊天
            await BeginChat(keepChatting, chat, mImageMsg);
        }

        /// <summary>
        /// 加载本地大模型
        /// </summary>
        public void LoadModles()
        {
            if (models.Any())
            {
                foreach (var model in models)
                {
                    if (model.Name.Contains("v2"))
                    {
                        Log($"大模型:{model.Name} {model.Size / 1024 / 1024} MB", 1, Color.MediumSeaGreen); // 输出模型名称和大小
                    }
                    Invoke(new Action(() =>
                    {
                        listBox1.Items.Add($"大模型:{model.Name} {model.Size / 1024 / 1024} MB");
                    }));

                }
            }
            else
            {
                Log("没有大模型环境,请自行下载大模型", 1, Color.Red);
                return;
            }
        }

        /// <summary>
        /// 开始聊天
        /// </summary>
        /// <param name="keepChatting"></param>
        /// <param name="chat"></param>
        /// <returns></returns>
        public async Task BeginChat(bool keepChatting, Chat chat, string ImageMsg)
        {
            cancellationTokenSource = new CancellationTokenSource();
            var tokenx = cancellationTokenSource.Token;

            Invoke(new Action(() =>
            {
                button1.Text = "思考回答中...";

            }));

            string message;
            message = textBox2.Text.TrimEnd('\r', '\n'); // 从文本框读取用户输入的消息
            if (message == "")
            {
                message = ImageMsg;
            }
            Clear(); // 清空文本框以便用户输入下一条消息

            Task sendTask = Task.Run(async () =>
            {
                if (string.IsNullOrEmpty(message.Trim()))
                {
                    return;
                }


                bool isFirstToken = true;
                try
                {
                    string mmsf = "";
                    await foreach (var answerToken in chat.SendAsync(message))
                    {
                        // 如果取消了操作,提前退出
                        if (cancellationTokenSource.Token.IsCancellationRequested)
                        {
                            continue;
                        }
                        if (answerToken != "<think>" && answerToken != "</think>")
                        {
                            mmsf += answerToken;
                            // 使用Invoke更新UI
                            richTextBox1.Invoke(new Action(() =>
                            {
                                if (isFirstToken)
                                {
                                    richTextBox1.Focus();
                                    isFirstToken = false;
                                }
                                richTextBox1.AppendText(answerToken.Trim());
                            }));
                        }

                    }
                    string newmsg = "";
                    if (mmsf.Contains("```sql")) 
                    {
                        newmsg= FormatSql(mmsf);
                        // 使用Invoke更新UI
                        richTextBox1.Invoke(new Action(() =>
                        {
                            if (isFirstToken)
                            {
                                richTextBox1.Focus();
                                isFirstToken = false;
                            }
                            richTextBox1.AppendText(newmsg.Trim());
                        }));
                    }
                   
                }
                catch (OperationCanceledException)
                {
                    // 处理取消操作时的异常
                    Invoke(new Action(() =>
                    {
                        if (button1.Text == "思考回答中...")
                        {
                            button1.Text = "发送";
                        }
                    }));
                }

                catch (Exception ex)
                {
                    if (mIsCancel == true)
                    {
                        // 捕获其他类型的异常并记录
                        Log(Environment.NewLine + $"用户哥取消了回答", 0, Color.Red);
                        mIsCancel = false;
                    }
                    else
                    {
                        // 捕获其他类型的异常并记录
                        Log(Environment.NewLine + $"哎呦出错了" + ex, 0, Color.Red);
                    }
                }


            });

            await sendTask;

            Invoke(new Action(() =>
            {
                button2.Visible = false;  // 隐藏取消按钮
                button1.Text = "发送";
                textBox2.Focus();
                richTextBox1.AppendText(Environment.NewLine);

            }));
        }


        /// <summary>
        /// 清空输入文本框
        /// </summary>
        public void Clear()
        {
            Invoke(new Action(() =>
            {
                textBox2.Clear();
            }));
        }

        /// <summary>
        /// 更新控件的一些值或者追加文字
        /// </summary>
        /// <param name="message"></param>
        /// <param name="mtype">0:追加文字,1:大模型使用</param>
        public void Log(string message, int mtype, Color color)
        {
            if (mtype == 0)
            {
                Invoke(new Action(() =>
                {
                    richTextBox1.AppendText(message + Environment.NewLine);
                    textBox2.Focus();
                }));
            }
            else
            {
                Invoke(new Action(() =>
                {
                    label1.Text = message;
                    label1.ForeColor = color;
                    textBox2.Focus();
                }));
            }

        }

        /// <summary>
        /// 发送按钮
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            if (models.Count == 0)
            {
                MessageBox.Show("没有大模型环境,怎么玩啊!");
                return;
            }
            if (button1.Text == "思考回答中...")
            {
                MessageBox.Show("正想着呢,别点了爷们");
            }
            else
            {
                string mm = textBox2.Text;
                textBox2.Text = "用户哥:" + mm + Environment.NewLine;
                Log(textBox2.Text.TrimEnd('\r', '\n'), 0, Color.Blue);
                var prompt = mm; // 从文本框读取提示词
                var keepChatting = true;
                var chat = new Chat(ollama, prompt);
                Invoke(new Action(() =>
                {
                    richTextBox1.AppendText("deepSeek-R1>:");
                }));
                BeginSiKao(keepChatting, chat, "");
                step = 3;
                if (button2.Visible == false)
                {
                    button2.Visible = true;

                }
            }

        }

        /// <summary>
        /// 取消回答
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            mIsCancel = true;
            StopThinking();
        }
        public void StopThinking()
        {
            cancellationTokenSource?.Cancel();  // 取消当前的操作
            Invoke(new Action(() =>
            {
                button2.Visible = false;  // 隐藏取消按钮
                button1.Enabled = true;  // 恢复发送按钮
                textBox2.Focus();  // 让用户可以继续输入
            }));
        }

        private int mSelectItem = 99;
        private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            mSelectItem = listBox1.SelectedIndex;
            WorkFololw();
        }

       

        public static string FormatSql(string input)
        {
            // 移除开头的 sql 和多余的空格
            input = input.Trim();

            // 用正则表达式找到从 sql 开头到下一个结束符号的 SQL 代码
            string pattern = @"`sql(.*?)```";
            var match = Regex.Match(input, pattern, RegexOptions.Singleline);

            if (match.Success)
            {
                // 获取 sql 语句部分
                string sql = match.Groups[1].Value.Trim();

                // 分析 SQL 的每个部分并格式化
                return FormatSqlServerCreateTable(sql);
            }

            return input;
        }

        private static string FormatSqlServerCreateTable(string sql)
        {
            // 分割 SQL 语句
            sql = sql.Replace("CREATETABLE", "CREATE TABLE")
                     .Replace("NOTNULL", "NOT NULL")
                     .Replace("VARCHAR", "VARCHAR")
                     .Replace("NVARCHAR", "NVARCHAR")
                     .Replace("CHECK", "CHECK")
                     .Replace("PRIMARYKEY", "PRIMARY KEY")
                     .Replace("UNIQUE", "UNIQUE")
                     .Replace("CHAR", "CHAR")
                     .Replace("DATENOTNULL", "DATE NOT NULL")
                     .Replace("TEXT", "TEXT")
                     .Replace("--", "-- "); // 确保注释有一个空格

            // 添加换行和缩进
            string formattedSql = "";
            int indentationLevel = 0;
            bool insideComment = false;

            for (int i = 0; i < sql.Length; i++)
            {
                char currentChar = sql[i];

                // 检查是否进入注释
                if (i < sql.Length - 1 && sql.Substring(i, 2) == "--")
                {
                    insideComment = true;
                }

                // 增加缩进处理
                if (currentChar == '(')
                {
                    formattedSql += " (";
                    indentationLevel++;
                }
                else if (currentChar == ')')
                {
                    formattedSql += "\n" + new string(' ', indentationLevel * 4) + ")";
                    indentationLevel--;
                }
                else if (currentChar == ',')
                {
                    formattedSql += ",\n" + new string(' ', indentationLevel * 4);
                }
                else
                {
                    if (insideComment)
                    {
                        formattedSql += currentChar;
                        if (currentChar == '\n')
                        {
                            insideComment = false;
                        }
                    }
                    else
                    {
                        formattedSql += currentChar;
                    }
                }
            }

            return formattedSql;
        }


       
    }
}

有些地方有些小bug,比如取消思考没有进行细节的处理,但是不影响正常的使用,

整体的逻辑就是:窗体启动时候在线程里面进行一个死循环,只要textBox文本框里面出现回车就根据变量step的值来进行对应的操作。目前无法给deepseek发送图片让他进行分析,只支持文字对话。断网也是可以继续运行的。

如有更好的想法,欢迎大家评论区畅所欲言!


http://www.niftyadmin.cn/n/5868943.html

相关文章

矩阵的 正定(Positive Definite)与负定(Negative Definite):从Fisher信息矩阵看“曲率”的秘密

矩阵的正定与负定&#xff1a;从Fisher信息矩阵看“曲率”的秘密 在数学和统计学中&#xff0c;矩阵的“正定性”和“负定性”是一对重要概念&#xff0c;尤其在优化、统计推断和机器学习中频繁出现。比如&#xff0c;Fisher信息矩阵&#xff08;Fisher Information Matrix, F…

STM32 缺一不可的最基础的初始化部分

STM32 缺一不可的最基础的初始化部分 初始化部分必须初始化作用关键配置系统时钟&#xff08;RCC&#xff09;​所有STM32程序的基础为CPU、总线和外设提供时钟信号1.选择时钟源&#xff08;HSI/HSE/PLL&#xff09; 2.配置系统时钟频率&#xff08;如168MHz&#xff09; 3.使…

数据结构(陈越,何钦铭) 第四讲 树(中)

4.1 二叉搜索树 4.1.1 二叉搜索树及查找 Position Find(ElementTyoe X,BinTree BST){if(!BST){return NULL;}if(X>BST->Data){return Find(X,BST->Right)}else if(X<BST->Data){return Find(X,BST->Left)}else{return BST;} } Position IterFind(ElementTyp…

C++中,关于用 size_t 还是用 int,永远要统一标准。

看以下例子&#xff1a; template<class T> class MyArray {public:T* _pData null; //指针&#xff0c;指向第一个元素size_t _nElementCount 0; //无素个数public:MyArray(const T* pt, const int nLen) { }size_t length() const { return _nElementCount…

HAProxy- https、四层负载实现与 负载均衡关键技术

目录 1、HAProxy实现四层负载 四层负载示例 ACL示例-四层访问控制 2、HAProxy- https实现 HAProxy https实现 证书制作 https配置示例 修改后端服务器的日志格式 验证https 3、 负载均衡关键技术 1、什么是 Session 2、什么是 Session 共享 1、基于 Cookie 的 Ses…

python爬虫学习第十一篇爬取指定类型数据

最近在学习Python爬虫的过程中&#xff0c;尝试用爬虫获取指定类型的数据。今天&#xff0c;我想和大家分享一下我的实践过程和遇到的问题。 一、实现目标 目标是从一个网站的API接口获取不同类型的食品数据。 比如&#xff0c;第一步我想获取汉堡、小食、甜品等不同类型的数…

ROS的action通信——实现阶乘运算(三)

在ROS中除了常见的话题(topic&#xff09;通信、服务(server)通信等方式&#xff0c;还有action通信这一方式&#xff0c;由于可以实时反馈任务完成情况&#xff0c;该通信方式被广泛运用于机器人导航等任务中。本文将通过三个小节的分享&#xff0c;实现基于action通信的阶乘运…

Linux 第三次脚本作业

源码编译安装httpd 2.4&#xff0c;提供系统服务管理脚本并测试&#xff08;建议两种方法实现&#xff09; 一、第一种方法 1、把 httpd-2.4.63.tar.gz 这个安装包上传到你的试验机上 2、 安装编译工具 (俺之前已经装好了&#xff09; 3、解压httpd包 4、解压后的httpd包的文…