using Microsoft.Extensions.Configuration; using System.Net.Http.Headers; namespace Health.Infrastructure.AI; /// /// DeepSeek LLM 客户端(对话 + Tool Calling) /// public sealed class DeepSeekClient(HttpClient http, IConfiguration config) { private readonly HttpClient _http = http; private readonly string _model = config["DEEPSEEK_MODEL"] ?? "deepseek-chat"; private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true }; /// /// 流式 Chat Completions /// public async IAsyncEnumerable ChatStreamAsync( List messages, List? tools = null, int maxTokens = 2048, float temperature = 0.7f, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) { var request = new ChatCompletionRequest { Model = _model, Messages = messages, Stream = true, MaxTokens = maxTokens, Temperature = temperature, Tools = tools, }; if (tools?.Count > 0) request.ToolChoice = "auto"; var json = JsonSerializer.Serialize(request, _jsonOptions); var content = new StringContent(json, Encoding.UTF8, "application/json"); var httpRequest = new HttpRequestMessage(HttpMethod.Post, "chat/completions") { Content = content }; httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); using var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(ct); using var reader = new StreamReader(stream); string? line; while ((line = await reader.ReadLineAsync(ct)) != null) { if (string.IsNullOrWhiteSpace(line)) continue; if (!line.StartsWith("data: ")) continue; var data = line["data: ".Length..]; if (data == "[DONE]") break; yield return data; } } /// /// 非流式 Chat Completions(用于 Tool Calling) /// public async Task ChatAsync( List messages, List? tools = null, int maxTokens = 2048, float temperature = 0.7f, CancellationToken ct = default) { var request = new ChatCompletionRequest { Model = _model, Messages = messages, Stream = false, MaxTokens = maxTokens, Temperature = temperature, Tools = tools, }; if (tools?.Count > 0) request.ToolChoice = "auto"; var json = JsonSerializer.Serialize(request, _jsonOptions); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _http.PostAsync("chat/completions", content, ct); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(ct); return JsonSerializer.Deserialize(body, _jsonOptions)!; } } /// /// VLM 视觉客户端——支持千问/豆包,通过 .env 切换 /// public sealed class VisionClient(HttpClient http, IConfiguration config) { private readonly HttpClient _http = http; private readonly string _model = config["VLM_MODEL"] ?? "doubao-vision-pro"; private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true }; public async Task VisionAsync( string systemPrompt, List imageUrls, string? userText = null, int maxTokens = 2048, CancellationToken ct = default) { var messages = new List(); if (!string.IsNullOrEmpty(systemPrompt)) messages.Add(new ChatMessage { Role = "system", Content = systemPrompt }); var contentParts = new List(); foreach (var url in imageUrls) contentParts.Add(new { type = "image_url", image_url = new { url } }); if (!string.IsNullOrEmpty(userText)) contentParts.Add(new { type = "text", text = userText }); var userMessage = new ChatMessage { Role = "user", Content = JsonSerializer.Serialize(contentParts, _jsonOptions) }; messages.Add(userMessage); var request = new ChatCompletionRequest { Model = _model, Messages = messages, MaxTokens = maxTokens, Stream = false, Temperature = 0.7f, TopP = 0.8f, }; var json = JsonSerializer.Serialize(request, _jsonOptions); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _http.PostAsync("chat/completions", content, ct); if (!response.IsSuccessStatusCode) { var errorBody = await response.Content.ReadAsStringAsync(ct); throw new HttpRequestException($"VLM API {response.StatusCode}: {errorBody}"); } var body = await response.Content.ReadAsStringAsync(ct); return JsonSerializer.Deserialize(body, _jsonOptions)!; } }