From aab4dfea794c70b317c1d63835bc83deda109d04 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 18 May 2026 03:12:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E6=94=B9=E9=80=A0=20chat=5Fhandler?= =?UTF-8?q?=20=E6=8E=A5=E5=85=A5=20AgentOrchestrator=20=E2=80=94=20ReAct?= =?UTF-8?q?=20Agent=20=E9=A6=96=E6=AC=A1=E8=B7=91=E9=80=9A=20+=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BC=9A=E8=AF=9D=E6=9D=83=E9=99=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/erp-ai/src/handler/chat_handler.rs | 150 ++++++++++++++-------- crates/erp-ai/src/module.rs | 25 ++++ 2 files changed, 125 insertions(+), 50 deletions(-) diff --git a/crates/erp-ai/src/handler/chat_handler.rs b/crates/erp-ai/src/handler/chat_handler.rs index 6fbcea4..37de2b9 100644 --- a/crates/erp-ai/src/handler/chat_handler.rs +++ b/crates/erp-ai/src/handler/chat_handler.rs @@ -4,7 +4,10 @@ use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, TenantContext}; use serde::{Deserialize, Serialize}; -use crate::dto::GenerateRequest; +use crate::agent::tool::ToolContext; +use crate::agent::tools::QueryPatientVitalsTool; +use crate::agent::{AgentOrchestrator, ToolRegistry}; +use crate::dto::{ChatMessage, ChatMessageRole}; use crate::state::AiState; // === 请求 / 响应 === @@ -13,6 +16,8 @@ use crate::state::AiState; pub struct ChatRequest { pub message: String, pub history: Option>, + /// 可选:关联患者 ID(从用户档案中获取) + pub patient_id: Option, } #[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] @@ -25,26 +30,46 @@ pub struct ChatHistoryItem { pub struct ChatResponse { pub reply: String, pub message_id: String, + pub iterations: usize, } -const SYSTEM_PROMPT: &str = r#"你是 HMS 健康管理平台的 AI 客服助手"小华"。你的职责是: -1. 回答用户的健康咨询问题 -2. 帮助用户了解体检报告指标 -3. 提供预约挂号、用药提醒等服务指导 -4. 推荐健康生活方式 +const SYSTEM_PROMPT: &str = r#"你是 HMS 健康管理平台的 AI 健康顾问"小华"。 -注意: -- 你不能替代医生的诊断,遇到需要诊断的问题请建议用户就医 -- 不能推荐具体药物,只能提供一般性健康建议 -- 语气要亲切、专业、耐心 -- 回复要简洁明了,避免过长 -- 如果用户问的问题超出健康范围,礼貌引导回到健康话题"#; +## 核心策略 +根据用户表达的内容和情绪,自然地采用以下策略方向: + +1. 【情绪安抚】当用户表达焦虑、恐惧、沮丧时: + - 先共情认可感受,不急于给建议 + - 用通俗语言解释,避免医学术语 + - 分享积极案例,降低恐惧感 + +2. 【医疗科普】当用户询问指标含义、疾病知识时: + - 调用 search_medical_knowledge 获取准确信息(如可用) + - 用比喻和类比让老年患者也能理解 + - 强调"具体请以医生诊断为准" + +3. 【服务推荐】当用户表达就医需求或身体不适时: + - 调用 query_appointments 查看已有预约(如可用) + - 主动提出帮用户预约 + +4. 【风险预警】当用户描述的症状或数据异常时: + - 调用 query_patient_vitals 查看体征数据 + - 明确告知风险等级和需要注意的事项 + - 高风险时建议尽快就医 + +5. 【引导到院】当用户有明确就诊意向或高风险预警时: + - 提供科室位置、出诊医生信息 + - 建议用户联系前台预约 + +## 策略不是互斥的,你可以在一轮对话中自然切换。 +## 永远不要:推荐具体药物、给出明确诊断、替代医生建议。 +## 如果没有可用的工具数据,就基于常识回答,并建议用户咨询医生。"#; #[utoipa::path( post, path = "/ai/chat", request_body = ChatRequest, - responses((status = 200, description = "AI 客服回复")), + responses((status = 200, description = "AI Agent 回复")), tag = "AI 客服", security(("bearer_auth" = [])), )] @@ -69,30 +94,41 @@ where )); } - let user_prompt = match body.history { - Some(ref hist) if !hist.is_empty() => { - let filtered: Vec<&ChatHistoryItem> = hist - .iter() - .filter(|h| h.role == "user" || h.role == "assistant") - .collect(); - let start = filtered.len().saturating_sub(10); - let ctx: String = filtered[start..] - .iter() - .map(|h| { - format!( - "{}: {}", - if h.role == "user" { "用户" } else { "助手" }, - h.content - ) - }) - .collect::>() - .join("\n"); - format!("历史对话:\n{}\n\n用户最新消息: {}", ctx, message) - } - _ => message.to_string(), - }; - let ai_state = AiState::from_ref(&state); + + // 构建 Agent 消息历史 + let mut messages = vec![]; + + // 将前端传来的历史转换为 Agent ChatMessage + if let Some(ref hist) = body.history { + let filtered: Vec<&ChatHistoryItem> = hist + .iter() + .filter(|h| h.role == "user" || h.role == "assistant") + .collect(); + let start = filtered.len().saturating_sub(10); + for h in &filtered[start..] { + messages.push(ChatMessage { + role: if h.role == "user" { + ChatMessageRole::User + } else { + ChatMessageRole::Assistant + }, + content: h.content.clone(), + tool_calls: None, + tool_call_id: None, + }); + } + } + + // 添加当前用户消息 + messages.push(ChatMessage { + role: ChatMessageRole::User, + content: message.to_string(), + tool_calls: None, + tool_call_id: None, + }); + + // 解析 Provider let resolved = ai_state .provider_registry .resolve("auto") @@ -102,37 +138,51 @@ where erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into()) })?; - let req = GenerateRequest { - system_prompt: SYSTEM_PROMPT.to_string(), - user_prompt, - model: String::new(), - temperature: 0.7, - max_tokens: 1024, + // 构建 ToolRegistry — Phase 0 只有 query_patient_vitals + let mut registry = ToolRegistry::new(); + registry.register(std::sync::Arc::new(QueryPatientVitalsTool)); + + let tool_ctx = ToolContext { + tenant_id: ctx.tenant_id, + user_id: ctx.user_id, + patient_id: body.patient_id, + db: ai_state.db.clone(), + health_provider: ai_state.health_provider.clone(), }; tracing::info!( tenant_id = %ctx.tenant_id, user_id = %ctx.user_id, + patient_id = ?body.patient_id, msg_len = message.len(), - "AI chat request" + "AI Agent chat request" ); - let resp = resolved.provider().generate(req).await.map_err(|e| { - tracing::error!(error = %e, "AI chat generate failed"); - erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into()) - })?; + // 执行 Agent ReAct 循环 + let provider_arc = resolved.into_arc(); + let orchestrator = AgentOrchestrator::new(provider_arc, std::sync::Arc::new(registry)); + let result = orchestrator + .run(SYSTEM_PROMPT, &mut messages, &tool_ctx) + .await + .map_err(|e| { + tracing::error!(error = %e, "AI Agent run failed"); + erp_core::error::AppError::Internal("AI 服务暂时不可用,请稍后再试".into()) + })?; let message_id = uuid::Uuid::now_v7().to_string(); tracing::info!( tenant_id = %ctx.tenant_id, message_id = %message_id, - tokens = resp.output_tokens, - "AI chat response sent" + iterations = result.iterations, + input_tokens = result.total_input_tokens, + output_tokens = result.total_output_tokens, + "AI Agent response sent" ); Ok(Json(ApiResponse::ok(ChatResponse { - reply: resp.content, + reply: result.reply, message_id, + iterations: result.iterations, }))) } diff --git a/crates/erp-ai/src/module.rs b/crates/erp-ai/src/module.rs index 2aefa91..a354670 100644 --- a/crates/erp-ai/src/module.rs +++ b/crates/erp-ai/src/module.rs @@ -100,6 +100,31 @@ impl ErpModule for AiModule { description: "创建/编辑/删除 Copilot 规则".into(), module: "ai".into(), }, + // AI 客服会话权限 + PermissionDescriptor { + code: "ai.chat.send".into(), + name: "AI 客服对话".into(), + description: "向 AI 客服发送消息".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.chat.session.list".into(), + name: "查看 AI 会话列表".into(), + description: "查看用户的 AI 客服会话列表".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.chat.session.manage".into(), + name: "管理 AI 会话".into(), + description: "创建/关闭 AI 客服会话".into(), + module: "ai".into(), + }, + PermissionDescriptor { + code: "ai.chat.session.history".into(), + name: "查看 AI 会话历史".into(), + description: "查看 AI 客服会话消息历史".into(), + module: "ai".into(), + }, ] }