Files
AI-Health/backend/src/Health.Infrastructure/AI/open_ai_compatible_client.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

234 lines
7.2 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 System.Net.Http.Headers;
namespace Health.Infrastructure.AI;
/// <summary>
/// OpenAI 兼容协议 HTTP 客户端,统一调用 DeepSeek / 千问 VL
/// </summary>
public sealed class OpenAiCompatibleClient(string baseUrl, string apiKey, string model)
{
private readonly HttpClient _http = CreateHttpClient(baseUrl, apiKey);
private readonly string _model = model;
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
private static HttpClient CreateHttpClient(string baseUrl, string apiKey)
{
var client = new HttpClient
{
BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"),
Timeout = TimeSpan.FromSeconds(60)
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
/// <summary>
/// 流式 Chat CompletionsSSE
/// </summary>
public async IAsyncEnumerable<string> ChatStreamAsync(
List<ChatMessage> messages,
List<ToolDefinition>? tools = null,
int maxTokens = 2048,
float temperature = 0.7f)
{
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"));
var response = await _http.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync()) != 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
/// </summary>
public async Task<ChatCompletionResponse> ChatAsync(
List<ChatMessage> messages,
List<ToolDefinition>? tools = null,
int maxTokens = 2048,
float temperature = 0.7f)
{
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);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
}
/// <summary>
/// Vision 图片理解(非流式)
/// </summary>
public async Task<ChatCompletionResponse> VisionAsync(
string systemPrompt,
List<string> imageUrls,
string? userText = null,
int maxTokens = 2048)
{
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);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
}
}
#region /
public sealed class ChatCompletionRequest
{
public string Model { get; set; } = string.Empty;
public List<ChatMessage> Messages { get; set; } = [];
public bool Stream { get; set; }
public int MaxTokens { get; set; } = 2048;
public float Temperature { get; set; } = 0.7f;
public float? TopP { get; set; }
public List<ToolDefinition>? Tools { get; set; }
public string? ToolChoice { get; set; }
}
public sealed class ChatMessage
{
public string Role { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string? ToolCallId { get; set; }
public List<ToolCall>? ToolCalls { get; set; }
}
public sealed class ToolDefinition
{
public string Type { get; set; } = "function";
public ToolFunction Function { get; set; } = new();
}
public sealed class ToolFunction
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public object Parameters { get; set; } = new();
}
public sealed class ToolCall
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = "function";
public ToolCallFunction Function { get; set; } = new();
}
public sealed class ToolCallFunction
{
public string Name { get; set; } = string.Empty;
public string Arguments { get; set; } = string.Empty;
}
public sealed class ChatCompletionResponse
{
public string Id { get; set; } = string.Empty;
public List<Choice> Choices { get; set; } = [];
}
public sealed class Choice
{
public int Index { get; set; }
public ResponseMessage? Message { get; set; }
public ResponseDelta? Delta { get; set; }
public string? FinishReason { get; set; }
}
public sealed class ResponseMessage
{
public string Role { get; set; } = string.Empty;
public string? Content { get; set; }
public List<ToolCall>? ToolCalls { get; set; }
}
public sealed class ResponseDelta
{
public string? Content { get; set; }
public string? Role { get; set; }
}
#endregion