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

推荐订阅源

Cyberwarzone
Cyberwarzone
F
Full Disclosure
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
有赞技术团队
有赞技术团队
J
Java Code Geeks
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
L
LINUX DO - 最新话题
T
Threatpost
S
SegmentFault 最新的问题
Vercel News
Vercel News
云风的 BLOG
云风的 BLOG
C
Cyber Attacks, Cyber Crime and Cyber Security
Google DeepMind News
Google DeepMind News
Know Your Adversary
Know Your Adversary
S
Schneier on Security
V
Vulnerabilities – Threatpost
D
DataBreaches.Net
G
GRAHAM CLULEY
Latest news
Latest news
P
Privacy International News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Scott Helme
Scott Helme
L
Lohrmann on Cybersecurity
T
The Exploit Database - CXSecurity.com
Security Latest
Security Latest
G
Google Developers Blog
L
LangChain Blog
MyScale Blog
MyScale Blog
Project Zero
Project Zero
N
News and Events Feed by Topic
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
阮一峰的网络日志
阮一峰的网络日志
N
News | PayPal Newsroom
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
T
Tor Project blog
C
Check Point Blog
Google Online Security Blog
Google Online Security Blog
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
WordPress大学
WordPress大学

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .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 PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
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!

联系方式

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

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