惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
量子位
小众软件
小众软件
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
I
InfoQ
Jina AI
Jina AI
The Cloudflare Blog
Recorded Future
Recorded Future
Recent Announcements
Recent Announcements
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Martin Fowler
Martin Fowler
T
Tailwind CSS Blog
博客园 - Franky
酷 壳 – CoolShell
酷 壳 – CoolShell
F
Fortinet All Blogs
WordPress大学
WordPress大学
P
Proofpoint News Feed
D
DataBreaches.Net
爱范儿
爱范儿
雷峰网
雷峰网
D
Docker
B
Blog
Engineering at Meta
Engineering at Meta
腾讯CDC
N
Netflix TechBlog - Medium
C
Check Point Blog
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
GbyAI
GbyAI
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 三生石上(FineUI控件)
T
The Blog of Author Tim Ferriss
博客园 - 聂微东
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
SecWiki News
SecWiki News
S
Security @ Cisco Blogs
S
Security Affairs
V
V2EX
Application and Cybersecurity Blog
Application and Cybersecurity Blog
云风的 BLOG
云风的 BLOG
C
CERT Recently Published Vulnerability Notes
Y
Y Combinator Blog

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 分享一下笔者的 Mac 装机必备软件 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布
王振耀 · 2026-04-17 · via 博客园_首页

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

SemanticKerne.AiProvider.Unified

.NET Semantic Kernel

基于 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!

联系方式

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

“老天爷给你的任何机会,你也不会珍惜,更不会深入下去,你认为自己就是穷命,最喜欢通过脉脉阅读免费的职场技巧。如果你还是杠精思维,任何事情,跑上来先论证困难,给自己一个不干不参与的充分理由,那你完了。人生不过是短短十五年的机会,到了四十岁,你还在屌丝堆里,啥也不干,那你没希望了。”