• 实现阿里云模型服务灵积 DashScope 的 Semantic Kernel Connector


    Semantic Kernel 内置的 IChatCompletionService 实现只支持 OpenAI 与 Azure OpenAI,而我却打算结合 DashScope(阿里云模型服务灵积) 学习 Semantic Kernel。

    于是决定自己动手实现一个支持 DashScope 的 Semantic Kernel Connector —— DashScopeChatCompletionService,实现的过程也是学习 Semantic Kernel 源码的过程,
    而且借助 Sdcb.DashScope,实现变得更容易了,详见前一篇博文 借助 .NET 开源库 Sdcb.DashScope 调用阿里云灵积通义千问 API

    这里只实现用于调用 chat completion 服务的 connector,所以只需实现 IChatCompletionService 接口,该接口继承了 IAIService 接口,一共需要实现2个方法+1个属性。

    public sealed class DashScopeChatCompletionService : IChatCompletionService
    {
        public IReadOnlyDictionary<string, object?> Attributes { get; }
    
        public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }
    
        public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
        {
            throw new NotImplementedException();
        }
    }
    

    先实现 GetChatMessageContentsAsync 方法,调用 Kernel.InvokePromptAsync 方法时会用到这个方法。

    实现起来比较简单,就是转手买卖:

    • 把 Semantic Kernel 的 ChatHistory 转换为 Sdcb.DashScope 的 IReadOnlyList
    • 把 Semantic Kernel 的 PromptExecutionSettings 转换为 Sdcb.DashScope 的 ChatParameters
    • 把 Sdcb.DashScope 的 ResponseWrapper 转换为 Semantic Kernel 的 IReadOnlyList

    实现代码如下:

    public async Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
    {
        var chatMessages = chatHistory
            .Where(x => !string.IsNullOrEmpty(x.Content))
            .Select(x => new ChatMessage(x.Role.ToString(), x.Content!)).
            ToList();
    
        ChatParameters? chatParameters = null;
        if (executionSettings?.ExtensionData?.Count > 0)
        {
            var json = JsonSerializer.Serialize(executionSettings.ExtensionData);
            chatParameters = JsonSerializer.Deserialize(
                json,
                new JsonSerializerOptions { NumberHandling = JsonNumberHandling.AllowReadingFromString });
        }
    
        var response = await _dashScopeClient.TextGeneration.Chat(_modelId, chatMessages, chatParameters, cancellationToken);
    
        return [new ChatMessageContent(new AuthorRole(chatMessages.First().Role), response.Output.Text)];
    }
    

    接下来实现 GetStreamingChatMessageContentsAsync,调用 Kernel.InvokePromptStreamingAsync 时会用到它,同样也是转手买卖。

    ChatHistoryPromptExecutionSettings 参数的转换与 GetChatMessageContentsAsync 一样,所以引入2个扩展方法 ChatHistory.ToChatMessagesPromptExecutionSettings.ToChatParameters 减少重复代码,另外需要将 ChatParameters.IncrementalOutput 设置为 true

    不同之处是返回值类型,需要将 Sdcb.DashScope 的 IAsyncEnumerable> 转换为 IAsyncEnumerable

    实现代码如下:

    public async IAsyncEnumerable GetStreamingChatMessageContentsAsync(
        ChatHistory chatHistory,
        PromptExecutionSettings? executionSettings = null,
        Kernel? kernel = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var chatMessages = chatHistory.ToChatMessages();
        var chatParameters = executionSettings?.ToChatParameters() ?? new ChatParameters();
        chatParameters.IncrementalOutput = true;
    
        var responses = _dashScopeClient.TextGeneration.ChatStreamed(_modelId, chatMessages, chatParameters, cancellationToken);
    
        await foreach (var response in responses)
        {
            yield return new StreamingChatMessageContent(new AuthorRole(chatMessages[0].Role), response.Output.Text);
        }
    }
    

    到这里2个方法就实现好了,还剩下很容易实现的1个属性,轻松搞定

    public sealed class DashScopeChatCompletionService : IChatCompletionService
    {
        private readonly DashScopeClient _dashScopeClient;
        private readonly string _modelId;
        private readonly Dictionary<string, object?> _attribues = [];
    
        public DashScopeChatCompletionService(
            IOptions options,
            HttpClient httpClient)
        {
            _dashScopeClient = new(options.Value.ApiKey, httpClient);
            _modelId = options.Value.ModelId;
            _attribues.Add(AIServiceExtensions.ModelIdKey, _modelId);
        }
    
        public IReadOnlyDictionary<string, object?> Attributes => _attribues;
    }
    

    到此,DashScopeChatCompletionService 的实现就完成了。

    接下来,实现一个扩展方法,将 DashScopeChatCompletionService 注册到依赖注入容器

    public static class DashScopeServiceCollectionExtensions
    {
        public static IKernelBuilder AddDashScopeChatCompletion(
            this IKernelBuilder builder,
            string? serviceId = null,
            Action? configureClient = null,
            string configSectionPath = "dashscope")
        {
            Funcobject?, DashScopeChatCompletionService> factory = (serviceProvider, _) =>
                serviceProvider.GetRequiredService();
    
            if (configureClient == null)
            {
                builder.Services.AddHttpClient();
            }
            else
            {
                builder.Services.AddHttpClient(configureClient);
            }
    
            builder.Services.AddOptions().BindConfiguration(configSectionPath);
            builder.Services.AddKeyedSingleton(serviceId, factory);
            return builder;
        }
    }
    

    为了方便通过配置文件配置 ModelId 与 ApiKey,引入了 DashScopeClientOptions

    public class DashScopeClientOptions : IOptions<DashScopeClientOptions>
    {
        public string ModelId { get; set; } = string.Empty;
    
        public string ApiKey { get; set; } = string.Empty;
    
        public DashScopeClientOptions Value => this;
    }
    

    最后就是写测试代码验证实现是否成功,为了减少代码块的长度,下面的代码片段只列出其中一个测试用例

    public class DashScopeChatCompletionTests
    {
        [Fact]
        public async Task ChatCompletion_InvokePromptAsync_WorksCorrectly()
        {
            // Arrange
            var builder = Kernel.CreateBuilder();
            builder.Services.AddSingleton(GetConfiguration());
            builder.AddDashScopeChatCompletion();
            var kernel = builder.Build();
    
            var prompt = @"博客园是什么网站";
            PromptExecutionSettings settings = new()
            {
                ExtensionData = new Dictionary<string, object>()
                {
                    { "temperature", "0.8" }
                }
            };
            KernelArguments kernelArguments = new(settings);
    
            // Act
            var result = await kernel.InvokePromptAsync(prompt, kernelArguments);
    
            // Assert
            Assert.Contains("博客园", result.ToString());
            Trace.WriteLine(result.ToString());
        }
    
        private static IConfiguration GetConfiguration()
        {
            return new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddUserSecrets()
                .Build();
        }
    }
    

    最后的最后就是运行测试,在 appsettings.json 中添加模型Id

    {
      "dashscope": {
        "modelId": "qwen-max"
      }
    }
    

    注:qwen-max 是通义千问千亿级大模型

    通过 user-secrets 添加 api key

    dotnet user-secrets set "dashscope:apiKey" "sk-xxx"
    

    dotnet test 命令运行测试

    A total of 1 test files matched the specified pattern.
    博客园是一个专注于提供信息技术(IT)领域知识分享和技术交流的中文博客平台,创建于2004年。博客园主要由软件开发人员、系统管理员以及对IT技术有深厚兴趣的人群使用,用户可以在该网站上撰写和发布自己的博客文章,内容涵盖编程、软件开发、云计算、人工智能等多个领域。同时,博客园也提供了丰富的技术文档、教程资源和社区互动功能,旨在促进IT专业人士之间的交流与学习。
    
    Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms - SemanticKernel.DashScope.IntegrationTest.dll (net8.0)
    

    测试通过!连接 DashScope 的 Semantic Kernel Connector 初步实现完成。

    完整实现代码放在 github 上,详见 https://github.com/cnblogs/semantic-kernel-dashscope/tree/v0.1.0

    针对这个实现发布了 nuget 包 Cnblogs.SemanticKernel.Connectors.DashScope

  • 相关阅读:
    马蹄集oj赛(双周赛第十五次)
    LeetCode146.LRU缓存
    MySQL高可用九种方案
    国产化框架PaddleClas结合Swanlab进行杂草分类
    共享WiFi贴推广项目怎么操作?
    TCP拥塞控制,拥塞窗口,携带应答,捎带应答,面向字节流,异常情况处理,最终完结弹
    【深度思考】聊聊CGLIB动态代理原理
    [动态规划] (十一) 简单多状态 LeetCode 面试题17.16.按摩师 和 198.打家劫舍
    如何在 Vue.js 和 Nuxt.js 之间做出选择?
    Windows 虚拟机服务器项目部署
  • 原文地址:https://www.cnblogs.com/dudu/p/18013405