.Net接入AzureOpenAI、OpenAI、通义千问、智谱AI、讯飞星火、文心一言大语言模型。

慈云数据 2024-03-13 技术支持 61 0

前言

现在在网上搜索.NET接入大模型的帖子很少,有些官方案例只提供java和python的SDK,所以有了这篇.Net的接入大模型文章,目前仅实现对话模型的调用。

.Net接入AzureOpenAI、OpenAI、通义千问、智谱AI、讯飞星火、文心一言大语言模型。
(图片来源网络,侵删)

这里仅举例通义千问,其他模型实现可以参考Gi他Hub 对您有帮助的话帮忙点个star

个人博客:FaceMan’ Blog 。

.Net接入AzureOpenAI、OpenAI、通义千问、智谱AI、讯飞星火、文心一言大语言模型。
(图片来源网络,侵删)

Github:FaceMan’ GitHub 。

实现方式

  1. 创建IModelExtensionsChatCompletionService对话服务,规范对话服务应实现的接口。

    public interface IModelExtensionsChatCompletionService
    {
    	 /// 
    	 /// 对话
    	 /// 
    	 /// 对话历史
    	 /// 参数配置
    	 /// SK的kernel
    	 /// 是否取消
    	 /// 
    	 Task GetChatMessageContentsAsync(ChatHistory chatHistory, OpenAIPromptExecutionSettings settings = null, Kernel kernel = null, CancellationToken cancellationToken = default);
    	 /// 
    	 /// 流式对话
    	 /// 
    	 /// 对话历史
    	 /// 参数配置
    	 /// SK的kernel
    	 /// 是否取消
    	 /// 
    	 IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, OpenAIPromptExecutionSettings settings = null, Kernel kernel = null, CancellationToken cancellationToken = default);
     }
    
  2. 创建ModelClient类做数据解析

    public class ModelClient : IDisposable
    {
        internal readonly HttpClient HttpClient = null!;
        public ModelClient(string apiKey, ModelType modelType, HttpClient? httpClient = null)
        {
            HttpClient = httpClient ?? new HttpClient();
            switch (modelType)
            {
                case ModelType.ZhiPu:
                    int expirationInSeconds = 3600; // 设置过期时间为1小时
                    apiKey = GenerateJwtToken(apiKey, expirationInSeconds);
                    HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
                    break;
                case ModelType.QianWen:
                    HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
                    break;
                case ModelType.XunFei:
                    break;
            }
            QianWen = new QianWenClient(this);
            ZhiPu = new ZhiPuClient(this);
            XunFei = new XunFeiClient(this);
            WenXin = new WenXinClient(this);
        }
        public QianWenClient QianWen { get; set; }
        public ZhiPuClient ZhiPu { get; set; }
        public XunFeiClient XunFei { get; set; }
        public WenXinClient WenXin { get; set; }
        /// 
        /// 处理基础HTTP客户端。
        /// 
        public void Dispose() => HttpClient.Dispose();
        /// 
        /// 数据流转换器
        /// 
        /// 
        /// 响应体
        /// 
        /// 
        /// 
        internal static async Task ReadResponse(HttpResponseMessage response, CancellationToken cancellationToken)
        {
            if (!response.IsSuccessStatusCode)
            {
                throw new Exception(await response.Content.ReadAsStringAsync());
            }
            try
            {
                var debug = await response.Content.ReadAsStringAsync();
                return (await response.Content.ReadFromJsonAsync(options: null, cancellationToken))!;
            }
            catch (Exception e) when (e is NotSupportedException or System.Text.Json.JsonException)
            {
                throw new Exception($"未能将以下json转换为: {typeof(T).Name}: {await response.Content.ReadAsStringAsync()}", e);
            }
        }
        /// 
        /// 讯飞星火 数据流转换器
        /// 
        /// 
        /// 
        /// 
        public static XunFeiResponseWrapper ReadResponse(string receivedMessage)
        {
            XunFeiResponseWrapper response = JsonConvert.DeserializeObject(receivedMessage);
            return response;
        }
        /// 
        /// 智谱生成JWT令牌
        /// 
        /// 
        /// 
        /// 
        /// 
        internal string GenerateJwtToken(string apiKey, int expSeconds)
        {
            // 分割API Key以获取ID和Secret
            var parts = apiKey.Split('.');
            if (parts.Length != 2)
            {
                throw new ArgumentException("Invalid API key format.");
            }
            var id = parts[0];
            var secret = parts[1];
            // 创建Header信息
            var header = new JwtHeader(new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)), SecurityAlgorithms.HmacSha256))
                {
                    {"sign_type", "SIGN"}
                };
            // 创建Payload信息
            long currentMillis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            var payload = new JwtPayload
            {
                {"api_key", id},
                {"exp", currentMillis + expSeconds * 1000},
                {"timestamp", currentMillis}
            };
            // 生成JWT Token
            var token = new JwtSecurityToken(header, payload);
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
    
  3. 定义ModelType区分不同模型供应商

    public enum ModelType
    {
    	[Description("通义千问")]
    	[EnumName("通义千问")]
    	QianWen = 1,
    	[Description("智谱AI")]
    	[EnumName("智谱AI")]
    	ZhiPu,
    	[Description("科大讯飞")]
    	[EnumName("科大讯飞")]
    	XunFei,
    	[Description("文心一言")]
    	[EnumName("文心一言")]
    	WenXin,
    }
    
  4. 以通义千问为例,创建QianWenChatCompletionService类继承IModelExtensionsChatCompletionService

    public class QianWenChatCompletionService : IModelExtensionsChatCompletionService
    {
    	private readonly string _apiKey;
    	private readonly string _model;
    	public QianWenChatCompletionService(string key, string model)
    	{
    		_apiKey = key;
    		_model = model;
    	}
    	/// 
    	/// 对话
    	/// 
    	/// 
    	/// 
    	/// 
    	/// 
    	/// 
    	public async Task GetChatMessageContentsAsync(ChatHistory chatHistory, OpenAIPromptExecutionSettings settings = null, Kernel kernel = null, CancellationToken cancellationToken = default)
    	{
    		var histroyList = new List();
    		ChatParameters chatParameters = null;
    		foreach (var item in chatHistory)
    		{
    			var history = new ChatMessage()
    			{
    				Role = item.Role.Label,
    				Content = item.Content,
    			};
    			histroyList.Add(history);
    		}
    		if (settings != null)
    		{
    			chatParameters = new ChatParameters()
    			{
    				TopP = settings != null ? (float)settings.TopP : default,
    				MaxTokens = settings != null ? settings.MaxTokens : default,
    				Temperature = settings != null ? (float)settings.Temperature : default,
    				Seed = settings.Seed != null ? (ulong)settings.Seed : default,
    				Stop = settings != null ? settings.StopSequences : default,
    				//RepetitionPenalty = (float)settings.FrequencyPenalty,
    				//TopK = (int)settings.PresencePenalty
    			};
    		}
    		ModelClient client = new(_apiKey, ModelType.QianWen);
    		QianWenResponseWrapper result = await client.QianWen.GetChatMessageContentsAsync(_model, histroyList, chatParameters, cancellationToken);
    		var message = new ChatMessageContent(AuthorRole.Assistant, result.Output.Text);
    		return message;
    	}
    	/// 
    	/// 流式对话
    	/// 
    	/// 
    	/// 
    	/// 
    	/// 
    	/// 
    	public async IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, OpenAIPromptExecutionSettings settings = null, Kernel kernel = null, CancellationToken cancellationToken = default)
    	{
    		var histroyList = new List();
    		ChatParameters chatParameters = null;
    		foreach (var item in chatHistory)
    		{
    			var history = new ChatMessage()
    			{
    				Role = item.Role.Label,
    				Content = item.Content,
    			};
    			histroyList.Add(history);
    		}
    		if (settings != null)
    		{
    			chatParameters = new ChatParameters()
    			{
    				TopP = settings != null ? (float)settings.TopP : default,
    				MaxTokens = settings != null ? settings.MaxTokens : default,
    				Temperature = settings != null ? (float)settings.Temperature : default,
    				Seed = settings.Seed != null ? (ulong)settings.Seed : default,
    				Stop = settings != null ? settings.StopSequences : default,
    			};
    		}
    		ModelClient client = new(_apiKey, ModelType.QianWen);
    		await foreach (string item in client.QianWen.GetStreamingChatMessageContentsAsync(_model, histroyList, chatParameters, cancellationToken))
    		{
    			yield return item;
    		}
    	}
    }
    

    其中,OpenAIPromptExecutionSettings 和 ChatHistory 来自于SK框架,ChatParameters属于自定义的参数类,因为每家模型供应商都不一样。

  5. ChatParameters的代码

    public record ChatParameters
    {
    	/// 
    	/// 结果的格式-“text”为旧文本版本,“message”为OpenAI兼容消息。
    	/// <para>对于语言模型,此字段必须是中的“text”,而不是VL模型中使用的字段
    	/// 
    	[JsonPropertyName("result_format")]
    	public string? ResultFormat { get; set; }
    	/// 
    	/// 随机数生成器的种子,用于控制模型生成的随机性。
    	/// 使用相同的种子允许模型输出的再现性。
    	/// 此字段为可选字段。默认值为1234。
    	/// 
    	[JsonPropertyName("seed")]
    	public ulong? Seed { get; set; }
    	/// 
    	/// 限制要生成的令牌数量。限制设置了最大值,但不能保证
    	/// 确切地说,将生成那么多令牌。此字段是可选的。
    	/// qwen turbo和qwen max longcontext的最大值和默认值为1500。
    	/// qwen max、qwen-max-1201和qwen plus的最大值和默认值为2048。
    	/// 
    	[JsonPropertyName("max_tokens")]
    	public int? MaxTokens { get; set; }
    	/// 
    	/// 细胞核取样的概率阈值。以0.8的值为例,
    	/// 仅保留累积概率总和大于或等于0.8的令牌。
    	/// 取值范围为(0,1.0)。取值越大,随机性越高
    	/// 值越小,随机性越低。此字段是可选的.
    	/// 默认值为0.8。请注意,该值不应大于或等于1.
    	/// 
    	[JsonPropertyName("top_p")]
    	public float? TopP { get; set; }
    	/// 
    	/// 要采样的候选集的大小。例如,当设置为50时,只有前50个令牌
    	/// 将考虑进行采样。此字段是可选的。较大的值会增加随机性;
    	/// 较小的值会增加确定性。注意:如果top_ k为null或大于100,
    	/// 没有使用topk策略,只有topp是有效的。默认值为null。
    	/// 
    	[JsonPropertyName("top_k")]
    	public int? TopK { get; set; }
    	/// 
    	/// 为减少模型生成中的冗余而应用重复的惩罚。
    	/// 值为1.0表示没有惩罚。此字段是可选的。
    	/// 默认值为1.1。
    	/// 
    	[JsonPropertyName("repetition_penalty")]
    	public float? RepetitionPenalty { get; set; }
    	/// 
    	/// 控制文本生成的随机性和多样性程度。
    	/// 高温度值会降低概率分布的峰值、
    	/// 允许选择更多低概率词,从而产生更多样化的输出。
    	/// 
    	/// 低温度值会增加峰度,使高概率词更有可能被选中、
    	/// 从而使输出结果更加确定。此字段为可选项。
    	/// 数值范围为 [0, 2)。系统默认值为 1.0。
    	/// 
    	/// 
    	[JsonPropertyName("temperature")]
    	public float? Temperature { get; set; }
    	/// 
    	/// 指定生成后应停止模型进一步输出的内容。
    	/// 这可以是一个字符串或字符串列表、一个标记 ID 列表或一个标记 ID 列表。
    	/// 例如,如果将 stop 设置为 "hello",则在生成 "hello "之前停止生成; new()
    	{
    			Model = model ?? throw new ArgumentNullException(nameof(model)),
    			Input = input ?? throw new ArgumentNullException(nameof(input)),
    			Parameters = parameters,
    	};
    	public static QianWenRequestWrapper Create(string model, TInput inputPrompt) => new()
    	{
    			Model = model ?? throw new ArgumentNullException(nameof(model)),
    			Input = inputPrompt ?? throw new ArgumentNullException(nameof(inputPrompt)),
    	};
    }
    public record QianWenRequestWrapper : QianWenRequestWrapper
    {
    		[JsonPropertyName("model")]
    		public string Model { get; set; }
    		[JsonPropertyName("input")]
    		public TInput Input { get; init; }
    		[JsonPropertyName("parameters")]
    		public TParameters? Parameters { get; init; }
    }
    
    1. 调用
    QianWenChatCompletionService ChatGPT = new("你的key", "模型名称:例如qwen-turbo");
    ChatHistory historys = new ChatHistory();
    historys.AddSystemMessage("你是一个C#编程高手,你将用代码回答我关于.net编程的技术问题,下面是我的第一个问题:");
    historys.AddUserMessage("用c#写一个冒泡排序");
    // 流式调用
    await foreach (string item in chatgpt.GetStreamingChatMessageContentsAsync(historys))
    {
        Console.Write(item);
    }
    //普通调用
    var result = await chatgpt.GetChatMessageContentsAsync(historys);
    Console.WriteLine(result);
    
微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon