Files
SzmediTools/WPFUI.Test/Web/LLMClient.cs
2025-09-19 09:18:09 +08:00

235 lines
8.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace WPFUI.Test.Web
{
/// <summary>
/// TODO
/// </summary>
internal class LLMClient
{
private readonly HttpClient _httpClient;
private readonly List<ChatMessage> _conversationHistory;
private bool _disposed;
/// <summary>
/// 创建LLM客户端实例
/// </summary>
/// <param name="apiKey">API密钥</param>
/// <param name="endpoint">API端点地址</param>
/// <param name="systemPrompt">可选的系统提示词</param>
public LLMClient(string apiKey, string endpoint, string systemPrompt = null)
{
if (string.IsNullOrEmpty(apiKey))
throw new ArgumentNullException(nameof(apiKey));
if (string.IsNullOrEmpty(endpoint))
throw new ArgumentNullException(nameof(endpoint));
_httpClient = new HttpClient
{
BaseAddress = new Uri(endpoint)
};
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
// 初始化对话历史,可选添加系统提示
_conversationHistory = new List<ChatMessage>();
if (!string.IsNullOrEmpty(systemPrompt))
{
_conversationHistory.Add(new ChatMessage("system", systemPrompt));
}
}
/// <summary>
/// 获取当前对话历史
/// </summary>
public IReadOnlyList<ChatMessage> ConversationHistory => _conversationHistory.AsReadOnly();
/// <summary>
/// 清除对话历史(保留系统提示)
/// </summary>
public void ClearHistory()
{
if (_conversationHistory.Count > 0 && _conversationHistory[0].Role == "system")
{
var systemMessage = _conversationHistory[0];
_conversationHistory.Clear();
_conversationHistory.Add(systemMessage);
}
else
{
_conversationHistory.Clear();
}
}
/// <summary>
/// 发送非流式请求获取完整响应
/// </summary>
public async Task<string> SendMessageAsync(string userMessage, string model = "qwen-max")
{
// 添加用户消息到历史
_conversationHistory.Add(new ChatMessage("user", userMessage));
// 创建请求内容(包含完整对话历史)
var request = CreateRequestContent(_conversationHistory, model);
HttpResponseMessage response = await _httpClient.PostAsync("", request);
response.EnsureSuccessStatusCode();
string jsonResponse = await response.Content.ReadAsStringAsync();
// 解析响应并添加到对话历史
var responseObj = JsonConvert.DeserializeObject<dynamic>(jsonResponse);
string assistantReply = responseObj?.output?.choices?[0]?.message?.content;
if (!string.IsNullOrEmpty(assistantReply))
{
_conversationHistory.Add(new ChatMessage("assistant", assistantReply));
}
return assistantReply;
}
// 修正关键使用SendAsync替代PostAsync解决.NET Framework参数问题
public async Task SendMessageStreamAsync(string userMessage,
Action<string> onChunkReceived,
string model = "qwen-max",
CancellationToken cancellationToken = default)
{
_conversationHistory.Add(new ChatMessage("user", userMessage));
_conversationHistory.Add(new ChatMessage("assistant", "")); // 占位符
var request = CreateRequestContent(_conversationHistory, model, true);
// 创建HttpRequestMessage对象.NET Framework兼容方式
var httpRequest = new HttpRequestMessage(HttpMethod.Post, _httpClient.BaseAddress)
{
Content = request
};
// 使用SendAsync并指定HttpCompletionOption.NET Framework兼容
using (var response = await _httpClient.SendAsync(
httpRequest,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken))
{
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync())
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
string fullResponse = "";
while (!reader.EndOfStream)
{
string line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line)) continue;
// 修正正确处理阿里云SSE格式data: {...}
if (line.StartsWith("data: "))
{
string jsonData = line.Substring(6);
try
{
if (jsonData.Trim() == "[DONE]")
continue;
var json = JsonConvert.DeserializeObject<dynamic>(jsonData);
string content = json?.output?.choices?[0]?.delta?.content;
if (!string.IsNullOrEmpty(content))
{
fullResponse += content;
onChunkReceived?.Invoke(content);
}
}
catch
{
// 忽略无法解析的数据
}
}
}
// 更新助手消息的实际内容
if (_conversationHistory.Count > 0)
{
_conversationHistory[_conversationHistory.Count - 1] =
new ChatMessage("assistant", fullResponse);
}
}
}
}
private StringContent CreateRequestContent(List<ChatMessage> messages, string model, bool stream = false)
{
// 构建符合通义千问API的消息结构
var requestBody = new
{
model,
input = new
{
messages = messages.ConvertAll(m => new { role = m.Role, content = m.Content })
},
parameters = new
{
result_format = "message"
},
stream
};
string json = JsonConvert.SerializeObject(requestBody);
return new StringContent(json, Encoding.UTF8, "application/json");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_httpClient?.Dispose();
}
_disposed = true;
}
}
}
/// <summary>
/// 表示对话中的单条消息
/// </summary>
public class ChatMessage
{
/// <summary>
/// 消息角色: user, assistant, system
/// </summary>
public string Role { get; set; }
/// <summary>
/// 消息内容
/// </summary>
public string Content { get; set; }
public ChatMessage(string role, string content)
{
Role = role;
Content = content;
}
}
}