0

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

 7 months ago
source link: https://www.cnblogs.com/dudu/p/18013405
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

1-20240213094734132-1446887117.jpg

于是决定自己动手实现一个支持 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<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }

    public IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}

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

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

  • 把 Semantic Kernel 的 ChatHistory 转换为 Sdcb.DashScope 的 IReadOnlyList<ChatMessage>
  • 把 Semantic Kernel 的 PromptExecutionSettings 转换为 Sdcb.DashScope 的 ChatParameters
  • 把 Sdcb.DashScope 的 ResponseWrapper<ChatOutput, ChatTokenUsage> 转换为 Semantic Kernel 的 IReadOnlyList<ChatMessageContent>

实现代码如下:

public async Task<IReadOnlyList<ChatMessageContent>> 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<ChatParameters>(
            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<ResponseWrapper<ChatOutput, ChatTokenUsage>> 转换为 IAsyncEnumerable<StreamingChatMessageContent>

实现代码如下:

public async IAsyncEnumerable<StreamingChatMessageContent> 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<DashScopeClientOptions> 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<HttpClient>? configureClient = null,
        string configSectionPath = "dashscope")
    {
        Func<IServiceProvider, object?, DashScopeChatCompletionService> factory = (serviceProvider, _) =>
            serviceProvider.GetRequiredService<DashScopeChatCompletionService>();

        if (configureClient == null)
        {
            builder.Services.AddHttpClient<DashScopeChatCompletionService>();
        }
        else
        {
            builder.Services.AddHttpClient<DashScopeChatCompletionService>(configureClient);
        }

        builder.Services.AddOptions<DashScopeClientOptions>().BindConfiguration(configSectionPath);
        builder.Services.AddKeyedSingleton<IChatCompletionService>(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 = @"<message role=""user"">博客园是什么网站</message>";
        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<DashScopeChatCompletionTests>()
            .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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK