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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|