首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
1,421 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
525 阅读
3
EasyExcel 实战:导出带图片的 Excel 完整方案
282 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
265 阅读
5
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
219 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
Spring AI
java虚拟机
JVM
Spring AI Alibaba
Java
保姆级教程
SpringBoot
Spring
WebFlux
MCP
大模型
Agent Skills
Nginx
Agent
Ubuntu
Mysql
Apache POI
自定义starter
Mybatis
响应式编程
Luca Ju
累计撰写
51
篇文章
累计收到
2
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
20
篇与
的结果
2026-01-13
玩转 MyBatis-Plus 多数据源配置,Spring Boot 项目快速实现多库操作
在实际开发中,我们经常会遇到一个 Spring Boot 项目需要操作多个数据库的场景,比如从 Oracle 拉取数据同步到 MySQL。如果手动管理多个数据源的连接和切换,不仅开发效率低,还容易出现数据源混淆的问题。而 MyBatis-Plus 提供的 dynamic-datasource-spring-boot-starter 依赖,能够帮助我们快速实现多数据源的配置和动态切换,极大简化多库操作的开发流程。本文就来详细讲解 Spring Boot 整合 MyBatis-Plus 多数据源的具体实现步骤。一、核心依赖引入首先,我们需要在项目的 pom.xml 文件中引入 MyBatis-Plus 多数据源的核心依赖。这里以 3.1.0 版本为例(可根据项目实际情况选择兼容版本):<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.2.0</version> </dependency>注意:如果项目中已经引入了 MyBatis-Plus 的核心依赖,无需重复引入,该 starter 已包含相关依赖。二、多数据源配置(application.yml)接下来,在 Spring Boot 的核心配置文件 application.yml 中配置多个数据源的连接信息。本文以 1 个 MySQL 数据源 + 2 个 Oracle 数据源为例,配置如下:spring: datasource: dynamic: # 设置默认的数据源,默认数据源的 key 需与下方 datasource 中的配置一致 primary: mysql-ocr # 严格模式:默认 true,未匹配到指定数据源时抛异常,false 则使用默认数据源 strict: true # 配置多个数据源,key 自定义(建议与业务相关,便于区分) datasource: # MySQL 数据源:ocr 业务库 mysql-ocr: url: jdbc:mysql://ip:port/ocr?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: username password: password driver-class-name: com.mysql.cj.jdbc.Driver # Oracle 数据源1:orcl 业务库 oracle-orcl: url: jdbc:oracle:thin:@ip:port:helowin username: username password: password driver-class-name: oracle.jdbc.OracleDriver # Oracle 数据源2:cbxx 业务库 oracle-cbxx: url: jdbc:oracle:thin:@ip:port:helowin username: username password: password driver-class-name: oracle.jdbc.OracleDriver配置参数说明参数作用primary指定默认数据源,当不指定数据源时,默认使用该配置的数据源strict严格模式开关,开启后若调用不存在的数据源会抛出异常,关闭则默认使用主数据源datasource多数据源的具体配置节点,每个子节点对应一个数据源,key 为自定义的数据源名称三、Mapper 层目录结构与数据源注解配置为了更清晰地管理不同数据源对应的 Mapper 接口,我们可以按照数据源划分包结构,同时通过 @DS 注解指定 Mapper 对应的数据源。1. Mapper 层目录结构推荐按照数据源名称创建独立的包,将不同数据源的 Mapper 接口分类存放,便于后期维护:src/main/java/com/xxx/mapper ├── cbxx // oracle-cbxx 数据源对应的 Mapper 包 │ └── VOcrCbxxMapper.java ├── ocr // mysql-ocr 数据源对应的 Mapper 包 │ └── OcrCmMapper.java └── orcl // oracle-orcl 数据源对应的 Mapper 包 └── VOcrMeterMapper.java2. @DS 注解指定数据源@DS 注解是 MyBatis-Plus 多数据源的核心注解,用于指定当前 Mapper 接口或方法对应的数据源,支持类级别和方法级别,遵循就近原则(方法上的注解优先级高于类上的注解)。在 Mapper 接口上添加 @DS 注解,指定该接口下所有方法都使用对应的数据源,也可以在方法上单独添加 @DS 注解,优先级更高:import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.dynamic.datasource.annotation.DS; import com.xxx.entity.VOcrCbxx; import org.springframework.stereotype.Repository; /** * oracle-cbxx 数据源对应的 Mapper 接口 * @DS 注解指定数据源名称,与 application.yml 中配置的 key 一致 */ @DS("oracle-cbxx") @Mapper public interface VOcrCbxxMapper extends BaseMapper<VOcrCbxx> { /** * 自定义查询方法:根据日期查询数据 * 该方法默认使用类上指定的 oracle-cbxx 数据源 */ default List<VOcrCbxx> selectByBqcbr(String date) { QueryWrapper<VOcrCbxx> wrapper = new QueryWrapper<>(); // 适配 Oracle 日期函数,匹配年月日 wrapper.apply("TRUNC(bqcbr) = TO_DATE({0}, 'YYYY-MM-DD')", date); return selectList(wrapper); } /** * 自定义方法:使用 oracle-orcl 数据源查询 * 方法级别 @DS 注解优先级高于类级别 */ @DS("oracle-orcl") List<VOcrCbxx> selectFromOrclByCondition(String condition); }四、核心使用说明注解优先级:方法上的 @DS 注解 > 类上的 @DS 注解 > 全局默认数据源。事务支持:多数据源下的事务需要使用 @DSTransactional 注解(而非 Spring 原生的 @Transactional),该注解能保证同一数据源内的事务一致性;跨数据源事务需结合分布式事务方案(如 TCC)。避免数据源混用:建议严格按照包结构划分 Mapper,避免不同数据源的 Mapper 混杂,降低维护成本。动态切换数据源:除了通过 @DS 注解静态指定数据源,还可以通过 DynamicDataSourceContextHolder 类手动切换数据源,适用于动态选择数据源的业务场景:// 手动切换到 mysql-ocr 数据源 DynamicDataSourceContextHolder.push("mysql-ocr"); // 执行数据库操作 ocrCmMapper.selectById(1L); // 清空当前数据源上下文 DynamicDataSourceContextHolder.clear();五、总结通过 MyBatis-Plus 的 dynamic-datasource-spring-boot-starter,我们可以在 Spring Boot 项目中零侵入式地实现多数据源配置,核心步骤总结如下:引入多数据源核心依赖;在 application.yml 中配置多数据源连接信息,指定默认数据源;按数据源划分 Mapper 包结构,通过 @DS 注解指定 Mapper/方法对应的数据源;业务层直接注入 Mapper 接口使用,无需关心数据源切换细节。这种配置方式简洁高效,极大降低了多数据源开发的复杂度,非常适合需要操作多个异构数据库的业务场景。
2026年01月13日
38 阅读
0 评论
2 点赞
2025-09-30
Java 实战:基于 WebSocket 获取大模型流式输出并转为 Flux
本文详细讲解 Java 如何基于 ReactorNettyWebSocketClient 实现 WebSocket 流式输出获取,附带完整代码演示将 WebSocket 消息转为 Flux 响应式流的全流程,包含连接建立、异常处理、会话管理等关键逻辑,助力开发者快速解决大模型流式对接需求,适配前端 HTTP 流式交互场景。
2025年09月30日
105 阅读
0 评论
2 点赞
2025-08-23
Spring AI Tool 工具方法调用源码深度解析:从流式交互到工具执行全流程
Spring AI Tool 工具方法调用源码深度解析:从流式交互到工具执行全流程前言:为什么需要读源码?如何高效读源码?在上一篇博客中,我们介绍了如何通过 Spring AI 快速调用本地 Tool 方法实现大模型的工具能力扩展。但对于开发者来说,仅仅会用还不够 —— 理解框架的底层逻辑,才能在遇到问题时快速定位、在定制需求时游刃有余。博客链接:https://www.lucaju.cn/index.php/archives/131/很多小伙伴对读源码望而却步,其实掌握方法就能事半功倍:详略得当:聚焦核心业务逻辑,忽略日志、校验等辅助代码从命名和注释入手:规范框架的源码命名和注释会清晰指引核心流程由浅入深:先抓整体流程,再钻关键细节,避免一开始陷入代码迷宫本文将从 Spring AI 调用大模型的业务代码出发,逐步深入源码,解析 Tool 工具方法调用的完整流程,重点剖析工具执行的核心逻辑。1. 业务代码回顾:流式调用大模型的入口先看一段典型的 Spring AI 流式调用大模型并启用工具的业务代码,这是我们源码解析的起点:public Flux<String> stream(String content) { // 创建chatModel对象,配置模型参数和工具回调管理器 OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.siliconflow.cn") .apiKey(System.getenv("SiliconFlow_API")) .build()) .defaultOptions(OpenAiChatOptions.builder() .model("Qwen/Qwen3-8B") .build()) // 关键:配置工具调用管理器 .toolCallingManager(SpringUtil.getBean(ToolCallingManager.class)) .build(); // 创建prompt对象 Prompt prompt = new Prompt(content); // 调用流式输出接口 Flux<ChatResponse> stream = chatModel.stream(prompt); return stream.map(chunk -> { String text = chunk.getResult() != null ? chunk.getResult().getOutput() != null ? chunk.getResult().getOutput().getText() : "" : ""; text = StrUtil.nullToDefault(text, ""); return text; }); }核心逻辑很清晰:创建配置好的OpenAiChatModel,构造Prompt,调用stream方法获取流式响应。其中toolCallingManager的配置是启用工具调用的关键。2. 入口:ChatModel 的 stream 方法从业务代码的chatModel.stream(prompt)进入源码,这是整个流程的入口:@Override public Flux<ChatResponse> stream(Prompt prompt) { // 合并运行时和默认选项,创建最终请求prompt Prompt requestPrompt = buildRequestPrompt(prompt); // 实际发起请求 return internalStream(requestPrompt, null); }2.1 配置合并:buildRequestPrompt 方法buildRequestPrompt的核心作用是合并运行时配置和默认配置,确保模型使用正确的参数(如工具列表、回调、上下文等):Prompt buildRequestPrompt(Prompt prompt) { // 处理运行时prompt options OpenAiChatOptions runtimeOptions = null; if (prompt.getOptions() != null) { // 转换运行时选项为OpenAiChatOptions类型 if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) { runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class, OpenAiChatOptions.class); } else { runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, OpenAiChatOptions.class); } } // 合并运行时选项和默认选项 OpenAiChatOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions, OpenAiChatOptions.class); // 显式合并特殊选项(如HTTP头、工具配置等) if (runtimeOptions != null) { requestOptions.setHttpHeaders(mergeHttpHeaders(runtimeOptions.getHttpHeaders(), this.defaultOptions.getHttpHeaders())); requestOptions.setInternalToolExecutionEnabled( ModelOptionsUtils.mergeOption(runtimeOptions.getInternalToolExecutionEnabled(), this.defaultOptions.getInternalToolExecutionEnabled())); // 合并工具名称、回调、上下文等关键配置 requestOptions.setToolNames(ToolCallingChatOptions.mergeToolNames(runtimeOptions.getToolNames(), this.defaultOptions.getToolNames())); requestOptions.setToolCallbacks(ToolCallingChatOptions.mergeToolCallbacks(runtimeOptions.getToolCallbacks(), this.defaultOptions.getToolCallbacks())); requestOptions.setToolContext(ToolCallingChatOptions.mergeToolContext(runtimeOptions.getToolContext(), this.defaultOptions.getToolContext())); } else { // 若无可运行时选项,直接使用默认配置 requestOptions.setHttpHeaders(this.defaultOptions.getHttpHeaders()); requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.getInternalToolExecutionEnabled()); requestOptions.setToolNames(this.defaultOptions.getToolNames()); requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks()); requestOptions.setToolContext(this.defaultOptions.getToolContext()); } // 校验工具回调配置 ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks()); return new Prompt(prompt.getInstructions(), requestOptions); }总结:该方法通过合并默认配置和运行时配置,生成最终的请求参数,确保工具调用相关的配置(工具列表、回调等)被正确传入。3. 核心流程:internalStream 方法的完整解析internalStream是实际处理流式请求的核心方法,流程可拆解为 7 个关键步骤。我们重点关注与工具调用相关的核心逻辑:return Flux.deferContextual(contextView -> { // 步骤一:生成请求request对象 ChatCompletionRequest request = createRequest(prompt, true); // 步骤二:语音类型流式输出校验(非核心,略) audioRequestCheck()... // 步骤三:发送调用请求,获取流式响应 Flux<OpenAiApi.ChatCompletionChunk> completionChunks = this.openAiApi.chatCompletionStream(request, getAdditionalHttpHeaders(prompt)); // 步骤四:角色缓存(非核心,略) ConcurrentHashMap<String, String> roleMap = new ConcurrentHashMap<>(); // 步骤五:生成监控observation对象(非核心,略) final ChatModelObservationContext observationContext = ...; Observation observation = ...; // 步骤六:转换响应格式(将分片转为ChatResponse) Flux<ChatResponse> chatResponse = completionChunks.map()...... // 步骤七:处理聊天响应流(核心:工具调用逻辑在这里) Flux<ChatResponse> flux = chatResponse.flatMap()...... return new MessageAggregator().aggregate(flux, observationContext::setResponse); });3.1 步骤三:发送流式请求(chatCompletionStream)chatCompletionStream负责向大模型 API 发送流式请求,并处理服务器返回的 SSE(Server-Sent Events)响应:public Flux<ChatCompletionChunk> chatCompletionStream(ChatCompletionRequest chatRequest, MultiValueMap<String, String> additionalHttpHeader) { // 断言校验:请求非空且流式开关为true Assert.notNull(chatRequest, "The request body can not be null."); Assert.isTrue(chatRequest.stream(), "Request must set the stream property to true."); AtomicBoolean isInsideTool = new AtomicBoolean(false); // 使用WebClient发送POST请求,处理流式响应 return this.webClient.post() .uri(this.completionsPath) .headers(headers -> headers.addAll(additionalHttpHeader)) .body(Mono.just(chatRequest), ChatCompletionRequest.class) .retrieve() // 将响应转为字符串流 .bodyToFlux(String.class) // 终止条件:收到"[DONE]" .takeUntil("[DONE]"::equals) // 过滤掉终止符 .filter("[DONE]"::equals.negate()) // 转换为ChatCompletionChunk对象 .map(content -> ModelOptionsUtils.jsonToObject(content, ChatCompletionChunk.class)) // 标记工具调用片段(关键:识别工具调用的分片) .map(chunk -> { if (this.chunkMerger.isStreamingToolFunctionCall(chunk)) { isInsideTool.set(true); } return chunk; }) // 窗口化合并工具调用分片(核心:合并工具调用的多个分片) .windowUntil(chunk -> { if (isInsideTool.get() && this.chunkMerger.isStreamingToolFunctionCallFinish(chunk)) { isInsideTool.set(false); return true; } return !isInsideTool.get(); }) // 合并分片内容 .concatMapIterable(window -> { Mono<ChatCompletionChunk> monoChunk = window.reduce( new ChatCompletionChunk(...), (previous, current) -> this.chunkMerger.merge(previous, current)); return List.of(monoChunk); }) .flatMap(mono -> mono); }为什么需要合并分片?大模型返回工具调用时,可能会将工具名称、参数等拆分到多个 SSE 分片中(如下例)。windowUntil和reduce通过finish_reason=tool_calls标记合并分片,确保工具调用信息完整。// 分片1:工具调用开始 { "choices": [{"delta": {"tool_calls": [{"name": "current_date", "arguments": ""}]}}] } // 分片2:工具调用结束 { "choices": [{"delta": {}, "finish_reason": "tool_calls"}] }3.2 步骤六:响应格式转换(ChatResponse 处理)这一步将模型返回的ChatCompletionChunk转换为 Spring AI 统一的ChatResponse格式,同时处理 token 用量统计:Flux<ChatResponse> chatResponse = completionChunks // 转换为ChatCompletion对象 .map(this::chunkToChatCompletion) // 构建ChatResponse .switchMap(chatCompletion -> Mono.just(chatCompletion).map(chatCompletion2 -> { try { String id = chatCompletion2.id() == null ? "NO_ID" : chatCompletion2.id(); // 转换为Generation列表(核心数据) List<Generation> generations = chatCompletion2.choices().stream().map(choice -> { // 缓存角色信息 if (choice.message().role() != null) { roleMap.putIfAbsent(id, choice.message().role().name()); } // 构建元数据(ID、角色、完成原因等) Map<String, Object> metadata = Map.of( "id", id, "role", roleMap.getOrDefault(id, ""), "index", choice.index() != null ? choice.index() : 0, "finishReason", choice.finishReason() != null ? choice.finishReason().name() : ""); return buildGeneration(choice, metadata, request); }).toList(); // 处理token用量统计(流式模式下用量通常在最后返回) OpenAiApi.Usage usage = chatCompletion2.usage(); Usage currentChatResponseUsage = usage != null ? getDefaultUsage(usage) : new EmptyUsage(); Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse); return new ChatResponse(generations, from(chatCompletion2, null, accumulatedUsage)); } catch (Exception e) { log.error("Error processing chat completion", e); return new ChatResponse(List.of()); } })) // 滑动窗口解决流式用量延迟问题 .buffer(2, 1) .map(bufferList -> { ChatResponse firstResponse = bufferList.get(0); if (request.streamOptions() != null && request.streamOptions().includeUsage()) { if (bufferList.size() == 2) { ChatResponse secondResponse = bufferList.get(1); // 用下一个响应的usage更新当前响应 Usage usage = secondResponse.getMetadata().getUsage(); if (!UsageCalculator.isEmpty(usage)) { return new ChatResponse(firstResponse.getResults(), from(firstResponse.getMetadata(), usage)); } } } return firstResponse; });总结:该步骤完成格式转换和用量统计,为后续工具调用判断提供标准化的ChatResponse对象。3.3 核心:Tool 工具方法的调用逻辑(步骤七详解)步骤七是工具调用的核心触发点,通过判断响应是否需要工具执行,决定是否调用ToolCallingManager:Flux<ChatResponse> flux = chatResponse.flatMap(response -> { // 判断是否需要执行工具调用(核心条件) if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) { return Flux.defer(() -> { // 执行工具调用(同步操作) var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response); // 判断是否直接返回工具结果给客户端 if (toolExecutionResult.returnDirect()) { return Flux.just(ChatResponse.builder().from(response) .generations(ToolExecutionResult.buildGenerations(toolExecutionResult)) .build()); } else { // 不直接返回:将工具结果作为新输入继续请求模型 return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()), response,false); } }).subscribeOn(Schedulers.boundedElastic()); } else { // 无需工具调用,直接返回原响应 return Flux.just(response); } }) // 监控相关处理(略) .doOnError(observation::error) .doFinally(s -> observation.stop()) .contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));3.3.1 工具调用的执行:executeToolCalls进入DefaultToolCallingManager的executeToolCalls方法,这是工具调用的统筹逻辑:@Override public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse) { // 验证输入 Assert.notNull(prompt, "prompt cannot be null"); Assert.notNull(chatResponse, "chatResponse cannot be null"); // 查找包含工具调用的响应 Optional<Generation> toolCallGeneration = chatResponse.getResults() .stream() .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())) .findFirst(); if (toolCallGeneration.isEmpty()) { throw new IllegalStateException("No tool call requested by the chat model"); } AssistantMessage assistantMessage = toolCallGeneration.get().getOutput(); // 构建工具上下文 ToolContext toolContext = buildToolContext(prompt, assistantMessage); // 实际执行工具调用 InternalToolExecutionResult internalToolExecutionResult = executeToolCall(prompt, assistantMessage, toolContext); // 构建工具执行后的对话历史 List<Message> conversationHistory = buildConversationHistoryAfterToolExecution(prompt.getInstructions(), assistantMessage, internalToolExecutionResult.toolResponseMessage()); return ToolExecutionResult.builder() .conversationHistory(conversationHistory) .returnDirect(internalToolExecutionResult.returnDirect()) .build(); }3.3.2 工具调用的核心执行:executeToolCallexecuteToolCall是工具方法实际被调用的地方,负责匹配工具、执行调用、收集结果:private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMessage assistantMessage, ToolContext toolContext) { // 从配置中获取工具回调列表 List<ToolCallback> toolCallbacks = List.of(); if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) { toolCallbacks = toolCallingChatOptions.getToolCallbacks(); } // 存储工具响应结果 List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>(); // 标记是否直接返回结果 Boolean returnDirect = null; // 遍历执行每个工具调用 for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) { // 提取工具名称和参数 String toolName = toolCall.name(); String toolInputArguments = toolCall.arguments(); // 匹配对应的ToolCallback(工具实现) ToolCallback toolCallback = toolCallbacks.stream() .filter(tool -> toolName.equals(tool.getToolDefinition().name())) .findFirst() .orElseGet(() -> this.toolCallbackResolver.resolve(toolName)); if (toolCallback == null) { throw new IllegalStateException("No ToolCallback found for tool name: " + toolName); } // 处理returnDirect标记(所有工具都要求直接返回才为true) if (returnDirect == null) { returnDirect = toolCallback.getToolMetadata().returnDirect(); } else { returnDirect = returnDirect && toolCallback.getToolMetadata().returnDirect(); } // 构建监控上下文 ToolCallingObservationContext observationContext = ToolCallingObservationContext.builder() .toolDefinition(toolCallback.getToolDefinition()) .toolMetadata(toolCallback.getToolMetadata()) .toolCallArguments(toolInputArguments) .build(); // 执行工具调用(含监控) String toolCallResult = ToolCallingObservationDocumentation.TOOL_CALL .observation(...) .observe(() -> { String toolResult; try { // 核心:调用工具的call方法执行实际逻辑 toolResult = toolCallback.call(toolInputArguments, toolContext); } catch (ToolExecutionException ex) { // 处理工具执行异常 toolResult = this.toolExecutionExceptionProcessor.process(ex); } observationContext.setToolCallResult(toolResult); return toolResult; }); // 收集工具响应 toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, toolCallResult != null ? toolCallResult : "")); } // 返回执行结果 return new InternalToolExecutionResult(new ToolResponseMessage(toolResponses, Map.of()), returnDirect); }总结:从响应中提取工具调用信息(名称、参数);通过ToolCallback匹配对应的工具实现;调用工具的call方法执行实际逻辑(如查询数据库、调用 API 等);收集工具执行结果,构建新的对话历史;根据returnDirect决定是否直接返回结果或继续请求模型。最后再来看一下call方法,比较简单,就是执行我们的Tool工具方法逻辑啦@Override public String call(String toolInput, @Nullable ToolContext toolContext) { Assert.hasText(toolInput, "toolInput cannot be null or empty"); logger.debug("Starting execution of tool: {}", this.toolDefinition.name()); I request = JsonParser.fromJson(toolInput, this.toolInputType); O response = this.toolFunction.apply(request, toolContext); logger.debug("Successful execution of tool: {}", this.toolDefinition.name()); return this.toolCallResultConverter.convert(response, null); }4. 整体流程梳理:Tool 调用的完整链路结合源码解析,Spring AI Tool 工具调用的完整流程可概括为:配置准备:合并默认配置与运行时配置,生成包含工具信息的Prompt;模型请求:通过chatCompletionStream向大模型发送流式请求,获取 SSE 响应;分片处理:合并工具调用相关的分片,确保工具信息完整;格式转换:将模型响应转为ChatResponse,标准化数据格式;工具判断:检查响应是否包含工具调用请求;工具执行:通过ToolCallingManager匹配工具实现,执行call方法获取结果;结果处理:根据配置返回工具结果或用结果继续请求模型,形成对话闭环。结语本文从业务代码出发,逐步深入 Spring AI 的源码细节,重点解析了 Tool 工具方法调用的核心逻辑。理解这一流程后,你不仅能更清晰地排查工具调用中的问题,还能基于源码实现自定义扩展(如自定义工具匹配逻辑、增强异常处理等)。源码阅读的关键在于 “抓大放小”,先理清整体流程,再深入核心细节。希望本文的解析方式能帮助你更高效地学习框架源码,真正做到 “知其然,更知其所以然”。如果有疑问或补充,欢迎在评论区交流!
2025年08月23日
54 阅读
1 评论
7 点赞
2025-08-20
Spring AI 实战:调用本地 Tool 工具方法实现大模型能力扩展
前言大模型虽强,但在实时信息获取(如当前日期、天气)、复杂计算等场景下存在局限。而 Tool 工具方法 正是解决这一问题的关键 —— 它让大模型能调用本地代码获取结果,弥补自身能力短板。本文将通过具体代码案例,手把手教你在 Spring AI 中集成本地 Tool,实现大模型与本地逻辑的联动。一、基础准备:Spring AI 调用大模型环境搭建在集成 Tool 前,先搭建 Spring AI 调用大模型的基础环境,确保能正常与大模型交互。1. 添加核心依赖在 pom.xml 中引入 Spring AI OpenAI 适配器依赖(兼容主流大模型平台):<!-- Spring AI 大模型接入核心依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>1.0.0</version> </dependency>2. 基础调用代码实现(1)Controller:定义接口入口创建一个简单的 HTTP 接口,接收用户提问并调用服务层处理:import com.yeeiee.ailogic.module.ai.service.chat.SpringAiTestService; import jakarta.annotation.Resource; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController @RequestMapping("ai-test") public class SpringAiTestController { @Resource private SpringAiTestService springAiTestService; // 流式输出接口(支持实时返回大模型响应) @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam("content") String content) { return springAiTestService.stream(content); } }(2)Service:实现大模型调用逻辑定义服务接口及实现类,配置大模型连接信息并发起调用:// 服务接口 public interface SpringAiTestService { // 大模型流式输出方法 Flux<String> stream(String content); } // 服务实现类 import cn.hutool.core.util.StrUtil; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @Service public class SpringAiTestServiceImpl implements SpringAiTestService { @Override public Flux<String> stream(String content) { // 配置大模型平台(以硅基流动为例,兼容 OpenAI 接口格式) OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.siliconflow.cn") // 硅基流动 API 地址 .apiKey(System.getenv("SiliconFlow_API")) // 从环境变量获取 API Key .build()) .defaultOptions(OpenAiChatOptions.builder() .model("Qwen/Qwen3-30B-A3B-Instruct-2507") // 指定模型 .build()) .build(); // 构建提问并发起流式调用 Prompt prompt = new Prompt(content); Flux<ChatResponse> stream = chatModel.stream(prompt); // 处理响应流,提取文本内容 return stream.map(chunk -> { String text = chunk.getResult() != null && chunk.getResult().getOutput() != null ? chunk.getResult().getOutput().getText() : ""; return StrUtil.nullToDefault(text, ""); // 避免 null 结果 }); } }3. 未集成 Tool 时的问题当我们调用接口提问 “今天是几号?” 时,大模型因无实时能力,回答明显不准确:这正是需要 Tool 工具方法的场景 —— 让大模型调用本地代码获取真实日期。二、核心实现:集成本地 Tool 工具方法通过 Spring AI 的 Tool 机制,让大模型在需要时自动调用本地代码(如获取当前日期),步骤如下:1. 创建本地 Tool 工具类定义一个获取当前日期的工具类,交给 Spring 管理(需实现 Function 接口):import com.fasterxml.jackson.annotation.JsonClassDescription; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; import java.util.function.Function; /** * 本地 Tool:获取当前日期 */ @Slf4j @Component("current_date") // 组件名称,用于后续指定 Tool public class CurrentDateToolFunction implements Function<CurrentDateToolFunction.Request, CurrentDateToolFunction.Response> { // 工具方法请求参数(无参数时为空类) @Data @JsonClassDescription("查询今天的日期") // 描述工具用途,帮助大模型理解 public static class Request { } // 工具方法响应结果 @Data @AllArgsConstructor @NoArgsConstructor public static class Response { private String date; // 日期结果(格式:yyyy-MM-dd) } // 核心逻辑:获取当前日期并返回 @Override public Response apply(Request request) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); String currentDate = dateFormat.format(new Date()); log.info("调用本地 Tool 获取当前日期:{}", currentDate); return new Response(currentDate); } }关键说明:类上添加 @Component("current_date"),指定 Tool 名称为 current_date;Request 和 Response 类用于定义工具的输入输出格式,需配合 JSON 注解描述用途;apply 方法实现具体逻辑(此处为获取当前日期)。2. 修改大模型配置,启用 Tool在 Service 实现类中,为大模型配置 Tool 相关参数,使其能调用本地工具:@Override public Flux<String> stream(String content) { // 配置大模型,添加 Tool 支持 OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.siliconflow.cn") .apiKey(System.getenv("SiliconFlow_API")) .build()) .defaultOptions(OpenAiChatOptions.builder() .model("Qwen/Qwen3-30B-A3B-Instruct-2507") .toolNames("current_date") // 指定启用的 Tool 名称(对应@Component的value) .build()) .toolCallingManager(SpringUtil.getBean(ToolCallingManager.class)) // 注入 Tool 调用管理器(Spring AI 提供) .build(); // 后续调用逻辑与之前一致... Prompt prompt = new Prompt(content); Flux<ChatResponse> stream = chatModel.stream(prompt); return stream.map(chunk -> { String text = chunk.getResult() != null && chunk.getResult().getOutput() != null ? chunk.getResult().getOutput().getText() : ""; return StrUtil.nullToDefault(text, ""); }); }核心配置:toolNames("current_date"):告诉大模型可调用名为 current_date 的 Tool;toolCallingManager:注入 Spring AI 提供的 ToolCallingManager,负责处理 Tool 调用流程。三、测试验证:Tool 工具调用效果再次调用接口提问 “今天是几号?”,大模型会自动触发 current_date 工具调用:1. 大模型响应结果大模型先说明 “将调用工具获取日期”,随后返回通过本地代码获取的准确日期。2. 本地日志验证查看应用日志,确认 Tool 被成功调用:2025-08-20 15:19:44.637 | INFO 28998 | boundedElastic-1 [TID: N/A] c.y.a.m.a.s.m.t.CurrentDateToolFunction | 调用本地 Tool 获取当前日期:2025-08-20总结通过本文案例,我们实现了 Spring AI 与本地 Tool 工具的集成,核心步骤可概括为:定义 Tool 工具类(实现 Function 接口,交给 Spring 管理);在大模型配置中指定 Tool 名称和调用管理器;大模型会根据提问自动判断是否调用 Tool,获取结果后整理回答。这种方式让大模型突破了自身局限,能灵活扩展实时数据获取、复杂计算等能力。下一篇将深入源码解析 Spring AI Tool 的底层实现原理,敬请期待!
2025年08月20日
76 阅读
0 评论
2 点赞
2025-08-13
Spring AI 无法获取大模型深度思考内容?解决方案来了
大模型的 “深度思考” 能力(即生成回答前的推理过程)正在成为提升交互体验的关键比如下面图中所看到的,Qwen 等模型会将推理过程放在reasoning_content字段中,让用户看到 “思考过程” 而非直接得到结果。但在 Java 开发中,使用 Spring AI 或 LangChain4J 等框架时,你可能会发现:这些框架会忽略reasoning_content字段,只返回最终回答。 这篇文章就来拆解这个问题的原因,并提供一套通过自定义接口调用实现 “获取完整思考过程” 的解决方案。问题:为什么框架无法获取深度思考内容?目前主流 Java 框架(Spring AI、LangChain4J)在设计时,默认只处理大模型的 “最终回答”(通常在content字段),而忽略了部分模型新增的 “思考过程” 字段(如reasoning_content)。具体表现为:字段被过滤:框架的响应解析逻辑中,没有处理reasoning_content的代码,导致这部分数据被直接丢弃。交互体验割裂:大模型的生成逻辑是 “先思考、后输出”,但框架会等待完整结果生成后才返回,用户需要长时间等待,且看不到中间思考过程。举个例子:当你调用 Qwen 模型时,原始接口返回会包含两部分内容:reasoning_content:“用户问我是谁,我需要先介绍自己的名字,再说明我的能力,还要保持友好的语气……”(思考过程)content:“我是通义千问,由通义实验室研发的超大规模语言模型。我能够帮助用户回答问题、创作文字,以及进行对话交流。如果你有任何问题或需要帮助,随时告诉我!”(最终回答)但通过 Spring AI 调用时,你只能拿到content部分,错失了展示 “思考过程” 的机会。解决方案:自定义接口调用,掌控原始数据既然框架的封装会丢失信息,那最直接的方案就是:绕过框架的限制,直接调用大模型的原生 API,获取完整响应数据。这种方式的优势在于:可获取所有字段(包括reasoning_content、usage等元数据);灵活控制流式输出逻辑,实现 “思考过程” 和 “最终回答” 的实时展示;便于业务扩展(如自定义缓存、日志、过滤规则等)。下面以 “调用硅基流动接口获取 Qwen 模型的思考过程” 为例,演示具体实现。实战:用 Spring WebFlux 调用硅基流动 API硅基流动是一个大模型聚合平台,支持 Qwen、GPT 等模型的调用,其 API 会返回完整的reasoning_content字段。我们用 Spring WebFlux(响应式 HTTP 客户端)实现调用,既能处理流式输出,又能获取原始响应。步骤 1:了解接口参数参考硅基流动官方 API 文档,核心请求参数如下:messages:对话历史(包含role和content);model:模型名称(如moonshotai/Kimi-Dev-72B);stream:是否开启流式输出(设为true,实时获取思考和回答);temperature:控制输出随机性(0-1 之间)。步骤 2:编码实现(获取完整响应)import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; public class SiliconFlowTest { // 硅基流动API地址 private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions"; // 你的API Key(从硅基流动控制台获取) private static final String API_KEY = "your api-key"; private static final String NULL_MESSAGE = "null"; @Test public void testGetReasoningContent() { // 1. 构建请求体(包含对话和参数) String requestBody = """ { "messages": [ { "content": "你是一个聊天大师,请回答用户的问题。", "role": "system" }, { "content": "什么是Java响应式编程?", "role": "user" } ], "model": "Qwen/Qwen3-8B", "stream": true, "temperature": 0.7 } """; // 2. 用WebClient发送POST请求(响应式客户端) WebClient webClient = WebClient.create(API_URL); Flux<String> responseFlux = webClient.post() .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + API_KEY) // 认证头 .bodyValue(requestBody) .retrieve() .bodyToFlux(String.class); // 流式接收原始响应 // 3. 处理响应(提取思考过程和最终回答) responseFlux.subscribe( // 每收到一段流式数据(SSE格式) chunk -> { // 解析原始响应(实际项目中建议用JSON工具解析) String reasoning = extractField(chunk, "reasoning_content"); String answer = extractField(chunk, "content"); if (!NULL_MESSAGE.equals(reasoning)) { System.out.println("[思考过程] " + reasoning); } if (!NULL_MESSAGE.equals(answer)) { System.out.println("[最终回答] " + answer); } }, // 错误处理 error -> System.err.println("请求错误:" + error.getMessage()), // 完成回调 () -> System.out.println("\n=== 输出结束 ===") ); // 阻塞等待(测试环境用,实际项目无需此代码) try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } } // 简易提取字段的工具方法(实际项目建议用Jackson/Gson解析) private String extractField(String chunk, String field) { String prefix = "\"" + field + "\":"; if (chunk.contains(prefix)) { String sub = chunk.split(prefix)[1].split(",\"")[0]; return sub.replace("\"", "").replace("\\n", "\n").trim(); } return ""; } }代码说明请求构建:通过 JSON 字符串定义对话和参数,重点开启stream: true以实时获取数据。响应处理:用Flux<String>接收流式响应(SSE 格式,每段是一个 JSON 片段);通过subscribe()回调实时解析reasoning_content(思考过程)和content(最终回答);实际项目中建议用 Jackson 等工具解析 JSON,替代示例中的简易字符串处理。核心价值:通过直接处理原始响应,同时获取 “思考过程” 和 “最终回答”,为前端展示(如分区域显示思考和回答)提供数据支持。业务扩展:如何在前端展示思考过程?获取到reasoning_content后,可通过以下方式提升用户体验:分区域展示:前端用两个容器,一个实时显示思考过程(灰色小字),一个显示最终回答(黑色大字)。格式标记:在后端给思考过程加上特殊标记(如<think>和</think>),前端解析时按标记区分:加载动画:在思考过程输出时,前端显示 “正在思考…” 的动画,降低用户等待焦虑。总结当 Spring AI 等框架无法满足 “获取大模型深度思考内容” 的需求时,直接调用原生 API 是最灵活的解决方案。通过 Spring WebFlux 处理流式响应,既能实时获取reasoning_content和content,又能保留扩展空间,轻松实现个性化业务逻辑。如果你需要更复杂的功能(如多模型切换、对话历史管理),可以在此基础上封装工具类,或结合响应式编程框架(如 Project Reactor)优化数据流处理。最后,附上两个链接:硅基流动 API 文档Spring WebFlux 响应式编程入门(我的另一篇博客)
2025年08月13日
525 阅读
0 评论
3 点赞
1
2
3
4