返回

Semantic Kernel连接本地Ollama PHI3模型404错误? 一招解决

Ai

搞定 Semantic Kernel 调用本地 Ollama PHI3 模型的 404 错误

写 C# 代码,想用 Semantic Kernel 框架连接你在本地跑的 Ollama 服务,调用像 phi3:3.8b 这样的小语言模型,结果一运行到 aiChatService.GetStreamingChatMessageContentsAsync(chatHistory) 这行,程序“砰”一下抛出 System.ClientModel.ClientResultException: Service request failed. Status: 404 (Not Found)?别挠头,这事儿不罕见。代码瞅着没啥大问题,模型名字也没错,Ollama 服务可能也确认在跑,那到底是哪里卡壳了呢?

咱们来捋一捋,看看这 404 到底是怎么回事,又该怎么解决它。

404 Not Found:问题在哪?

“404 Not Found” 这个 HTTP 状态码,大白话就是“找不到”。在你这个场景下,具体意思是:你的 C# 程序(通过 Semantic Kernel)尝试去访问 Ollama 服务上的某个网络地址(API Endpoint),但 Ollama 服务器说:“嘿,你找的这个地址,我这儿没有!”

这通常指向以下几个可能的原因:

  1. Ollama 服务没跑起来,或者监听的地址不对。 这是最基础的,服务都没启动,自然谁也连不上。
  2. Semantic Kernel 配置的 Ollama 地址不对。 可能是 IP、端口号写错了。
  3. 关键:API Endpoint 路径错了。 这是最常见的原因!Semantic Kernel 的 AddOpenAIChatCompletion 这个扩展方法,设计初衷是用来连接 OpenAI 或者兼容 OpenAI API 格式的服务的。它会按照 OpenAI 的 API 规范去拼接请求路径。而 Ollama 虽然提供了一个兼容 OpenAI 的接口,但这个兼容接口的“门牌号”(路径)跟你代码里 HttpClient 设置的基础地址 (http://localhost:11434) 可能对不上。Ollama 的兼容接口通常需要一个特定的前缀,比如 /v1
  4. 模型名字不对或者模型没下载。 虽然你的代码里 phi3:3.8b 看起来是对的,但最好还是确认下 Ollama 里确实有这个模型,并且名字完全一致。
  5. 本地防火墙捣乱。 防火墙可能阻止了你的 C# 程序访问 localhost 的指定端口。

重装 Ollama 服务和 AI 模型通常解决不了根本的配置或路径问题,所以我们得从上面这些点入手。

解决步骤

下面我们一步步来排查和解决。

第一步:确认 Ollama 服务状态和模型

这是最基础的检查,不能跳过。

  1. 检查 Ollama 服务是否在运行:
    打开你的终端或命令行工具,尝试访问 Ollama 的基础 URL。如果 Ollama 跑在默认的 http://localhost:11434,你可以用 curl 命令(Linux/macOS/Windows PowerShell)或者直接在浏览器里访问:

    curl http://localhost:11434
    

    如果服务正常,它应该会返回类似 Ollama is running 的信息。如果返回“连接拒绝”或超时,说明 Ollama 服务没启动,或者监听的地址/端口不是 11434。你需要先启动 Ollama 服务。通常的启动命令是 ollama serve,但具体看你的安装方式。

  2. 确认模型已下载且名称正确:
    在终端执行以下命令,列出 Ollama 已经下载的模型:

    ollama list
    

    检查列表里是不是确实有一个叫做 phi3:3.8b (或者你实际使用的精确模型名) 的模型。如果没有,需要先拉取模型:

    ollama pull phi3:3.8b
    

    确保模型名字在你的 C# 代码 (modelId: "phi3:3.8b") 和 ollama list 输出里完全一致,大小写敏感。

如果这两步都没问题,服务在跑,模型也在,那我们接着往下看。

第二步:修正 API Endpoint 地址 (最可能的解决方案)

这个大概率是症结所在。Semantic Kernel 的 AddOpenAIChatCompletion 默认认为它连接的是一个遵循 OpenAI API 规范的服务端点。Ollama 为了兼容性,确实提供了一个这样的接口,但它通常不在根路径 / 下,而是在 /v1/ 路径下。

你的代码里是这样配置 HttpClient 的:

using var httpClient = new HttpClient()
{
    BaseAddress = new Uri("http://localhost:11434"),
};

然后 AddOpenAIChatCompletion 会使用这个 httpClient。当它尝试调用聊天补全接口时,它可能会在 BaseAddress 后面拼接上 OpenAI 标准的相对路径,比如 /chat/completions。但它缺少了关键的 /v1 部分。

解决方案: 修改 HttpClientBaseAddress,让它指向 Ollama 的 OpenAI 兼容 API 的基础路径

怎么改?

BaseAddress 改成 http://localhost:11434/v1/。注意末尾的斜杠 / 不能少,这有助于 HttpClient 正确处理相对路径的拼接。

// 原来的 HttpClient
// using var httpClient = new HttpClient()
// {
//    BaseAddress = new Uri("http://localhost:11434"),
// };

// --- 修改后 ---
using var httpClient = new HttpClient()
{
    // 注意这里加上了 /v1/
    BaseAddress = new Uri("http://localhost:11434/v1/"),
};

// Kernel 创建部分通常不需要改动
// (注意:apiKey 对于本地 Ollama 通常不需要,但 Semantic Kernel 的这个方法可能要求传一个非空值,
// 可以随便填一个字符串,比如 "ollama" 或 "none")
#pragma warning disable SKEXP0010 // 抑制实验性 API 警告
Kernel kernel = Kernel.CreateBuilder()
                    .AddOpenAIChatCompletion(
                        modelId: "phi3:3.8b",       // 确认模型名称和 ollama list 中一致
                        httpClient: httpClient,   // 使用配置了正确 BaseAddress 的 httpClient
                        apiKey: "not-needed")     // 提供一个占位 API Key
                    .Build();

var aiChatService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory([new ChatMessageContent(AuthorRole.System, "Sos un asistente de programación de C#")]); ; // 原来的西班牙语 System Prompt

while (true)
{
    Console.WriteLine("Your prompt:");
    var userPrompt = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userPrompt)) break; // 添加退出循环的方式

    chatHistory.Add(new ChatMessageContent(AuthorRole.User, userPrompt));

    Console.WriteLine("AI Response:");
    var response = "";
    try // 增加异常处理
    {
        await foreach (var item in aiChatService.GetStreamingChatMessageContentsAsync(chatHistory))
        {
            if (item.Content is not null)
            {
                Console.Write(item.Content);
                response += item.Content;
            }
        }
        chatHistory.Add(new ChatMessageContent(AuthorRole.Assistant, response));
        Console.WriteLine();
    }
    catch (Exception ex)
    {
        // 捕获并打印调用 API 时可能发生的异常
        Console.WriteLine(
// 原来的 HttpClient
// using var httpClient = new HttpClient()
// {
//    BaseAddress = new Uri("http://localhost:11434"),
// };

// --- 修改后 ---
using var httpClient = new HttpClient()
{
    // 注意这里加上了 /v1/
    BaseAddress = new Uri("http://localhost:11434/v1/"),
};

// Kernel 创建部分通常不需要改动
// (注意:apiKey 对于本地 Ollama 通常不需要,但 Semantic Kernel 的这个方法可能要求传一个非空值,
// 可以随便填一个字符串,比如 "ollama" 或 "none")
#pragma warning disable SKEXP0010 // 抑制实验性 API 警告
Kernel kernel = Kernel.CreateBuilder()
                    .AddOpenAIChatCompletion(
                        modelId: "phi3:3.8b",       // 确认模型名称和 ollama list 中一致
                        httpClient: httpClient,   // 使用配置了正确 BaseAddress 的 httpClient
                        apiKey: "not-needed")     // 提供一个占位 API Key
                    .Build();

var aiChatService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory([new ChatMessageContent(AuthorRole.System, "Sos un asistente de programación de C#")]); ; // 原来的西班牙语 System Prompt

while (true)
{
    Console.WriteLine("Your prompt:");
    var userPrompt = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userPrompt)) break; // 添加退出循环的方式

    chatHistory.Add(new ChatMessageContent(AuthorRole.User, userPrompt));

    Console.WriteLine("AI Response:");
    var response = "";
    try // 增加异常处理
    {
        await foreach (var item in aiChatService.GetStreamingChatMessageContentsAsync(chatHistory))
        {
            if (item.Content is not null)
            {
                Console.Write(item.Content);
                response += item.Content;
            }
        }
        chatHistory.Add(new ChatMessageContent(AuthorRole.Assistant, response));
        Console.WriteLine();
    }
    catch (Exception ex)
    {
        // 捕获并打印调用 API 时可能发生的异常
        Console.WriteLine($"\n[Error calling AI service]: {ex.Message}");
        // 可以选择在这里 break 或者让用户继续尝试
        // break;
    }
}
#pragma warning restore SKEXP0010
quot;\n[Error calling AI service]: {ex.Message}"
); // 可以选择在这里 break 或者让用户继续尝试 // break; } } #pragma warning restore SKEXP0010

原理:
通过将 BaseAddress 设置为 http://localhost:11434/v1/,你告诉了 HttpClient(以及随后使用它的 Semantic Kernel 服务)所有后续的相对路径请求(比如 /chat/completions)都应该基于这个 /v1/ 路径进行。这样,最终发出的请求就会是 http://localhost:11434/v1/chat/completions,这正好是 Ollama 提供的 OpenAI 兼容聊天接口的地址。

注意那个 apiKey: AddOpenAIChatCompletion 方法签名通常需要一个 apiKey 参数。对于本地运行、不需要鉴权的 Ollama 实例,这个 key 其实没用。但方法可能不允许传入 null 或空字符串。所以,提供一个像 "not-needed" 或者 "ollama" 这样的占位符字符串通常就可以。

修改完之后,重新运行你的 C# 程序,看看 404 错误是不是消失了。

进阶技巧:环境变量配置 Endpoint

硬编码 http://localhost:11434/v1/ 不太灵活。更好的做法是把这个地址放到配置文件(比如 appsettings.json)或者环境变量里,然后在代码里读取。

// 示例:从环境变量读取 Ollama Endpoint
var ollamaEndpoint = Environment.GetEnvironmentVariable("OLLAMA_ENDPOINT_URL");
if (string.IsNullOrEmpty(ollamaEndpoint))
{
    ollamaEndpoint = "http://localhost:11434/v1/"; // 提供一个默认值
    Console.WriteLine("Warning: OLLAMA_ENDPOINT_URL environment variable not set. Using default: " + ollamaEndpoint);
}

using var httpClient = new HttpClient()
{
    BaseAddress = new Uri(ollamaEndpoint),
};

// ... 后续代码使用这个 httpClient ...

这样,如果以后 Ollama 的地址或者端口变了,只需要修改环境变量或配置文件,不用改代码。

第三步:检查防火墙设置

虽然是本地调用 (localhost),理论上不应该经过复杂的网络路由,但有些防火墙或安全软件策略比较严格,可能会干预进程间的本地网络通信。

  1. 临时禁用防火墙测试: 为了快速判断是不是防火墙的问题,可以临时 禁用你的防火墙(比如 Windows Defender 防火墙,或者其他第三方安全软件),然后再次运行你的 C# 程序。如果能成功连接,那就说明确实是防火墙规则挡住了。
    • 警告: 禁用防火墙会降低系统安全性,测试完毕后务必重新启用!
  2. 添加防火墙规则: 如果确定是防火墙问题,不要一直禁用它。更好的方法是添加一条出站规则(outbound rule),允许你的 C# 应用程序(或者所有应用程序,如果你不确定具体是哪个进程)访问 localhost (或 127.0.0.1) 的 11434 TCP 端口。具体怎么添加规则,取决于你用的操作系统和防火墙软件。

第四步:考虑 Semantic Kernel 版本

极少数情况下,可能是 Semantic Kernel 的某个特定版本与 Ollama 的 OpenAI 兼容层之间存在不匹配。可以尝试更新 Semantic Kernel 相关的 NuGet 包到最新稳定版,或者查阅 Semantic Kernel 的 GitHub Issues 看有没有其他用户报告类似问题和特定版本的解决方案。

# 在你的项目目录下,使用 .NET CLI 更新包
dotnet add package Microsoft.SemanticKernel --version <最新版本号>
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI --version <最新版本号>
# 其他相关的 SK 包也一并检查更新

安全建议

  • 本地访问为主: 在开发和测试阶段,让 Ollama 只监听本地地址 (localhost127.0.0.1) 是最安全的。Ollama 默认通常就是这样。
  • 谨慎暴露到网络: 如果你需要让局域网内的其他机器访问你的 Ollama 服务(比如把 ollama serve 绑定到 0.0.0.0),务必确保你的网络环境是受信任的。考虑使用防火墙规则,只允许特定 IP 地址访问 Ollama 的端口。目前 Ollama 本身对 API 访问的认证支持有限,所以网络层面的保护很重要。不要直接把没有安全防护的 Ollama 服务暴露到公网上。

总结一下

遇到 Semantic Kernel 调用本地 Ollama 报 404,别慌,按下面的顺序排查:

  1. 基础检查: curl http://localhost:11434 确认服务运行,ollama list 确认模型存在且名字对得上。
  2. 修正 Endpoint: 大概率是这里的问题。修改 C# 代码里 HttpClientBaseAddress,在原来的地址后面加上 /v1/,变成 http://localhost:11434/v1/。记得给 apiKey 提供一个占位符。
  3. 防火墙: 临时关掉防火墙试试,如果管用,就去给应用程序添加访问 localhost:11434 的允许规则。
  4. 版本更新: 检查 Semantic Kernel 包是否是最新稳定版。

通常,修正了 API Endpoint 地址(第二步)就能解决这个 404 问题。试试看吧!

相关资源