Files
AI-Health/backend/src/Health.Infrastructure/AI/ai_clients.cs
MingNian 78573eaa5f fix: VLM 参数优化 - temperature 0.7, top_p 0.8, 指令放 system+user
- VisionAsync 新增 Temperature=0.7, TopP=0.8
- system prompt 用专业营养识别指令
- userText 用简短"请看图识别食物"配合图片
- 修复重复 prompt 导致 VLM 误读文本的 bug
2026-06-03 11:12:06 +08:00

142 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,
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<ChatCompletionResponse>(body, _jsonOptions)!;
}
}