首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
231 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
202 阅读
3
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
91 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
66 阅读
5
解决 Mac 版 PicGo 无法打开问题:“已损坏,无法打开” 报错处理指南
37 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
java虚拟机
JVM
保姆级教程
Java
Spring AI
SpringBoot
Nginx
WebFlux
Spring
cdn
https
dcdn
网站加速
Tool
图片导出
服务部署
源码解析
单点登录
google
sso
Luca Ju
累计撰写
35
篇文章
累计收到
1
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
35
篇与
的结果
2025-09-09
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
在小程序开发中,经常会遇到这样的场景:用户在填写表单、编辑数据等页面操作时,若误触左上角返回按钮或右滑返回,可能导致未保存的数据丢失。本文将详细介绍如何基于微信小程序原生组件 page-container,实现页面离开前的确认弹窗功能,确保用户操作安全。一、功能核心:为什么选择 page-container?实现返回确认的关键在于拦截页面的返回行为,而微信小程序原生组件 page-container 提供了 bindbeforeleave 事件,能完美监听以下两种返回场景:左上角原生返回按钮点击右滑手势返回(包括安卓物理返回键)相比自定义导航栏 + 拦截路由的方案,page-container 无需修改页面路由结构,且能原生兼容所有返回方式,稳定性更高。官方文档参考:page-container | 微信开放文档二、实现步骤:三步完成返回确认1. 页面结构:嵌套 page-container 与条件渲染核心思路是用 wx:if 控制页面整体显示 / 隐藏,内部嵌套 page-container 组件拦截返回事件,具体结构如下:<!-- 最外层用 wx:if 控制页面是否显示 --> <view wx:if="{{isShow}}"> <!-- page-container 核心组件:拦截返回行为 --> <page-container show="{{isShow}}" <!-- 控制组件显示,与页面显示状态同步 --> overlay="{{false}}" <!-- 关闭遮罩层(根据需求选择,true则显示半透明遮罩) --> custom-style="height:100vh;overflow:scroll" <!-- 让组件占满屏幕并支持滚动 --> bindbeforeleave="onBeforeLeave" <!-- 返回前触发的事件 --> > <!-- 你的业务页面内容 --> <view class="page-content"> <view class="form-item"> <label>用户名:</label> <input placeholder="请输入用户名" value="{{username}}" bindinput="handleInput" /> </view> <view class="form-item"> <label>备注:</label> <textarea placeholder="请输入备注" value="{{remark}}" bindinput="handleTextarea" /> </view> <!-- 其他业务组件... --> </view> </page-container> </view>关键属性说明:属性名作用取值建议show控制 page-container 显示 / 隐藏与外层 wx:if 的 isShow 同步,确保状态一致overlay是否显示背景遮罩表单页建议设为 false(避免遮罩遮挡表单),弹窗类页面可设为 truecustom-style自定义组件样式必须设置 height:100vh(占满屏幕),加 overflow:scroll 确保页面内容可滚动bindbeforeleave返回前触发的事件核心事件,用于弹出确认弹窗2. JS 逻辑:处理返回确认与页面状态通过 onBeforeLeave 事件拦截返回行为,弹出确认弹窗,根据用户选择决定是否真正返回;同时用 isShow 控制页面显示状态,避免弹窗消失后页面残留。Page({ /** * 页面初始数据 */ data: { isShow: true, // 控制页面显示/隐藏 username: '', // 示例:表单数据 remark: '' // 示例:表单数据 }, /** * 页面返回前触发:核心逻辑 */ onBeforeLeave() { const that = this; // 先隐藏页面(避免弹窗显示时页面仍可操作) that.setData({ isShow: false }); // 弹出确认弹窗 wx.showModal({ title: '确认退出吗?', content: '当前数据未保存,退出后将丢失', cancelText: '取消', // 自定义按钮文本(可选) confirmText: '确认退出', success: (res) => { if (res.confirm) { // 用户确认退出:返回上一页 wx.navigateBack({ delta: 1, // 返回层级(1=上一页) fail: (err) => { // 异常处理:若返回失败,强制关闭当前页面 wx.redirectTo({ url: '/pages/index/index' }); } }); } else if (res.cancel) { // 用户取消退出:重新显示页面 that.setData({ isShow: true }); } }, fail: (err) => { // 弹窗调用失败时,恢复页面显示 that.setData({ isShow: true }); console.error('弹窗调用失败:', err); } }); }, // 示例:表单输入事件(根据业务需求添加) handleInput(e) { this.setData({ username: e.detail.value }); }, handleTextarea(e) { this.setData({ remark: e.detail.value }); } });逻辑关键点:先隐藏页面:调用 setData({ isShow: false }) 后,页面会暂时消失,避免用户在弹窗显示时继续操作页面;用户选择分支:确认退出:调用 wx.navigateBack() 返回上一页,同时添加 fail 回调处理异常;取消退出:重新设置 isShow: true,恢复页面显示;异常兜底:在 showModal 的 fail 回调中恢复页面显示,避免因弹窗调用失败导致页面永久隐藏。3. 样式优化:确保页面正常显示为避免 page-container 嵌套导致的样式异常,需添加基础样式确保页面占满屏幕且布局正常:/* 页面外层容器:确保无默认边距 */ page { margin: 0; padding: 0; background-color: #f5f5f5; /* 与页面内容背景协调 */ } /* 业务页面内容样式:根据需求调整 */ .page-content { padding: 20rpx; } .form-item { display: flex; flex-direction: column; margin-bottom: 30rpx; } .form-item label { font-size: 28rpx; color: #333; margin-bottom: 10rpx; } .form-item input, .form-item textarea { padding: 20rpx; border: 1rpx solid #eee; border-radius: 8rpx; font-size: 26rpx; }三、效果演示当用户触发返回行为(左上角按钮 / 右滑)时,会弹出如下确认弹窗:点击「取消」:弹窗关闭,页面重新显示,用户可继续操作;点击「确认退出」:弹窗关闭,页面返回上一页,未保存数据提示生效。四、总结本文基于微信小程序原生组件 page-container,通过 “条件渲染 + 事件拦截 + 状态控制” 三步,实现了兼容所有返回方式的确认弹窗功能。核心优势在于:原生兼容:无需自定义导航栏,直接支持左上角返回和右滑返回;状态安全:通过 isShow 控制页面显示,避免弹窗交互时的页面异常;易于扩展:可根据业务需求修改弹窗文案、添加数据保存逻辑(如点击 “确认” 前自动保存草稿)。如果需要进一步优化,可考虑添加 “自动保存草稿” 功能,或根据页面是否有修改过的内容动态判断是否需要弹出确认弹窗(避免无操作时冗余弹窗)。
2025年09月09日
91 阅读
0 评论
1 点赞
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日
16 阅读
1 评论
4 点赞
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日
14 阅读
0 评论
1 点赞
2025-08-18
从零开始:Java 服务部署与 HTTPS 访问全攻略
前言在现代 Web 应用开发中,将 Java 服务部署到服务器并通过 HTTPS 安全访问是一项基础且重要的技能。本文将详细记录从环境准备到最终实现 HTTPS 访问的完整过程,适合新手开发者参考学习。一、JDK 环境安装Java 服务运行依赖 JDK 环境,我们选择安装开源免费的 OpenJDK 17 版本。1. 更新系统索引包首先需要更新 Ubuntu 系统的软件包索引,确保能获取到最新的软件版本信息:sudo apt update2. 安装 OpenJDK 17执行以下命令安装 OpenJDK 17:sudo apt install openjdk-17-jdk安装过程中系统会自动处理依赖关系,无需额外操作。3. 验证 JDK 安装结果安装完成后,通过以下命令验证是否安装成功:java -version如果安装成功,会显示类似如下的版本信息:openjdk version "17.0.x" 20xx-xx-xx OpenJDK Runtime Environment (build 17.0.x+xx-Ubuntu-1ubuntux) OpenJDK 64-Bit Server VM (build 17.0.x+xx-Ubuntu-1ubuntux, mixed mode, sharing)4. 配置 JAVA_HOME 环境变量为了让系统和其他工具能正确识别 JDK 位置,需要配置 JAVA_HOME 环境变量:echo "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> ~/.bashrc source ~/.bashrc执行完成后,JAVA_HOME 环境变量将在每次登录时自动生效。二、Java 服务打包与部署完成 JDK 环境配置后,接下来需要将开发好的 Java 服务打包并上传到服务器。1. 服务打包使用 Maven 工具对 Java 项目进行打包,推荐通过 IDEA 的 Maven 插件操作:在 IDEA 右侧的 Maven 面板中找到项目展开 Lifecycle 选项双击 package 命令执行打包操作打包成功后,会在项目的 target 目录下生成对应的 JAR 文件2. 上传服务到服务器将打包好的 JAR 文件通过 SFTP 工具(如 Termius)或 scp 命令上传到服务器的指定目录,例如/opt/java/services/目录。3. 启动 Java 服务通过以下命令启动 Java 服务:nohup java -jar /opt/java/services/your-service.jar &其中&符号表示让服务在后台运行。如果需要更完善的服务管理,建议配置 systemd 服务。三、Nginx 配置实现 HTTPS 访问为了实现 HTTPS 访问并优化请求转发,我们需要配置 Nginx 作为反向代理服务器。1. 创建或编辑 Nginx 配置文件执行以下命令创建新的 Nginx 配置文件:sudo nano /etc/nginx/sites-available/your_domain.conf如果你已有配置文件,直接编辑对应文件即可。2. 配置 HTTPS 与反向代理在配置文件中添加以下内容,实现 HTTP 到 HTTPS 的跳转以及请求转发:server { listen 80; server_name yourdomain.com; # 替换为你的实际域名 # 将所有HTTP请求强制跳转至HTTPS return 301 https://$host$request_uri; } server { listen 443 ssl; server_name yourdomain.com; # 替换为你的实际域名 # SSL证书配置(请替换为你的证书实际路径) ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # SSL安全优化配置 ssl_protocols TLSv1.2 TLSv1.3; # 支持的TLS协议版本 ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_session_cache shared:SSL:10m; # SSL会话缓存设置 ssl_session_timeout 10m; # SSL会话超时时间 # 将所有 /app 开头的请求转发到Java服务的9999端口 location /app/ { proxy_pass http://127.0.0.1:9999; # 传递原始请求信息(HTTPS环境关键配置) proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 重要:传递HTTPS协议信息 # 超时设置(根据实际服务需求调整) proxy_connect_timeout 300s; proxy_read_timeout 300s; } # 其他路径处理 location / { # 可以根据需求配置为返回404或指向其他服务 return 404; } }3. 启用 Nginx 配置创建符号链接将配置文件添加到 sites-enabled 目录,使 Nginx 能够识别该配置:sudo ln -s /etc/nginx/sites-available/your_domain.conf /etc/nginx/sites-enabled/4. 检查并重启 Nginx配置完成后,先检查配置文件是否有语法错误:sudo nginx -t如果显示 "nginx: configuration file /etc/nginx/nginx.conf test is successful",说明配置无误,可以重启 Nginx 使配置生效:sudo systemctl restart nginx四、验证 HTTPS 访问完成以上所有步骤后,通过浏览器访问https://yourdomain.com/app/user/login(替换为你的实际域名和接口路径),如果能正常访问并获得服务响应,说明整个部署过程成功完成。常见问题排查如果无法访问 HTTPS 服务,先检查服务器防火墙是否开放 443 端口证书路径错误会导致 Nginx 启动失败,需确保证书文件存在且权限正确Java 服务未启动或端口被占用会导致 502 错误,可通过netstat -tlnp查看端口占用情况配置文件修改后未重启 Nginx 会导致配置不生效,记得每次修改后重启服务通过本文的步骤,你已经成功实现了 Java 服务的部署和 HTTPS 访问配置。在实际生产环境中,还可以根据需求添加服务监控、日志管理等功能,进一步提升服务的稳定性和可维护性。
2025年08月18日
16 阅读
0 评论
0 点赞
2025-08-15
个人博客提交搜索引擎收录的保姆级教程
前言搭建完个人博客后,持续输出优质内容的同时,让博客被搜索引擎收录是提升曝光的关键。本文以 Google、Baidu、Bing 为例,手把手教你完成收录提交,其他搜索引擎可举一反三~一、Google 收录:最快见效的全球引擎Google 是全球影响力最大的搜索引擎,收录速度快且覆盖范围广,优先推荐配置。1. 登录 Google 站长平台访问 Google 搜索控制台,点击 “添加属性”,选择 “网址前缀”,输入你的博客完整地址(如 https://www.lucaju.cn)。2. 网站验证:证明你是网站主人选择 “HTML 文件上传” 验证方式,下载 Google 提供的验证文件(如 google123456.html)。将文件上传至博客根目录(以 Typecho 为例,上传到Typecho的安装路径下)。点击 “验证” 按钮,提示成功即完成验证。3. 提交 Sitemap:引导搜索引擎抓取什么是 Sitemap?Sitemap(网站地图)是包含网站所有页面链接的 XML 文件,帮助搜索引擎快速了解网站结构,提升抓取效率。(1)生成 SitemapTypecho 博客:安装 Sitemap 插件,启用后自动在根目录生成 sitemap.xml(访问 https://你的域名/sitemap.xml 可查看)。其他建站工具:搜索对应平台的 Sitemap 插件(如 WordPress 直接安装 Yoast SEO 插件)。(2)提交 Sitemap 到 Google在 Google 搜索控制台左侧菜单选择 “sitemaps”,输入 sitemap.xml 并提交,等待 Google 解析抓取。4. 验证收录结果提交后约 1-2 小时(Google 收录速度极快),可通过 Google 搜索 site:你的域名(如 site:lucaju.cn)验证是否收录。二、Baidu 收录:聚焦中文用户百度是国内主流搜索引擎,流程与 Google 类似,但需注意细节差异。1. 登录百度搜索资源平台访问 百度搜索资源平台,注册并登录后,点击 “添加网站”,输入博客域名。2. 网站验证与类型选择选择网站类型验证方式推荐 “HTML 文件验证”,步骤同 Google(下载验证文件并上传至根目录)3. 内容提交:替代 Sitemap 的方案目前百度对 Sitemap.xml 支持有限,可通过以下方式提交内容:手动提交:在 “链接提交” 页面逐条输入新文章 URL。API 提交:通过技术手段自动推送新文章(适合开发者)。三、Bing 收录:一键复用 Google 配置Bing 是全球第二大搜索引擎,支持直接导入 Google 的收录配置,省时省力。1. 登录 Bing 站长平台访问 Bing 网站管理工具,点击 “添加站点”,输入博客地址。2. 一键导入 Google 数据在验证步骤选择 “导入 Google 搜索控制台数据”,授权后可直接复用 Google 的验证状态和 Sitemap 配置,无需重复操作。3. 等待收录提交后 Bing 会自动解析 Sitemap,收录进度可在 “站点地图” 页面查看。总结三大搜索引擎收录核心流程:平台验证 → 提交 Sitemap / 链接 → 等待抓取。Google:收录最快,优先推荐,适合全球用户;Baidu:聚焦国内,需手动提交新内容;Bing:支持导入 Google 配置,操作最简单。持续输出优质内容能加速收录,快去让你的博客被更多人看到吧!
2025年08月15日
23 阅读
0 评论
0 点赞
1
2
3
...
7