本地部署deepseek大模型后使用c# winform调用(可离线)
介于最近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发送图片让他进行分析,只支持文字对话。断网也是可以继续运行的。
如有更好的想法,欢迎大家评论区畅所欲言!