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; }
+}