diff --git a/backend/src/Health.Infrastructure/AI/fastgpt_knowledge_client.cs b/backend/src/Health.Infrastructure/AI/fastgpt_knowledge_client.cs new file mode 100644 index 0000000..6eb1f57 --- /dev/null +++ b/backend/src/Health.Infrastructure/AI/fastgpt_knowledge_client.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Health.Infrastructure.AI; + +/// +/// FastGPT 知识库检索客户端(仅做 RAG 检索,不参与生成)。 +/// 我们后端的 DeepSeek 依旧负责对话生成与工具调用; +/// 这里只把检索到的医学知识片段拼回 system prompt,提升回答准确度。 +/// +public sealed class FastGptKnowledgeClient(HttpClient http, IConfiguration config, ILogger logger) +{ + private readonly HttpClient _http = http; + private readonly string _datasetId = config["FASTGPT_DATASET_ID"] ?? ""; + private readonly float _similarity = float.TryParse(config["FASTGPT_SIMILARITY"], out var s) ? s : 0.4f; + private readonly int _limit = int.TryParse(config["FASTGPT_LIMIT"], out var l) ? l : 3000; + private readonly ILogger _logger = logger; + private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true }; + + public bool IsEnabled => !string.IsNullOrWhiteSpace(_datasetId); + + /// + /// 按用户问题检索知识库,返回最相关的文档片段。 + /// 失败或未配置时返回空字符串,调用方应当容错——RAG 只是增强,不是必需。 + /// + public async Task SearchAsync(string question, CancellationToken ct = default) + { + if (!IsEnabled || string.IsNullOrWhiteSpace(question)) return ""; + + try + { + var payload = new + { + datasetId = _datasetId, + text = question, + limit = _limit, + similarity = _similarity, + searchMode = "embedding", + }; + var json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + using var response = await _http.PostAsync("core/dataset/searchTest", content, ct); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("FastGPT 检索失败: {Status}", response.StatusCode); + return ""; + } + + var body = await response.Content.ReadAsStringAsync(ct); + using var doc = JsonDocument.Parse(body); + if (!doc.RootElement.TryGetProperty("data", out var data)) return ""; + if (!data.TryGetProperty("list", out var list) || list.ValueKind != JsonValueKind.Array) return ""; + + var snippets = new List(); + foreach (var item in list.EnumerateArray()) + { + var q = item.TryGetProperty("q", out var qEl) ? qEl.GetString() ?? "" : ""; + var a = item.TryGetProperty("a", out var aEl) ? aEl.GetString() ?? "" : ""; + if (string.IsNullOrWhiteSpace(q) && string.IsNullOrWhiteSpace(a)) continue; + snippets.Add(string.IsNullOrWhiteSpace(a) ? q : $"{q}:{a}"); + } + + return snippets.Count == 0 ? "" : string.Join("\n", snippets); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "FastGPT 检索异常,已忽略"); + return ""; + } + } +} diff --git a/backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs b/backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs index cbe5082..436b8db 100644 --- a/backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs @@ -33,6 +33,7 @@ public static class AiChatEndpoints HttpContext http, DeepSeekClient llmClient, PromptManager promptManager, + FastGptKnowledgeClient knowledgeClient, IAiToolExecutionService toolExecution, IAiWriteConfirmationStore confirmations, IAiConversationService conversations, @@ -110,9 +111,24 @@ public static class AiChatEndpoints var systemPrompt = promptManager.GetSystemPrompt(parsedType); var patientContext = await patientContexts.BuildAsync(userId.Value, ct); + // ── RAG 知识库检索 ── + // 仅对需要医学知识的 Agent 启用检索;记数据/管用药/排运动这些纯工具调用类跳过。 + // 失败/空结果不阻塞流程,仅作为 prompt 增强。 + var knowledgeSnippet = ""; + if (NeedsKnowledgeBase(parsedType) && knowledgeClient.IsEnabled) + { + knowledgeSnippet = await knowledgeClient.SearchAsync(message, ct); + } + + var enhancedSystem = systemPrompt + "\n\n当前患者信息:\n" + patientContext; + if (!string.IsNullOrWhiteSpace(knowledgeSnippet)) + { + enhancedSystem += "\n\n知识库参考资料(如与用户问题相关请优先采用):\n" + knowledgeSnippet; + } + var messages = new List { - new() { Role = "system", Content = systemPrompt + "\n\n当前患者信息:\n" + patientContext }, + new() { Role = "system", Content = enhancedSystem }, }; // 加载历史对话(最近 10 条) @@ -303,6 +319,20 @@ public static class AiChatEndpoints // ── Agent / Tool 调度 ── + /// + /// 哪些 Agent 需要走 FastGPT 知识库 RAG。 + /// 纯工具调用类(Health/Medication/Exercise)跳过 RAG,避免拖慢响应。 + /// + private static bool NeedsKnowledgeBase(AgentType agentType) => agentType switch + { + AgentType.Default => true, + AgentType.Consultation => true, + AgentType.Diet => true, + AgentType.Report => true, + AgentType.Unified => true, + _ => false, + }; + private static List GetToolsForAgent(AgentType agentType) => agentType switch { AgentType.Health => HealthDataAgentHandler.Tools, diff --git a/backend/src/Health.WebApi/Program.cs b/backend/src/Health.WebApi/Program.cs index 3b72192..5a342e1 100644 --- a/backend/src/Health.WebApi/Program.cs +++ b/backend/src/Health.WebApi/Program.cs @@ -162,6 +162,12 @@ builder.Services.AddHttpClient(client => client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", builder.Configuration["VLM_API_KEY"] ?? ""); client.Timeout = TimeSpan.FromSeconds(120); }); +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri((builder.Configuration["FASTGPT_BASE_URL"] ?? "https://cloud.fastgpt.cn/api").TrimEnd('/') + "/"); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", builder.Configuration["FASTGPT_API_KEY"] ?? ""); + client.Timeout = TimeSpan.FromSeconds(15); +}); // ---- 后台服务 ---- builder.Services.AddHostedService(); diff --git a/health_app/lib/core/api_client.dart b/health_app/lib/core/api_client.dart index 5012dd7..52077c9 100644 --- a/health_app/lib/core/api_client.dart +++ b/health_app/lib/core/api_client.dart @@ -8,7 +8,7 @@ import 'local_database.dart'; /// Android 真机 WiFi: flutter run --dart-define=API_BASE_URL=http://电脑IP:5000 const String baseUrl = String.fromEnvironment( 'API_BASE_URL', - defaultValue: 'http://192.168.1.29:5000', + defaultValue: 'http://10.4.170.202:5000', ); class ApiException implements Exception {