235 lines
8.0 KiB
C#
235 lines
8.0 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|