Files
AI-Health/backend/src/Health.Infrastructure/AI/ai_clients.cs
MingNian d095832a10 feat: VLM 模型切换 qwen3-vl-plus + Diet Agent 患者档案联动
- VLM 模型切换为 qwen3-vl-plus(中餐识别准确率大幅提升)
- VLM Prompt 简化为仅识别食物名+份量+热量
- 营养分析/禁忌提醒移至 Diet Agent(可查患者档案)
- Diet Agent Prompt 强化:过敏→红色警告,低盐低脂→黄色提醒
- 上传限制调整至 20MB
- 服务端图片压缩参数优化(960px/Q72)
2026-06-02 14:23:40 +08:00

141 lines
5.3 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 Microsoft.Extensions.Configuration;
using System.Net.Http.Headers;
namespace Health.Infrastructure.AI;
/// <summary>
/// DeepSeek LLM 客户端(对话 + Tool Calling
/// </summary>
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
};
/// <summary>
/// 流式 Chat Completions
/// </summary>
public async IAsyncEnumerable<string> ChatStreamAsync(
List<ChatMessage> messages,
List<ToolDefinition>? 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;
}
}
/// <summary>
/// 非流式 Chat Completions用于 Tool Calling
/// </summary>
public async Task<ChatCompletionResponse> ChatAsync(
List<ChatMessage> messages,
List<ToolDefinition>? 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<ChatCompletionResponse>(body, _jsonOptions)!;
}
}
/// <summary>
/// VLM 视觉客户端——支持千问/豆包,通过 .env 切换
/// </summary>
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<ChatCompletionResponse> VisionAsync(
string systemPrompt,
List<string> imageUrls,
string? userText = null,
int maxTokens = 2048,
CancellationToken ct = default)
{
var messages = new List<ChatMessage>();
if (!string.IsNullOrEmpty(systemPrompt))
messages.Add(new ChatMessage { Role = "system", Content = systemPrompt });
var contentParts = new List<object>();
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,
};
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<ChatCompletionResponse>(body, _jsonOptions)!;
}
}