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

文章正文
发布时间:2025-08-21 03:08

介于最近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读取用户输入的文字

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

附上源代码:

cs 复制代码

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(":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发送图片让他进行分析,只支持文字对话。断网也是可以继续运行的。

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