ai项目(一)

新建会话功能

需要有sessionid:因为sessionid不能重复,所以我们选择唯一的uuid,并且因为我们要记录会话历史,所以数据库中也要存储sessionid
方法再页面上查看如下接口以及路径:
/ais是网关路由

  • 请求路径:/ais/session
    n=3是限制后面我热点数据要3个
  • 请求参数(Query):/ais/session?n=3
    这个是创建会话的响应数据:
{
    "code": 200,
    "msg": "OK",
    "data": {
        "sessionId": "718f331076e442059a10da26be5f292d",
        "title": "Hello,我是玄枢AI助理",
        "describe": "我是由玄枢学堂倾力打造的智能助理,我不仅能推荐课程、答疑解惑,还能为您激发创意、畅聊心事。",
        "examples": [
            {
                "title": "知识讲解",
                "describe": "面向对象的三大特征是什么?"
            },
            {
                "title": "课程介绍",
                "describe": "《Java高级架构与微服务》能学到什么内容?"
            },
            {
                "title": "课程购买",
                "describe": "《Java进阶编程实战》的课程价格是多少?"
            }
        ]
    },
    "requestId": "0e3d0acb9c884a539424f857be3e0cb1"
}

功能实现:

  1. 创建实体类,mapper
  2. 创建返回给前端的vo,可以看我们上面的响应数据,都要什么了
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SessionVO {
    // 会话ID,用于唯一标识当前的AI助手会话。
    private String sessionId;
    // AI助手的标题,用于显示助手的名称或身份。
    private String title;
    // AI助手的描述,简要介绍助手的功能或特点。
    private String describe;
    //示例列表,包含一些使用助手的示例。
    private List<Example> examples;
 // Example类表示每个示例的标题和描述。
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Example {
 //示例的标题,描述了示例的类型或内容。
        private String title;
 // 示例的描述,具体说明了示例的内容或问题。
        private String describe;
    }
}
  1. 映射nacos的配置,把卸载nacos里的热点问题都读下来
    image
@Data
@Configuration
@ConfigurationProperties(prefix = "xs.ai.session")
public class SessionProperties {
 // AI助手的标题,用于显示助手的名称或身份。
    private String title;
 //AI助手的描述,简要介绍助手的功能或特点。
    private String describe;
 //示例列表,包含一些使用助手的示例。
    private List<SessionVO.Example> examples;
}
  1. 写controller
@RestController
@RequestMapping("/session")
@RequiredArgsConstructor
public class SessionController {
    private final IChatSessionService chatSessionService;
//新建会话
    @PostMapping
    public SessionVO createSession(@RequestParam(value = "n", defaultValue = "3") Integer num) {
        return this.chatSessionService.createSession(num);
    }
}
  1. 写service
@Service
//要注入数据,就要写这个注解
@RequiredArgsConstructor
public class ChatSessionServiceImpl extends ServiceImpl<ChatSessionMapper, ChatSession> implements IChatSessionService {
//从ioc中把SessionProperties里的数据注入下来
    private final SessionProperties sessionProperties;
    @Override
    public SessionVO createSession(Integer num) {
        SessionVO ret=new SessionVO();
        //1 生产sessionId
        String uuid = IdUtil.simpleUUID();
        ret.setSessionId(uuid);
        //2 读取 nacos 数据 num个随机问题
        ret.setTitle(sessionProperties.getTitle());
        ret.setDescribe(sessionProperties.getDescribe());
        // 从list中随机num个元素,重新组织成集合
	//使用hutool工具,随机从list里取num个数据组成一个新的数组
        List<SessionVO.Example> examples = RandomUtil.randomEleList(sessionProperties.getExamples(), num);
	//ret数据现在全了
        ret.setExamples(examples);
        //3 数据库保存会话信息
        Long userId = 2l;
	//chatsession建造者构建了一个ChatSession对象,把数据库需要的数据赋值进来,title我们前端是写死的,不需要存进来
        ChatSession chatSession = ChatSession.builder()
                .sessionId(uuid)
                //.title(sessionProperties.getTitle())
                .creater(userId)
                .createTime(java.time.LocalDateTime.now())
                .updater(userId)
                .updateTime(java.time.LocalDateTime.now())
                .userId(userId)
                .build();
	//mp存进去
        this.save(chatSession);
        //4 返回vo对象
        return ret;
    }
}

换一换热点功能

热门问题我们把他放到nacos里了

  1. controller
    @GetMapping("/hot")
    public List<SessionVO.Example> hotExamples(@RequestParam(value = "n", defaultValue = "3") Integer num) {
        return this.chatSessionService.hotExamples(num);
    }
  1. service
    @Override
    public List<SessionVO.Example> hotExamples(Integer num) {
        // 从list中随机num个元素,重新组织成集合
        List<SessionVO.Example> examples = RandomUtil.randomEleList(sessionProperties.getExamples(), num);
        return examples;
    }

新建会话&换一换流程图:

image

流式对话

SSE(Server-Sent Events),是基于 HTTP 的单向服务器推送技术,通过 text/event-stream 响应类型维持长连接,后端持续分段推送文本数据;多用于 AI 大模型流式输出,只能服务端向客户端发消息,双向实时通信用 WebSocket。

响应结构说明:(SSE)
1、每行数据,都是一个json数据
2、json中有eventData和eventType,两个属性
3、eventData:文本内容
4、eventType:事件类型,1001-数据事件,1002-停止事件,1003-参数事件
响应数据如下:

data:{"eventData":"课程","eventType":1001} 
data:{"eventData":"id\n","eventType":1001} 
data:{"eventData":":","eventType":1001} 
data:{"eventData":"1880","eventType":1001} 
data:{"eventData":"5332","eventType":1001} 
data:{"eventData":"5357","eventType":1001} 
data:{"eventData":"5225","eventType":1001} 
data:{"eventData":"346\n\n","eventType":1001} 
data:{"eventData":"发展。","eventType":1001} 
data:{"eventData":"","eventType":1001} 
data:{"eventType":1002}
  1. 定义了一下前端返回数据的vo(响应对象)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatEventVO {
//文本内容
private Object eventData;
//事件类型,1001-数据事件,1002-停止事件,1003-参数事件
private int eventType;
}
  1. 定义一下枚举事件类型eventType
@Getter
public enum ChatEventTypeEnum {
    DATA(1001, "数据事件"),
    STOP(1002, "停止事件"),
    PARAM(1003, "参数事件");
        private final int value;
        private final String desc;
        ChatEventTypeEnum(int value, String desc) {
            this.value = value;
            this.desc = desc;
        }
    @Override
    public String toString() {
        return this.name();
    }
}
  1. 定义dto(前端到controller数据)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatDTO {
//用户的问题
    private String question;
    /**
     * 会话id,一个用户可以有多个
     */
    private String sessionId;
}
  1. 创建SpringAi配置
@Configuration
@RequiredArgsConstructor
public class SpringAIConfig {
    //private final SystemPromptConfig systemPromptConfig;
    @Bean
//SpringAI 对接阿里通义千问(DashScope)的大模型客户端,底层封装了调用通义千问接口的能力。
    public ChatClient chatClient(DashScopeChatModel dashScopeChatModel) {
	//建造者模式创建 ChatClient。`ChatClient` 是 SpringAI 提供的核心 API,用来**发送 prompt、和大模型对话,支持 SSE 流式返回**。
        return ChatClient.builder(dashScopeChatModel)
                //.defaultSystem(systemPromptConfig.getChatSystemMessage().get())
		//打印日志
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}
  1. controller
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/chat")
public class ChatController {
    private final IChatService chatService;
    /**
     * 流式对话
     * @param chatDTO
     * @return
     */
	 //自定义注解,因为controller里有@RestController这个,所以默认都是返回json类型数据,家里这个注解之后,就是数据本身是什么类型就返回什么类型,像这里就是流式数据,流式数据不能用json类型返回
    @NoWrapper
	```
// 定义一个注解 `NoWrapper`,用于标记Controller中的方法,表示该方法不需要包装返回结果。
@Retention(RetentionPolicy.RUNTIME)  // 保留策略为运行时
@Target(ElementType.METHOD)  // 该注解仅适用于方法
public @interface NoWrapper {
}
	```
	//这里是指定用的是流式输出,SSE
    @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ChatEventVO> chat(@RequestBody ChatDTO chatDTO) {
        return this.chatService.chat(chatDTO.getQuestion(), chatDTO.getSessionId());
    }
}
  1. service
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements IChatService {
    private final ChatClient chatClient;
    private final SystemPromptConfig systemPromptConfig;
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
	//开始构建一次对话请求。
        return chatClient.prompt()
		//设置**用户消息**
                .user(question)
		//设置**系统提示词 System Prompt**
                .system(p -> p.text(
		systemPromptConfig.getChatSystemMessage().get().param("now", DateUtil.now()))
                //流式返回
		.stream()
                .chatResponse()
		//`map` 是响应式流的转换操作:每收到大模型一段 token,就把文本取出,构建一个`ChatEventVO`,事件类型标记为`DATA`(正常内容片段)。这里就是把大模型原生流式数据,包装成项目自定义 VO。
                .map(response -> ChatEventVO.builder()  
				.eventData(response.getResult().getOutput().getText())    
				.eventType(ChatEventTypeEnum.DATA.getValue()).build())  
		//`concatWith`:追加消息,在前面所有流元素全部发送完成之后,追加一条消息事件类型是`STOP`,告诉前端:大模型输出结束,流关闭。
		.concatWith(Flux.just(ChatEventVO.builder()
				.eventData(null)
				.eventType(ChatEventTypeEnum.STOP.getValue()).build()));
    }
}

nacos中存储系统提示词

(通过nacos listener实现提示词动态更新)

  1. yml里加配置,nacos
    image

  2. 定义配置类,读取到了配置文件的名称、分组、超时时间,根据yml写的

@Data
@Configuration
@ConfigurationProperties(prefix = "xs.ai.prompt")
public class AIProperties {
    private System system;
    @Data
    public static class System {
        private Chat chat;
        @Data
        public static class Chat {
            private String dataId;
            private String group = "DEFAULT_GROUP";
            private long timeoutMs = 20000L;
        }
    }
}
  1. 定制提示词类,读取nacos配置文件内容

@Slf4j
@Getter
@Configuration
@RequiredArgsConstructor
public class SystemPromptConfig {
//注入nacos刚才的配置
    private final AIProperties aiProperties;
	//这个是专门读nacos的
    private final NacosConfigManager nacosConfigManager;

    private final AtomicReference<String> chatSystemMessage=new AtomicReference<>();

    @PostConstruct // 初始化时加载配置
    public void init() {
        // 读取配置文件
     loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);
    }

    private void loadConfig(AIProperties.System.Chat chatConfig, AtomicReference<String> target) {
        try {
            var dataId = chatConfig.getDataId();
            var group = chatConfig.getGroup();
            var timeoutMs = chatConfig.getTimeoutMs();

            // 1. 主动读取配置
            String config = nacosConfigManager.getConfigService().getConfig(dataId, group, timeoutMs);
            target.set(config);//放入到原子对象中
            log.info("读取{}成功,内容为:{}", target, config);

            // 2. 添加监听器,实现动态更新
            nacosConfigManager.getConfigService().addListener(dataId, group, new Listener() {
                @Override
                public Executor getExecutor() {
                    return null; // 使用 Nacos 客户端的默认线程池
                }

                @Override
                public void receiveConfigInfo(String info) {
                    target.set(info);
                    log.info("更新{}成功,内容为:{}", target, info);
                }
            });
        } catch (Exception e) {
            log.error("加载配置失败", e);
        }
    }

}

image
image

  1. 加一个系统提示词,在service里
    private final SystemPromptConfig systemPromptConfig;

    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        return chatClient.prompt()
              //这里加进来系统提示词了
                .system(p -> p.text(systemPromptConfig.getChatSystemMessage().get())
                        .param("now", DateUtil.now()))
                
    }

新建会话流程概括:
前端发送请求,传了一个num=3,新建一个sessionid,从nacos读取数据(包括热点问题)
创建聊天会话对象存到mysql中,返回一个vo对象(包括三个热点问题)

热点流程:
点击换一换功能,从nacos读取数据(包括所有热点数据),其中num个生成新数组,返回给前端num个热点问题

  1. Nacos 系统提示词
    通过NacosConfigManager读取提示词,注册 Listener,Nacos 页面修改提示词不用重启服务,自动生效;用AtomicReference保证多线程安全读取。
  2. 流式 SSE 对话
    前端传 ChatDTO(question+sessionId),Controller 加@NoWrapper + produces = MediaType.TEXT_EVENT_STREAM_VALUE,返回 Flux 流;Service 调用 SpringAI,每拿到一段 token 封装成 DATA 事件,模型输出结束追加 STOP 事件;SSE 逐段推送给前端打字机效果。
posted @ 2026-09-11 22:18  Jennifer_F  阅读(16)  评论(0)    收藏  举报