Files
hms/docs/superpowers/plans/2026-05-18-ai-agent-breakthrough-plan.md

1653 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 CallTool 通过已有的 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<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
#[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<String>,
pub tool_calls: Option<Vec<ToolCall>>,
/// 复用已有的 TokenUsagedto/mod.rs 中的定义input/output u32
pub usage: Option<crate::dto::TokenUsage>,
}
```
- [ ] **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<crate::dto::ChatMessage>,
tools: Vec<crate::dto::ToolDefinition>,
system_prompt: &str,
model: &str,
temperature: f32,
max_tokens: u32,
) -> crate::error::AiResult<crate::dto::AgentGenerateResponse> {
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<f32>,
system: String,
messages: Vec<ClaudeMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<ClaudeTool>>,
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<crate::dto::ChatMessage>,
tools: Vec<crate::dto::ToolDefinition>,
system_prompt: &str,
model: &str,
temperature: f32,
max_tokens: u32,
) -> crate::error::AiResult<crate::dto::AgentGenerateResponse> {
let claude_messages: Vec<ClaudeMessage> = messages.iter().map(|m| {
// 根据角色和内容构建 Anthropic 格式消息
// assistant 带 tool_calls 时构造 tool_use content blocks
// tool 角色时构造 tool_result content block
// ... 完整转换逻辑
}).collect();
let claude_tools: Vec<ClaudeTool> = 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<f32>,
messages: Vec<OpenAiMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<ChatTool>>,
stream: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct OpenAiMessage {
role: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_calls: Option<Vec<OpenAiToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
}
#[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<crate::dto::ChatMessage>,
_tools: Vec<crate::dto::ToolDefinition>,
_system_prompt: &str,
_model: &str,
_temperature: f32,
_max_tokens: u32,
) -> crate::error::AiResult<crate::dto::AgentGenerateResponse> {
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<Vec<AppointmentSummaryDto>>;
/// 获取患者当前用药列表
async fn get_medication_list(
&self,
tenant_id: Uuid,
patient_id: Uuid,
) -> AppResult<Vec<MedicationSummaryDto>>;
```
新增 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<chrono::Utc>,
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-onlytenant_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<Uuid>,
pub db: DatabaseConnection,
pub health_provider: std::sync::Arc<dyn HealthDataProvider>,
}
pub struct ToolResult {
pub output: String,
pub display_hint: Option<DisplayHint>,
}
#[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<String, Arc<dyn AgentTool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self { tools: HashMap::new() }
}
pub fn register(&mut self, tool: Arc<dyn AgentTool>) {
self.tools.insert(tool.name().to_string(), tool);
}
pub fn get(&self, name: &str) -> Option<&Arc<dyn AgentTool>> {
self.tools.get(name)
}
pub fn all_tools(&self) -> Vec<&Arc<dyn AgentTool>> {
self.tools.values().collect()
}
/// 生成传给 LLM 的 ToolDefinition 列表
pub fn tool_definitions(&self) -> Vec<crate::dto::ToolDefinition> {
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<dyn AiProvider>,
tool_registry: Arc<ToolRegistry>,
max_iterations: usize, // 默认 5
}
impl AgentOrchestrator {
pub fn new(provider: Arc<dyn AiProvider>, tool_registry: Arc<ToolRegistry>) -> Self {
Self { provider, tool_registry, max_iterations: 5 }
}
/// 执行 Agent ReAct 循环
pub async fn run(
&self,
system_prompt: &str,
messages: &mut Vec<ChatMessage>,
ctx: &ToolContext,
) -> AiResult<AgentRunResult> {
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_callsAgent 给出最终回复
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<String> = 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<crate::agent::ToolRegistry>,
```
- [ ] **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 扩展 + 策略 Prompt5-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<impl Stream>` 模式,参考 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<ChatSession>
export async function listSessions(): Promise<ChatSession[]>
export async function sendMessage(sessionId: string, message: string): Promise<ChatMessage>
export async function getMessageHistory(sessionId: string): Promise<ChatMessage[]>
// SSE 支持
export function sendMessageStream(sessionId: string, message: string): Promise<EventSource>
```
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 关键数字已更新(新增迁移数/实体数/路由数)
- [ ] 所有代码已提交并推送