AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布

最近项目中需要继承AI对话功能,由于甲方对部署方式不确定,所以云端和本地部署我们都实验了,用的Microsoft Semantic Kernel ,但是官方的SDK对非标准OpenAI协议的适配做的不够好,所以我自己基于它重新小小封装了一下。

SemanticKerne.AiProvider.Unified

基于 Microsoft Semantic Kernel 的统一 AI 服务提供者封装库,支持多种 AI 服务商(OpenAI、Ollama、DashScope),提供流式聊天、MCP 插件、工具调用等功能。

特性

  • 🚀 多服务商支持:统一接口支持 OpenAI、Ollama、DashScope(阿里云)
  • 🧠 思考过程输出:支持模型的思考过程流式输出(reasoning_content)
  • 🔧 工具调用:支持 MCP(Model Context Protocol)插件和自定义工具
  • ⚙️ 灵活配置:所有参数可通过配置文件或代码自定义
  • 📦 开箱即用:完整的依赖注入支持,快速集成到 ASP.NET Core 项目
  • 🔍 真正流式:基于 SSE 的实时流式输出,低延迟

安装

复制代码
dotnet add package SemanticKerne.AiProvider.Unified

快速开始

1. 配置文件(appsettings.json)

复制代码
{
  "SemanticKernel": {
    "AiServiceType": "dashscope",
    "ModelId": "qwen3.6-plus-2026-04-02",
    "Endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
    "ApiKey": "sk-your-api-key-here",
    "HttpClientTimeout": "00:05:00",
    "ExtensionData": {
      "enable_thinking": true,
      "preserve_thinking": true,
      "temperature": 0.7,
      "max_tokens": 4096
    }
  },
  "Mcp": {
    "Enabled": true,
    "Servers": [
      {
        "Name": "sql-mcp-http",
        "Enabled": true,
        "Description": "Data API Builder MCP 服务",
        "Transport": "http",
        "Endpoint": "http://localhost:5000/mcp",
        "TimeoutSeconds": 30
      }
    ]
  }
}

2. 服务注册(Program.cs)

复制代码
using Microsoft.Extensions.Options;
using SemanticKerne.AiProvider.Unified.Models;
using SemanticKerne.AiProvider.Unified.Services;
using SemanticKerne.AiProvider.Unified.Services.Mcp;

var builder = WebApplication.CreateBuilder(args);


// 注册 SemanticKernelOptions 配置
builder.Services.Configure<SemanticKernelOptions>(
    builder.Configuration.GetSection("SemanticKernel"));

builder.Services.AddSingleton<ISemanticKernelService, SemanticKernelService>();
builder.Services.AddSingleton<ISessionManager, SessionManager>();
builder.Services.AddSingleton<BailianErrorHandler>();

// 注册 MCP 服务
builder.Services.AddHttpClient();
builder.Services.AddSingleton<IMcpClientService, McpClientService>(sp =>
{
    var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
    var logger = sp.GetRequiredService<ILogger<McpClientService>>();
    return new McpClientService(httpClientFactory, logger, builder.Configuration);
});
var app = builder.Build();
app.Run();

3. 使用示例

复制代码
/// <summary>
/// 聊天控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ChatController : ControllerBase
{
    private readonly ISessionManager _sessionManager;
    private readonly ISemanticKernelService _kernelService;
    private readonly ILogger<ChatController> _logger;
    private readonly BailianErrorHandler _errorHandler;

    public ChatController(
        ISessionManager sessionManager,
        ISemanticKernelService kernelService,
        ILogger<ChatController> logger,
        BailianErrorHandler errorHandler)
    {
        _sessionManager = sessionManager;
        _kernelService = kernelService;
        _logger = logger;
        _errorHandler = errorHandler;
    }

    private string UserId => User.FindFirst(ClaimTypes.Name)?.Value ?? "test-user";

    /// <summary>
    /// 获取当前用户的所有会话
    /// </summary>
    [HttpGet("sessions")]
    public IActionResult GetSessions()
    {
        _logger.LogInformation("GetSessions called, UserId: {UserId}, AllClaims: {Claims}", 
            UserId, 
            string.Join(", ", User.Claims.Select(c => $"{c.Type}={c.Value}")));
        var sessions = _sessionManager.GetUserSessions(UserId);
        _logger.LogInformation("GetSessions result, count: {Count}", sessions.Count());
        return Ok(sessions);
    }

    /// <summary>
    /// 创建新的聊天会话
    /// </summary>
    [HttpPost("sessions")]
    public IActionResult CreateSession()
    {
        _logger.LogInformation("CreateSession called, UserId: {UserId}", UserId);
        var session = _sessionManager.CreateSession(UserId);
        _logger.LogInformation("CreateSession created, SessionId: {SessionId}, UserId: {UserId}", session.SessionId, UserId);
        return Ok(new CreateSessionResponse
        {
            SessionId = session.SessionId,
            CreatedAt = session.CreatedAt
        });
    }

    /// <summary>
    /// 删除指定的聊天会话
    /// </summary>
    [HttpDelete("sessions/{sessionId}")]
    public IActionResult DeleteSession(string sessionId)
    {
        _logger.LogInformation("DeleteSession called, UserId: {UserId}, SessionId: {SessionId}", UserId, sessionId);
        var result = _sessionManager.DeleteSession(UserId, sessionId);
        if (!result)
        {
            _logger.LogWarning("DeleteSession failed, session not found, UserId: {UserId}, SessionId: {SessionId}", UserId, sessionId);
            return NotFound(new { message = "会话不存在" });
        }
        return NoContent();
    }

    /// <summary>
    /// 停止会话当前正在进行的请求(不删除会话)
    /// </summary>
    [HttpPost("sessions/{sessionId}/stop")]
    public IActionResult StopSession(string sessionId)
    {
        var session = _sessionManager.GetSession(UserId, sessionId);
        _logger.LogInformation("StopSession called, UserId: {UserId}, SessionId: {SessionId}, SessionExists: {Exists}, IsProcessing: {IsProcessing}", 
            UserId, sessionId, session != null, session?.IsProcessing);
        var result = _sessionManager.StopSession(UserId, sessionId);
        _logger.LogInformation("StopSession result: {Result}", result);
        if (!result)
        {
            return NotFound(new { message = "会话不存在或没有正在进行的请求" });
        }
        return Ok(new { message = "已停止当前请求" });
    }

    /// <summary>
    /// 发送消息并获取流式响应(SSE)
    /// </summary>
    [HttpPost("sessions/{sessionId}/chat")]
    public async Task Chat(string sessionId, [FromBody] ChatRequest request, CancellationToken cancellationToken)
    {
        var session = _sessionManager.GetSession(UserId, sessionId);
        if (session == null)
        {
            Response.StatusCode = 404;
            await Response.WriteAsync("会话不存在");
            return;
        }

        // 设置 SSE 响应头
        Response.ContentType = "text/event-stream";
        Response.Headers.CacheControl = "no-cache";
        Response.Headers.Connection = "keep-alive";

        try
        {
            await foreach (var response in _kernelService.StreamChatAsync(session, request.Message, cancellationToken))
            {
                // 构建响应对象
                if (response.Type == StreamingResponseType.Error)
                {
                    // 错误响应,包含详细错误信息
                    var errorObj = new
                    {
                        type = response.Type.ToString().ToLower(),
                        content = response.Content,
                        errorCode = response.ErrorCode,
                        httpStatus = response.HttpStatus,
                        title = response.ErrorTitle,
                        reason = response.ErrorReason,
                        solution = response.ErrorSolution,
                        isCritical = response.IsCritical
                    };
                    
                    var json = JsonSerializer.Serialize(errorObj);
                    
                    // SSE 格式: data: {json}\n\n
                    await Response.WriteAsync($"data: {json}\n\n", cancellationToken: cancellationToken);
                    await Response.Body.FlushAsync(cancellationToken);
                    break; // 遇到细错类型的响应后停止继续发送
                }
                else if(response.Type == StreamingResponseType.Exception)
                {
                    // 错误响应,包含详细错误信息
                    var errorObj = new
                    {
                        type = response.Type.ToString().ToLower(),
                        content = response.Content,
                        errorCode = response.ErrorCode,
                        httpStatus = response.HttpStatus,
                        title = response.ErrorTitle,
                        reason = response.ErrorReason,
                        solution = response.ErrorSolution,
                        isCritical = response.IsCritical
                    };

                    var json = JsonSerializer.Serialize(errorObj);

                    // SSE 格式: data: {json}\n\n
                    await Response.WriteAsync($"data: {json}\n\n", cancellationToken: cancellationToken);
                    await Response.Body.FlushAsync(cancellationToken);
                    break; // 遇到异常类型的响应后停止继续发送
                }
                else
                {
                    // 普通内容响应
                    var responseObj = new
                    {
                        type = response.Type.ToString().ToLower(),
                        content = response.Content
                    };
                    
                    var json = JsonSerializer.Serialize(responseObj);
                    
                    // SSE 格式: data: {json}\n\n
                    await Response.WriteAsync($"data: {json}\n\n", cancellationToken: cancellationToken);
                    await Response.Body.FlushAsync(cancellationToken);
                }
            }

            // 发送结束标记
            await Response.WriteAsync("data: [DONE]\n\n", cancellationToken: cancellationToken);
            await Response.Body.FlushAsync(cancellationToken);
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("客户端断开连接,SessionId: {SessionId}", sessionId);
        }
        catch (Exception ex)
        {
            try
            {
                _logger.LogError(ex, "聊天处理出错,SessionId: {SessionId}", sessionId);
                
                // 使用错误处理器转换异常
                var errorInfo = _errorHandler.HandleException(ex);
                _logger.LogWarning("错误信息: {ErrorCode} - {Title}: {Reason}", 
                    errorInfo.ErrorCode, errorInfo.Title, errorInfo.Reason);
                
                // 构建用户友好的错误消息
                var friendlyErrorMessage = BuildFriendlyErrorMessage(errorInfo);
                
                // 将错误信息作为系统消息添加到会话历史中(不影响AI对后续消息的理解)
                session?.History.AddSystemMessage(friendlyErrorMessage);
                
                // 返回错误信息 - 包含用户可读的内容
                var errorObj = new
                {
                    type = "error",
                    content = friendlyErrorMessage,  // 使用用户友好的错误消息
                    errorCode = errorInfo.ErrorCode,
                    httpStatus = errorInfo.HttpStatus,
                    title = errorInfo.Title,
                    reason = errorInfo.Reason,
                    solution = errorInfo.Solution,
                    isCritical = errorInfo.IsCritical
                };
                
                var json = JsonSerializer.Serialize(errorObj);
                await Response.WriteAsync($"data: {json}\n\n", cancellationToken: cancellationToken);
                await Response.WriteAsync($"data: [DONE]\n\n", cancellationToken: cancellationToken);
            }
            catch (Exception innerEx)
            {
                // 即使错误处理失败,也确保返回一些内容
                _logger.LogCritical(innerEx, "错误处理也失败了,SessionId: {SessionId}", sessionId);
                try
                {
                    var fallbackError = new
                    {
                        type = "error",
                        content = "系统内部错误,请稍后重试",
                        errorCode = "InternalError",
                        httpStatus = 500,
                        title = "系统错误",
                        reason = "处理请求时发生内部错误",
                        solution = "请稍后重试或联系管理员",
                        isCritical = true
                    };
                    
                    var fallbackJson = JsonSerializer.Serialize(fallbackError);
                    await Response.WriteAsync($"data: {fallbackJson}\n\n", cancellationToken: cancellationToken);
                    await Response.WriteAsync($"data: [DONE]\n\n", cancellationToken: cancellationToken);
                }
                catch
                {
                    // 如果连回退错误都无法发送,至少尝试发送一个简单的错误
                    await Response.WriteAsync("data: {\"type\":\"error\",\"content\":\"系统错误\"}\n\n", cancellationToken: cancellationToken);
                }
            }
        }
    }

    /// <summary>
    /// 发送消息并获取完整响应(非流式,便于 Swagger 测试)
    /// </summary>
    [HttpPost("sessions/{sessionId}/chat/complete")]
    public async Task<IActionResult> ChatComplete(string sessionId, [FromBody] ChatRequest request, CancellationToken cancellationToken)
    {
        var session = _sessionManager.GetSession(UserId, sessionId);
        if (session == null)
        {
            return NotFound(new { message = "会话不存在" });
        }

        var thinkingContent = new List<string>();
        var responseContent = new List<string>();

        try
        {
            await foreach (var response in _kernelService.StreamChatAsync(session, request.Message, cancellationToken))
            {
                if (response.Type == StreamingResponseType.Thinking)
                {
                    thinkingContent.Add(response.Content);
                }
                else
                {
                    responseContent.Add(response.Content);
                }
            }

            return Ok(new
            {
                thinking = thinkingContent.Count > 0 ? string.Join("", thinkingContent) : null,
                content = string.Join("", responseContent)
            });
        }
        catch (Exception ex)
        {
            try
            {
                _logger.LogError(ex, "聊天处理出错,SessionId: {SessionId}", sessionId);
                
                // 使用错误处理器转换异常
                var errorInfo = _errorHandler.HandleException(ex);
                _logger.LogWarning("错误信息: {ErrorCode} - {Title}: {Reason}", 
                    errorInfo.ErrorCode, errorInfo.Title, errorInfo.Reason);
                
                // 构建用户友好的错误消息
                var friendlyErrorMessage = BuildFriendlyErrorMessage(errorInfo);
                
                // 将错误信息作为系统消息添加到会话历史中(不影响AI对后续消息的理解)
                session?.History.AddSystemMessage(friendlyErrorMessage);
                
                return StatusCode(errorInfo.HttpStatus, new 
                { 
                    type = "error",
                    content = friendlyErrorMessage,  // 使用用户友好的错误消息
                    errorCode = errorInfo.ErrorCode,
                    httpStatus = errorInfo.HttpStatus,
                    title = errorInfo.Title,
                    reason = errorInfo.Reason,
                    solution = errorInfo.Solution,
                    isCritical = errorInfo.IsCritical
                });
            }
            catch (Exception innerEx)
            {
                // 即使错误处理失败,也确保返回一些内容
                _logger.LogCritical(innerEx, "错误处理也失败了,SessionId: {SessionId}", sessionId);
                
                return StatusCode(500, new 
                { 
                    type = "error",
                    content = "系统内部错误,请稍后重试",
                    errorCode = "InternalError",
                    httpStatus = 500,
                    title = "系统错误",
                    reason = "处理请求时发生内部错误",
                    solution = "请稍后重试或联系管理员",
                    isCritical = true
                });
            }
        }
    }

    /// <summary>
    /// 获取会话的聊天历史
    /// </summary>
    [HttpGet("sessions/{sessionId}/history")]
    public IActionResult GetHistory(string sessionId)
    {
        var session = _sessionManager.GetSession(UserId, sessionId);
        if (session == null)
        {
            return NotFound(new { message = "会话不存在" });
        }

        var history = session.History.Select(msg => new
        {
            Role = msg.Role.ToString(),
            Content = msg.Content
        });

        return Ok(history);
    }

    /// <summary>
    /// 构建用户友好的错误消息
    /// </summary>
    private static string BuildFriendlyErrorMessage(BailianErrorMessage errorInfo)
    {
        // 根据错误类型构建不同的友好消息
        var errorType = errorInfo.Category switch
        {
            BailianErrorCategory.ParameterError => "参数配置问题",
            BailianErrorCategory.AuthenticationError => "认证失败",
            BailianErrorCategory.PermissionError => "权限不足",
            BailianErrorCategory.NotFoundError => "资源不存在",
            BailianErrorCategory.RateLimitError => "请求频率过高",
            BailianErrorCategory.ServerError => "服务器内部错误",
            BailianErrorCategory.FileError => "文件处理问题",
            BailianErrorCategory.ValidationError => "输入验证失败",
            BailianErrorCategory.QuotaError => "配额不足",
            BailianErrorCategory.NetworkError => "网络连接问题",
            BailianErrorCategory.ContentError => "内容安全检查失败",
            _ => "系统错误"
        };

        // 添加时间戳和上下文信息
        var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        
        // 构建更详细的错误消息
        var isRetryable = !errorInfo.IsCritical;
        var retryAdvice = isRetryable ? "您可以稍后重试此操作。" : "此错误无法通过重试解决,请按照解决方案进行操作。";
        
        return $""""
抱歉,处理您的请求时遇到了问题:

**错误类型**: {errorType}
**具体原因**: {errorInfo.Reason}
**解决方案**: {errorInfo.Solution}

**错误代码**: {errorInfo.ErrorCode}
**发生时间**: {timestamp}
**状态**: {(isRetryable ? "可重试" : "严重错误")}
**建议**: {retryAdvice}

如果您已按照解决方案操作但问题仍然存在,请联系系统管理员。
"""";
    }
}

配置项说明

必需配置项

配置项 类型 说明 示例
AiServiceType string AI 服务类型 "dashscope", "openai", "ollama"
ModelId string 模型 ID "qwen3.6-plus-2026-04-02"
Endpoint string API 端点地址 "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" 只有阿里的url必须使用完全URL
ApiKey string API 密钥 "sk-xxx"

可选配置项

配置项 类型 默认值 说明
HttpClientTimeout TimeSpan 00:05:00 HttpClient 超时时间
ExtensionData Dictionary<string, object> 见下方 扩展配置数据,可自定义 AI 服务参数

ExtensionData 默认值

复制代码
{
  "enable_thinking": true,
  "preserve_thinking": true
}

ExtensionData 支持的所有配置项【参数效果要看对接的AI是否支持,另外也可自行根据对接的AI进行扩展】

配置项 类型 说明
enable_thinking bool 是否启用思考过程输出
preserve_thinking bool 是否保留思考过程(用于后续分析)
temperature float 采样温度(0-2),值越高越随机
max_tokens int 最大生成 token 数
top_p float 核采样参数(0-1)
frequency_penalty float 频率惩罚(0-2)
presence_penalty float 存在惩罚(0-2)

返回数据格式

StreamingResponse 类型说明

StreamingResponse 是包的核心返回类型,包含以下属性:

复制代码
public class StreamingResponse
{
    /// <summary>
    /// 响应类型
    /// </summary>
    public StreamingResponseType Type { get; set; }

    /// <summary>
    /// 响应内容
    /// </summary>
    public string Content { get; set; }

    /// <summary>
    /// 错误码(仅当 Type = Error 时有效)
    /// </summary>
    public string? ErrorCode { get; set; }

    /// <summary>
    /// 错误标题(仅当 Type = Error 时有效)
    /// </summary>
    public string? ErrorTitle { get; set; }

    /// <summary>
    /// 错误原因(仅当 Type = Error 时有效)
    /// </summary>
    public string? ErrorReason { get; set; }

    /// <summary>
    /// 错误解决方案(仅当 Type = Error 时有效)
    /// </summary>
    public string? ErrorSolution { get; set; }
}

StreamingResponseType 枚举

复制代码
public enum StreamingResponseType
{
    /// <summary>
    /// 思考过程(thinking / reasoning_content)
    /// </summary>
    Thinking,

    /// <summary>
    /// 正常回答内容
    /// </summary>
    Content,

    /// <summary>
    /// 工具调用结果
    /// </summary>
    ToolResult,

    /// <summary>
    /// 错误信息
    /// </summary>
    Error
}

各类型响应示例

1. Thinking - 思考过程

当模型进行推理时,会逐字输出思考过程:

复制代码
{
  "Type": "Thinking",
  "Content": "让我先分析一下用户的问题..."
}

前端处理建议

复制代码
if (response.Type === 'Thinking') {
  // 在侧边栏或单独区域显示思考过程
  displayThinking(response.Content);
}
2. Content - 正常回答

模型生成的正常回答内容:

复制代码
{
  "Type": "Content",
  "Content": "根据您的问题分析..."
}

前端处理建议

复制代码
if (response.Type === 'Content') {
  // 添加到聊天主窗口的回答区域
  appendToChat(response.Content);
}
3. ToolResult - 工具调用结果

当模型调用工具(如 MCP 插件)后返回的执行结果:

复制代码
{
  "Type": "ToolResult",
  "Content": "{\"table\": \"users\", \"count\": 150}"
}

前端处理建议

复制代码
if (response.Type === 'ToolResult') {
  // 可选:显示工具执行状态
  showToolExecutionStatus('工具执行完成');
  // 工具结果会自动传递给模型继续生成回答
}
4. Error - 错误信息

发生错误时的详细信息:

复制代码
{
  "Type": "Error",
  "Content": "API 调用失败,请检查网络连接",
  "ErrorCode": "HttpRequestFailed",
  "ErrorTitle": "请求失败",
  "ErrorReason": "Connection timeout",
  "ErrorSolution": "请检查网络配置或稍后重试"
}

前端处理建议

复制代码
if (response.Type === 'Error') {
  // 显示友好的错误提示
  showErrorToast(response);
  // 记录错误日志(可选)
  logError({
    code: response.ErrorCode,
    title: response.ErrorTitle,
    reason: response.ErrorReason,
    solution: response.ErrorSolution
  });
}

完整响应流示例

以下是一个完整的流式调用过程示例:

复制代码
用户输入:"查询数据库中有多少用户"

[Thinking]  "让我先分析一下这个问题..."
[Thinking]  "这个问题需要调用数据库查询工具..."
[ToolResult]  "{\"table\": \"users\", \"count\": 150}"
[Content]  "根据数据库查询结果,"
[Content]  "当前系统中共有 "
[Content]  "**150 个用户**。"
[Content]  "这些用户包括管理员、普通用户等不同角色..."

前端渲染效果

复制代码
🤔 [思考过程] 让我先分析一下这个问题...
🤔 [思考过程] 这个问题需要调用数据库查询工具...
🔧 [工具执行] 调用成功,返回 150 条记录
🤖 [回答] 根据数据库查询结果,当前系统中共有 **150 个用户**。

服务商配置示例

OpenAI

复制代码
{
  "SemanticKernel": {
    "AiServiceType": "openai",
    "ModelId": "gpt-4",
    "Endpoint": "https://api.openai.com/v1",
    "ApiKey": "sk-xxx",
    "ExtensionData": {
      "temperature": 0.7,
      "max_tokens": 4096
    }
  }
}

Ollama

复制代码
{
  "SemanticKernel": {
    "AiServiceType": "ollama",
    "ModelId": "qwen3.5:9b",
    "Endpoint": "http://localhost:11434",
    "ApiKey": "",
    "ExtensionData": {
      "enable_thinking": true
    }
  }
}

DashScope(阿里云)

复制代码
{
  "SemanticKernel": {
    "AiServiceType": "dashscope",
    "ModelId": "qwen3.6-plus-2026-04-02",
    "Endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
    "ApiKey": "sk-xxx",
    "HttpClientTimeout": "00:10:00",
    "ExtensionData": {
      "enable_thinking": true,
      "preserve_thinking": true,
      "temperature": 0.7,
      "top_p": 0.9
    }
  }
}

环境变量配置

支持使用环境变量覆盖配置(适用于 Docker、K8s 等环境):

复制代码
# Linux/Mac
export SemanticKernel__ApiKey="sk-new-api-key"
export SemanticKernel__HttpClientTimeout="00:10:00"
export SemanticKernel__ExtensionData__enable_thinking=true

# Windows
set SemanticKernel__ApiKey=sk-new-api-key
set SemanticKernel__HttpClientTimeout=00:10:00

Docker Compose 示例

复制代码
version: '3.8'
services:
  app:
    image: your-app:latest
    environment:
      - SemanticKernel__ApiKey=${AI_API_KEY}
      - SemanticKernel__Endpoint=https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
      - SemanticKernel__ModelId=qwen3.6-plus-2026-04-02
      - SemanticKernel__AiServiceType=dashscope
      - SemanticKernel__HttpClientTimeout=00:10:00
    ports:
      - "5000:80"

高级用法

1. 自定义工具调用

复制代码
// 创建自定义插件
public class WeatherPlugin
{
    [KernelFunction("get_weather")]
    [Description("获取指定城市的天气信息")]
    public string GetWeather(
        [Description("城市名称")] string city)
    {
        // 调用天气 API...
        return $"天气晴朗,温度 25°C";
    }
}

// 注册插件
kernel.Plugins.AddFromType<WeatherPlugin>("Weather");

2. 多会话管理

复制代码
// 每个会话独立管理历史对话和取消令牌
var session = await _sessionManager.GetOrCreateSessionAsync(sessionId);
session.UserId = userId;
session.History.AddUserMessage(userInput);

// 支持会话级别的取消
session.CancelCurrentRequest();

3. 流式响应前端集成

JavaScript/TypeScript 示例

复制代码
async function streamChat(input: string) {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ 
      sessionId: 'session-123',
      userInput: input 
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split('\n');

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        handleStreamingResponse(data);
      }
    }
  }
}

function handleStreamingResponse(data: StreamingResponse) {
  switch (data.Type) {
    case 'Thinking':
      updateThinkingPanel(data.Content);
      break;
    case 'Content':
      appendToChatBox(data.Content);
      break;
    case 'ToolResult':
      showToolStatus('工具执行完成');
      break;
    case 'Error':
      showError(data.ErrorTitle, data.Content);
      break;
  }
}

依赖项

  • .NET 8.0
  • Microsoft.Extensions.AI (9.10.2)
  • Microsoft.SemanticKernel (1.66.0)
  • Microsoft.SemanticKernel.Connectors.Ollama (1.66.0-alpha)
  • ModelContextProtocol (0.4.0-preview.3)
  • OllamaSharp (5.3.5)

兼容性

包版本 .NET 版本 Semantic Kernel 版本
1.x.x net8.0 1.66.0

常见问题

Q: 如何禁用思考过程输出?

在配置文件中设置:

复制代码
"ExtensionData": {
  "enable_thinking": false
}

Q: 如何增加 API 请求超时时间?

复制代码
"HttpClientTimeout": "00:10:00"

Q: 如何调用自定义工具?

  1. 创建包含 [KernelFunction] 标记的类
  2. 通过 kernel.Plugins.AddFromType<T>() 注册
  3. 模型会自动识别并调用可用的工具

Q: 支持哪些服务商?

目前支持:

  • ✅ OpenAI(包括兼容 OpenAI API 格式的服务商)
  • ✅ Ollama(本地部署)
  • ✅ DashScope(阿里云通义千问)

更新日志

v1.0.0

  • ✨ 初始版本发布
  • ✨ 支持 OpenAI、Ollama、DashScope 服务商
  • ✨ 支持思考过程流式输出(reasoning_content)
  • ✨ 支持 MCP 插件和工具调用
  • ✨ 配置化 HttpClient 超时时间
  • ✨ 可自定义 ExtensionData 配置项
  • ✨ 完整的使用示例和文档

许可证

MIT License - 详见 LICENSE 文件

贡献

欢迎提交 Issue 和 Pull Request!

联系方式

如有问题或建议,请通过以下方式联系: