# AI Agent 突破口实施计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 erp-ai 的 AI 客服从简单问答升级为 ReAct Agent,通过 Function Calling 串联后端分析能力,实现多策略主动关怀对话。 **Architecture:** 在 AiProvider trait 新增 `generate_with_tools()` 方法,实现 Agent Orchestrator 的 ReAct 循环(最多 5 轮 Tool Call),Tool 通过已有的 HealthDataProvider trait 访问 erp-health 数据。会话管理从本地 Storage 迁移到 DB 持久化。 **Tech Stack:** Rust (Axum + SeaORM + reqwest)、TypeScript/React (Taro 4.2 小程序 + Ant Design Web)、PostgreSQL、SSE **Spec:** `docs/superpowers/specs/2026-05-18-ai-agent-breakthrough-design.md` --- ## Chunk 1: Phase 0 — 基础设施(5-6 天) > 目标:Agent 核心循环跑通,能用一个 Tool 完成完整对话 ### Task 0.1: Agent DTO — ChatMessage / ToolDefinition / ToolCall / AgentGenerateResponse **Files:** - Modify: `crates/erp-ai/src/dto/mod.rs:62-77` (GenerateRequest/GenerateResponse 之后) - Test: `crates/erp-ai/src/dto/mod.rs` (编译检查即可,纯数据结构) - [ ] **Step 1: 在 dto/mod.rs 末尾添加 Agent 相关 DTO** 在 `GenerateResponse` 定义之后,添加: ```rust // === Agent Function Calling DTO === /// Agent 对话消息 #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct ChatMessage { pub role: ChatMessageRole, pub content: String, #[serde(skip_serializing_if = "Option::is_none")] pub tool_calls: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "lowercase")] pub enum ChatMessageRole { User, Assistant, Tool, } /// Tool 定义(传给 LLM 的 Function Schema) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { pub name: String, pub description: String, pub parameters: serde_json::Value, } /// LLM 返回的 Tool Call #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolCall { pub id: String, pub name: String, pub arguments: serde_json::Value, } /// Agent 专用生成响应 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentGenerateResponse { pub content: Option, pub tool_calls: Option>, /// 复用已有的 TokenUsage(dto/mod.rs 中的定义:input/output u32) pub usage: Option, } ``` - [ ] **Step 2: cargo check 验证编译** Run: `cargo check -p erp-ai` Expected: 编译通过(新增类型无外部依赖) - [ ] **Step 3: Commit** ```bash git add crates/erp-ai/src/dto/mod.rs git commit -m "feat(ai): 添加 Agent Function Calling DTO — ChatMessage/ToolDefinition/ToolCall/AgentGenerateResponse" ``` --- ### Task 0.2: AiProvider trait 新增 generate_with_tools 方法 **Files:** - Modify: `crates/erp-ai/src/provider/mod.rs:1-30` - Test: 编译检查 - [ ] **Step 1: 在 AiProvider trait 中新增 generate_with_tools 默认方法** 在 `crates/erp-ai/src/provider/mod.rs` 的 trait 定义中,`health_check` 之后添加: ```rust /// Agent 专用生成方法 — 支持 Function Calling /// 不支持 FC 的 Provider 使用默认实现(返回错误) async fn generate_with_tools( &self, messages: Vec, tools: Vec, system_prompt: &str, model: &str, temperature: f32, max_tokens: u32, ) -> crate::error::AiResult { Err(crate::error::AiError::UnsupportedOperation( "Function Calling not supported by this provider".into(), )) } ``` 同时在 `src/error.rs` 中添加 `UnsupportedOperation` 变体(如果不存在): ```rust #[error("unsupported operation: {0}")] UnsupportedOperation(String), ``` - [ ] **Step 2: cargo check 验证** Run: `cargo check -p erp-ai` Expected: 编译通过(默认实现不破坏现有 Provider) - [ ] **Step 3: Commit** ```bash git add crates/erp-ai/src/provider/mod.rs crates/erp-ai/src/error.rs git commit -m "feat(ai): AiProvider trait 新增 generate_with_tools 默认方法" ``` --- ### Task 0.3: Claude Provider 实现 generate_with_tools **Files:** - Modify: `crates/erp-ai/src/provider/claude.rs` (添加 tools 字段 + 响应解析) - [ ] **Step 1: 扩展 ClaudeRequest 结构体** 在 `claude.rs` 的 `ClaudeRequest` struct 中添加 `tools` 和 `system` 字段(如无 system 字段则添加): ```rust #[derive(Debug, Serialize)] #[serde(rename_all = "snake_case")] pub struct ClaudeTool { name: String, description: String, input_schema: serde_json::Value, } #[derive(Debug, Serialize)] struct ClaudeRequest { model: String, max_tokens: u32, #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, system: String, messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] tools: Option>, stream: bool, } #[derive(Debug, Serialize, Deserialize)] struct ClaudeMessage { role: String, content: serde_json::Value, // 改为 Value 以支持 tool_use/tool_result 内容块 } ``` - [ ] **Step 2: 实现 generate_with_tools** 在 `impl AiProvider for ClaudeProvider` 中添加: ```rust async fn generate_with_tools( &self, messages: Vec, tools: Vec, system_prompt: &str, model: &str, temperature: f32, max_tokens: u32, ) -> crate::error::AiResult { let claude_messages: Vec = messages.iter().map(|m| { // 根据角色和内容构建 Anthropic 格式消息 // assistant 带 tool_calls 时构造 tool_use content blocks // tool 角色时构造 tool_result content block // ... 完整转换逻辑 }).collect(); let claude_tools: Vec = tools.iter().map(|t| ClaudeTool { name: t.name.clone(), description: t.description.clone(), input_schema: t.parameters.clone(), }).collect(); let req = ClaudeRequest { model: model.to_string(), max_tokens, temperature: Some(temperature), system: system_prompt.to_string(), messages: claude_messages, tools: Some(claude_tools), stream: false, }; let resp = self.client.post(&self.api_url) .header("x-api-key", &self.api_key) .header("anthropic-version", "2023-06-01") .json(&req) .send().await .map_err(|e| AiError::ProviderError(e.to_string()))?; let parsed: serde_json::Value = resp.json().await .map_err(|e| AiError::ProviderError(e.to_string()))?; // 解析 content blocks — 区分 text 和 tool_use let mut content_text = None; let mut tool_calls = None; if let Some(blocks) = parsed["content"].as_array() { for block in blocks { match block["type"].as_str() { Some("text") => { content_text = block["text"].as_str().map(|s| s.to_string()); } Some("tool_use") => { let tc = ToolCall { id: block["id"].as_str().unwrap_or_default().to_string(), name: block["name"].as_str().unwrap_or_default().to_string(), arguments: block["input"].clone(), }; tool_calls.get_or_insert_with(Vec::new).push(tc); } _ => {} } } } let usage = parsed["usage"].as_object().map(|u| crate::dto::TokenUsage { input: u["input_tokens"].as_u64().unwrap_or(0) as u32, output: u["output_tokens"].as_u64().unwrap_or(0) as u32, }); Ok(AgentGenerateResponse { content: content_text, tool_calls, usage }) } ``` - [ ] **Step 3: cargo check + cargo test -p erp-ai** Run: `cargo check -p erp-ai && cargo test -p erp-ai` Expected: 编译通过,现有测试不受影响 - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/provider/claude.rs git commit -m "feat(ai): Claude Provider 实现 generate_with_tools — tool_use/tool_result 解析" ``` --- ### Task 0.4: OpenAI Provider 实现 generate_with_tools **Files:** - Modify: `crates/erp-ai/src/provider/openai.rs` - [ ] **Step 1: 扩展 ChatRequest 和 ChatMessageResp** 在 `openai.rs` 中: ```rust #[derive(Debug, Serialize)] struct ChatTool { r#type: String, // "function" function: ChatFunction, } #[derive(Debug, Serialize)] struct ChatFunction { name: String, description: String, parameters: serde_json::Value, } #[derive(Debug, Serialize)] struct ChatRequest { model: String, max_tokens: u32, #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] tools: Option>, stream: bool, } #[derive(Debug, Serialize, Deserialize)] struct OpenAiMessage { role: String, #[serde(skip_serializing_if = "Option::is_none")] content: Option, #[serde(skip_serializing_if = "Option::is_none")] tool_calls: Option>, #[serde(skip_serializing_if = "Option::is_none")] tool_call_id: Option, } #[derive(Debug, Serialize, Deserialize)] struct OpenAiToolCall { id: String, r#type: String, function: OpenAiFunction, } #[derive(Debug, Serialize, Deserialize)] struct OpenAiFunction { name: String, arguments: String, } ``` - [ ] **Step 2: 实现 generate_with_tools** 在 `impl AiProvider for OpenAiProvider` 中,转换消息格式(user→user, assistant+tool_calls→assistant, tool→tool),发送请求,解析 `choices[0].message.tool_calls`。 - [ ] **Step 3: cargo check + cargo test -p erp-ai** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/provider/openai.rs git commit -m "feat(ai): OpenAI Provider 实现 generate_with_tools — function calling 支持" ``` --- ### Task 0.5: Ollama Provider 降级处理 **Files:** - Modify: `crates/erp-ai/src/provider/ollama.rs` - [ ] **Step 1: Ollama 使用默认的 generate_with_tools(返回 UnsupportedOperation)** Ollama 的 Function Calling 支持不稳定,Phase 0 不实现。依赖 trait 默认方法即可。 如果需要显式声明不支持(更好的错误信息),在 `impl AiProvider for OllamaProvider` 中添加: ```rust async fn generate_with_tools( &self, _messages: Vec, _tools: Vec, _system_prompt: &str, _model: &str, _temperature: f32, _max_tokens: u32, ) -> crate::error::AiResult { Err(crate::error::AiError::UnsupportedOperation( "Ollama does not support Function Calling. Use Claude or OpenAI provider for Agent features.".into(), )) } ``` - [ ] **Step 2: cargo check -p erp-ai** - [ ] **Step 3: Commit** ```bash git add crates/erp-ai/src/provider/ollama.rs git commit -m "feat(ai): Ollama Provider 声明不支持 Function Calling" ``` --- ### Task 0.6: HealthDataProvider 扩展 — 新增 appointments 和 medication 方法 **Files:** - Modify: `crates/erp-core/src/health_provider.rs:10-42` (trait 定义) - Create: `crates/erp-core/src/health_provider.rs` (新增 DTO: AppointmentSummaryDto, MedicationSummaryDto) - Modify: `crates/erp-health/src/health_provider_impl.rs` (实现新方法) - Test: `cargo test -p erp-health` - [ ] **Step 1: 在 trait 中新增两个方法 + DTO** 在 `health_provider.rs` 的 trait 定义末尾添加: ```rust /// 获取患者即将到来的预约 async fn get_upcoming_appointments( &self, tenant_id: Uuid, patient_id: Uuid, ) -> AppResult>; /// 获取患者当前用药列表 async fn get_medication_list( &self, tenant_id: Uuid, patient_id: Uuid, ) -> AppResult>; ``` 新增 DTO: ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppointmentSummaryDto { pub id: Uuid, pub department: String, pub doctor_name: String, pub scheduled_at: chrono::DateTime, pub status: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MedicationSummaryDto { pub name: String, pub dosage: String, pub frequency: String, } ``` - [ ] **Step 2: 在 erp-health 实现新方法** 在 `health_provider_impl.rs` 的 `impl HealthDataProvider for HealthDataProviderImpl` 中,基于现有的 `appointment_service` 和 `medication_record_service` 实现查询,返回脱敏后的 DTO。 - [ ] **Step 3: cargo check + cargo test -p erp-health** Run: `cargo check -p erp-health && cargo test -p erp-health` Expected: 编译通过 - [ ] **Step 4: Commit** ```bash git add crates/erp-core/src/health_provider.rs crates/erp-health/src/health_provider_impl.rs git commit -m "feat(core): HealthDataProvider 新增 get_upcoming_appointments + get_medication_list" ``` --- ### Task 0.7: 数据库迁移 — 会话/消息/日志/用户画像 4 张表 **Files:** - Create: `crates/erp-server/migration/src/m20260518_000148_create_ai_chat_tables.rs` - Modify: `crates/erp-server/migration/src/lib.rs` (注册新迁移) - [ ] **Step 1: 创建迁移文件** 参考现有迁移文件格式(如 m20260516_000147),创建包含 4 张表的迁移: - `ai_chat_sessions` — 会话表(含 tenant_id, user_id, patient_id, title, status, metadata + 标准字段) - `ai_chat_messages` — 消息表(含 session_id FK, role, content, tool_calls JSONB, tool_call_id, token_count + 标准字段) - `ai_tool_call_logs` — 日志表(append-only:tenant_id, session_id, message_id, tool_name, parameters, result_summary, execution_ms, success, created_at, created_by) - `ai_user_profiles` — 用户画像表(tenant_id, user_id UNIQUE, preferences JSONB, health_interests TEXT[], frequent_topics TEXT[], personality_summary, last_updated_at + 标准字段,省略 created_by/updated_by 由 Agent 自动维护) - [ ] **Step 2: 在 lib.rs 注册迁移** 在 `migration/src/lib.rs` 的 `MigratorTrait` 列表中添加新迁移。 - [ ] **Step 3: cargo check -p erp-server** - [ ] **Step 4: 启动后端验证迁移执行** Run: `cd crates/erp-server && cargo run` Expected: 日志显示迁移 000148 执行成功 - [ ] **Step 5: Commit** ```bash git add crates/erp-server/migration/src/m20260518_000148_create_ai_chat_tables.rs crates/erp-server/migration/src/lib.rs git commit -m "feat(db): 迁移 000148 — AI 聊天会话/消息/工具日志/用户画像 4 张表" ``` --- ### Task 0.8: AgentTool trait + ToolRegistry + ToolContext + DisplayHint **Files:** - Create: `crates/erp-ai/src/agent/mod.rs` (模块入口) - Create: `crates/erp-ai/src/agent/tool.rs` (AgentTool trait + ToolContext + ToolResult + DisplayHint) - Create: `crates/erp-ai/src/agent/registry.rs` (ToolRegistry) - Test: `crates/erp-ai/src/agent/tool_test.rs` (单元测试) - [ ] **Step 1: 创建 agent 模块骨架** `crates/erp-ai/src/agent/mod.rs`: ```rust pub mod tool; pub mod registry; pub mod orchestrator; pub use tool::{AgentTool, ToolContext, ToolResult, DisplayHint}; pub use registry::ToolRegistry; pub use orchestrator::AgentOrchestrator; ``` - [ ] **Step 2: 实现 AgentTool trait + ToolContext + ToolResult + DisplayHint** `crates/erp-ai/src/agent/tool.rs`: ```rust use async_trait::async_trait; use chrono::{DateTime, Utc}; use erp_core::health_provider::HealthDataProvider; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[async_trait] pub trait AgentTool: Send + Sync { fn name(&self) -> &str; fn description(&self) -> &str; fn parameters_schema(&self) -> serde_json::Value; async fn execute(&self, ctx: &ToolContext, params: serde_json::Value) -> ToolResult; } pub struct ToolContext { pub tenant_id: Uuid, pub user_id: Uuid, pub patient_id: Option, pub db: DatabaseConnection, pub health_provider: std::sync::Arc, } pub struct ToolResult { pub output: String, pub display_hint: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum DisplayHint { VitalCard { indicator_type: String, values: Vec<(String, f64)>, unit: String, }, LabReportCard { report_date: String, abnormal_count: usize, }, ActionConfirm { action_type: String, summary: String, confirm_payload: serde_json::Value, }, RiskAlert { level: String, message: String, }, Text, } ``` - [ ] **Step 3: 实现 ToolRegistry** `crates/erp-ai/src/agent/registry.rs`: ```rust use std::collections::HashMap; use std::sync::Arc; use super::tool::AgentTool; pub struct ToolRegistry { tools: HashMap>, } impl ToolRegistry { pub fn new() -> Self { Self { tools: HashMap::new() } } pub fn register(&mut self, tool: Arc) { self.tools.insert(tool.name().to_string(), tool); } pub fn get(&self, name: &str) -> Option<&Arc> { self.tools.get(name) } pub fn all_tools(&self) -> Vec<&Arc> { self.tools.values().collect() } /// 生成传给 LLM 的 ToolDefinition 列表 pub fn tool_definitions(&self) -> Vec { self.tools.values().map(|t| crate::dto::ToolDefinition { name: t.name().to_string(), description: t.description().to_string(), parameters: t.parameters_schema(), }).collect() } } ``` - [ ] **Step 4: 在 lib.rs 注册 agent 模块** 在 `crates/erp-ai/src/lib.rs` 添加 `pub mod agent;` - [ ] **Step 5: cargo check -p erp-ai** - [ ] **Step 6: Commit** ```bash git add crates/erp-ai/src/agent/ crates/erp-ai/src/lib.rs git commit -m "feat(ai): AgentTool trait + ToolRegistry + ToolContext + DisplayHint" ``` --- ### Task 0.9: AgentOrchestrator — ReAct 循环 **Files:** - Create: `crates/erp-ai/src/agent/orchestrator.rs` - Test: `crates/erp-ai/src/agent/orchordinator_test.rs` - [ ] **Step 1: 实现 AgentOrchestrator** `crates/erp-ai/src/agent/orchestrator.rs`: ```rust use crate::agent::registry::ToolRegistry; use crate::agent::tool::{AgentTool, ToolContext, ToolResult}; use crate::dto::{AgentGenerateResponse, ChatMessage, ChatMessageRole, ToolCall}; use crate::error::AiResult; use crate::provider::AiProvider; use std::sync::Arc; pub struct AgentOrchestrator { provider: Arc, tool_registry: Arc, max_iterations: usize, // 默认 5 } impl AgentOrchestrator { pub fn new(provider: Arc, tool_registry: Arc) -> Self { Self { provider, tool_registry, max_iterations: 5 } } /// 执行 Agent ReAct 循环 pub async fn run( &self, system_prompt: &str, messages: &mut Vec, ctx: &ToolContext, ) -> AiResult { let tools = self.tool_registry.tool_definitions(); let mut iterations = 0; let mut total_input_tokens = 0u32; let mut total_output_tokens = 0u32; loop { iterations += 1; let response = self.provider.generate_with_tools( messages.clone(), tools.clone(), system_prompt, "auto", // 模型由 Provider 内部决定 0.7, 2048, ).await?; if let Some(ref usage) = response.usage { total_input_tokens += usage.input_tokens; total_output_tokens += usage.output_tokens; } // 如果没有 tool_calls,Agent 给出最终回复 let tool_calls = match response.tool_calls { Some(tc) if !tc.is_empty() => tc, _ => { return Ok(AgentRunResult { reply: response.content.unwrap_or_default(), total_input_tokens, total_output_tokens, iterations, }); } }; // 达到上限:强制结束 if iterations >= self.max_iterations { // 追加 User 角色指令让 LLM 基于已有信息生成最终回复 messages.push(ChatMessage { role: ChatMessageRole::User, content: "(系统提示:已收集足够信息,请直接总结回复用户,不要再调用工具)".to_string(), tool_calls: None, tool_call_id: None, }); continue; } // 将 assistant 的 tool_calls 加入消息历史 messages.push(ChatMessage { role: ChatMessageRole::Assistant, content: response.content.unwrap_or_default(), tool_calls: Some(tool_calls.clone()), tool_call_id: None, }); // 执行每个 Tool Call for tc in &tool_calls { let tool_result = match self.tool_registry.get(&tc.name) { Some(tool) => { match tool.execute(ctx, tc.arguments.clone()).await { Ok(result) => result.output, Err(e) => format!("Tool '{}' 执行失败: {}", tc.name, e), } } None => format!("未知 Tool: {}", tc.name), }; messages.push(ChatMessage { role: ChatMessageRole::Tool, content: tool_result, tool_calls: None, tool_call_id: Some(tc.id.clone()), }); } } } } pub struct AgentRunResult { pub reply: String, pub total_input_tokens: u32, pub total_output_tokens: u32, pub iterations: usize, } ``` - [ ] **Step 2: cargo check -p erp-ai** - [ ] **Step 3: Commit** ```bash git add crates/erp-ai/src/agent/orchestrator.rs git commit -m "feat(ai): AgentOrchestrator — ReAct 循环(最多 5 轮 Tool Call + 强制终止)" ``` --- ### Task 0.10: 实现 query_patient_vitals Tool — 端到端验证 **Files:** - Create: `crates/erp-ai/src/agent/tools/mod.rs` - Create: `crates/erp-ai/src/agent/tools/query_vitals.rs` - Modify: `crates/erp-ai/src/agent/mod.rs` (注册 tools 子模块) - [ ] **Step 1: 创建 tools 子模块** `crates/erp-ai/src/agent/tools/mod.rs`: ```rust pub mod query_vitals; ``` `crates/erp-ai/src/agent/tools/query_vitals.rs`: ```rust use async_trait::async_trait; use crate::agent::tool::{AgentTool, ToolContext, ToolResult, DisplayHint}; use serde::{Deserialize, Serialize}; use erp_core::health_provider::TimeRange; use chrono::Utc; pub struct QueryPatientVitalsTool; #[async_trait] impl AgentTool for QueryPatientVitalsTool { fn name(&self) -> &str { "query_patient_vitals" } fn description(&self) -> &str { "查询患者最近的体征数据(血压、血糖、心率等)。需要提供患者 ID 和天数范围(默认 7 天)。" } fn parameters_schema(&self) -> serde_json::Value { serde_json::json!({ "type": "object", "properties": { "days": { "type": "integer", "description": "查询最近多少天的数据,默认 7 天" } } }) } async fn execute(&self, ctx: &ToolContext, params: serde_json::Value) -> ToolResult { let patient_id = match ctx.patient_id { Some(id) => id, None => return ToolResult { output: "未关联患者档案,无法查询体征数据".to_string(), display_hint: None, }, }; let days = params["days"].as_i64().unwrap_or(7); let now = Utc::now(); let start = now - chrono::Duration::days(days); let range = TimeRange { start, end: now }; let metrics = vec![ "blood_pressure_systolic".into(), "blood_pressure_diastolic".into(), "heart_rate".into(), "blood_glucose".into(), ]; match ctx.health_provider.get_vital_signs(ctx.tenant_id, patient_id, &metrics, &range).await { Ok(vitals) => { if vitals.is_empty() { return ToolResult { output: "该时间段内无体征数据".to_string(), display_hint: None, }; } let mut output = String::from("最近体征数据:\n"); for v in &vitals { output.push_str(&format!("- {}: ", v.metric)); let values_str: Vec = v.values.iter() .take(10) .map(|(date, val)| format!("{}={}", date, val)) .collect(); output.push_str(&values_str.join(", ")); output.push_str(&format!(" ({})\n", v.unit)); } ToolResult { output, display_hint: Some(DisplayHint::VitalCard { indicator_type: vitals[0].metric.clone(), values: vitals[0].values.iter().take(10) .map(|(d, v)| (d.clone(), *v)) .collect(), unit: vitals[0].unit.clone(), }), } } Err(e) => ToolResult { output: format!("查询体征数据失败: {}", e), display_hint: None, }, } } } ``` - [ ] **Step 2: 更新 agent/mod.rs 注册 tools 子模块** 添加 `pub mod tools;` 并在 `pub use` 中导出。 - [ ] **Step 3: cargo check -p erp-ai** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/agent/mod.rs git commit -m "feat(ai): 实现 query_patient_vitals Tool — 首个端到端 Agent Tool" ``` --- ### Task 0.11: 改造 chat_handler — 接入 AgentOrchestrator **Files:** - Modify: `crates/erp-ai/src/handler/chat_handler.rs` (替换原有简单逻辑) - Modify: `crates/erp-ai/src/state.rs` (添加 ToolRegistry 字段) - Modify: `crates/erp-ai/src/module.rs` (注册新权限码 + 初始化 ToolRegistry) - [ ] **Step 1: 在 AiState 中添加 ToolRegistry** `state.rs` 新增字段: ```rust pub tool_registry: Arc, ``` - [ ] **Step 2: 在 module.rs 中初始化 ToolRegistry 并注入 AiState** 在模块初始化时: ```rust let mut tool_registry = ToolRegistry::new(); tool_registry.register(Arc::new(QueryPatientVitalsTool)); // 后续 Phase 1 添加更多 Tool ``` - [ ] **Step 3: 重写 chat_handler 使用 AgentOrchestrator** 替换原有的 `chat()` 函数核心逻辑: 1. 从请求中获取 session_id(或创建新会话) 2. 从 DB 加载会话历史消息 3. 将用户消息保存到 DB 4. 构建 ToolContext(从 AiState 获取 health_provider, db) 5. 构建 system prompt(多策略,Phase 1 完善) 6. 创建 AgentOrchestrator 并调用 `run()` 7. 将 Agent 回复保存到 DB 8. 返回 ChatResponse 注意:Phase 0 先用简化版 session 管理(直接传 session_id 参数),完整的 Session CRUD API 留到 Phase 2。 > **路由说明**:Phase 0 复用现有 `POST /ai/chat` 路由(module.rs:361 已注册),改造 handler 内部逻辑。Phase 2 会变更为 Spec 定义的 `/api/v1/ai/chat/sessions/{id}/messages`。 > **模型选择**:Phase 0 硬编码 `"auto"` 由 Provider 内部决定模型。后续可通过 AiState.provider_registry 动态选择。 - [ ] **Step 4: 在 module.rs 注册新权限码** ```rust // 现有权限码补充 ("ai.chat.session.list", "查看 AI 会话列表"), ("ai.chat.session.manage", "创建/关闭 AI 会话"), ("ai.chat.session.history", "查看 AI 会话消息历史"), ``` - [ ] **Step 5: cargo check + cargo test --workspace** - [ ] **Step 6: 功能验证 — 启动后端,用 Postman 测试** ```bash cd crates/erp-server && cargo run ``` Postman 发送 POST `/api/v1/ai/chat`: ```json { "message": "我最近血压怎么样", "history": [] } ``` Expected: Agent 返回包含血压数据的自然语言回复。 - [ ] **Step 7: Commit** ```bash git add crates/erp-ai/src/handler/chat_handler.rs crates/erp-ai/src/state.rs crates/erp-ai/src/module.rs git commit -m "feat(ai): 改造 chat_handler 接入 AgentOrchestrator — ReAct Agent 首次跑通" ``` --- ### Task 0.12: Phase 0 集成测试 **Files:** - Create: `crates/erp-server/tests/integration/ai_agent_test.rs` - [ ] **Step 1: 编写集成测试** 测试场景: 1. 发送简单问候 → Agent 直接回复(无 Tool Call) 2. 发送体征查询 → Agent 调用 query_patient_vitals Tool → 回复包含数据 3. 达到 5 轮上限 → Agent 正常结束回复 4. 无关联患者 → Tool 返回提示信息 注意:集成测试需要 mock LLM Provider(避免真实 API 调用),可创建 `MockProvider` 实现 `AiProvider` trait。 - [ ] **Step 2: cargo test --workspace** - [ ] **Step 3: Commit** ```bash git add crates/erp-server/tests/integration/ai_agent_test.rs git commit -m "test(ai): Phase 0 集成测试 — Agent 循环 + Tool 执行 + 降级场景" ``` --- ### Phase 0 完成标准 - [ ] `cargo check` 全 workspace 通过 - [ ] `cargo test --workspace` 全部通过 - [ ] Postman 调用 `/api/v1/ai/chat`,Agent 能查到患者体征数据并自然回复 - [ ] 代码已提交并推送 --- ## Chunk 2: Phase 1 — Tool 扩展 + 策略 Prompt(5-7 天) > 目标:覆盖全部核心 Tool,多策略对话流生效 ### Task 1.1: 数据查询类 Tool — query_lab_reports + query_patient_profile **Files:** - Create: `crates/erp-ai/src/agent/tools/query_lab_reports.rs` - Create: `crates/erp-ai/src/agent/tools/query_patient_profile.rs` - Modify: `crates/erp-ai/src/agent/tools/mod.rs` (注册新模块) - Modify: `crates/erp-ai/src/module.rs` (注册新 Tool 到 ToolRegistry) - [ ] **Step 1: 实现 query_lab_reports Tool** 调用 `ctx.health_provider.get_lab_report()`,参数为 `report_id`。输出格式化的化验指标列表,标注异常项。 - [ ] **Step 2: 实现 query_patient_profile Tool** 调用 `ctx.health_provider.get_patient_summary()`,返回脱敏后的患者摘要(年龄组、性别、慢性病、用药、家族史)。 - [ ] **Step 3: 注册到 ToolRegistry + cargo check** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs git commit -m "feat(ai): 添加 query_lab_reports + query_patient_profile Tool" ``` --- ### Task 1.2: 数据查询类 Tool — query_appointments + query_medication **Files:** - Create: `crates/erp-ai/src/agent/tools/query_appointments.rs` - Create: `crates/erp-ai/src/agent/tools/query_medication.rs` - [ ] **Step 1: 实现 query_appointments Tool** 调用 `ctx.health_provider.get_upcoming_appointments()`(Task 0.6 新增的方法),返回患者即将到来的预约列表。 - [ ] **Step 2: 实现 query_medication Tool** 调用 `ctx.health_provider.get_medication_list()`(Task 0.6 新增的方法),返回患者当前用药列表。 - [ ] **Step 3: 注册到 ToolRegistry + cargo check + cargo test -p erp-ai** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs git commit -m "feat(ai): 添加 query_appointments + query_medication Tool" ``` --- ### Task 1.3: AI 分析类 Tool — analyze_lab_report + analyze_health_trends **Files:** - Create: `crates/erp-ai/src/agent/tools/analyze_lab_report.rs` - Create: `crates/erp-ai/src/agent/tools/analyze_health_trends.rs` - [ ] **Step 1: 实现 analyze_lab_report Tool** 调用 `AiState.analysis`(AnalysisService)的非流式分析方法。参数:`report_id`。返回化验报告的 AI 解读摘要。 注意:现有 `analysis_service` 使用 SSE 流式输出,Tool 内需要走同步路径。检查 `analysis.rs` 是否有 `analyze_sync()` 方法,如果没有需要添加。 - [ ] **Step 2: 实现 analyze_health_trends Tool** 调用 `ctx.health_provider.get_trend_analysis_data()` 获取预计算的统计数据,再用 `AnalysisService` 做趋势解读。 - [ ] **Step 3: 注册到 ToolRegistry + cargo check** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs git commit -m "feat(ai): 添加 analyze_lab_report + analyze_health_trends 分析类 Tool" ``` --- ### Task 1.4: AI 分析类 Tool — get_health_insights **Files:** - Create: `crates/erp-ai/src/agent/tools/get_health_insights.rs` - [ ] **Step 1: 实现 get_health_insights Tool** 调用 `AiState.insight_service`(InsightService)+ `AiState.risk_service`(RiskService),获取患者的风险洞察和 AI 建议。参数:`patient_id`(默认使用当前患者)。 - [ ] **Step 2: 注册到 ToolRegistry + cargo check** - [ ] **Step 3: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs git commit -m "feat(ai): 添加 get_health_insights Tool — Copilot 风险洞察接入" ``` --- ### Task 1.5: 知识类 Tool — search_medical_knowledge + recommend_services + check_alert_rules **Files:** - Create: `crates/erp-ai/src/agent/tools/search_medical_knowledge.rs` - Create: `crates/erp-ai/src/agent/tools/recommend_services.rs` - Create: `crates/erp-ai/src/agent/tools/check_alert_rules.rs` - [ ] **Step 1: 实现 search_medical_knowledge Tool** 调用 `AiState` 中的 `knowledge_structured_source`,按关键词搜索医疗知识库(KDIGO 规则、科室指南、科普文章)。参数:`query`(搜索关键词)、`category`(可选分类过滤)。 - [ ] **Step 2: 实现 recommend_services Tool** 基于规则 + 知识库推荐科室或服务。Phase 1 用简化规则映射(如"头晕"→"神经内科/心内科","血压高"→"心内科")。参数:`symptoms`(症状列表)。 - [ ] **Step 3: 实现 check_alert_rules Tool** 调用 `AiState` 中的 `local_rules_engine`,评估当前患者数据是否触发告警阈值。参数:`patient_id`。 - [ ] **Step 4: 注册全部 3 个 Tool 到 ToolRegistry + cargo check** - [ ] **Step 5: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-ai/src/module.rs git commit -m "feat(ai): 添加知识类 Tool — medical_knowledge + recommend_services + check_alert_rules" ``` --- ### Task 1.6: 多策略 System Prompt 设计 + 调优 **Files:** - Modify: `crates/erp-ai/src/agent/prompt.rs` (新建或修改现有 prompt 模块) - [ ] **Step 1: 实现 build_system_prompt 函数** 从 Spec §4.2 的 System Prompt 模板生成完整 prompt。函数签名: ```rust pub fn build_agent_system_prompt( user_profile: Option<&UserProfileSummary>, patient_profile: Option<&PatientSummaryDto>, ) -> String ``` 动态注入: - 用户画像偏好(如有长期记忆) - 患者基本信息(如已关联患者) - 可用 Tool 列表描述 - [ ] **Step 2: 更新 chat_handler 使用新 prompt** 替换 Phase 0 中的硬编码 prompt 为 `build_agent_system_prompt()` 调用。 - [ ] **Step 3: cargo check + 手动对话调优** 启动后端,用不同场景测试 Agent 策略选择是否正确: - "我最近血压有点高"(应触发查询 → 分析 → 预警 → 推荐流程) - "糖尿病有什么并发症"(应触发知识搜索 → 科普) - "我很担心我的检查结果"(应先安抚 → 再查数据) - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/prompt.rs crates/erp-ai/src/handler/chat_handler.rs git commit -m "feat(ai): 多策略 System Prompt — 安抚/科普/推荐/预警/引导到院" ``` --- ### Task 1.7: 配额检查 + Token 计量 **Files:** - Modify: `crates/erp-ai/src/agent/orchestrator.rs` (添加配额检查) - Modify: `crates/erp-ai/src/handler/chat_handler.rs` (记录总 token 消耗) - [ ] **Step 1: 在 Orchestrator 每轮 Tool Call 前添加配额检查** 在 `run()` 循环的 `generate_with_tools()` 调用前,检查 `QuotaService`。配额不足时直接返回提示而非调用 LLM。 注意:QuotaService 在 `AiState.quota` 中,需要在 ToolContext 或 Orchestrator 构造时传入。 - [ ] **Step 2: 在 chat_handler 中记录每轮 token 消耗到 usage_service** - [ ] **Step 3: cargo check + cargo test -p erp-ai** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/orchestrator.rs crates/erp-ai/src/handler/chat_handler.rs git commit -m "feat(ai): Agent 配额检查 + Token 计量" ``` --- ### Task 1.8: Phase 1 测试覆盖 **Files:** - Create: `crates/erp-ai/src/agent/tools/query_lab_reports_test.rs` (及其他 Tool 的单元测试) - Modify: `crates/erp-server/tests/integration/ai_agent_test.rs` (扩展集成测试) - [ ] **Step 1: 每个 Tool 编写单元测试** 测试模式:mock `HealthDataProvider`(用 `MockHealthDataProvider`),验证 Tool 的参数解析、输出格式、错误处理。 - [ ] **Step 2: 扩展集成测试** 新增场景: - 发送"我最近化验报告有什么问题" → Agent 调用 query_lab_reports + analyze_lab_report - 发送"帮我推荐个科室" → Agent 调用 recommend_services - 配额耗尽 → Agent 返回降级提示 - [ ] **Step 3: cargo test --workspace** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/ crates/erp-server/tests/integration/ai_agent_test.rs git commit -m "test(ai): Phase 1 测试覆盖 — Tool 单元测试 + 5 策略集成测试" ``` --- ### Phase 1 完成标准 - [ ] `cargo check` + `cargo test --workspace` 全部通过 - [ ] 模拟 5 种典型场景(安抚/科普/推荐/预警/引导到院),Agent 自主选择正确策略和 Tool - [ ] 配额检查和 Token 计量正常工作 - [ ] 代码已提交并推送 --- ## Chunk 3: Phase 2 — 前端升级 + 流式输出(7-9 天) > 目标:小程序 + Web 都有完整 AI 客服体验 ### Task 2.1: 后端 — 会话 CRUD API **Files:** - Create: `crates/erp-ai/src/handler/chat_session_handler.rs` (Session CRUD) - Create: `crates/erp-ai/src/entity/ai_chat_session.rs` (SeaORM Entity) - Create: `crates/erp-ai/src/entity/ai_chat_message.rs` (SeaORM Entity) - Create: `crates/erp-ai/src/service/chat_session_service.rs` (Service 层) - Modify: `crates/erp-ai/src/module.rs` (注册新路由) - Modify: `crates/erp-ai/src/entity/mod.rs` (导出新 Entity) - Modify: `crates/erp-ai/src/handler/mod.rs` (导出新 handler) - [ ] **Step 1: 创建 SeaORM Entity** 为 `ai_chat_sessions` 和 `ai_chat_messages` 表创建 Entity 文件。参考现有 Entity 格式(如 `entity/ai_analysis.rs`),包含所有标准字段。 - [ ] **Step 2: 实现 ChatSessionService** CRUD 方法: - `create_session(tenant_id, user_id, patient_id?)` → 创建会话 - `list_sessions(tenant_id, user_id)` → 列出用户会话 - `get_session(session_id, tenant_id)` → 获取会话详情 - `close_session(session_id, tenant_id)` → 软关闭 - `save_message(session_id, role, content, tool_calls?, tool_call_id?)` → 保存消息 - `list_messages(session_id, tenant_id, limit, offset)` → 分页获取消息 - [ ] **Step 3: 实现 Session Handler** 4 个端点:`POST /sessions`、`GET /sessions`、`DELETE /sessions/{id}`、`GET /sessions/{id}/messages` 每个端点添加对应权限守卫(`ai.chat.session.manage`/`.list`/`.history`)。 - [ ] **Step 4: 改造 chat_handler 使用会话模式** `POST /sessions/{id}/messages` 替代原有 `POST /ai/chat`。从 DB 加载会话历史,不再依赖前端传 history。 - [ ] **Step 5: 在 module.rs 注册新路由** ```rust // 新增会话管理路由 let session_routes = Router::new() .route("/", post(session_handler::create_session)) .route("/", get(session_handler::list_sessions)) .route("/{session_id}", delete(session_handler::close_session)) .route("/{session_id}/messages", get(session_handler::list_messages)) .route("/{session_id}/messages", post(chat_handler::send_message)); ``` 保留原 `POST /ai/chat` 兼容一段时间。 - [ ] **Step 6: cargo check + cargo test -p erp-ai** - [ ] **Step 7: Commit** ```bash git add crates/erp-ai/src/handler/chat_session_handler.rs crates/erp-ai/src/entity/ai_chat_session.rs crates/erp-ai/src/entity/ai_chat_message.rs crates/erp-ai/src/service/chat_session_service.rs crates/erp-ai/src/module.rs git commit -m "feat(ai): 会话 CRUD API — sessions/messages 端点 + DB 持久化" ``` --- ### Task 2.2: 后端 — Agent 回复 SSE 流式输出 **Files:** - Modify: `crates/erp-ai/src/handler/chat_handler.rs` (send_message 支持 SSE) - [ ] **Step 1: send_message 端点支持 SSE** 当客户端请求 `Accept: text/event-stream` 时: 1. Agent Orchestrator 的 Tool Call 过程在后台执行(不在 SSE 中传输) 2. 最终回复生成后,通过 SSE 流式推送给客户端 3. 复用现有 SSE 架构(`Sse` 模式,参考 analysis_handler) 当客户端请求 `Accept: application/json` 时,走原有同步模式。 - [ ] **Step 2: cargo check + Postman 测试 SSE** 用 Postman 发送带 `Accept: text/event-stream` 头的请求,验证流式输出。 - [ ] **Step 3: Commit** ```bash git add crates/erp-ai/src/handler/chat_handler.rs git commit -m "feat(ai): Agent 回复 SSE 流式输出 — Tool 过程后台执行" ``` --- ### Task 2.3: 小程序 — SSE 兼容层 + 会话列表页 **Files:** - Rewrite: `apps/miniprogram/src/services/ai-chat.ts` (从本地 Storage 迁移到 API) - Create: `apps/miniprogram/src/pages/ai-chat/sessions/index.tsx` (会话列表页) - Create: `apps/miniprogram/src/pages/ai-chat/sessions/index.scss` - Modify: `apps/miniprogram/src/pages/messages/index.tsx` (改造为使用会话 API) - Modify: `apps/miniprogram/src/pages/messages/index.scss` - Modify: `apps/miniprogram/src/app.config.ts` (注册新页面) - [ ] **Step 1: 重写 ai-chat.ts 服务层** ```typescript // 新 API export async function createSession(patientId?: string): Promise export async function listSessions(): Promise export async function sendMessage(sessionId: string, message: string): Promise export async function getMessageHistory(sessionId: string): Promise // SSE 支持 export function sendMessageStream(sessionId: string, message: string): Promise ``` Taro 不支持原生 SSE,使用轮询或 `requestTask` 长连接实现。 - [ ] **Step 2: 改造 messages 页面** 将现有 `messages/index.tsx` 改造为使用新 API: - 页面加载时创建或恢复会话 - 发送消息调用 `sendMessage(sessionId, text)` - 消息列表从 `getMessageHistory()` 获取而非本地 Storage - 支持富消息渲染(基于 `display_hint` 字段) - [ ] **Step 3: 创建会话列表页** 新页面 `ai-chat/sessions/`:展示历史会话列表,点击进入对话。添加到 `app.config.ts` 的 subPackages 中。 - [ ] **Step 4: 旧数据迁移** 首次打开新版本时,检测本地 `ai_chat_history`,如有数据则提示"历史记录已迁移到云端"并清除本地缓存。 - [ ] **Step 5: 编译 + 真机预览** Run: `cd apps/miniprogram && pnpm build` 在微信开发者工具中验证页面渲染和消息收发。 - [ ] **Step 6: Commit** ```bash git add apps/miniprogram/src/services/ai-chat.ts apps/miniprogram/src/pages/messages/ apps/miniprogram/src/pages/ai-chat/ apps/miniprogram/src/app.config.ts git commit -m "feat(mp): AI 客服升级 — 会话 API + SSE 兼容 + 会话列表页" ``` --- ### Task 2.4: Web — AI 客服页面从零构建 **Files:** - Create: `apps/web/src/services/ai-chat.ts` (API 模块) - Create: `apps/web/src/pages/ai/ChatPage.tsx` (聊天主页面) - Create: `apps/web/src/pages/ai/ChatPage.scss` - Create: `apps/web/src/components/ai/MessageBubble.tsx` (消息气泡组件) - Create: `apps/web/src/components/ai/RichMessageCard.tsx` (富消息卡片) - Modify: `apps/web/src/router/routeConfig.ts` (注册新路由) - [ ] **Step 1: 创建 Web 端 ai-chat.ts API 模块** 与小程序相同的 API 接口,但使用 `fetch` + `EventSource` 实现 SSE。 - [ ] **Step 2: 创建 ChatPage** 参考小程序 `messages/index.tsx` 的功能,使用 Ant Design 组件构建: - 左侧会话列表 - 右侧聊天区域 - 底部输入框 - 消息气泡区分 user/assistant - [ ] **Step 3: 实现富消息渲染** 基于 `display_hint` 字段,渲染不同类型的富消息: - `VitalCard` → ECharts 小图表 - `LabReportCard` → 指标异常高亮 - `ActionConfirm` → 确认/取消按钮 - `RiskAlert` → 彩色风险等级卡片 - [ ] **Step 4: 注册路由** 在 `routeConfig.ts` 中添加 `/ai/chat` 路由,权限码 `ai.chat.session.list`。 - [ ] **Step 5: 编译 + 浏览器验证** Run: `cd apps/web && pnpm build` 在浏览器中打开 `/ai/chat`,验证页面功能和 SSE 流式输出。 - [ ] **Step 6: Commit** ```bash git add apps/web/src/services/ai-chat.ts apps/web/src/pages/ai/ apps/web/src/components/ai/ apps/web/src/router/routeConfig.ts git commit -m "feat(web): AI 客服页面 — 会话列表 + 聊天界面 + 富消息渲染 + SSE" ``` --- ### Task 2.5: 端到端测试 **Files:** - Modify: `crates/erp-server/tests/integration/ai_agent_test.rs` (扩展) - Create: `apps/miniprogram/src/__tests__/ai-chat.test.ts` (小程序服务层测试) - [ ] **Step 1: 后端集成测试扩展** 新增测试: - Session CRUD 全流程 - 发送消息 → DB 持久化验证 - SSE 流式输出格式验证 - 权限守卫验证(无权限返回 403) - [ ] **Step 2: 小程序服务层测试** 测试 ai-chat.ts 的 API 调用逻辑(mock fetch)。 - [ ] **Step 3: cargo test --workspace + pnpm build** - [ ] **Step 4: Commit** ```bash git add crates/erp-server/tests/integration/ai_agent_test.rs apps/miniprogram/src/__tests__/ git commit -m "test(ai): Phase 2 端到端测试 — Session CRUD + SSE + 权限验证" ``` --- ### Phase 2 完成标准 - [ ] `cargo check` + `cargo test --workspace` 全部通过 - [ ] `pnpm build`(小程序 + Web)通过 - [ ] 小程序打开 AI 客服,能自然对话,能看到数据卡片 - [ ] Web 端打开 AI 客服,聊天界面正常,SSE 流式输出正常 - [ ] 会话历史持久化到 DB,不再依赖本地 Storage - [ ] 代码已提交并推送 --- ## Chunk 4: Phase 3 — 行动类 Tool + 人机协作(3-5 天) > 目标:AI 客服能帮用户预约、转接人工 ### Task 3.1: create_appointment Tool(带二次确认) **Files:** - Create: `crates/erp-ai/src/agent/tools/create_appointment.rs` - Modify: `crates/erp-ai/src/agent/tools/mod.rs` - [ ] **Step 1: 实现 create_appointment Tool** 参数:`department`(科室)、`preferred_date`(偏好日期)、`preferred_time`(偏好时段)。 逻辑: 1. 查询可用排班(调用 `HealthDataProvider`,需新增 `get_available_slots` 方法) 2. 推荐最近可用时段 3. **不直接创建**,返回 `DisplayHint::ActionConfirm`,前端展示确认卡片 4. 用户确认后,前端发送确认请求到独立端点 `POST /api/v1/ai/chat/sessions/{id}/confirm-action` 注意:二次确认机制确保 LLM 不能在用户不知情的情况下创建预约。 - [ ] **Step 2: 新增确认端点** `POST /sessions/{id}/confirm-action`:接收 `action_type` + `confirm_payload`,调用 `appointment_service.create_appointment()`。 - [ ] **Step 3: cargo check + cargo test** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/create_appointment.rs crates/erp-ai/src/handler/ git commit -m "feat(ai): create_appointment Tool — 二次确认机制 + 预约创建" ``` --- ### Task 3.2: transfer_to_human Tool + WebSocket 通知 **Files:** - Create: `crates/erp-ai/src/agent/tools/transfer_to_human.rs` - Create: `crates/erp-ai/src/handler/chat_transfer_handler.rs` (WebSocket 通知) - [ ] **Step 1: 实现 transfer_to_human Tool** 参数:`reason`(转接原因)、`urgency`(紧急程度 low/medium/high)。 逻辑: 1. 记录转接请求到会话 metadata 2. 通过事件总线发布 `ai.chat.transferred` 事件 3. 返回 `DisplayHint::RiskAlert` 提示用户"正在转接" 4. 值班医护端收到通知(通过 WebSocket 或 polling) - [ ] **Step 2: WebSocket 通知值班医护** 在 `chat_transfer_handler.rs` 中实现 WebSocket 端点: - `WS /api/v1/ai/chat/notifications` — 值班医护连接此端点接收转接通知 - 收到转接请求时推送 JSON 消息(包含 session_id、患者信息、转接原因) 注意:WebSocket 基础设施需检查 Axum 是否已有 WebSocket 支持(项目依赖中已有 tokio-tungstenite)。 - [ ] **Step 3: cargo check + cargo test** - [ ] **Step 4: Commit** ```bash git add crates/erp-ai/src/agent/tools/transfer_to_human.rs crates/erp-ai/src/handler/chat_transfer_handler.rs git commit -m "feat(ai): transfer_to_human Tool + WebSocket 通知值班医护" ``` --- ### Task 3.3: 前端 — 操作确认 UI + 转接状态 **Files:** - Modify: `apps/miniprogram/src/pages/messages/index.tsx` (确认卡片 + 转接状态) - Modify: `apps/web/src/pages/ai/ChatPage.tsx` (同上) - Modify: `apps/web/src/components/ai/RichMessageCard.tsx` (ActionConfirm + RiskAlert 渲染) - [ ] **Step 1: 小程序实现操作确认卡片** 当消息包含 `display_hint.type === 'action_confirm'` 时,渲染确认按钮。用户点击后调用 `POST /sessions/{id}/confirm-action`。 - [ ] **Step 2: 小程序实现转接状态提示** 当 `display_hint.type === 'risk_alert'` 且包含转接信息时,显示"正在转接值班医生"动画。 - [ ] **Step 3: Web 端同步实现** 同样的确认卡片和转接状态提示。 - [ ] **Step 4: 编译 + 验证** Run: `pnpm build` (小程序 + Web) - [ ] **Step 5: Commit** ```bash git add apps/miniprogram/src/pages/messages/ apps/web/src/pages/ai/ apps/web/src/components/ai/ git commit -m "feat: 操作确认 UI + 转接状态提示 — 小程序 + Web" ``` --- ### Task 3.4: 安全边界加固 **Files:** - Modify: `crates/erp-ai/src/agent/orchestrator.rs` (行动类 Tool 标记) - Modify: `crates/erp-ai/src/agent/tool.rs` (ToolCategory 枚举) - Modify: `crates/erp-ai/src/handler/chat_handler.rs` (审计日志增强) - [ ] **Step 1: 添加 ToolCategory 枚举** ```rust pub enum ToolCategory { ReadOnly, // 数据查询 Analysis, // AI 分析 Knowledge, // 知识检索 Action, // 写入操作(需更高权限) } ``` 在 `AgentTool` trait 中添加 `fn category(&self) -> ToolCategory`。 - [ ] **Step 2: Orchestrator 对 Action 类 Tool 额外检查** Action Tool 只能在用户明确意图时调用。Orchestrator 记录 Action Tool 的调用,chat_handler 写入审计日志。 - [ ] **Step 3: 审计日志增强** `ai_tool_call_logs` 表记录完整的 Tool 调用参数(已脱敏)和结果摘要,用于事后审计。 - [ ] **Step 4: cargo check + cargo test --workspace** - [ ] **Step 5: Commit** ```bash git add crates/erp-ai/src/agent/ crates/erp-ai/src/handler/ git commit -m "feat(ai): 安全边界加固 — ToolCategory + Action 权限标记 + 审计日志" ``` --- ### Task 3.5: 端到端验证 **Files:** - Modify: `crates/erp-server/tests/integration/ai_agent_test.rs` - [ ] **Step 1: 端到端测试** 完整流程测试: 1. 用户说"帮我预约个号" → Agent 调用 query_appointments 查空档 → recommend_services 推荐科室 → create_appointment 返回确认卡片 → 用户确认 → 预约创建成功 2. 用户说"我要找医生" → Agent 调用 transfer_to_human → WebSocket 通知发送 → 用户看到转接提示 3. 用户说"帮我取消预约" → Agent 提示"暂不支持取消,请联系前台" - [ ] **Step 2: 浏览器手动验证** 启动后端 + Web 前端,在浏览器中走完整预约流程。 - [ ] **Step 3: 小程序真机验证** 在微信开发者工具中测试预约确认和转接流程。 - [ ] **Step 4: Commit** ```bash git add crates/erp-server/tests/integration/ai_agent_test.rs git commit -m "test(ai): Phase 3 端到端测试 — 预约创建 + 转接人工 + 完整对话流" ``` --- ### Phase 3 完成标准 - [ ] `cargo check` + `cargo test --workspace` 全部通过 - [ ] `pnpm build`(小程序 + Web)通过 - [ ] 用户对 AI 说"帮我预约个号",全流程跑通 - [ ] 用户说"找医生",转接通知正常发送 - [ ] 行动类 Tool 有权限标记和审计日志 - [ ] 代码已提交并推送 --- ## 全局完成标准 - [ ] `cargo check` + `cargo test --workspace` + `pnpm build` 全部通过 - [ ] AI 客服能自然处理 5 种策略场景(安抚/科普/推荐/预警/引导) - [ ] 小程序 + Web 两端 AI 客服功能完整 - [ ] 所有 Tool 有单元测试,核心流程有集成测试 - [ ] wiki 关键数字已更新(新增迁移数/实体数/路由数) - [ ] 所有代码已提交并推送