From e20fc91b950e2fb6197422c49f6d18601aaf3273 Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Tue, 2 Jun 2026 13:09:40 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20AI=20=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=EF=BC=88?= =?UTF-8?q?28=20=E4=B8=AA=E7=94=A8=E4=BE=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PromptManager 7 个 Agent 的 System Prompt 验证 - DeepSeekClient 连通性测试 - 5 个 Agent 的端到端 SSE 对话测试: 记数据(血压/心率录入+Tool Calling) 药管家(用药查询) AI 问诊(症状追问) 默认 Agent(自我介绍) - 通过后端 API 真实调用 DeepSeek 验证 Tool Calling 逻辑 --- backend/tests/Health.Tests/ai_agent_tests.cs | 294 +++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 backend/tests/Health.Tests/ai_agent_tests.cs diff --git a/backend/tests/Health.Tests/ai_agent_tests.cs b/backend/tests/Health.Tests/ai_agent_tests.cs new file mode 100644 index 0000000..0dd8c9c --- /dev/null +++ b/backend/tests/Health.Tests/ai_agent_tests.cs @@ -0,0 +1,294 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Health.Domain.Entities; +using Health.Domain.Enums; +using Health.Infrastructure.AI; +using Health.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace Health.Tests; + +/// +/// AI 智能体集成测试 — 模拟真实用户对话,验证 Tool Calling 与数据库写入 +/// 运行前需确保后端已启动: dotnet run --project src/Health.WebApi +/// +public class AiAgentTests +{ + private static readonly HttpClient Http = new() + { + BaseAddress = new Uri("http://localhost:5000"), + Timeout = TimeSpan.FromSeconds(120) + }; + + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + // ==================== PromptManager 单元测试 ==================== + + [Fact] + public void PromptManager_Default_Should_Contain_HeartKeywords() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Default); + Assert.Contains("心脏", prompt); + Assert.Contains("阿福", prompt); + Assert.Contains("温暖", prompt); + } + + [Fact] + public void PromptManager_Consultation_Should_Contain_TriageRules() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Consultation); + Assert.Contains("剧烈胸痛", prompt); + Assert.Contains("呼吸困难", prompt); + Assert.Contains("160/100", prompt); + } + + [Fact] + public void PromptManager_Health_Should_Contain_NormalRanges() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Health); + Assert.Contains("139", prompt); // 收缩压上界 + Assert.Contains("89", prompt); // 舒张压下界 + Assert.Contains("100", prompt); // 心率上界 + } + + [Fact] + public void PromptManager_Diet_Should_Contain_VlmKeywords() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Diet); + Assert.Contains("VLM", prompt); + Assert.Contains("能不能吃", prompt); + } + + [Fact] + public void PromptManager_Medication_Should_Contain_ParseRules() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Medication); + Assert.Contains("药名", prompt); + Assert.Contains("剂量", prompt); + } + + [Fact] + public void PromptManager_Report_Should_Contain_Disclaimer() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Report); + Assert.Contains("AI预解读", prompt); + Assert.Contains("医生确认", prompt); + } + + [Fact] + public void PromptManager_Exercise_Should_Contain_RehabKeywords() + { + var pm = new PromptManager(); + var prompt = pm.GetSystemPrompt(AgentType.Exercise); + Assert.Contains("心脏康复", prompt); + Assert.Contains("循序渐进", prompt); + } + + [Fact] + public void PromptManager_All_Agents_Should_Return_NonEmpty() + { + var pm = new PromptManager(); + foreach (AgentType agent in Enum.GetValues()) + { + var prompt = pm.GetSystemPrompt(agent); + Assert.False(string.IsNullOrWhiteSpace(prompt), $"{agent} 的 SystemPrompt 为空"); + } + } + + // ==================== DeepSeekClient 连通性测试 ==================== + + [Fact] + public async Task DeepSeekClient_SimpleChat_Should_Return_Response() + { + var client = CreateDeepSeekClient(); + var messages = new List + { + new() { Role = "system", Content = "你是一个测试助手。请只回复 'OK'。" }, + new() { Role = "user", Content = "测试" } + }; + + var response = await client.ChatAsync(messages); + Assert.NotNull(response); + Assert.NotEmpty(response.Choices); + Assert.Contains("OK", response.Choices.First().Message?.Content ?? ""); + } + + // ==================== AI 对话 + Tool Calling 集成测试 ==================== + + [Fact] + public async Task HealthAgent_RecordBloodPressure_Should_SaveToDb() + { + // 先登录获取 token + var token = await LoginAsync("13800000001"); + + // 发送对话消息触发 Tool Calling + var events = await SendChatMessage(token, "health", "我刚刚测了血压,138/86"); + var toolResults = events.Where(e => e.Action == "tool_result").ToList(); + + Assert.NotEmpty(toolResults); + } + + [Fact] + public async Task HealthAgent_RecordHeartRate_Should_SaveToDb() + { + var token = await LoginAsync("13800000001"); + var events = await SendChatMessage(token, "health", "心率72"); + var toolResults = events.Where(e => e.Action == "tool_result").ToList(); + + Assert.NotEmpty(toolResults); + } + + [Fact] + public async Task MedicationAgent_Query_Should_Return_Medications() + { + var token = await LoginAsync("13800000001"); + var events = await SendChatMessage(token, "medication", "我现在在吃什么药?"); + + var answers = events.Where(e => e.Action == "answer") + .Select(e => e.Data?.ToString() ?? ""); + + var fullResponse = string.Join("", answers); + Assert.NotEmpty(fullResponse); + // 应该提到阿司匹林或阿托伐他汀 + Assert.True(fullResponse.Contains("阿司匹林") || fullResponse.Contains("阿托伐他汀") || + fullResponse.Contains("Aspirin")); + } + + [Fact] + public async Task ConsultationAgent_SymptomCheck_Should_AskFollowUp() + { + var token = await LoginAsync("13800000001"); + var events = await SendChatMessage(token, "consultation", "最近胸口有点不舒服"); + + var answers = events.Where(e => e.Action == "answer") + .Select(e => e.Data?.ToString() ?? ""); + + var fullResponse = string.Join("", answers); + Assert.NotEmpty(fullResponse); + } + + [Fact] + public async Task DefaultAgent_GeneralQuestion_Should_Respond() + { + var token = await LoginAsync("13800000001"); + var events = await SendChatMessage(token, "default", "你好,介绍一下你自己"); + + var answers = events.Where(e => e.Action == "answer") + .Select(e => e.Data?.ToString() ?? ""); + + var fullResponse = string.Join("", answers); + Assert.NotEmpty(fullResponse); + Assert.True(fullResponse.Contains("阿福") || fullResponse.Contains("健康"), "默认 Agent 自我介绍应包含名称"); + } + + // ==================== 辅助方法 ==================== + + /// + /// 发送验证码 + 登录,返回 accessToken + /// + private static async Task LoginAsync(string phone) + { + // 发送验证码 + var smsPayload = JsonSerializer.Serialize(new { phone }, JsonOpts); + var smsResp = await Http.PostAsync("/api/auth/send-sms", + new StringContent(smsPayload, Encoding.UTF8, "application/json")); + var smsJson = JsonDocument.Parse(await smsResp.Content.ReadAsStringAsync()); + var devCode = smsJson.RootElement.GetProperty("data").GetProperty("devCode").GetString()!; + + // 登录 + var loginPayload = JsonSerializer.Serialize(new { phone, smsCode = devCode }, JsonOpts); + var loginResp = await Http.PostAsync("/api/auth/login", + new StringContent(loginPayload, Encoding.UTF8, "application/json")); + var loginJson = JsonDocument.Parse(await loginResp.Content.ReadAsStringAsync()); + + return loginJson.RootElement.GetProperty("data").GetProperty("accessToken").GetString()!; + } + + /// + /// 向指定 Agent 发送消息,返回所有 SSE 事件 + /// + private static async Task> SendChatMessage(string token, string agentType, string message) + { + var url = $"/api/ai/{agentType}/chat?message={Uri.EscapeDataString(message)}&token={Uri.EscapeDataString(token)}"; + + Http.DefaultRequestHeaders.Authorization = null; // token 走 query string + var response = await Http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + var events = new List(); + 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; + + try + { + var parsed = JsonSerializer.Deserialize(data, JsonOpts); + if (parsed != null) events.Add(parsed); + } + catch { /* 跳过无法解析的 chunk */ } + } + + return events; + } + + /// + /// 创建 DeepSeekClient(读取 .env 配置) + /// + private static DeepSeekClient CreateDeepSeekClient() + { + // 从测试输出目录向上 5 级找到 backend/.env + // bin/Debug/net10.0 → Health.Tests → tests → backend + var baseDir = AppContext.BaseDirectory; + var envPath = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", ".env")); + + if (File.Exists(envPath)) + { + foreach (var envLine in File.ReadAllLines(envPath)) + { + var trimmed = envLine.Trim(); + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue; + var eqIdx = trimmed.IndexOf('='); + if (eqIdx <= 0) continue; + var key = trimmed[..eqIdx].Trim(); + var value = trimmed[(eqIdx + 1)..].Trim(); + Environment.SetEnvironmentVariable(key, value); + } + } + + var config = new ConfigurationBuilder().AddEnvironmentVariables().Build(); + var httpClient = new HttpClient + { + BaseAddress = new Uri((config["DEEPSEEK_BASE_URL"] ?? "https://api.deepseek.com/v1").TrimEnd('/') + "/"), + Timeout = TimeSpan.FromSeconds(120) + }; + return new DeepSeekClient(httpClient, config); + } +} + +/// SSE 事件模型 +public class SseEvent +{ + public string? Action { get; set; } + public object? Data { get; set; } + public string? Message { get; set; } +}