该死的石头 發表於 2024-9-9 09:24:00

C#整合Ollama实现本地LLMs调用

<h3 id="前言">前言</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;近两年<code>AIGC</code>发展的非常迅速,从刚开始的只有<code>ChatGPT</code>到现在的很百家争鸣。从开始的大参数模型,再到后来的小参数模型,从一开始单一的文本模型到现在的多模态模型等等。随着一起进步的不仅仅是模型的多样化,还有模型的使用方式。大模型使用的门槛越来越低,甚至现在每个人都可以在自己的电脑上运行模型。今天我们要说的就是大模型工具中的佼佼者<code>Ollama</code>,并演示如何通过<code>C#</code>来使用<code>Ollama</code>。</p>
<h3 id="ollama">Ollama</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;<code>Ollama</code>是一个开源的大语言模型(LLM)服务工具,它允许用户在本地PC环境快速实验、管理和部署大型语言模型。它支持多种流行的开源大型语言模型,如 <code>Llama 3.1</code>、<code>Phi 3</code>、<code>Qwen 2</code>、<code>GLM 4</code>等,并且可以通过命令行界面轻松下载、运行和管理这些模型。<code>Ollama</code>的出现是为了降低使用大型语言模型的门槛,是让大型语言模型更加普及和易于访问。一言以蔽之就是<code>Ollama让使用模型更简单</code>。无论是<code>CPU</code>或是<code>GPU</code>都可以,算力高的话推理速度更快,算力不足的话推理的慢,而且容易胡言乱语。</p>
<h4 id="安装">安装</h4>
<p><code>Ollama</code>的安装方式常用的有两种,一种是去官网下载,另一种是去GitHub下载,可以选择对应的系统版本进行下载</p>
<ul>
<li>官网首页直接下载 https://ollama.com/</li>
<li>Github Relase下载 https://github.com/ollama/ollama/releases</li>
</ul>
<p>我的是Windows操作系统,所以直接下载一路Next就可以,默认安装在<code>C盘</code>无法更改,强迫症的话可以通过<code>mklink</code>做链接,但是自动更新之后还是在<code>C盘</code>。自动升级这一块不用太担心,联网的情况,如果有新版本<code>Ollama</code>会推送更新。</p>
<p>安装完成之后可以修改常用的环境变量</p>
<ul>
<li>通过<code>OLLAMA_MODELS</code>环境变量设置模型下载的位置,默认是在<code>C盘</code>,可以换成其他地址。</li>
<li>通过<code>OLLAMA_HOST</code>设置<code>Ollama</code>服务监听的端口,默认的是<code>11434</code>。</li>
</ul>
<p>安装完成之后通过<code>version</code>查看,如果显示版本号则安装成功。</p>
<pre><code>ollama --version
</code></pre>
<p>比较常用的指令不多,也很简单</p>
<ul>
<li><code>ollama list</code>列出本地下载的模型</li>
<li><code>ollama ps</code>查看正在运行的模型</li>
<li><code>ollama pull 模型标识</code>下载模型到本地,比如我要下载<code>qwen2 7b</code>则使用<code>ollama pull qwen2:7b</code></li>
<li><code>ollama run 模型标识</code>运行模型,如果已下载则直接运行,如果没下载则先下载再运行。比如我要运行<code>qwen2 7b</code>可以直接运行<code>ollama run qwen2:7b</code></li>
</ul>
<p>也可以将本地已有的<code>GGUF</code>模型导入到<code>Ollama</code>中去,操作也很简单。</p>
<ol>
<li>编写一个名为<code>Modelfile</code>的文件,写入以下内容</li>
</ol>
<pre><code>FROM 模型路径/qwen2-0_5b-instruct-q8_0.gguf
</code></pre>
<ol start="2">
<li>通过<code>Ollama</code>创建模型</li>
</ol>
<pre><code>ollama create qwen2:0.5b -f Modelfile
</code></pre>
<ol start="3">
<li>运行刚创建的模型</li>
</ol>
<pre><code>ollama run qwen2:0.5b
</code></pre>
<p>需要注意的是运行<code>7B</code>至少需要<code>8GB</code>的内存或显存,运行<code>13B</code>至少需要<code>16GB</code>内存或显存。我电脑的配置信息如下</p>
<pre><code>型号: 小新Pro16 AI元启
CPU: AMD Ryzen 7 8845H
内存: 32.0 GB
</code></pre>
<p><code>AMD Ryzen 7 8845H</code>内置<code>NPU</code>,整体算力还可以, 运行运行<code>13B</code>及以下的模型没太大问题。当然这种级别参数大小的模型不会是一个无所不能的模型,这种量级的模型运行成本相对较低,适合做一些特定场景的推理任务。如果需要无所不能的模型建议还是直接使用<code>ChatGPT</code>这种商业模型。</p>
<h4 id="命令启动">命令启动</h4>
<p>下载模型完成之后可以测试运行,通过<code>cmd</code>运行指令,比如我运行起来<code>qwen2:7b</code>模型<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240904102931244-1275660501.png" alt="" loading="lazy"><br>
这种方式比较简单,只能是文字对话的方式而且没有样式,简单粗暴。</p>
<h4 id="接口访问">接口访问</h4>
<p><code>Ollama</code>提供服务的本质还是<code>http</code>接口,我们可以通过http接口的方式来调用<code>/api/generate</code>接口</p>
<pre><code>curl http://localhost:11434/api/generate -d '{
"model": "qwen2:7b",
"prompt": "请你告诉我你知道的天气有哪些?用json格式输出",
"stream": false
}'
</code></pre>
<ul>
<li><code>model</code>设置模型的名称</li>
<li><code>prompt</code>提示词</li>
<li><code>stream</code>设置为<code>false</code>要求不要流式返回</li>
</ul>
<p>因为是一次性返回所有内容,所以需要等待一会,如果需要流式输出可以设置为<code>true</code>。等待一会后接口返回的信息如下所示</p>
<pre><code class="language-json">{
    "model": "qwen2:7b",
    "created_at": "2024-09-04T06:13:53.1082355Z",
    "response": "```json\n{\n\"常见天气\": [\n    {\n      \"类型\": \"晴\",\n      \"描述\": \"天空无云或有少量高薄云,日间阳光充足。\",\n      \"符号\": \"☀️\"\n    },\n    {\n      \"类型\": \"多云\",\n      \"描述\": \"大部分天空被云层覆盖,但能见蓝天,太阳时隐时现。\",\n      \"符号\": \"🌤️\"\n    },\n    {\n      \"类型\": \"阴天\",\n      \"描述\": \"全天或大部分时间云量较多,几乎看不到阳光,光线较暗。\",\n      \"符号\": \"☁️\"\n    },\n    {\n      \"类型\": \"雨\",\n      \"子类型\": [\n      {\n          \"类型\": \"小雨\",\n          \"描述\": \"降水量不大,通常不会形成积水。\",\n          \"符号\": \"🌦️\"\n      },\n      {\n          \"类型\": \"中雨\",\n          \"描述\": \"降水量适中,可能会有局部积水。\",\n          \"符号\": \"🌧️\"\n      },\n      {\n          \"类型\": \"大雨\",\n          \"描述\": \"降水量大,可能伴有雷电和强风。\",\n          \"符号\": \"⛈️\"\n      }\n      ]\n    },\n    {\n      \"类型\": \"雪\",\n      \"子类型\": [\n      {\n          \"类型\": \"小雪\",\n          \"描述\": \"积雪较轻,地面可能仅局部有薄雪覆盖。\",\n          \"符号\": \"❄️\"\n      },\n      {\n          \"类型\": \"中雪\",\n          \"描述\": \"降雪量中等,地面和部分植被可能有积雪。\",\n          \"符号\": \"🌨️\"\n      },\n      {\n          \"类型\": \"大雪\",\n          \"描述\": \"降雪量很大,地面积雪深厚,交通和生活受严重影响。\",\n          \"符号\": \"❄️💨\"\n      }\n      ]\n    },\n    {\n      \"类型\": \"雾\",\n      \"描述\": \"大气中的水汽在地面或近地面凝结形成大量悬浮的微小水滴或冰晶的现象。\",\n      \"符号\": \"🌫️\"\n    },\n    {\n      \"类型\": \"雷阵雨\",\n      \"描述\": \"突然而短暂的强降雨伴有闪电和雷鸣,通常持续时间较短。\",\n      \"符号\": \"⚡🌧️\"\n    }\n]\n}\n```",
    "done": true,
    "done_reason": "stop",
    "context": [
      151644,
      872,
      198,
      //...省略...
      73594
    ],
    "total_duration": 70172634700,
    "load_duration": 22311300,
    "prompt_eval_count": 19,
    "prompt_eval_duration": 151255000,
    "eval_count": 495,
    "eval_duration": 69997676000
}
</code></pre>
<p>还有一种比较常用的操作就是大家比较关注的<code>嵌入模型</code>,通俗点就是对文本或者图片、视频等信息进行特征提取转换成向量的方式,这时候需要使用<code>/api/embed</code>接口,请求格式如下所示,这里使用的向量化模型是<code>nomic-embed-text</code>大家可以自行去用<code>ollama pull</code>这个模型</p>
<pre><code>curl http://localhost:11434/api/embed -d '{
"model": "nomic-embed-text:latest",
"input": "我是中国人,我爱我的祖国"
}'
</code></pre>
<p>嵌入接口返回的数据格式如下所示</p>
<pre><code class="language-json">{
    "model": "nomic-embed-text:latest",
    "embeddings": [
      [
            0.012869273,
            0.015905218,
            -0.13998738,
            //...省略很多...
            -0.035138983,
            -0.03351391
      ]
    ],
    "total_duration": 619728100,
    "load_duration": 572422600,
    "prompt_eval_count": 12
}
</code></pre>
<p>当然<code>Ollama</code>提供的接口还有很多,比如对话、模型管理等待,这里我们就不一一介绍了,有需要的同学可以自行查阅接口文档地址https://github.com/ollama/ollama/blob/main/docs/api.md</p>
<h4 id="可视化ui">可视化UI</h4>
<p>上面我们提到了两种方式访问<code>Ollama</code>服务,一种是命令行的方式,另一种是接口的方式。这两种虽然方式原始,但是并没有界面操作显得直观,如果你想通过界面的方式通过<code>Ollama</code>完成对话服务,官方<code>Github</code>推荐的也比较多,有兴趣的同学可以自行查看文档https://github.com/ollama/ollama?tab=readme-ov-file#web--desktop,我选用的是第一个Open WebUI,简单的方式是通过<code>Docker</code>直接运行</p>
<pre><code>docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://你的ollama服务ip:11434 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
</code></pre>
<p>亦或者可以通过构建源码的方式构建启动,按照下面的命令一步一步来</p>
<ol>
<li>git clone https://github.com/open-webui/open-webui.git</li>
<li>cd open-webui/</li>
<li>cp -RPp .env.example .env(复制一份<code>.env.example</code>文件重命名为<code>.env</code>。Windows系统的话使用copy .env.example .env)</li>
<li>npm install</li>
<li>npm run build</li>
<li>cd ./backend</li>
<li>conda create --name open-webui-env python=3.11(用conda创建一个名为pen-webui-env的虚拟环境)</li>
<li>conda activate open-webui-env(激活虚拟环境)</li>
<li>pip install -r requirements.txt -U(安装python依赖)</li>
<li>bash start.sh( Windows操作系统的话直接启动start_windows)</li>
</ol>
<p>如你所见,它是依赖<code>NodeJs</code>和<code>Python</code>的,还需要安装<code>Conda</code></p>
<ul>
<li>🐰 Node.js &gt;= 20.10</li>
<li>🐍 Python &gt;= 3.11</li>
<li>conda我用的是24.5.0</li>
</ul>
<p>启动成功后,在浏览器上输入<code>http://localhost:8080/</code>,注册一个用户名登陆进来之后界面如下所示<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240904151109805-1320033524.png" alt="" loading="lazy"><br>
可以直接选择模型进行对话,类似<code>ChatGPT</code>那种对话风格。</p>
<h3 id="c整合ollama">C#整合Ollama</h3>
<p>上面我们了解到了<code>Ollama</code>的基本安装和使用,明白了它的调用是基于<code>Http接口</code>来完成的。其实我也可以参考接口文档自行封装一套调用,但是没必要, 因为有很多现成的SDK可以直接使用。</p>
<h4 id="使用ollama-sdk">使用Ollama Sdk</h4>
<p>这里使用的C#的SDK就叫0llama,它的Github地址是https://github.com/tryAGI/Ollama, 为什么选择它呢,其实也很简单,因为它支持<code>function call</code>,这方便我们更早的体验新功能。安装它非常简单,相信同学们都会</p>
<pre><code>dotnet add package Ollama --version 1.9.0
</code></pre>
<h5 id="简单对话">简单对话</h5>
<p>简单的对话功能上手也没什么难度,都是简单代码</p>
<pre><code class="language-csharp">string modelName = "qwen2:7b";
using var ollama = new OllamaApiClient(baseUri: new Uri("http://127.0.0.1:11434/api"));

Console.WriteLine("开始对话!!!");
string userInput = "";
do
{
    Console.WriteLine("User:");
    userInput = Console.ReadLine()!;
    var enumerable = ollama.Completions.GenerateCompletionAsync(modelName, userInput);
    Console.WriteLine("Agent:");
    await foreach (var response in enumerable)
    {
      Console.Write($"{response.Response}");
    }
    Console.WriteLine();

} while (!string.Equals(userInput, "exit", StringComparison.OrdinalIgnoreCase));
Console.WriteLine("对话结束!!!");
</code></pre>
<p>模型名称是必须要传递的,而且默认的是<code>流式输出</code>,如果想一次返回同样的是设置<code>stream为false</code>。示例使用的是<code>qwen2:7b</code>模型。执行起来之后便可以直接对话,如下所示<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905104244799-945119164.png" alt="" loading="lazy"><br>
整体来说国产模型里面<code>qwen2:7b</code>整体的效果还是不错的,至少还不是扭曲事实。</p>
<h5 id="多轮对话">多轮对话</h5>
<p>如果需要进行分角色的多轮对话,要换一个方式使用,使用提供的<code>Chat</code>方式,如下所示</p>
<pre><code class="language-csharp">string modelName = "glm4:9b";
using var ollama = new OllamaApiClient(baseUri: new Uri("http://127.0.0.1:11434/api"));
Console.WriteLine("开始对话!!!");
string userInput = "";
List&lt;Message&gt; messages = [];
do
{
    //只取最新的五条消息
    messages = messages.TakeLast(5).ToList();
    Console.WriteLine("User:");
    userInput = Console.ReadLine()!;
    //加入用户消息
    messages.Add(new Message(MessageRole.User, userInput));
    var enumerable = ollama.Chat.GenerateChatCompletionAsync(modelName, messages, stream: true);
    Console.WriteLine("Agent:");
    StringBuilder builder = new();
    await foreach (var response in enumerable)
    {
      string content = response.Message.Content;
      builder.AppendLine(content);
      Console.Write(content);
    }
    //加入机器消息
    messages.Add(new Message(MessageRole.Assistant, builder.ToString()));
    Console.WriteLine();

} while (!string.Equals(userInput, "exit", StringComparison.OrdinalIgnoreCase));
Console.WriteLine("对话结束!!!");
</code></pre>
<p>这次换了另一个国产模型<code>glm4:9b</code>, 多轮对话和完全对话使用的对象不同。</p>
<ul>
<li>完全对话使用的是Completions对象,多轮对话使用的是<code>Chat</code>对象。</li>
<li>多轮对话需要用<code>List&lt;Message&gt;</code>存储之前的对话记录,这里模型才能捕获上下文。</li>
</ul>
<p>运行起来,执行效果如下所示<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905105611315-1456276216.png" alt="" loading="lazy"><br>
第一次我问他会c#吗,它说了一堆表示会。第二句我让它写一个简单的示例,但是我并没有说写c#示例,但是它可以通过上面的对话了解到意图,所以直接用c#给我写了一个示例。</p>
<h5 id="function-call">function call</h5>
<p>高版本的<code>Ollama</code>支持<code>function call</code>,当然这也要求模型也必须支持,如果模型本身不支持,那也是没有效果的,其中<code>llama3.1</code>支持的比较好,美中不足是<code>llama3.1</code>对中文支持的不太好,所以我们简单的演示一下,这里使用的是<code>llama3.1:8b</code>模型,首先需要定义方法,这样和模型对话的时候,框架会把方法的元信息抽出来发给模型,让模型判断调用哪个,这里我简单定义了一个计算增删改查的接口,并实现这个接口。</p>
<pre><code class="language-csharp">//定义一个接口,提供元信息

public interface IMathFunctions
{
   
    int Add(int a, int b);
   
    int Subtract(int a, int b);
   
    int Multiply(int a, int b);
   
    int Divide(int a, int b);
}

//实现上面的接口提供具体的操作方法
public class MathService : IMathFunctions
{
    public int Add(int a, int b) =&gt; a + b;
    public int Subtract(int a, int b) =&gt; a - b;
    public int Multiply(int a, int b) =&gt; a * b;
    public int Divide(int a, int b) =&gt; a / b;
}
</code></pre>
<p>有了上面的接口和实现类之后,我们就可以通过<code>Ollama</code>使用它们了,使用方式如下</p>
<pre><code class="language-csharp">string modelName = "llama3.1:8b";
using var ollama = new OllamaApiClient(baseUri: new Uri("http://127.0.0.1:11434/api"));
var chat = ollama.Chat(
    model: modelName,
    systemMessage: "You are a helpful assistant.",
    autoCallTools: true);

//给Ollama注册刚才定义的类
var mathService = new MathService();
chat.AddToolService(mathService.AsTools(), mathService.AsCalls());

while (true)
{
    try
    {
      Console.WriteLine("User&gt;");
      var newMessage = Console.ReadLine();
      var msg = await chat.SendAsync(newMessage);
      Console.WriteLine("Agent&gt; " + msg.Content);
    }
    finally
    {
      //打印本次对话的所有消息
      Console.WriteLine(chat.PrintMessages());
    }
}
</code></pre>
<p>这里需要设置<code>autoCallTools</code>为<code>true</code>才能自动调用方法,<code>PrintMessages()</code>方法用来打印本轮会话中所有的消息,一般自动调用<code>function call</code>的时候会产生多次请求,但是我们使用的时候是无感知的,因为框架已将帮我自动处理了,比如我的提示词是一个数学计算公式<code>(12+8)*4/2=?</code>,如下所所示<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905133400093-159717952.png" alt="" loading="lazy"><br>
通过<code>PrintMessages()</code>方法打印的对话消息可知,虽然我只提供了一句提示词,但是<code>Ollama SDK</code>因为支持自动调用工具,<code>llama3.1:8b</code>将提示词算式<code>(12+8)*4/2)</code>进行了拆分,计算步骤如下所示</p>
<ul>
<li>先拆分了括号里的逻辑<code>12+8</code>并调用<code>Add</code>方法得到结果20</li>
<li>然后第二步用上一步得到的结果调用<code>Multiply</code>计算<code>20*4</code>得到80</li>
<li>再用上一步的结果调用<code>Divide</code>计算<code>80/2</code>得到结果40</li>
<li>最后把Tools调用的步骤及结果一起在通过对话发送给<code>llama3.1</code>模型,模型得到最终的输出</li>
</ul>
<p>如果我们不打印过程日志的话,模型只会输出</p>
<pre><code>Assistant:
The correct calculation is:
(12+8)=20
20*4=80
80/2=40
Therefore,the answer is:40.
</code></pre>
<h5 id="嵌入模型">嵌入模型</h5>
<p>上面我们提到过<code>Ollama</code>不仅可以使用对话模型还可以使用<code>嵌入模型</code>的功能,<code>嵌入模型</code>简单的来说就是对文本、图片、语音等利用模型进行特征提起,得到向量数据的过程。通过<code>Ollama SDK</code>可以使用<code>Ollama</code>的嵌入功能,代码如下所示</p>
<pre><code class="language-csharp"> string modelName = "nomic-embed-text:latest";
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://127.0.0.1:11434/api");
client.Timeout = TimeSpan.FromSeconds(3000);
using var ollama = new OllamaApiClient(client);
var embeddingResp =await ollama.Embeddings.GenerateEmbeddingAsync(modelName, "c#是一门不错的编程语言");
Console.WriteLine($"[{string.Join(",", embeddingResp.Embedding!)}]");
</code></pre>
<p>得到的就是如下所示的向量信息<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905142033490-575462444.png" alt="" loading="lazy"><br>
向量数据是可以计算相似度的,利用<code>余弦夹角</code>的概念可以计算向量的空间距离,空间距离越近,两个向量的相似度便越高。如果大家了解颜色表<code>RGB</code>的话就比较容易理解,举个例子<code>(255, 0, 0)</code>就是纯红色,<code>(255, 10, 10)</code>也是红色,但是不是纯红色。如果把<code>(255, 0, 0)</code>和<code>(255, 10, 10)</code>映射到一个三维的空间坐标图上它们的距离就很近,但是它们和纯蓝色<code>(0, 0, 255)</code>的空间距离就很远,因为一个贴近<code>X</code>轴,一个贴近<code>Z</code>轴。现在大家锁熟知的向量数据库,大概采用的就是类似的原理。也是现在流行的<code>RAG</code>检索增强生成的基础。</p>
<p>比如我把下面两句话嵌入模型得到向量值,然后通过计算<code>余弦夹角</code>来比较它们的相似度</p>
<pre><code class="language-csharp">var embeddingResp =await ollama.Embeddings.GenerateEmbeddingAsync(modelName, "c#是一门不错的编程语言");
var embeddingResp2 = await ollama.Embeddings.GenerateEmbeddingAsync(modelName, "c#是很好的语言");
Console.WriteLine("相似度:" + CosineSimilarity([.. embeddingResp.Embedding!], [.. embeddingResp2!.Embedding]));

//计算余弦夹角
public static double CosineSimilarity(double[] vector1, double[] vector2)
{
    if (vector1.Length != vector2.Length)
      throw new ArgumentException("向量长度必须相同");

    double dotProduct = 0.0;
    double magnitude1 = 0.0;
    double magnitude2 = 0.0;

    for (int i = 0; i &lt; vector1.Length; i++)
    {
      dotProduct += vector1 * vector2;
      magnitude1 += vector1 * vector1;
      magnitude2 += vector2 * vector2;
    }

    magnitude1 = Math.Sqrt(magnitude1);
    magnitude2 = Math.Sqrt(magnitude2);

    if (magnitude1 == 0.0 || magnitude2 == 0.0)
      return 0.0; // 避免除以零

    return dotProduct / (magnitude1 * magnitude2);
}
</code></pre>
<p>上面的得到的相似度结果是</p>
<pre><code>相似度:0.9413230998586363
</code></pre>
<p>因为它们两句话表达的含义差不多,所以相似度很高。但是如果我要计算下面的两句话的相似度</p>
<pre><code class="language-csharp">var embeddingResp =await ollama.Embeddings.GenerateEmbeddingAsync(modelName, "c#是一门不错的编程语言");
var embeddingResp2 = await ollama.Embeddings.GenerateEmbeddingAsync(modelName, "我喜欢吃芒果和草莓");
</code></pre>
<p>那么利用余弦值计算出来它们的相似度只有<code>0.59</code>,因为这两句话几乎没有任何关联。</p>
<pre><code>相似度:0.5948448463206064
</code></pre>
<h5 id="多模态模型">多模态模型</h5>
<p>刚开始的对话模型都比较单一,都是简单的文本对话,随着不断的升级,有些模型已经支持多种格式的输入输出而不仅仅是单一的文本,比如支持图片、视频、语音等等,这些模型被称为多模态模型。使用<code>Ollama</code>整合<code>llava</code>模型体验一把,这里我是用的是<code>llava:13b</code>。我在网上随便找了一张图片存放本地<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905145135636-2080743229.jpg" alt="" loading="lazy"><br>
用这张图片对模型进行提问,代码如下所示</p>
<pre><code class="language-csharp">HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://127.0.0.1:11434/api");
client.Timeout = TimeSpan.FromSeconds(3000);
using var ollama = new OllamaApiClient(client);
string modelName = "llava:13b";
string prompt = "What is in this picture?";
System.Drawing.Image image = System.Drawing.Image.FromFile("1120.jpg");
var enumerable = ollama.Completions.GenerateCompletionAsync(modelName, prompt, images: , stream: true);
await foreach (var response in enumerable)
{
    Console.Write($"{response.Response}");
}

//Image转base64
public static string BitmapToBase64(System.Drawing.Image bitmap)
{
    MemoryStream ms1 = new MemoryStream();
    bitmap.Save(ms1, System.Drawing.Imaging.ImageFormat.Jpeg);
    byte[] arr1 = new byte;
    ms1.Position = 0;
    ms1.Read(arr1, 0, (int)ms1.Length);
    ms1.Close();
    return Convert.ToBase64String(arr1);
}
</code></pre>
<p>我用提示词让模型描述图片里面的内容,然后把这张图片转换成<code>base64</code>编码格式一起发送给模型,模型返回的内容如下所示<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905145851802-1222105384.png" alt="" loading="lazy"><br>
确实够强大,描述的信息很准确,措词也相当不错,如果让人去描述图片中的内容,相信大部分人描述的也没这么好,不得不说模型越来越强大了。</p>
<h4 id="使用semantickernel">使用SemanticKernel</h4>
<p>除了整合<code>Ollama SDK</code>以外,你还可以用<code>Semantic Kernel</code>来整合<code>Ollama</code>,我们知道默认情况下<code>Semantic Kernel</code>只能使用<code>OpenAI</code>和<code>Azure OpenAI</code>的接口格式,但是其他模型接口并不一定和<code>OpenAI</code>接口格式做兼容,有时候甚至可以通过<code>one-api</code>这样的服务来适配一下。不过不用担心<code>Ollama</code>兼容了<code>OpenAI</code>接口的格式,即使不需要任何的适配服务也可以直接使用,我们只需要重新适配一下请求地址即可。</p>
<pre><code class="language-csharp">using HttpClient httpClient = new HttpClient(new RedirectingHandler());
httpClient.Timeout = TimeSpan.FromSeconds(120);

var kernelBuilder = Kernel.CreateBuilder()
    .AddOpenAIChatCompletion(
       modelId: "glm4:9b",
       apiKey: "ollama",
       httpClient: httpClient);
Kernel kernel = kernelBuilder.Build();

var chatCompletionService = kernel.GetRequiredService&lt;IChatCompletionService&gt;();
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};

var history = new ChatHistory();
string? userInput;
do
{
    Console.Write("User &gt; ");
    userInput = Console.ReadLine();
    history.AddUserMessage(userInput!);

    var result = chatCompletionService.GetStreamingChatMessageContentsAsync(
      history,
      executionSettings: openAIPromptExecutionSettings,
      kernel: kernel);
    string fullMessage = "";
    System.Console.Write("Assistant &gt; ");
    await foreach (var content in result)
    {
      System.Console.Write(content.Content);
      fullMessage += content.Content;
    }
    System.Console.WriteLine();

    history.AddAssistantMessage(fullMessage);
} while (userInput is not null);


public class RedirectingHandler : HttpClientHandler
{
    protected override Task&lt;HttpResponseMessage&gt; SendAsync(
      HttpRequestMessage request, CancellationToken cancellationToken)
    {
      var uriBuilder = new UriBuilder(request.RequestUri!) { Scheme = "http", Host = "localhost", Port = 11434 };
      //对话模型
      if (request!.RequestUri!.PathAndQuery.Contains("v1/chat/completions"))
      {
            uriBuilder.Path = "/v1/chat/completions";
            request.RequestUri = uriBuilder.Uri;
      }
      //嵌入模型
      if (request!.RequestUri!.PathAndQuery.Contains("v1/embeddings"))
      {
            uriBuilder.Path = "/v1/embeddings";
            request.RequestUri = uriBuilder.Uri;
      }            
      return base.SendAsync(request, cancellationToken);
    }
}
</code></pre>
<p>这里我们使用的是国产模型<code>glm4:9b</code>,需要注意的是因为这里我们使用的是本地服务,所以需要适配一下服务的地址,通过编写<code>RedirectingHandler</code>类,并用其构造一个<code>HttpClient</code>实例传递给<code>Kernel</code>。细心的同学可能已经发现了,这里我转发的<code>Ollama</code>服务的路径也变成了和<code>OpenAI</code>服务一样的路径,但是上面我调用<code>Ollama</code>服务用的是<code>/api/chat</code>和<code>/api/embed</code>这种地址的接口。这是因为<code>Ollama</code>为了兼容<code>OpenAI</code>的标准,专门开发了一套和<code>OpenAI</code>路径和参数都一样的接口,这一点是需要注意的。当然<code>Ollama</code>暂时还没有全部兼容<code>OpenAI</code>接口的全部特征,有兴趣的同学可以去看一下https://github.com/ollama/ollama/blob/main/docs/openai.md文档地址,了解更详细的内容。</p>
<p>上面的服务运行起来,我们同样可以进行对话,效果如下所示<br>
<img src="https://img2024.cnblogs.com/blog/2042116/202409/2042116-20240905160706423-26310012.png" alt="" loading="lazy"><br>
同样的你可以通过<code>SemanticKernel</code>使用嵌入模型的功能,如下所示</p>
<pre><code class="language-csharp">using HttpClient httpClient = new HttpClient(new RedirectingHandler());
httpClient.Timeout = TimeSpan.FromSeconds(120);

var kernelBuilder = Kernel.CreateBuilder()
    .AddOpenAITextEmbeddingGeneration(
       modelId:"nomic-embed-text:latest",
       apiKey:"ollama",
       httpClient: httpClient);
Kernel kernel = kernelBuilder.Build();
var embeddingService= kernel.GetRequiredService&lt;ITextEmbeddingGenerationService&gt;();
var embeddings = await embeddingService.GenerateEmbeddingsAsync(["我觉得c#是一门不错的编程语言"]);
Console.WriteLine($"[{string.Join(",", embeddings.ToArray())}]");
</code></pre>
<p>这里休要注意的是<code>AddOpenAITextEmbeddingGeneration</code>方法是评估方法,将来版本有可能会删除的,所以默认的用VS使用该方法会有错误提醒,可以在<code>csproj</code>的<code>PropertyGroup</code>标签中设置一下<code>NoWarn</code>来忽略这个提醒。</p>
<pre><code class="language-xml">&lt;PropertyGroup&gt;
   &lt;OutputType&gt;Exe&lt;/OutputType&gt;
   &lt;TargetFramework&gt;net8.0&lt;/TargetFramework&gt;
   &lt;NoWarn&gt;SKEXP0010;SKEXP0001&lt;/NoWarn&gt;
&lt;/PropertyGroup&gt;
</code></pre>
<h3 id="总结">总结</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;本文介绍了如何通过<code>C#</code>结合<code>Ollama</code>实现本地大语言模型的部署与调用,重点演示了在<code>C#</code>应用中集成该功能的具体步骤。通过详细的安装指南与代码示例,帮助开发者快速上手。</p>
<ul>
<li>首先我们介绍了<code>Ollama</code>的安装及基本设置和命令的使用。</li>
<li>然后介绍了如何通过<code>Ollama</code>调用大模型,比如使用<code>命令行</code>、<code>Http接口服务</code>、<code>可视乎界面</code>。</li>
<li>再次我们我们通过<code>C#</code>使用了<code>Ollama SDK</code>来演示了<code>对话模式</code>、<code>文本嵌入</code>、<code>多模态模型</code>如何使用,顺便说了一下相似度计算相关。</li>
<li>最后,我们展示了通过<code>Semantic Kernel</code>调用<code>Ollama</code>服务,因为<code>Ollama</code>对<code>OpenAI</code>的接口数据格式做了兼容,虽然还有部分未兼容,但是日常使用问题不大。</li>
</ul>
<p>&nbsp;&nbsp;&nbsp;&nbsp;通过本文希望没有了解过大模型的同学可以入门或者大概了解一下相关的基础,毕竟这是近两年或者未来几年都比较火的一个方向。即使我们不能深入的研究他,但是我们也得知道它了解它的基本原理与使用。我们为什么要持续学习,因为这些东西很多时候确实是可以给我们提供方便。接触它,了解它,才能真正的知道它可以帮助我解决什么问题。<br>
<br></p>
<div align="center">
<span style="font-size: 15px">👇欢迎扫码关注我的公众号👇</span>
<img src="https://img2020.cnblogs.com/blog/2042116/202006/2042116-20200622133425514-1420050576.png">
</div><br><br>
来源:https://www.cnblogs.com/wucy/p/18400124/csharp-ollama
頁: [1]
查看完整版本: C#整合Ollama实现本地LLMs调用