news 2026/5/7 17:14:02

删除 基于Spring AI的课程查询与卡片展示实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
删除 基于Spring AI的课程查询与卡片展示实现

一、背景与需求

在天机AI助手中,学生可以通过自然语言查询课程信息。例如,学生提供课程ID后,系统需要调用课程微服务的接口,获取课程详细信息,并在前端以卡片形式展示(包含课程名称、价格、适用人群、详情等,并支持点击跳转)。

原有的课程微服务已经提供了通过课程ID查询课程基础的Feign接口。我们需要在AI助手中集成该能力,同时解决一个关键难题:大模型返回的是文本内容,而前端需要结构化的JSON数据来渲染卡片。

二、整体实现流程

  1. 定义课程信息结果类(CourseInfo

  2. 编写工具类(CourseTools),通过@Tool注解暴露给Spring AI

  3. ChatClient中注册工具

  4. 解决卡片渲染问题:通过ToolContext传递requestId,使用ToolResultHolder暂存工具结果,最后在响应流中追加结构化数据

下面我们逐步展开。

三、代码实现

3.1 定义课程信息结果类

java

@Data @Builder @NoArgsConstructor @AllArgsConstructor public class CourseInfo { @JsonPropertyDescription("课程id") private Long id; @JsonPropertyDescription("课程名称") private String name; @JsonPropertyDescription("课程价格,单位为元,货币为人民币") private double price; @JsonPropertyDescription("课程学习有效期,单位:月") private Integer validDuration; @JsonPropertyDescription("适用人群,例如:初学者") private String usePeople; @JsonPropertyDescription("课程详细介绍") private String detail; public static CourseInfo of(CourseBaseInfoDTO dto) { if (dto == null) return null; CourseInfo info = BeanUtil.toBeanIgnoreError(dto, CourseInfo.class); info.setPrice(Optional.ofNullable(dto.getPrice()) .map(p -> NumberUtil.round(p / 100.0, 2).doubleValue()) .orElse(0.0)); return info; } }

CourseBaseInfoDTO来自课程微服务的Feign接口,其中价格字段单位为分。我们在of方法中将其转换为元,并保留两位小数。

3.2 定义常量

java

public interface Constant { String REQUEST_ID = "requestId"; interface Tools { String QUERY_COURSE_BY_ID = "根据课程id查询课程详细信息"; } interface ToolParams { String COURSE_ID = "课程id"; } }

良好的常量管理有助于代码维护。

3.3 编写工具类CourseTools

java

@Component @RequiredArgsConstructor public class CourseTools { private final CourseClient courseClient; private static final String FIELD_NAME_FORMAT = "{}_{}"; @Tool(description = Constant.Tools.QUERY_COURSE_BY_ID) public CourseInfo queryCourseById(@ToolParam(description = Constant.ToolParams.COURSE_ID) Long courseId, ToolContext toolContext) { return Optional.ofNullable(courseId) .map(id -> CourseInfo.of(courseClient.baseInfo(id, true))) .map(courseInfo -> { String field = StrUtil.format(FIELD_NAME_FORMAT, StrUtil.lowerFirst(CourseInfo.class.getSimpleName()), courseInfo.getId()); String requestId = Convert.toStr(toolContext.getContext().get(Constant.REQUEST_ID)); ToolResultHolder.put(requestId, field, courseInfo); return courseInfo; }) .orElse(null); } }

关键点:

  • 方法参数中增加了ToolContext,用于接收从调用链传递的requestId

  • 查询结果后,将CourseInfo存入ToolResultHolder,key为requestId,value中再使用field区分具体的课程数据。

3.4 注册工具到ChatClient

java

@Bean public ChatClient chatClient(ChatClient.Builder builder, Advisor loggerAdvisor, Advisor messageChatMemoryAdvisor, CourseTools courseTools) { return builder .defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor) .defaultTools(courseTools) .build(); }
3.5 实现ToolResultHolder(工具结果暂存器)

java

public class ToolResultHolder { private static final Map<String, Map<String, Object>> HANDLER_MAP = new ConcurrentHashMap<>(); public static void put(String key, String field, Object result) { HANDLER_MAP.computeIfAbsent(key, k -> new HashMap<>()).put(field, result); } public static Map<String, Object> get(String key) { return key == null ? null : HANDLER_MAP.get(key); } public static void remove(String key) { HANDLER_MAP.remove(key); } }

这个线程安全的容器用于在一次请求中保存多个工具执行结果。

3.6 在聊天服务中生成requestId并处理输出流

java

@Override public Flux<ChatEventVO> chat(String question, String sessionId) { var conversationId = ChatService.getConversationId(sessionId); StringBuilder outputBuilder = new StringBuilder(); var requestId = IdUtil.fastSimpleUUID(); // 每次请求生成唯一ID return chatClient.prompt() .system(promptSystem -> promptSystem .text(systemPromptConfig.getChatSystemMessage().get()) .param("now", DateUtil.now())) .advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)) .toolContext(Map.of(Constant.REQUEST_ID, requestId)) // 传递requestId .user(question) .stream() .chatResponse() .doFirst(() -> GENERATE_STATUS.put(sessionId, true)) .doOnComplete(() -> GENERATE_STATUS.remove(sessionId)) .doOnError(e -> GENERATE_STATUS.remove(sessionId)) .doOnCancel(() -> saveStopHistoryRecord(conversationId, outputBuilder.toString())) .takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false)) .map(chatResponse -> { String text = chatResponse.getResult().getOutput().getText(); outputBuilder.append(text); return ChatEventVO.builder() .eventData(text) .eventType(ChatEventTypeEnum.DATA.getValue()) .build(); }) .concatWith(Flux.defer(() -> { Map<String, Object> map = ToolResultHolder.get(requestId); if (CollUtil.isNotEmpty(map)) { ToolResultHolder.remove(requestId); ChatEventVO paramEvent = ChatEventVO.builder() .eventData(map) .eventType(ChatEventTypeEnum.PARAM.getValue()) .build(); return Flux.just(paramEvent, STOP_EVENT); } return Flux.just(STOP_EVENT); })); }

注意:

  • toolContext(Map.of(Constant.REQUEST_ID, requestId))将请求ID传递给Tool

  • 在流结束时,通过ToolResultHolder.get(requestId)获取本次请求产生的所有工具结果,将其封装为PARAM类型的事件发送给前端。

  • 前端约定的事件类型1003对应卡片渲染,eventData中存放课程信息。

四、测试效果

用户输入:查询课程,课程id为:1589905661084430337

  • 后端接收到请求,生成requestId,调用AI模型。

  • 模型识别到需要查询课程,触发CourseTools.queryCourseById,传入课程ID和ToolContext

  • 工具从课程微服务获取数据,转成CourseInfo,并存入ToolResultHolder(key为requestId)。

  • 模型生成文本回复(如“已为您查询到课程信息如下:”)。

  • 流式传输文本内容,最后追加一个PARAM事件,携带课程JSON数据。

  • 前端根据PARAM事件渲染课程卡片,用户可点击跳转详情页。

五、小结

本文介绍了在天机AI助手中集成课程查询卡片功能的完整实现,核心要点包括:

  1. 工具的定义与注册:使用Spring AI的@Tool注解,并通过defaultTools注册到ChatClient

  2. 数据的跨层传递:通过ToolContext传递请求级唯一ID,保证并发场景下数据不会错乱。

  3. 结构化数据输出:借助ToolResultHolder暂存工具结果,在响应流末尾追加JSON数据,满足前端卡片渲染需求。

  4. 代码规范:常量化、工具类设计、线程安全容器等最佳实践。

这一模式可推广到其他需要结构化展示(如商品卡片、用户卡片、图表数据)的AI助手场景。希望本文能为您提供有益的参考。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 17:13:17

实现锁的方式之中断的开关

如大家所了解的&#xff0c;关于编程中锁的具体实现&#xff0c;需要脱离我们的编程语言&#xff0c;往操作系统、硬件的支持这些更下的一层去探究。中断的开关在线程切换的时候需要用到中断&#xff0c;因此若关闭了中断&#xff0c;则可以阻止当前 CPU 运行的任务被其他任务所…

作者头像 李华
网站建设 2026/5/7 17:11:31

理想校招怎么准备:别把现代 C++ 背成名词表,量产系统和项目追问更关键

理想校招怎么准备:别把现代 C++ 背成名词表,量产系统和项目追问更关键 适合人群:目标偏 C++、自动驾驶系统、智能座舱、车载软件方向,想搞清理想为什么特别爱追“项目里你到底怎么用过”的同学 很多人准备理想时,会下意识把重点放在两件事上: 算法题 现代 C++ 新特性 这…

作者头像 李华
网站建设 2026/5/7 17:10:05

2025届学术党必备的六大降重复率平台横评

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 撰写文本之际&#xff0c;为把AI生成痕迹予以降低&#xff0c;得从词汇、句法以及结构这三个…

作者头像 李华
网站建设 2026/5/7 17:09:26

法大大发布智能合同助手,打造人人可用的AI合同助手

2026年以来&#xff0c;AI在合同领域正从“实验”进入“量产”&#xff0c;但信任与数据质量仍是关键门槛。调研显示&#xff0c;56%的企业已正式部署智能体&#xff0c;55%的管理者却仍对AI准确性存疑&#xff1b;62%的团队坦承&#xff0c;历史合同中的未知风险才是最大焦虑。…

作者头像 李华