Commit 626473e4 authored by yuguo's avatar yuguo

fix

parent ef9bb5d9
package internalagent
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"internet-hospital/internal/model"
"internet-hospital/pkg/agent"
"internet-hospital/pkg/database"
)
// Handler Agent HTTP处理器
......@@ -21,6 +27,7 @@ func (h *Handler) RegisterRoutes(r gin.IRouter) {
g.GET("/sessions", h.ListSessions)
g.DELETE("/session/:session_id", h.DeleteSession)
g.GET("/list", h.ListAgents)
g.GET("/tools", h.ListTools)
}
func (h *Handler) Chat(c *gin.Context) {
......@@ -54,10 +61,96 @@ func (h *Handler) ListAgents(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": h.svc.ListAgents()})
}
// ListSessions 获取用户的 Agent 会话列表
func (h *Handler) ListSessions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": []interface{}{}})
userID, _ := c.Get("user_id")
agentID := c.Query("agent_id")
var sessions []model.AgentSession
query := database.GetDB().Where("user_id = ?", userID).Order("updated_at DESC")
if agentID != "" {
query = query.Where("agent_id = ?", agentID)
}
query.Find(&sessions)
type SessionSummary struct {
model.AgentSession
LastMessage string `json:"last_message"`
}
result := make([]SessionSummary, 0, len(sessions))
for _, s := range sessions {
var history []map[string]string
json.Unmarshal([]byte(s.History), &history)
lastMsg := ""
if len(history) > 0 {
lastMsg = history[len(history)-1]["content"]
if len(lastMsg) > 60 {
lastMsg = lastMsg[:60] + "..."
}
}
result = append(result, SessionSummary{s, lastMsg})
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// DeleteSession 删除会话
func (h *Handler) DeleteSession(c *gin.Context) {
userID, _ := c.Get("user_id")
sessionID := c.Param("session_id")
database.GetDB().Where("session_id = ? AND user_id = ?", sessionID, userID).
Delete(&model.AgentSession{})
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
// ListTools 获取所有已注册工具列表
func (h *Handler) ListTools(c *gin.Context) {
registry := agent.GetRegistry()
allTools := registry.All()
type ToolInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Parameters map[string]interface{} `json:"parameters"`
IsEnabled bool `json:"is_enabled"`
CreatedAt string `json:"created_at"`
}
categoryMap := map[string]string{
"query_symptom_knowledge": "knowledge",
"recommend_department": "recommendation",
"query_medical_record": "medical",
"search_medical_knowledge": "knowledge",
"query_drug": "pharmacy",
"check_drug_interaction": "safety",
"check_contraindication": "safety",
"calculate_dosage": "pharmacy",
"generate_follow_up_plan": "follow_up",
"send_notification": "notification",
}
result := make([]ToolInfo, 0, len(allTools))
i := 1
for name, tool := range allTools {
params := make(map[string]interface{})
for _, p := range tool.Parameters() {
params[p.Name] = p.Type
}
category := categoryMap[name]
if category == "" {
category = "other"
}
result = append(result, ToolInfo{
ID: fmt.Sprintf("%d", i),
Name: name,
Description: tool.Description(),
Category: category,
Parameters: params,
IsEnabled: true,
CreatedAt: "2026-01-01",
})
i++
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
package admin
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"internet-hospital/internal/model"
"internet-hospital/pkg/database"
)
// GetAgentExecutionLogs 获取 Agent 执行日志(分页)
func (h *Handler) GetAgentExecutionLogs(c *gin.Context) {
agentID := c.Query("agent_id")
start := c.Query("start")
end := c.Query("end")
page := 1
pageSize := 20
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
page = v
}
}
if ps := c.Query("page_size"); ps != "" {
if v, err := strconv.Atoi(ps); err == nil && v > 0 && v <= 100 {
pageSize = v
}
}
query := database.GetDB().Model(&model.AgentExecutionLog{})
if agentID != "" {
query = query.Where("agent_id = ?", agentID)
}
if start != "" {
query = query.Where("created_at >= ?", start)
}
if end != "" {
query = query.Where("created_at <= ?", end)
}
var total int64
query.Count(&total)
var logs []model.AgentExecutionLog
query.Order("created_at DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&logs)
c.JSON(http.StatusOK, gin.H{"data": gin.H{
"list": logs,
"total": total,
"page": page,
"page_size": pageSize,
}})
}
// GetAgentStats 获取 Agent 执行统计
func (h *Handler) GetAgentStats(c *gin.Context) {
start := c.Query("start")
end := c.Query("end")
type AgentStat struct {
AgentID string `json:"agent_id"`
Count int64 `json:"count"`
AvgIterations float64 `json:"avg_iterations"`
AvgTokens float64 `json:"avg_tokens"`
SuccessRate float64 `json:"success_rate"`
}
db := database.GetDB()
query := db.Model(&model.AgentExecutionLog{})
if start != "" {
query = query.Where("created_at >= ?", start)
}
if end != "" {
query = query.Where("created_at <= ?", end)
}
var stats []AgentStat
query.Select("agent_id, COUNT(*) as count, AVG(iterations) as avg_iterations, AVG(total_tokens) as avg_tokens, SUM(CASE WHEN success THEN 1 ELSE 0 END)::float / COUNT(*) as success_rate").
Group("agent_id").
Scan(&stats)
c.JSON(http.StatusOK, gin.H{"data": stats})
}
......@@ -102,7 +102,13 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
// 工作流管理
adm.GET("/workflows", h.ListWorkflows)
adm.POST("/workflows", h.CreateWorkflow)
adm.PUT("/workflows/:id", h.UpdateWorkflow)
adm.PUT("/workflows/:id/publish", h.PublishWorkflow)
adm.GET("/workflow/executions", h.ListWorkflowExecutions)
// Agent 执行监控
adm.GET("/agent/logs", h.GetAgentExecutionLogs)
adm.GET("/agent/stats", h.GetAgentStats)
}
}
......
......@@ -2,6 +2,7 @@ package admin
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
......@@ -42,6 +43,33 @@ func (h *Handler) CreateWorkflow(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": wf})
}
// UpdateWorkflow 更新工作流(名称/描述/定义)
func (h *Handler) UpdateWorkflow(c *gin.Context) {
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Definition string `json:"definition"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := map[string]interface{}{}
if req.Definition != "" {
updates["definition"] = req.Definition
}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Description != "" {
updates["description"] = req.Description
}
database.GetDB().Model(&model.WorkflowDefinition{}).
Where("id = ?", c.Param("id")).
Updates(updates)
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
// PublishWorkflow 发布工作流
func (h *Handler) PublishWorkflow(c *gin.Context) {
database.GetDB().Model(&model.WorkflowDefinition{}).
......@@ -49,3 +77,40 @@ func (h *Handler) PublishWorkflow(c *gin.Context) {
Update("status", "active")
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
// ListWorkflowExecutions 工作流执行记录列表
func (h *Handler) ListWorkflowExecutions(c *gin.Context) {
workflowID := c.Query("workflow_id")
page := 1
pageSize := 20
if p := c.Query("page"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
page = v
}
}
if ps := c.Query("page_size"); ps != "" {
if v, err := strconv.Atoi(ps); err == nil && v > 0 && v <= 100 {
pageSize = v
}
}
var total int64
query := database.GetDB().Model(&model.WorkflowExecution{})
if workflowID != "" {
query = query.Where("workflow_id = ?", workflowID)
}
query.Count(&total)
var executions []model.WorkflowExecution
query.Order("created_at DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&executions)
c.JSON(http.StatusOK, gin.H{"data": gin.H{
"list": executions,
"total": total,
"page": page,
"page_size": pageSize,
}})
}
......@@ -10,8 +10,8 @@ import (
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
internalagent "internet-hospital/internal/agent"
"internet-hospital/internal/model"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/database"
)
......@@ -196,107 +196,53 @@ func (s *Service) CancelConsult(ctx context.Context, consultID string) error {
return s.db.Model(&model.Consultation{}).Where("id = ?", consultID).Update("status", "cancelled").Error
}
// AIAssist AI辅助分析(鉴别诊断/用药建议)
// AIAssist AI辅助分析(鉴别诊断/用药建议)—— 通过 Agent 调用
func (s *Service) AIAssist(ctx context.Context, consultID string, scene string) (map[string]interface{}, error) {
// 获取问诊信息
var consult model.Consultation
if err := s.db.Where("id = ?", consultID).First(&consult).Error; err != nil {
return nil, fmt.Errorf("问诊不存在")
}
// 获取对话消息
var messages []model.ConsultMessage
s.db.Where("consult_id = ?", consultID).Order("created_at ASC").Find(&messages)
// 获取预问诊信息
// 获取预问诊信息以提供上下文
var preConsult model.PreConsultation
s.db.Where("consultation_id = ?", consultID).First(&preConsult)
// 构建对话上下文
var chatContext string
if preConsult.AIAnalysis != "" {
chatContext += "【预问诊AI分析报告】\n" + preConsult.AIAnalysis + "\n\n"
}
if consult.ChiefComplaint != "" {
chatContext += "【主诉】" + consult.ChiefComplaint + "\n\n"
}
chatContext += "【问诊对话记录】\n"
for _, msg := range messages {
role := "患者"
if msg.SenderType == "doctor" {
role = "医生"
} else if msg.SenderType == "system" {
role = "系统"
agentCtx := map[string]interface{}{
"patient_id": consult.PatientID,
"consult_id": consultID,
"chief_complaint": consult.ChiefComplaint,
}
chatContext += role + ":" + msg.Content + "\n"
if preConsult.AIAnalysis != "" {
agentCtx["pre_consult_analysis"] = preConsult.AIAnalysis
}
// 获取场景对应的Prompt模板
prompt := ai.GetActivePromptByScene(scene)
if prompt == "" {
var agentID, message string
switch scene {
case "consult_diagnosis":
prompt = `你是一位资深的临床医学专家,请根据以下问诊信息进行鉴别诊断分析。
请以如下markdown格式输出:
## 初步诊断
(最可能的诊断,附简要理由)
## 鉴别诊断
1. **疾病名称1** - 可能性:高/中/低,依据:...
2. **疾病名称2** - 可能性:高/中/低,依据:...
3. **疾病名称3** - 可能性:高/中/低,依据:...
## 建议检查
1. 检查项目1 - 目的:...
2. 检查项目2 - 目的:...
## 注意事项
(需要特别关注的情况)`
agentID = "diagnosis_agent"
message = "请对患者当前情况进行诊断分析,提供鉴别诊断建议"
case "consult_medication":
prompt = `你是一位资深的临床药学专家,请根据以下问诊信息给出用药建议。
请以如下markdown格式输出:
## 推荐用药方案
1. **药品名称1** - 规格:...,用法用量:...,疗程:...
2. **药品名称2** - 规格:...,用法用量:...,疗程:...
## 用药注意事项
1. 注意事项1
2. 注意事项2
## 禁忌与过敏提示
(相关药物禁忌和需要询问的过敏史)
## 随访建议
(用药后的观察要点和复诊建议)`
agentID = "prescription_agent"
message = "请根据患者情况给出用药建议,包括推荐药物、用法用量和注意事项"
default:
return nil, fmt.Errorf("不支持的AI场景: %s", scene)
}
}
// 调用AI(使用统一接口,自动记录日志)
aiMessages := []ai.ChatMessage{
{Role: "system", Content: prompt},
{Role: "user", Content: chatContext},
agentSvc := internalagent.GetService()
output, err := agentSvc.Chat(ctx, agentID, consult.DoctorID, "", message, agentCtx)
if err != nil {
return nil, fmt.Errorf("AI分析失败: %w", err)
}
result := ai.Call(ctx, ai.CallParams{
Scene: scene,
UserID: consult.DoctorID,
Messages: aiMessages,
RequestSummary: chatContext,
})
if result.Error != nil {
return nil, fmt.Errorf("AI分析失败: %w", result.Error)
if output == nil {
return nil, fmt.Errorf("agent不存在: %s", agentID)
}
return map[string]interface{}{
"scene": scene,
"content": result.Content,
"response": output.Response,
"tool_calls": output.ToolCalls,
"iterations": output.Iterations,
"total_tokens": output.TotalTokens,
}, nil
}
......
import { get, post, put, del } from './request';
// ==================== 类型定义 ====================
export interface ToolCall {
tool_name: string;
call_id: string;
arguments: string;
result: { success: boolean; data?: unknown; error?: string };
success: boolean;
}
export interface AgentOutput {
response: string;
session_id?: string;
tool_calls?: ToolCall[];
iterations?: number;
total_tokens?: number;
finish_reason?: string;
}
export interface AgentSession {
id: number;
session_id: string;
agent_id: string;
user_id: string;
history: string;
context: string;
status: string;
last_message?: string;
created_at: string;
updated_at: string;
}
export interface AgentExecutionLog {
id: number;
session_id: string;
agent_id: string;
user_id: string;
input: string;
output: string;
tool_calls: string;
iterations: number;
total_tokens: number;
duration_ms: number;
finish_reason: string;
success: boolean;
error_message: string;
created_at: string;
}
export interface WorkflowExecution {
id: number;
execution_id: string;
workflow_id: string;
trigger_type: string;
trigger_by: string;
input: string;
output: string;
status: string;
started_at: string;
completed_at: string;
}
export interface WorkflowCreateParams {
workflow_id: string;
name: string;
description?: string;
category?: string;
definition?: string;
}
export interface KnowledgeCollectionParams {
name: string;
description?: string;
category?: string;
}
export interface KnowledgeDocumentParams {
collection_id: string;
title: string;
content: string;
}
// ==================== Agent API ====================
export const agentApi = {
chat: (agentId: string, params: {
session_id?: string;
message: string;
context?: Record<string, unknown>;
}) => post<AgentOutput>(`/agent/${agentId}/chat`, params),
listAgents: () =>
get<{ id: string; name: string; description: string }[]>('/agent/list'),
listTools: () =>
get<{ id: string; name: string; description: string; category: string; parameters: Record<string, unknown>; is_enabled: boolean; created_at: string }[]>('/agent/tools'),
getSessions: (agentId?: string) =>
get<AgentSession[]>('/agent/sessions', { params: agentId ? { agent_id: agentId } : {} }),
deleteSession: (sessionId: string) =>
del<null>(`/agent/session/${sessionId}`),
getExecutionLogs: (params: {
agent_id?: string;
start?: string;
end?: string;
page?: number;
page_size?: number;
}) => get<{ list: AgentExecutionLog[]; total: number }>('/admin/agent/logs', { params }),
getStats: (params?: { start?: string; end?: string }) =>
get<{ agent_id: string; count: number; avg_iterations: number; avg_tokens: number; success_rate: number }[]>(
'/admin/agent/stats', { params }
),
};
// ==================== Workflow API ====================
export const workflowApi = {
list: () => get<unknown[]>('/admin/workflows'),
create: (data: WorkflowCreateParams) => post<unknown>('/admin/workflows', data),
update: (id: number, data: Partial<WorkflowCreateParams>) =>
put<unknown>(`/admin/workflows/${id}`, data),
publish: (id: number) => post<null>(`/admin/workflows/${id}/publish`),
execute: (workflowId: string, input?: Record<string, unknown>) =>
post<{ execution_id: string }>(`/workflow/${workflowId}/execute`, input || {}),
getExecution: (executionId: string) =>
get<WorkflowExecution>(`/workflow/execution/${executionId}`),
listExecutions: (params?: { workflow_id?: string; page?: number; page_size?: number }) =>
get<{ list: WorkflowExecution[]; total: number }>('/admin/workflow/executions', { params }),
getTasks: () => get<unknown[]>('/workflow/tasks'),
completeTask: (taskId: string, result: Record<string, unknown>) =>
post<null>(`/workflow/task/${taskId}/complete`, result),
};
// ==================== Knowledge API ====================
export const knowledgeApi = {
listCollections: () => get<unknown[]>('/knowledge/collections'),
createCollection: (data: KnowledgeCollectionParams) =>
post<unknown>('/knowledge/collections', data),
listDocuments: (collectionId?: string) =>
get<unknown[]>('/knowledge/documents', { params: collectionId ? { collection_id: collectionId } : {} }),
createDocument: (data: KnowledgeDocumentParams) =>
post<unknown>('/knowledge/documents', data),
search: (query: string, topK = 5, collectionId?: string) =>
post<unknown[]>('/knowledge/search', { query, top_k: topK, collection_id: collectionId }),
};
......@@ -3,10 +3,10 @@
import { useState } from 'react';
import { Card, Table, Tag, Button, Modal, Input, message, Space, Collapse, Timeline, Typography } from 'antd';
import { RobotOutlined, PlayCircleOutlined, ToolOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
import { agentApi } from '@/api/agent';
import type { ToolCall } from '@/api/agent';
const { Text } = Typography;
const API = '';
const BUILTIN_AGENTS = [
{ id: 'pre_consult_agent', name: '预问诊智能助手', description: '通过多轮对话收集患者症状,生成预问诊报告', category: 'pre_consult', tools: ['query_symptom_knowledge', 'recommend_department'], max_iterations: 5 },
......@@ -29,14 +29,6 @@ const categoryLabel: Record<string, string> = {
follow_up: '随访管理',
};
interface ToolCall {
tool_name: string;
call_id: string;
arguments: string;
result: { success: boolean; data?: unknown; error?: string };
success: boolean;
}
interface AgentResponse {
response: string;
tool_calls?: ToolCall[];
......@@ -46,7 +38,6 @@ interface AgentResponse {
}
export default function AgentsPage() {
const { accessToken: token } = useUserStore();
const [testModal, setTestModal] = useState<{ open: boolean; agentId: string; agentName: string }>({ open: false, agentId: '', agentName: '' });
const [testMessages, setTestMessages] = useState<{ role: string; content: string; toolCalls?: ToolCall[]; meta?: { iterations?: number; tokens?: number } }[]>([]);
const [inputMsg, setInputMsg] = useState('');
......@@ -66,13 +57,8 @@ export default function AgentsPage() {
setTestMessages(prev => [...prev, { role: 'user', content: userMsg }]);
setLoading(true);
try {
const res = await fetch(`${API}/api/v1/agent/${testModal.agentId}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ session_id: sessionId, message: userMsg }),
});
const data = await res.json();
const agentData = data.data as AgentResponse;
const res = await agentApi.chat(testModal.agentId, { session_id: sessionId, message: userMsg });
const agentData = res.data as AgentResponse;
const reply = agentData?.response || '无响应';
setTestMessages(prev => [...prev, {
role: 'assistant',
......
......@@ -3,10 +3,9 @@
import { useEffect, useState } from 'react';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, Tabs, Typography } from 'antd';
import { BookOutlined, PlusOutlined, SearchOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
import { knowledgeApi } from '@/api/agent';
const { Text } = Typography;
const API = '';
const categoryLabel: Record<string, string> = {
clinical_guideline: '临床指南', drug: '药品说明', disease: '疾病百科', paper: '医学论文',
......@@ -16,7 +15,6 @@ const categoryColor: Record<string, string> = {
};
export default function KnowledgePage() {
const { accessToken: token } = useUserStore();
const [collections, setCollections] = useState<any[]>([]);
const [documents, setDocuments] = useState<any[]>([]);
const [colLoading, setColLoading] = useState(false);
......@@ -30,23 +28,19 @@ export default function KnowledgePage() {
const [docForm] = Form.useForm();
const [searchForm] = Form.useForm();
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
const fetchCollections = async () => {
setColLoading(true);
try {
const res = await fetch(`${API}/api/v1/knowledge/collections`, { headers });
const data = await res.json();
setCollections(data.data || []);
const res = await knowledgeApi.listCollections();
setCollections((res.data as any[]) || []);
} catch {} finally { setColLoading(false); }
};
const fetchDocuments = async () => {
setDocLoading(true);
try {
const res = await fetch(`${API}/api/v1/knowledge/documents`, { headers });
const data = await res.json();
setDocuments(data.data || []);
const res = await knowledgeApi.listDocuments();
setDocuments((res.data as any[]) || []);
} catch {} finally { setDocLoading(false); }
};
......@@ -54,7 +48,7 @@ export default function KnowledgePage() {
const createCollection = async (values: any) => {
try {
await fetch(`${API}/api/v1/knowledge/collections`, { method: 'POST', headers, body: JSON.stringify(values) });
await knowledgeApi.createCollection(values);
message.success('集合创建成功');
setColModal(false);
colForm.resetFields();
......@@ -64,7 +58,7 @@ export default function KnowledgePage() {
const createDocument = async (values: any) => {
try {
await fetch(`${API}/api/v1/knowledge/documents`, { method: 'POST', headers, body: JSON.stringify(values) });
await knowledgeApi.createDocument(values);
message.success('文档已添加');
setDocModal(false);
docForm.resetFields();
......@@ -75,9 +69,8 @@ export default function KnowledgePage() {
const doSearch = async (values: any) => {
setSearching(true);
try {
const res = await fetch(`${API}/api/v1/knowledge/search`, { method: 'POST', headers, body: JSON.stringify(values) });
const data = await res.json();
setSearchResults(data.data || []);
const res = await knowledgeApi.search(values.query, values.top_k || 5);
setSearchResults((res.data as any[]) || []);
} catch { message.error('检索失败'); } finally { setSearching(false); }
};
......
......@@ -6,10 +6,9 @@ import {
ToolOutlined, SearchOutlined, InfoCircleOutlined, ApiOutlined,
CheckCircleOutlined, CodeOutlined, ReloadOutlined,
} from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
import { agentApi } from '@/api/agent';
const { Text } = Typography;
const API = '';
interface AgentTool {
id: string;
......@@ -52,7 +51,6 @@ const statCards = [
];
export default function ToolsPage() {
const { accessToken: token } = useUserStore();
const [tools, setTools] = useState<AgentTool[]>(BUILTIN_TOOLS);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
......@@ -63,11 +61,8 @@ export default function ToolsPage() {
const fetchTools = async () => {
setLoading(true);
try {
const res = await fetch(`${API}/api/v1/agent/tools`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) {
const data = await res.json();
if (data.data?.length > 0) setTools(data.data);
}
const res = await agentApi.listTools();
if (res.data?.length > 0) setTools(res.data as AgentTool[]);
} catch { /* 使用内置工具列表 */ } finally {
setLoading(false);
}
......
......@@ -3,12 +3,10 @@
import { useEffect, useState, useCallback } from 'react';
import { Card, Table, Tag, Button, Modal, Form, Input, Select, message, Space, Badge } from 'antd';
import { DeploymentUnitOutlined, PlayCircleOutlined, PlusOutlined, EditOutlined } from '@ant-design/icons';
import { useUserStore } from '@/store/userStore';
import { workflowApi } from '@/api/agent';
import VisualWorkflowEditor from '@/components/workflow/VisualWorkflowEditor';
import type { Node, Edge } from '@xyflow/react';
const API = '';
interface Workflow {
id: number;
workflow_id: string;
......@@ -31,7 +29,6 @@ const categoryLabel: Record<string, string> = {
};
export default function WorkflowsPage() {
const { accessToken: token } = useUserStore();
const [workflows, setWorkflows] = useState<Workflow[]>([]);
const [createModal, setCreateModal] = useState(false);
const [editorModal, setEditorModal] = useState(false);
......@@ -43,9 +40,8 @@ export default function WorkflowsPage() {
const fetchWorkflows = async () => {
setTableLoading(true);
try {
const res = await fetch(`${API}/api/v1/admin/workflows`, { headers: { Authorization: `Bearer ${token}` } });
const data = await res.json();
setWorkflows(data.data || []);
const res = await workflowApi.list();
setWorkflows((res.data as Workflow[]) || []);
} catch {} finally {
setTableLoading(false);
}
......@@ -64,11 +60,7 @@ export default function WorkflowsPage() {
},
edges: [{ id: 'e1', source_node: 'start', target_node: 'end' }],
};
await fetch(`${API}/api/v1/admin/workflows`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ ...values, definition: JSON.stringify(definition) }),
});
await workflowApi.create({ ...values, definition: JSON.stringify(definition) });
message.success('创建成功');
setCreateModal(false);
form.resetFields();
......@@ -83,38 +75,24 @@ export default function WorkflowsPage() {
const handleSaveWorkflow = useCallback(async (nodes: Node[], edges: Edge[]) => {
if (!editingWorkflow) return;
try {
await fetch(`${API}/api/v1/admin/workflows/${editingWorkflow.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ definition: JSON.stringify({ nodes, edges }) }),
});
await workflowApi.update(editingWorkflow.id, { definition: JSON.stringify({ nodes, edges }) });
message.success('工作流已保存');
fetchWorkflows();
} catch { message.error('保存失败'); }
}, [editingWorkflow, token]);
}, [editingWorkflow]);
const handleExecuteFromEditor = useCallback(async (nodes: Node[], edges: Edge[]) => {
if (!editingWorkflow) return;
try {
const res = await fetch(`${API}/api/v1/workflow/${editingWorkflow.workflow_id}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ workflow_data: { nodes, edges } }),
});
const result = await res.json();
const result = await workflowApi.execute(editingWorkflow.workflow_id, { workflow_data: { nodes, edges } });
message.success(`执行已启动: ${result.data?.execution_id}`);
} catch { message.error('执行失败'); }
}, [editingWorkflow, token]);
}, [editingWorkflow]);
const handleExecute = async (workflowId: string) => {
try {
const res = await fetch(`${API}/api/v1/workflow/${workflowId}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({}),
});
const data = await res.json();
message.success(`执行已启动: ${data.data?.execution_id}`);
const result = await workflowApi.execute(workflowId);
message.success(`执行已启动: ${result.data?.execution_id}`);
} catch { message.error('执行失败'); }
};
......
# 互联网医院平台智能体融合升级方案
> 版本:v1.0 | 日期:2026-03-02
---
## 一、现状诊断:端到端断点全景
经过完整代码审查,当前平台的**后端智能体框架已具备生产级能力**,但与主业务流程之间存在多处"功能孤岛"断点。以下为关键断点清单:
### 1.1 API 基础路径不一致(阻断性)
| 位置 | 当前写法 | 正确写法 |
|------|---------|---------|
| `admin/agents/page.tsx` | `const API = ''`(直接 fetch) | 使用统一 axios 客户端 |
| `admin/tools/page.tsx` | `const API = ''`(直接 fetch) | 使用统一 axios 客户端 |
| `admin/workflows/page.tsx` | `const API = ''`(直接 fetch) | 使用统一 axios 客户端 |
| `admin/knowledge/page.tsx` | `const API = ''`(直接 fetch) | 使用统一 axios 客户端 |
| `GlobalAIFloat/ChatPanel.tsx` | `const API = ''`(直接 fetch) | 使用统一 axios 客户端 |
**影响**:上述页面的 Token 认证、统一错误处理、环境变量切换均失效。
---
### 1.2 智能体与主业务流程的断点
```
患者预问诊 ──▶ SSE Chat(已接通)──▶ AI 报告(已接通)──▶ 推荐医生(已接通)
问诊中 ──▶ /consult/:id/ai-assist(仅调用基础 LLM) ✗ diagnosis_agent(带 tools)
↑ 未接通
处方创建 ──▶ 医生手动填写 ✗ prescription_agent(已有 4 个安全工具)
↑ 未接通
随访管理 ──▶ 患者慢病续药 AI 建议(独立调用) ✗ follow_up_agent
↑ 完全未接通
工作流执行 ──▶ 仅管理员手动触发 ✗ 问诊完成/预问诊完成/审核通过 等事件
↑ 无任何触发入口
```
---
### 1.3 知识库是数据孤岛
- 知识库 CRUD 功能完整,但**库内没有任何内容**
- `knowledge_search` 工具实际使用 11 条硬编码数据(非数据库内容)
- 预问诊 / 诊断辅助 / 处方审核均未从知识库获取临床指南
- 无任何内容导入入口(仅支持逐条手动添加)
---
### 1.4 管理端监控缺失
- `AgentExecutionLog` 模型存在且后端已写入,但**管理端无任何展示**
- `AgentToolLog` 模型存在但工具执行时**从未写入**
- AI 配置页面有使用统计,但统计来源与 Agent 执行日志脱节
- 工作流执行记录(`WorkflowExecution`)无管理端查看入口
---
### 1.5 Agent 会话管理残缺
- `GET /agent/sessions` → 返回空数组(存根)
- `DELETE /agent/session/:id` → 无任何实现
- 患者/医生无法查看 AI 对话历史
- 前端 GlobalAIFloat 每次刷新页面对话历史丢失
---
## 二、升级目标
将智能体系统从"可独立运行的功能孤岛"升级为"深度嵌入临床业务流程的智能引擎",实现:
1. **数据互通**:患者病历、问诊记录、处方历史成为智能体的上下文输入
2. **流程融合**:智能体在问诊、处方、随访的关键节点自动介入
3. **知识赋能**:真实医学知识库驱动 AI 决策,而非硬编码规则
4. **可观测性**:管理员可监控所有智能体执行情况
5. **工作流触发**:复杂临床场景通过工作流编排自动化
---
## 三、升级方案详解
### P0 — 基础修复(本周)
#### 3.1 统一 Agent API 客户端
`web/src/api/` 新增 `agent.ts`,将所有智能体相关 API 统一接入 axios:
```typescript
// web/src/api/agent.ts
import request from './request';
export const agentApi = {
// 与智能体对话(含认证 header、统一错误处理)
chat: (agentId: string, params: {
session_id?: string;
message: string;
context?: Record<string, unknown>;
}) => request.post(`/agent/${agentId}/chat`, params),
// 获取会话列表
getSessions: (userId: string) =>
request.get('/agent/sessions', { params: { user_id: userId } }),
// 获取智能体列表
listAgents: () => request.get('/agent/list'),
// 获取工具列表
listTools: () => request.get('/agent/tools'),
};
export const workflowApi = {
list: () => request.get('/admin/workflows'),
create: (data: WorkflowCreateParams) => request.post('/admin/workflows', data),
update: (id: number, data: Partial<WorkflowCreateParams>) =>
request.put(`/admin/workflows/${id}`, data),
publish: (id: number) => request.post(`/admin/workflows/${id}/publish`),
execute: (workflowId: string, input?: Record<string, unknown>) =>
request.post(`/workflow/${workflowId}/execute`, input || {}),
getExecution: (executionId: string) =>
request.get(`/workflow/execution/${executionId}`),
getTasks: () => request.get('/workflow/tasks'),
completeTask: (taskId: string, result: Record<string, unknown>) =>
request.post(`/workflow/task/${taskId}/complete`, result),
};
export const knowledgeApi = {
listCollections: () => request.get('/knowledge/collections'),
createCollection: (data: KnowledgeCollectionParams) =>
request.post('/knowledge/collections', data),
listDocuments: (collectionId?: string) =>
request.get('/knowledge/documents', { params: { collection_id: collectionId } }),
createDocument: (data: KnowledgeDocumentParams) =>
request.post('/knowledge/documents', data),
search: (query: string, topK = 5, collectionId?: string) =>
request.post('/knowledge/search', { query, top_k: topK, collection_id: collectionId }),
};
```
同步将四个管理页面的 `fetch()` 调用全部替换为上述 API 方法。
---
#### 3.2 修复工作流 Update 接口(后端缺失)
当前后端只有 `PublishWorkflow`,没有 `UpdateWorkflow`,导致可视化编辑器保存功能无效。
```go
// server/internal/service/admin/workflow_handler.go 增加
func (h *Handler) UpdateWorkflow(c *gin.Context) {
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Definition string `json:"definition"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := map[string]interface{}{}
if req.Definition != "" {
updates["definition"] = req.Definition
}
if req.Name != "" {
updates["name"] = req.Name
}
database.GetDB().Model(&model.WorkflowDefinition{}).
Where("id = ?", c.Param("id")).
Updates(updates)
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
```
路由注册:`admin.PUT("/workflows/:id", h.UpdateWorkflow)`
---
### P1 — 核心流程融合(第 1~2 周)
#### 3.3 诊断辅助:从基础 LLM 升级为带工具的 Agent
**当前状态**`/consult/:id/ai-assist` 调用基础 LLM,无工具调用能力
**升级方案**:后端 `consult/service.go``AIAssist()` 改为调用 `diagnosis_agent`
```go
// server/internal/service/consult/service.go
func (s *Service) AIAssist(ctx context.Context, consultID string, scene string) (map[string]interface{}, error) {
// 加载问诊上下文(现有逻辑保留)
var consult model.Consultation
s.db.Where("id = ?", consultID).First(&consult)
var preConsult model.PreConsultation
s.db.Where("consultation_id = ?", consultID).First(&preConsult)
// 构建 Agent 上下文(传入患者信息,让工具能查询病历)
agentCtx := map[string]interface{}{
"patient_id": consult.PatientID,
"consult_id": consultID,
"chief_complaint": consult.ChiefComplaint,
"scene": scene,
}
if preConsult.AIAnalysis != "" {
agentCtx["pre_consult_analysis"] = preConsult.AIAnalysis
}
// 根据场景选择 Agent
agentID := "diagnosis_agent"
message := "请对患者当前情况进行诊断分析,提供鉴别诊断建议"
if scene == "consult_medication" {
agentID = "prescription_agent"
message = "请分析患者情况,提供用药建议"
}
agentSvc := internalagent.GetService()
output, err := agentSvc.Chat(ctx, agentID, consult.DoctorID, "", message, agentCtx)
if err != nil {
return nil, err
}
return map[string]interface{}{
"response": output.Response,
"tool_calls": output.ToolCalls,
"iterations": output.Iterations,
}, nil
}
```
**前端**:医生工作台 `AIPanel.tsx` 展示 tool_calls 时间线(已有组件,需接数据)
---
#### 3.4 处方创建:接入 prescription_agent 安全审核
**当前状态**:医生手动填写处方,无自动安全检查
**升级方案**:处方创建 Modal 增加"AI 安全审核"步骤
**后端**:在 `doctor-portal/prescription/check`(新增接口)调用 `prescription_agent`
```go
// server/internal/service/doctorportal/prescription_service.go 增加
func (s *Service) CheckPrescriptionSafety(ctx context.Context, patientID string, drugs []string) (map[string]interface{}, error) {
drugList := strings.Join(drugs, "、")
agentCtx := map[string]interface{}{
"patient_id": patientID,
"drugs": drugs,
}
message := fmt.Sprintf("请审核以下处方的安全性:%s", drugList)
agentSvc := internalagent.GetService()
output, err := agentSvc.Chat(ctx, "prescription_agent", "system", "", message, agentCtx)
if err != nil {
return nil, err
}
return map[string]interface{}{
"safe": !strings.Contains(output.Response, "禁忌") && !strings.Contains(output.Response, "危险"),
"report": output.Response,
"tool_calls": output.ToolCalls,
"warnings": extractWarnings(output),
}, nil
}
```
**前端**:处方 Modal 新增"AI 安全审核"按钮,提交前自动或手动触发,展示警告 Tag
---
#### 3.5 知识库内容建设
知识库是 AI 决策的基础,当前为空库。需要:
**方案 A:批量导入工具(管理端)**
在知识库管理页面增加批量导入入口,支持:
- 粘贴大段文本(自动分块)
- JSON 格式批量导入
**方案 B:初始化种子数据**
后端提供初始化脚本,预置核心医学知识:
| 集合 | 内容 | 预计文档数 |
|------|------|---------|
| 临床指南 | 常见病诊疗规范(高血压、糖尿病、冠心病等 10 种) | 10 |
| 药品说明 | 高频使用药品说明书摘要(50 种) | 50 |
| 疾病百科 | 常见病症状、检查、鉴别诊断 | 30 |
| 药物相互作用 | 危险药对列表 | 1 批 |
**方案 C:`knowledge_search` 工具优先走数据库**
修改 `pkg/agent/tools/knowledge_search.go`:只有在知识库完全没有结果时才降级到硬编码
```go
// 当前:先用硬编码 fallback,几乎不走数据库
// 修改:先查数据库,结果数 < topK 时才补充硬编码
results, err := r.retriever.Search(ctx, query, topK)
if err != nil || len(results) == 0 {
return builtinSearch(query) // 降级
}
```
---
#### 3.6 后端 Agent 会话接口补全
```go
// server/internal/agent/handler.go - 补充完整实现
// ListSessions 获取用户的所有 Agent 会话
func (h *Handler) ListSessions(c *gin.Context) {
userID, _ := c.Get("user_id")
agentID := c.Query("agent_id")
var sessions []model.AgentSession
query := database.GetDB().Where("user_id = ?", userID).Order("updated_at DESC")
if agentID != "" {
query = query.Where("agent_id = ?", agentID)
}
query.Find(&sessions)
// 解析每个会话的最后一条消息作为摘要
type SessionSummary struct {
model.AgentSession
LastMessage string `json:"last_message"`
}
result := make([]SessionSummary, 0, len(sessions))
for _, s := range sessions {
var history []map[string]string
json.Unmarshal([]byte(s.History), &history)
lastMsg := ""
if len(history) > 0 {
lastMsg = history[len(history)-1]["content"]
if len(lastMsg) > 60 {
lastMsg = lastMsg[:60] + "..."
}
}
result = append(result, SessionSummary{s, lastMsg})
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// DeleteSession 删除会话(清除历史)
func (h *Handler) DeleteSession(c *gin.Context) {
userID, _ := c.Get("user_id")
sessionID := c.Param("session_id")
database.GetDB().Where("session_id = ? AND user_id = ?", sessionID, userID).
Delete(&model.AgentSession{})
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
```
**前端**:GlobalAIFloat 加入会话历史侧栏,可切换历史对话 / 新建对话
---
### P2 — 工作流触发入口融合(第 2~3 周)
#### 3.7 工作流触发入口设计
工作流的核心价值在于**事件驱动的自动化**,当前只能手动触发。需要在以下业务节点植入工作流触发:
| 触发事件 | 推荐工作流 | 触发位置 |
|---------|---------|---------|
| 预问诊完成 | `pre_consult_complete_flow`:生成标准化报告 → 推荐科室 → 匹配医生 | `POST /pre-consult/chat/finish` 执行后 |
| 问诊结束 | `post_consult_flow`:生成病历摘要 → 触发随访提醒 → 推送用药指导 | `POST /consult/:id/end` 执行后 |
| 处方开具 | `prescription_review_flow`:AI 安全审核 → 超阈值转人工审核 → 药房通知 | `POST /doctor-portal/prescription/create` 后 |
| 慢病续药申请 | `chronic_renewal_flow`:AI 评估 → 医生审核 → 药房处理 | `POST /chronic/renewals` 后 |
**后端实现**:在各业务服务的关键节点异步触发工作流
```go
// 在 preconsult/service.go 的 FinishChat 末尾添加(异步,不阻塞主流程)
go func() {
workflowEngine.TriggerByCategory("pre_consult", map[string]interface{}{
"pre_consult_id": preConsult.ID,
"patient_id": preConsult.UserID,
"department": preConsult.AIDepartment,
"severity": preConsult.AISeverity,
})
}()
```
**工作流引擎增加**`TriggerByCategory()` 方法——查找该分类下状态为 active 的第一个工作流并执行
---
#### 3.8 工作流执行结果回写业务数据
工作流不仅要被触发,其输出也要回写到相关业务实体:
| 工作流输出 | 回写目标 |
|---------|---------|
| 标准化病历摘要 | `Consultation.summary` 字段 |
| 随访计划 | `ChronicRecord.follow_up_plan` 字段 |
| 处方审核结果 | `Prescription.ai_review_result` 字段 |
| 下次就诊提醒 | 推送到患者 APP 通知 |
---
#### 3.9 随访 Agent 接入慢病管理
**当前状态**:慢病续药的 `getAIAdvice()` 调用的是 `/chronic/renewals/:id/ai-advice`,后端实现未可见
**升级方案**
```go
// server/internal/service/chronic/service.go - GetAIAdvice 接入 follow_up_agent
func (s *Service) GetAIAdvice(ctx context.Context, renewalID, patientID string) (string, error) {
var renewal model.ChronicRenewal
s.db.Where("id = ?", renewalID).First(&renewal)
agentCtx := map[string]interface{}{
"patient_id": patientID,
"diagnosis": renewal.Diagnosis,
"current_drugs": renewal.DrugList,
"renewal_duration_months": renewal.Duration,
}
message := fmt.Sprintf("患者%s需要续药%d个月,请评估续药合理性并给出建议",
renewal.Diagnosis, renewal.Duration)
agentSvc := internalagent.GetService()
output, err := agentSvc.Chat(ctx, "follow_up_agent", "system", "", message, agentCtx)
if err != nil {
return "暂时无法获取AI建议", nil
}
return output.Response, nil
}
```
---
### P3 — 可观测性与数据质量(第 3~4 周)
#### 3.10 管理端智能体监控大屏
在现有 `admin/agents` 页面新增"执行监控"标签页,展示:
```
智能体管理
├── 智能体列表(当前)
├── 执行日志(新增) ← 来源:AgentExecutionLog 表
│ ├── 时间范围筛选
│ ├── 按智能体筛选
│ ├── 每条记录:时间、用户、输入摘要、响应摘要、迭代次数、Token数、耗时、工具调用数
│ └── 详情展开:完整 tool_calls 时间线
└── 统计概览(新增)
├── 各 Agent 调用次数(饼图)
├── 平均迭代次数趋势(折线图)
├── 工具调用成功率(条形图)
└── Token 消耗统计
```
**后端接口**(新增):
```
GET /admin/agent/logs?agent_id=&start=&end=&page=&page_size=
GET /admin/agent/stats?start=&end=
```
---
#### 3.11 工具调用日志补全
修改 `pkg/agent/executor.go`,在工具执行后写入 `AgentToolLog`
```go
// pkg/agent/executor.go - Execute() 末尾补充日志
start := time.Now()
result, err := tool.Execute(ctx, args)
duration := int(time.Since(start).Milliseconds())
// 写入工具调用日志
go database.GetDB().Create(&model.AgentToolLog{
ToolName: name,
AgentID: e.agentID, // 需要在 Executor 中传入
SessionID: e.sessionID,
InputParams: string(argsJSON),
OutputResult: marshalResult(result),
Success: err == nil,
DurationMs: duration,
})
```
---
#### 3.12 GlobalAIFloat 会话持久化
前端对话面板刷新后历史丢失,需接入会话 API:
```typescript
// web/src/components/GlobalAIFloat/ChatPanel.tsx
// 1. 页面加载时拉取该 Agent 的最近会话
useEffect(() => {
agentApi.getSessions(user.id)
.then(res => {
const sessions = res.data?.filter(s => s.agent_id === agentId);
if (sessions?.length > 0) {
const latest = sessions[0];
setSessionId(latest.session_id);
// 恢复历史消息
const history = JSON.parse(latest.history || '[]');
setMessages(history.map(convertHistoryToMessage));
}
});
}, [agentId]);
// 2. 发消息时携带 session_id(实现连续对话)
const response = await agentApi.chat(agentId, {
session_id: sessionId,
message: inputMsg,
context: patientContext,
});
setSessionId(response.data.session_id);
```
---
#### 3.13 pgvector 向量检索升级
当前知识库检索退化为 ILIKE 关键词匹配,语义搜索能力缺失。
**升级步骤**
```sql
-- 在 PostgreSQL 中安装 pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- 为 knowledge_chunks 表添加向量列
ALTER TABLE knowledge_chunks ADD COLUMN IF NOT EXISTS embedding vector(1536);
CREATE INDEX IF NOT EXISTS knowledge_chunks_embedding_idx
ON knowledge_chunks USING ivfflat (embedding vector_cosine_ops);
```
**配置**:在 AI Config 中启用嵌入模型(如 `text-embedding-3-small`),知识库新增文档时自动向量化。
**效果**:搜索"患者胸闷气短"能匹配到"心力衰竭诊疗指南"而非仅靠关键词
---
## 四、数据流融合全景图(升级后)
```
患者端
├─ 预问诊 Chat(SSE)
│ └─ pre_consult_agent ──工具──▶ query_symptom_knowledge
│ ──工具──▶ recommend_department
│ ──输出──▶ AI报告 + 推荐医生
│ ──触发──▶ pre_consult_complete_flow(工作流)
├─ 问诊中 GlobalAIFloat
│ └─ pre_consult_agent / follow_up_agent(持久化会话,刷新可恢复)
├─ 慢病续药
│ └─ follow_up_agent ──工具──▶ query_medical_record
│ ──工具──▶ query_drug
│ ──输出──▶ 续药合理性评估
医生端
├─ 问诊大厅 AI 面板
│ └─ diagnosis_agent ──工具──▶ query_medical_record(患者病史)
│ ──工具──▶ search_medical_knowledge(临床指南)
│ ──工具──▶ query_symptom_knowledge
│ ──输出──▶ 鉴别诊断建议(带 tool 调用时间线)
├─ 处方创建
│ └─ prescription_agent ──工具──▶ query_drug(药品信息)
│ ──工具──▶ check_drug_interaction(药物相互作用)
│ ──工具──▶ check_contraindication(禁忌症)
│ ──工具──▶ calculate_dosage(剂量验证)
│ ──输出──▶ 安全审核报告 + 警告标注
├─ 问诊结束
│ └─ 触发 post_consult_flow(工作流)
│ ├─ agent 节点:生成病历摘要
│ ├─ tool 节点:更新 Consultation.summary
│ └─ human_review 节点:高风险病例转主任审核
管理端
├─ 智能体监控
│ ├─ AgentExecutionLog 实时展示
│ └─ 工具调用成功率统计
├─ 知识库管理
│ ├─ 批量导入(文件/粘贴)
│ ├─ 向量化状态展示
│ └─ 检索测试(语义搜索)
└─ 工作流管理
├─ 可视化编辑器(已有)
├─ 执行记录查看(新增)
└─ 触发规则配置(事件 → 工作流 映射)
```
---
## 五、优先级排期
| 优先级 | 任务 | 工作量 | 影响 |
|--------|------|--------|------|
| **P0** | 统一 Agent API 客户端(api/agent.ts) | 0.5 天 | 修复认证、错误处理 |
| **P0** | 修复工作流 Update 接口 | 0.5 天 | 可视化编辑器可保存 |
| **P1** | 诊断辅助接入 diagnosis_agent | 1 天 | 医生获得真正的智能诊断 |
| **P1** | 处方创建接入 prescription_agent | 1 天 | 处方安全审核生效 |
| **P1** | knowledge_search 工具优先走数据库 | 0.5 天 | 知识库内容生效 |
| **P1** | 知识库种子数据导入 | 2 天 | AI 决策有真实依据 |
| **P1** | Agent 会话接口补全 + 前端持久化 | 1 天 | 对话历史不丢失 |
| **P2** | 工作流触发入口(预问诊/问诊结束) | 2 天 | 自动化流程 |
| **P2** | follow_up_agent 接入慢病续药 | 0.5 天 | 随访 Agent 生效 |
| **P2** | 管理端智能体执行监控 | 1 天 | 可观测性 |
| **P3** | 工具调用日志写入 AgentToolLog | 0.5 天 | 工具层监控 |
| **P3** | pgvector 语义检索升级 | 1.5 天 | 语义搜索能力 |
| **P3** | 工作流执行记录管理端展示 | 1 天 | 工作流可观测 |
**总工作量预估**:约 2~3 周(后端 + 前端并行)
---
## 六、质量保障
### 6.1 端到端测试用例(主要场景)
| 场景 | 验收标准 |
|------|---------|
| 患者完成预问诊 | 能看到 AI 报告 + 3 位推荐医生,点击可直接创建问诊 |
| 医生查看 AI 诊断面板 | 能看到带 tool_calls 时间线的鉴别诊断建议 |
| 医生开具多种药物处方 | AI 安全审核自动检测药物相互作用并显示警告 |
| 患者申请慢病续药 | follow_up_agent 返回续药合理性评估 |
| 刷新页面后打开 AI 助手 | 对话历史自动恢复 |
| 知识库搜索测试 | 输入"心力衰竭"返回数据库中的临床指南内容 |
| 管理员查看 Agent 执行日志 | 能看到每次对话的 tool_calls、Token 消耗 |
### 6.2 回归风险点
- 接入 `diagnosis_agent``ai-assist` 响应时间会增加(Agent 需多轮 LLM 调用),需加 loading 状态
- 工作流异步触发不得影响主流程响应时间(已用 goroutine 隔离)
- prescription_agent 的安全审核结果仅作为建议,最终决定权仍在医生
---
*文档结束*
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment