Agent Skill 踩坑记录 | SpringBoot 打包后 Skill 加载失败问题排查与解决
在之前的开发中，我使用 ClasspathSkillRegistry 作为 Agent Skill 的查找策略，本地开发环境下调试、运行都一切正常，没有出现任何异常。感兴趣的小伙伴可以回顾上一篇相关内容：https://www.lucaju.cn/index.php/archives/168/一、问题出现：上线后频繁报「Skill not found」本以为本地测试无误后，上线就能顺利运行，结果程序部署到服务器后，日志中频繁出现 Skill not found: 错误，导致相关功能完全无法使用。二、问题排查：排除常见误区遇到问题后，首先排查了最容易出现问题的两个点，均排除异常：确认 Skill 已成功打包：检查部署的 jar 包，解压后确认 skill 相关文件已正常包含在内，不存在打包遗漏的情况；确认 Skill Name 无误：核对代码中调用 Skill 的名称与配置文件中的名称，完全一致，排除拼写错误、大小写错误等问题。三、问题根源：找到官方 Issue排除上述常见问题后，推测可能是框架本身的兼容性问题，于是去 GitHub 上搜索相关问题，果然发现已有其他开发者遇到过相同情况，并且提交了 Issue：https://github.com/alibaba/spring-ai-alibaba/issues/4426从 Issue 中得知，问题根源在于 SpringBoot 项目打包成 jar 包后，路径机制发生变化，导致 ClasspathSkillRegistry 无法正常加载 resources 下的 Skill 目录，这是目前 spring-ai-alibaba 框架的一个已知 bug。四、临时解决方案：改用 FileSystemSkillRegistry由于官方尚未修复该 bug，为了不影响线上功能正常使用，我采用了临时解决方案：将 Skill 目录存放在服务器的文件系统中，通过指定真实路径获取 Skill，对应的查找策略改用 FileSystemSkillRegistry。修改后的核心代码如下：/** * 获取技能智能体钩子。 */ private static SkillsAgentHook getSkillsAgentHook() { // 注释掉原有的ClasspathSkillRegistry方式（本地可用，打包后失效） // SkillRegistry registry = ClasspathSkillRegistry.builder() // .classpathPath("skills") // .build(); /* 临时解决方案说明： spring ai alibaba 目前使用 ClasspathSkillRegistry 打包后，无法获取 Resource 下的目录 因此修改为使用 FileSystemSkillRegistry，从文件系统真实路径获取 Skill 相关 Issue：https://github.com/alibaba/spring-ai-alibaba/issues/4426 */ String skillPath = SpringUtils.getProperty("doc.skills-path"); log.info("skillPath: {}",skillPath); FileSystemSkillRegistry registry = FileSystemSkillRegistry.builder() .projectSkillsDirectory(skillPath) .build(); return SkillsAgentHook.builder() .skillRegistry(registry) .build(); }五、后续需要说明的是，这只是一个临时解决方案——使用该方案后，需要手动将 Skill 配置文件同步到服务器的指定路径（即配置项 doc.skills-path 对应的路径），增加了少量部署成本。后续会持续关注官方 Issue 的修复进度，待 bug 修复后，再切换回 ClasspathSkillRegistry 方式，减少部署环节的手动操作。也希望遇到相同问题的小伙伴，能通过这篇踩坑记录少走弯路～
上一篇博客，我们介绍了如何将本地工具封装为 MCP 服务，并成功创建客户端实现连接，感兴趣的朋友可以回顾：https://www.lucaju.cn/index.php/archives/169/今天，我们聚焦实操——如何调用 MCP 市场上的公开服务，以「高德地图 MCP 服务」为例，步骤清晰可复现，新手也能快速上手。一、先了解：MCP 公开服务市场首先给大家推荐一个优质的 MCP 公开服务平台：https://mcp.so/zh这个平台类似 MCP 服务的「GitHub」，目前已收录超过一万八千个公开 MCP 服务，涵盖地图、工具、接口等各类场景，我们今天要用的高德地图 MCP 服务也收录其中。高德地图 MCP 服务直达链接：https://mcp.so/zh/server/amap-maps/amap进入链接后，注意保存页面中的「服务器配置 JSON」（如下所示），后续配置项目时会直接用到，重点留意 env 中的 AMAP_MAPS_API_KEY 字段。二、前置准备：申请高德 API-Key调用高德地图 MCP 服务，需先获取个人 API-Key，步骤如下（3步搞定）：1. 登录高德开放平台访问高德开放平台：https://lbs.amap.com/ ，注册一个用户，完成认证后登录到后台2. 创建一个应用3. 生成 API-Key注意：服务平台选择Web服务，填写完成后提交，即可生成 API-Key，保存好该 Key，后续替换配置使用。三、编写客户端代码连接高德服务前置准备完成后，开始配置项目、编写代码，全程分为3个步骤，也很简单操作。1. 拷贝高德MCP服务中的json文件到项目中注意: 替换api_key，使用我们刚刚创建好的api_key{ "mcpServers": { "amap-maps": { "command": "npx", "args": [ "-y", "@amap/amap-maps-mcp-server" ], "env": { "AMAP_MAPS_API_KEY": "api_key" } } } }把这个json放到项目resource目录下，命名为 mcp-servers-config.json （可自定义，但需与后续配置对应）2. 修改application.yml配置文件添加 MCP 客户端配置，核心是指定上述 JSON 文件的路径spring: application: name: spring-ai-alibaba-agent ai: dashscope: api-key: ${AliQwen_API} mcp: client: type: async request-timeout: 10s toolcallback: enabled: true stdio: servers-configuration: classpath:/mcp-servers-config.json3. 编写测试代码测试代码其实和上一篇的客户端代码基本没有什么改动，我就简单放上来一些啦～@GetMapping("mcpTest") private void mcpTest() throws GraphRunnerException { ChatModel chatModel = getChatModel(); ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks(); System.out.printf(""" =====Find the tools from spring ToolCallbackProvider===== %s """, JSON.toJSONString(toolCallbacks)); // 构建智能体并绑定mcp服务 ReactAgent agent = ReactAgent.builder() .name("ip_search") .model(chatModel) .description("你是一个天气查询助手") .saver(new MemorySaver()) .toolCallbackProviders(toolCallbackProvider) .build(); // 运行时配置 RunnableConfig config = RunnableConfig.builder() .threadId("session") .build(); // 流式调用agent Flux<NodeOutput> stream = agent.stream("上海未来天气怎么样", config); StringBuffer answerString = new StringBuffer(); stream.doOnNext(output -> { if (output.node().equals("_AGENT_MODEL_")) { answerString.append(((StreamingOutput<?>) output).message().getText()); } else if (output.node().equals("_AGENT_TOOL_")) { answerString.append("\nTool Call:").append(((ToolResponseMessage) ((StreamingOutput<?>) output).message()).getResponses().get(0)).append("\n"); } }) .doOnComplete(() -> System.out.println(answerString)) .doOnError(e -> System.err.println("Stream Processing Error: " + e.getMessage())) .blockLast(); }代码编写完成后，启动项目，访问接口：http://localhost:8080/mcpTest，即可测试高德 MCP 服务调用效果。四、效果演示首先可以看到我们已经加载到了高德MCP的服务列表接下来可以看到大模型输出了对未来7天天气的回答五、总结以上就是「Spring AI Alibaba 调用 MCP 公开服务」的完整实操流程，核心是「获取 API-Key → 配置 MCP 服务 → 编写测试代码」，步骤简洁且可复现。本次实操的全部代码（含之前 Agent 相关测试代码）已上传至 GitHub，需要的朋友可以自行获取，如有疑问，欢迎在评论区交流～GitHub 链接：https://github.com/Jucunqi/spring-ai-alibaba-agent.git
一文吃透 Spring AI Alibaba + MCP：服务端搭建 + 客户端调用全流程
一、MCP概念介绍MCP（Model Context Protocol，模型上下文协议）是 Anthropic 于 2024 年推出的AI 领域统一连接协议，被称为 “AI 的 USB-C 接口”，核心是让大模型（LLM）通过标准化方式安全、灵活地调用外部工具、数据库、API 与文件系统，打破数据孤岛。从架构来看，MCP基于C/S（客户端-服务端）模式实现，因此要完成MCP调用，需分别搭建MCP服务端（暴露工具方法）和MCP客户端（调用服务端方法）。本文将基于Spring Ai Alibaba生态，完整实现“本地方法封装为MCP服务 + 客户端调用MCP服务”的全流程，步骤清晰、可直接落地。二、Spring AI MCP的介绍Spring AI MCP 为模型上下文协议提供 Java 和 Spring 框架集成。它使 Spring AI 应用程序能够通过标准化的接口与不同的数据源和工具进行交互，支持同步和异步通信模式。整体架构如下：三、搭建本地MCP服务端1. 添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> <version>1.1.2</version> </dependency>注意：此处未使用常规的spring-boot-starter-web（内置Tomcat），因为spring-ai-starter-mcp-server-webflux与Tomcat存在冲突。使用spring-boot-starter会默认通过Netty启动服务，适配MCP服务端要求。2. 配置服务端application.ymlserver: port: 8088 # 服务端口，可自定义 servlet: encoding: enabled: true force: true charset: UTF-8 # 避免中文乱码 spring: application: name: local-mcp-server # 服务端应用名称 ai: mcp: server: type: async # 异步模式，提升调用性能 name: local-mcp-server # MCP服务名称 version: 1.0.0 # 服务版本3. 添加工具方法创建工具类，将需要对外暴露的方法用@Tool注解标记，并将该类交给Spring容器管理：@Service public class WeatherService { /** * 根据城市名称获取天气信息 * @param city 城市名称 * @return 天气描述 */ @Tool(description = "根据城市名称获取天气信息") public String getWeatherByCity(String city) { return city + " 今天天气很好！"; } }4. 添加MCP服务配置（McpServerConfig）创建配置类，通过ToolCallbackProvider将工具类（WeatherService）封装为MCP服务：@Configuration public class McpServerConfig { @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }5. 启动mcp-server 服务启动Spring Boot应用，查看控制台输出，确认服务启动成功（重点关注Netty启动信息）：2026-03-19T14:33:17.730+08:00 INFO 35517 --- [local-mcp-server] [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8088 (http) 2026-03-19T14:33:17.734+08:00 INFO 35517 --- [local-mcp-server] [ main] com.jcq.server.McpServerApplication : Started McpServerApplication in 1.198 seconds 可以看到服务使用netty成功启动，端口是8088四、搭建MCP客户端1. 添加依赖client端正常配置spring-boot-starter-web，使用tomcat启动服务。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-agent-framework</artifactId> <version>1.1.2.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.0</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> <version>1.1.2</version> </dependency>2. 配置客户端application.ymlspring: application: name: spring-ai-alibaba-agent # 客户端应用名称 ai: dashscope: api-key: ${AliQwen_API} # 通义千问API密钥，建议通过环境变量配置，避免硬编码 mcp: client: type: async # 与服务端一致，异步调用 request-timeout: 60s # 调用超时时间，可根据实际调整 toolcallback: enabled: true # 启用工具回调，用于接收服务端响应 sse: # mcp类型 connections: local-mcp-server: # 这里表示mcp服务名称 url: http://localhost:8088 # MCP服务端地址（对应服务端ip端口）3. 编写测试接口@RestController public class McpClientController { @Resource private ToolCallbackProvider toolCallbackProvider; /** * 测试MCP服务调用：查询指定城市天气 * 访问地址：http://localhost:8080/mcpTest（客户端端口默认8080，可自定义） */ @GetMapping("mcpTest") private void mcpTest() throws GraphRunnerException { // 1. 初始化DashScope聊天模型（可替换为其他LLM模型） ChatModel chatModel = getChatModel(); // 2. 获取MCP服务端暴露的工具方法 ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks(); System.out.printf(""" =====Find the tools from spring ToolCallbackProvider===== %s """, JSON.toJSONString(toolCallbacks)); // 3. 构建智能体并绑定mcp服务 ReactAgent agent = ReactAgent.builder() .name("ip_search") .model(chatModel) .description("你是一个天气查询助手") .saver(new MemorySaver()) .toolCallbackProviders(toolCallbackProvider) .build(); // 4. 配置运行参数 RunnableConfig config = RunnableConfig.builder() .threadId("session") .build(); // 5. 流式调用agent Flux<NodeOutput> stream = agent.stream("上海天气怎么样", config); StringBuffer answerString = new StringBuffer(); stream.doOnNext(output -> { if (output.node().equals("_AGENT_MODEL_")) { answerString.append(((StreamingOutput<?>) output).message().getText()); } else if (output.node().equals("_AGENT_TOOL_")) { answerString.append("\nTool Call:").append(((ToolResponseMessage) ((StreamingOutput<?>) output).message()).getResponses().get(0)).append("\n"); } }) .doOnComplete(() -> System.out.println(answerString)) .doOnError(e -> System.err.println("Stream Processing Error: " + e.getMessage())) .blockLast(); } /** * 初始化DashScope聊天模型（通义千问） * @return ChatModel 聊天模型实例 */ private static ChatModel getChatModel() { DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) .build(); return DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build(); } }4. 运行测试，查看结果确保MCP服务端（8088端口）已启动；启动MCP客户端，访问接口：http://localhost:8080/mcpTest查看客户端控制台输出，若出现以下内容，说明MCP服务调用成功：Tool Call:ToolResponse[id=call_b8f00f883a784fc1b35603, name=getWeatherByCity, responseData=[{"text":"\"上海 今天天气很好！\""}]]五、总结本文通过Spring Ai Alibaba，实现了MCP协议的本地服务落地，服务端获取天气逻辑后续可以替换为真实调用api接口。欢迎大家关注我，下一篇文章我将介绍一下如何调用MCP市场上的公开服务，敬请期待～
Agent Skills | Spring Ai Alibaba从零构建可扩展 AI 智能体
在当今AI应用开发领域，让AI智能体具备灵活的工具调用能力和可扩展的技能体系，是落地复杂业务场景的核心挑战。Spring AI Alibaba 原生提供了完善的 Skill 技能支持，能让智能体实现「技能发现→按需加载→工具执行」的全流程自动化。本文将从核心概念、组件解析、实战场景、最佳实践四个维度，手把手教你掌握 Spring AI Alibaba Skill 的使用方法。一、核心概念1.1 渐进式披露（核心设计思想）Spring AI Alibaba Skill 采用渐进式披露机制，最大化提升模型效率：系统初始仅注入技能元数据（名称、描述、路径）；模型判断需要使用某技能时，调用 read_skill(skill_name) 加载完整的 SKILL.md 文档；最后按需访问技能资源、执行绑定工具。1.2 Skill 标准目录结构每个技能独立为一个子目录，SKILL.md 为强制必需文件，目录结构如下：skills/ # 技能根目录 └── pdf-extractor/ # 单个技能目录（自定义命名） ├── SKILL.md # ✅ 必需：技能核心定义文档 ├── references/ # 可选：技能参考资料 ├── examples/ # 可选：使用示例 └── scripts/ # 可选：执行脚本1.3 SKILL.md 格式规范SKILL.md 采用元数据+正文结构，元数据通过 --- 包裹，是模型识别技能的关键：--- name: pdf-extractor # 必需：小写字母、数字、连字符，最长64字符 description: 用于从PDF文档中提取文本、表格和表单数据 # 必需：简洁描述，超长会被截断 --- # PDF 提取技能 ## 功能说明 You are a PDF extraction specialist. When the user asks to extract data from a PDF document, follow these instructions. ## 使用方法 1. **Validate Input** - Confirm the PDF file path is provided. - The default path for the pdf file is the current working directory. - Use the `shell` or `read_file` tool to check if the file exists - Verify it's a valid PDF format 2. **Extract Content** - Execute the extraction script using the `shell` tool: ```bash python scripts/extract_pdf.py <pdf_file_path> ``` - The script will output JSON format with extracted data 3. **Process Results** - Parse the JSON output from the script - Structure the data in a readable format - Handle any encoding issues (UTF-8, special characters) 4. **Present Output** - Summarize what was extracted - Present data in the requested format (JSON, Markdown, plain text) - Highlight any issues or limitations ## Script Location The extraction script is located at: `scripts/extract_pdf.py` ## Output Format The script returns JSON: ```json { "success": true, "filename": "report.pdf", "text": "Full text content...", "page_count": 10, "tables": [ { "page": 1, "data": [["Header1", "Header2"], ["Value1", "Value2"]] } ], "metadata": { "title": "Document Title", "author": "Author Name", "created": "2024-01-01" } } ```scripts/extract_pdf.py简介，因为目前测试脚本的执行，所以mock返回了一个json格式的解析内容。二、核心组件解析Spring AI Alibaba Skill 体系由四大核心组件构成，各司其职、协同工作：2.1 对话模型（ChatModel）对接阿里通义千问大模型，是智能体的「大脑」，负责推理决策：private static ChatModel getChatModel() { // 构建通义千问API客户端 DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) // 环境变量配置API Key .build(); // 创建对话模型 return DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build(); }2.2 技能注册中心（SkillRegistry）负责加载、管理所有技能，Spring AI Alibaba 提供两种常用实现：ClasspathSkillRegistry：从项目类路径（resources）加载技能FileSystemSkillRegistry：从本地文件系统加载技能代码示例：SkillRegistry registry = ClasspathSkillRegistry.builder() .classpathPath("skills") // 指向resources/skills目录 .build();2.3 技能钩子（SkillsAgentHook）连接智能体与技能系统的桥梁，负责注入技能上下文、提供工具调用能力：SkillsAgentHook hook = SkillsAgentHook.builder() .skillRegistry(registry) // 绑定技能注册中心 .build();2.4 ReAct 智能体（ReactAgent）基于 ReAct（推理+执行） 模式的智能体，自主完成「思考→选技能→执行任务」的全流程：ReactAgent agent = ReactAgent.builder() .name("skills-agent") // 智能体名称 .model(chatModel) // 绑定大模型 .saver(new MemorySaver()) // 内存记忆（保存对话上下文） .hooks(List.of(hook)) // 绑定技能钩子 .build();三、实战场景场景一：技能发现（智能体自我认知）让AI智能体主动披露自身具备的所有技能，验证技能加载是否成功。核心代码private static void findSkills() throws GraphRunnerException { // 1. 初始化核心组件 ChatModel chatModel = getChatModel(); SkillsAgentHook hook = getSkillsAgentHook(); // 2. 构建技能智能体 ReactAgent agent = ReactAgent.builder() .name("skills-agent") .model(chatModel) .saver(new MemorySaver()) .hooks(List.of(hook)) .build(); // 3. 执行对话：查询技能 AssistantMessage resp = agent.call("请介绍你有哪些技能"); System.out.println(resp.getText()); }执行结果11:29:36.067 [main] INFO com.alibaba.cloud.ai.graph.skills.registry.classpath.ClasspathSkillRegistry -- Loaded 1 skills from classpath: skills 我目前拥有的技能包括： - **pdf-extractor**: 用于从 PDF 文档中提取文本、表格和表单数据，适用于分析和处理 PDF 文件。当用户需要提取、解析或分析 PDF 文件时可使用此技能。 如需了解某项技能的详细说明（例如具体操作步骤、支持的文件类型、使用示例等），我可以为您读取其完整的技能文档（`SKILL.md`）。您也可以告诉我您的具体需求（例如：“请帮我从一份PDF中提取所有表格”），我会判断是否适用某项技能并执行相应操作。 是否需要我为您详细展开某一项技能？场景二：技能使用（PDF 信息提取）结合技能系统 + Python 执行工具 + Shell 工具，实现多工具协作，完成 PDF 文本/表格提取。步骤1：自定义 Python 执行工具（GraalVM 支持）依赖如下 <dependency> <groupId>org.graalvm.polyglot</groupId> <artifactId>polyglot</artifactId> <version>24.2.1</version> </dependency> <dependency> <groupId>org.graalvm.polyglot</groupId> <artifactId>python-community</artifactId> <version>24.2.1</version> <type>pom</type> </dependency>用于执行 Python 脚本完成 PDF 解析，工具代码如下：package com.jcq.springaialibabaagent.tools; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.function.FunctionToolCallback; import java.util.function.BiFunction; /** * GraalVM 沙箱环境执行 Python 代码的工具 * 安全沙箱：禁用文件IO、进程创建、本地访问 */ public class PythonTool implements BiFunction<PythonTool.PythonRequest, ToolContext, String> { public static final String DESCRIPTION = """ Executes Python code and returns the result. - 代码必须为合法Python语法 - 沙箱执行，安全无风险 - 支持返回数字、字符串、数组、执行结果 """; private static final Logger log = LoggerFactory.getLogger(PythonTool.class); private final Engine engine; public PythonTool() { this.engine = Engine.newBuilder() .option("engine.WarnInterpreterOnly", "false") .build(); } /** 构建Spring AI 标准工具回调 */ public static ToolCallback createPythonToolCallback(String description) { return FunctionToolCallback.builder("python_tool", new PythonTool()) .description(description) .inputType(PythonRequest.class) .build(); } @Override public String apply(PythonRequest request, ToolContext toolContext) { if (request.code == null || request.code.trim().isEmpty()) { return "Error：Python代码不能为空"; } // 沙箱环境执行Python try (Context context = Context.newBuilder("python") .engine(engine) .allowAllAccess(false) .allowIO(false) .allowNativeAccess(false) .allowCreateProcess(false) .allowHostAccess(true) .build()) { log.debug("执行Python代码：{}", request.code); Value result = context.eval("python", request.code); // 结果类型转换 if (result.isNull()) return "执行完成，无返回值"; if (result.isString()) return result.asString(); if (result.isNumber() || result.isBoolean()) return result.toString(); if (result.hasArrayElements()) { StringBuilder sb = new StringBuilder("["); long size = result.getArraySize(); for (long i = 0; i < size; i++) { if (i > 0) sb.append(", "); sb.append(result.getArrayElement(i)); } return sb.append("]").toString(); } return result.toString(); } catch (PolyglotException e) { log.error("Python执行异常", e); return "执行错误：" + e.getMessage(); } } /** 工具请求参数 */ public static class PythonRequest { @JsonProperty(required = true) @JsonPropertyDescription("需要执行的Python代码") public String code; public PythonRequest() {} public PythonRequest(String code) { this.code = code; } } }步骤2：构建多技能协作智能体private static void useSkills() throws Exception { // 1. 初始化大模型 ChatModel chatModel = getChatModel(); // 2. 绑定技能钩子 + Shell工具钩子 SkillsAgentHook skillsHook = getSkillsAgentHook(); ShellToolAgentHook shellHook = ShellToolAgentHook.builder() .shellTool2(ShellTool2.builder(System.getProperty("user.dir")).build()) .build(); // 3. 构建智能体（绑定技能+工具+日志） ReactAgent agent = ReactAgent.builder() .name("skills-integration-agent") .model(chatModel) .saver(new MemorySaver()) .tools(PythonTool.createPythonToolCallback(PythonTool.DESCRIPTION)) // 自定义Python工具 .hooks(List.of(skillsHook, shellHook)) // 多钩子组合 .enableLogging(true) // 开启日志，便于调试 .build(); // 4. 执行PDF提取任务 String pdfPath = getTestSkillsDirectory() + "/pdf-extractor/skill-test.pdf"; AssistantMessage response = agent.call(String.format("请从 %s 文件中提取关键信息", pdfPath)); // 5. 输出结果 System.out.println("========== PDF提取结果 =========="); System.out.println(response.getText()); }执行结果========== The PDF extraction was successful! Here's the key information extracted from the `skill-test.pdf` file: ## Document Metadata - **Title**: Sample PDF Document - **Author**: Test Author - **Created**: 2024-01-01 - **Modified**: 2024-01-15 - **Page Count**: 5 pages ## Extracted Text The document contains: "This is extracted text from the PDF document. It contains multiple paragraphs and sections." ## Tables Found ### Table 1 (Page 1) - Product Inventory | Product | Price | Quantity | |---------|-------|----------| | Widget A | $10.00 | 100 | | Widget B | $15.00 | 50 | ### Table 2 (Page 3) - Financial Summary | Month | Revenue | Expenses | |-------|---------|----------| | January | $50,000 | $30,000 | | February | $55,000 | $32,000 |四、最佳实践建议技能规范化管理所有技能统一放在 resources/skills 目录，按业务拆分子目录；SKILL.md 严格遵循格式规范，保证模型准确识别。多钩子灵活组合基础能力：SkillsAgentHook（技能系统）；系统操作：ShellToolAgentHook（Shell命令）；自定义能力：自定义工具（Python/Java/第三方API）。开发与调试优化开发阶段开启 enableLogging(true)，查看智能体推理全流程；测试使用 MemorySaver 快速验证，生产环境替换为持久化存储。模型选型策略简单技能查询：基础版通义千问；复杂工具协作/多步骤推理：高级版大模型。
Spring AI Alibaba 人工介入实战｜Human-in-the-Loop 让 AI 更可靠
引言在构建AI智能体应用时，我们经常面临一个关键挑战：如何让AI在执行某些敏感操作前获得人工确认？Spring AI Alibaba框架提供了强大的人工介入（Human-in-the-Loop）机制，让开发者能够精确控制AI工具的执行流程，在关键节点引入人工审批环节。本文将通过一个完整的实战示例，详细介绍如何在Spring AI Alibaba应用中实现人工介入功能。什么是人工介入？人工介入是一种机制，它允许AI智能体在执行特定工具前暂停执行，等待人工审批后再继续。这种机制特别适用于：敏感操作：如数据删除、资金转账等内容生成：如文章发布、诗歌创作等需要质量把控的场景权限控制：某些需要特定权限才能执行的操作审计要求：需要记录人工决策过程的场景实战示例：诗歌创作的人工审批让我们通过一个具体的例子来理解人工介入Hook的使用。这个示例展示了如何让AI在创作诗歌前获得人工确认。1. 项目依赖配置首先，确保你的项目中包含了Spring AI Alibaba相关依赖：<dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-agent-framework</artifactId> <version>1.1.2.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.0</version> </dependency> </dependencies>2. 代码实现解析步骤1：构建AI模型// 构建DashScope API对象 DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) .build(); // 创建聊天模型 ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build();步骤2：配置工具public class PoetTool implements BiFunction<String, ToolContext, String> { public int count = 0; public PoetTool() { } @Override public String apply( @ToolParam(description = "The original user query that triggered this tool call") String originalUserQuery, ToolContext toolContext) { count++; System.out.println("Poet tool called : " + originalUserQuery); return "在城市的缝隙里， \n" + "一束光悄悄发芽， \n" + "穿过钢筋水泥的沉默， \n" + "在风中轻轻说话。 \n" + "\n" + "夜色如墨，却不再黑， \n" + "星星点亮了每一个角落， \n" + "我站在时间的边缘， \n" + "等一朵云，轻轻落下"; } public static ToolCallback createPoetToolCallback() { return FunctionToolCallback.builder("poem", new PoetTool()) .description("用来写诗的工具") .inputType(String.class) .build(); } public static ToolCallback createPoetToolCallback(String name, PoetTool poetTool) { return FunctionToolCallback.builder(name, poetTool) .description("用来写诗的工具") .inputType(String.class) .build(); } }步骤3：构建带有Hook的智能体// 这里我们配置了poem工具需要人工审批，并提供了审批时的描述信息。 Map<String, ToolConfig> approvalOn = Map.of( "poem", ToolConfig.builder() .description("请确认诗歌工具执行") .build() ); ReactAgent agent = ReactAgent.builder() .name("single_agent") .model(chatModel) .saver(new MemorySaver()) // 使用内存保存状态 .tools(List.of(createPoetToolCallback())) // 添加诗歌创作工具 .hooks(HumanInTheLoopHook.builder() .approvalOn(approvalOn) // 添加人工介入Hook .build()) .outputKey("article") .build();步骤4：创建会话配置String threadId = "user-session-001"; RunnableConfig config = RunnableConfig.builder() .threadId(threadId) .build();步骤5：执行并处理中断// 第一次调用 - 触发中断 Optional<NodeOutput> result = agent.invokeAndGetOutput( "帮我写一首100字左右的诗", config ); // 检查是否触发中断 if (result.isPresent() && result.get() instanceof InterruptionMetadata) { InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); System.out.println("检测到中断，需要人工审批"); // 获取工具反馈信息 List<InterruptionMetadata.ToolFeedback> toolFeedbacks = interruptionMetadata.toolFeedbacks(); for (InterruptionMetadata.ToolFeedback feedback : toolFeedbacks) { System.out.println("id: " + feedback.getId()); System.out.println("工具: " + feedback.getName()); System.out.println("参数: " + feedback.getArguments()); System.out.println("描述: " + feedback.getDescription()); } // 模拟人工决策（批准） InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() .nodeId(interruptionMetadata.node()) .state(interruptionMetadata.state()); toolFeedbacks.forEach(toolFeedback -> { InterruptionMetadata.ToolFeedback approvedFeedback = InterruptionMetadata.ToolFeedback.builder(toolFeedback) .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) .build(); feedbackBuilder.addToolFeedback(approvedFeedback); }); InterruptionMetadata approvalMetadata = feedbackBuilder.build(); // 使用人工反馈恢复执行 RunnableConfig resumeConfig = RunnableConfig.builder() .threadId(threadId) .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata) .build(); Optional<NodeOutput> finalResult = agent.invokeAndGetOutput("", resumeConfig); if (finalResult.isPresent()) { System.out.println("执行完成"); // 因为创建智能体的时候，指定了outputKey，所以这里我们直接获取 Object article = finalResult.get().state().data().get("article"); System.out.println("最终结果: " + article); } }3. 执行流程分析这个示例的执行流程如下：触发阶段：用户请求AI创作诗歌中断阶段：AI检测到poem工具需要人工审批，暂停执行审批阶段：系统展示工具信息，等待人工决策恢复阶段：人工批准后，AI继续执行并生成诗歌完成阶段：返回最终结果高级特性多工具审批你可以为多个工具配置审批：Map<String, ToolConfig> approvalOn = Map.of( "poem", ToolConfig.builder().description("诗歌创作工具").build(), "delete", ToolConfig.builder().description("数据删除工具").build(), "publish", ToolConfig.builder().description("内容发布工具").build() );审批结果类型支持多种审批结果：APPROVED：批准执行REJECTED：拒绝执行MODIFIED：修改参数后执行最佳实践1. 明确审批策略只为真正需要人工确认的工具配置审批提供清晰的审批描述信息考虑审批的时效性2. 用户体验优化提供友好的审批界面支持批量审批操作记录审批历史便于审计3. 错误处理try { Optional<NodeOutput> result = agent.invokeAndGetOutput(request, config); // 处理中断和结果 } catch (GraphRunnerException e) { // 处理执行异常 log.error("智能体执行失败", e); }4. 状态管理// 使用合适的Saver .saver(new MemorySaver()) // 内存存储，适合开发测试 .saver(new RedisSaver()) // Redis存储，适合生产环境 .saver(new DatabaseSaver()) // 数据库存储，适合需要持久化的场景5. 执行结果拓展Spring Ai Alibaba还为我们内置了几个其他的HookSummarizationHook（消息压缩）当对话很长时，自动压缩对话历史，防止超出模型上下文限制ModelCallLimitHook（模型调用限制）防止Agent无限调用模型，控制成本另外，我们也可以自定义Hook，这部分内容如果大家感兴趣的话，后面可以单独介绍一下下～参考资料HumanInTheLoopHook API文档
前言随着大语言模型(LLM)技术的快速发展，构建智能Agent应用变得越来越简单。本文将通过两个实际的代码示例，展示如何使用Spring AI Alibaba框架构建功能丰富的天气查询Agent，从基础的测试实现到生产级的完整应用。技术栈概述Spring AI Alibaba: 阿里巴巴开源的AI应用开发框架DashScope: 阿里云的AI模型服务平台React Agent: 基于ReAct(Reasoning and Acting)范式的智能代理示例一：SimpleTest - 快速入门添加核心依赖<dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-agent-framework</artifactId> <version>1.1.2.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.0</version> </dependency> </dependencies>代码结构分析@Test void agentTest() throws GraphRunnerException { // 1. 初始化 DashScope API DashScopeApi dashScopeApi = DashScopeApi.builder() .apiKey(System.getenv("AliQwen_API")) .build(); // 2. 创建 ChatModel ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .build(); // 3. 定义天气工具 ToolCallback weatherTool = FunctionToolCallback.builder("get_weather", new WeatherTool()) .description("获取某个城市的天气") .inputType(String.class) .build(); // 4. 构建React Agent ReactAgent agent = ReactAgent.builder() .name("weather_agent") .model(chatModel) .tools(weatherTool) .systemPrompt("你是一个非常有帮助的助手") .saver(new MemorySaver()) .build(); // 5. 调用Agent AssistantMessage response = agent.call("上海今天天气怎么样？"); System.out.println(response.getText()); }核心特性简洁的配置: 通过Builder模式快速构建Agent工具集成: 使用FunctionToolCallback将自定义函数包装为Agent可调用的工具内存存储: 使用MemorySaver保存对话历史中文支持: 完整的中文提示词和工具描述自定义工具实现class WeatherTool implements BiFunction<String, ToolContext, String> { @Override public String apply(String city, ToolContext toolContext) { return city + "今天天气非常好！"; } }这个简单的工具类展示了如何将业务逻辑封装为Agent可调用的函数。示例二：RealAgent - 真实的智能体高级特性概览相比SimpleTest，RealAgent展示了更多生产级特性：精细的模型配置多工具协同结构化输出对话上下文管理核心代码解析1. 系统提示词设计String SYSTEM_PROMPT = """ 你是一位擅长说**天气冷笑话/谐音梗**的专业天气预报员。 你可以使用两个工具： - **get_weather_for_location**：用于获取指定地点的天气 - **get_user_location**：用于获取用户当前所在位置 如果用户询问天气，**必须先确认地点**。 如果从问题中能判断出他们指的是**自己所在的地方**， 就使用 **get_user_location** 工具获取他们的位置。 """;这个提示词体现了几个重要设计原则：角色定位: 明确Agent的身份和特色工具说明: 清晰描述可用工具的功能行为约束: 规定了工具使用的逻辑顺序2. 模型参数优化ChatModel chatModel = DashScopeChatModel.builder() .dashScopeApi(dashScopeApi) .defaultOptions(DashScopeChatOptions.builder() .model(DashScopeChatModel.DEFAULT_MODEL_NAME) .temperature(0.5) // 平衡创造性和准确性 .maxToken(1000) // 控制响应长度 .build()) .build();3. 多工具协同// 天气查询工具 ToolCallback getWeatherTool = FunctionToolCallback .builder("getWeatherForLocation", new WeatherForLocationTool()) .description("获取一个给定城市的天气") .inputType(String.class) .build(); // 用户定位工具 ToolCallback getUserLocationTool = FunctionToolCallback .builder("getUserLocation", new UserLocationTool()) .description("根据User Id获取用户位置") .inputType(String.class) .build();4. 结构化输出配置ReactAgent agent = ReactAgent.builder() // ... 其他配置 .outputType(ResponseFormat.class) // 指定输出格式 .hooks(humanInTheLoopHook) .build();5. 对话上下文管理RunnableConfig runnableConfig = RunnableConfig.builder() .threadId(Thread.currentThread().getId() + "") .build(); // 第一次调用 AssistantMessage response1 = agent.call("上海今天天气怎么样", runnableConfig); // 第二次调用（保持上下文） AssistantMessage response2 = agent.call("明天天气怎么样", runnableConfig);通过RunnableConfig的threadId实现多轮对话的上下文保持。总结通过这两个示例，我们可以看到Spring AI Alibaba框架在构建智能Agent应用方面的强大能力：SimpleTest展示了快速原型开发的能力，适合概念验证和学习RealAgent则创建了一个基础的 ReactAgent，接下来可以：探索更多的工具集成学习如何使用不同的 Checkpoint 实现对话持久化了解如何使用 Hooks 扩展 agent 功能学习如何创建多 agent 系统参考资料Spring AI Alibaba官方文档DashScope平台
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 工具方法调用的核心逻辑。理解这一流程后，你不仅能更清晰地排查工具调用中的问题，还能基于源码实现自定义扩展（如自定义工具匹配逻辑、增强异常处理等）。源码阅读的关键在于 “抓大放小”，先理清整体流程，再深入核心细节。希望本文的解析方式能帮助你更高效地学习框架源码，真正做到 “知其然，更知其所以然”。如果有疑问或补充，欢迎在评论区交流！
Spring AI 实战：调用本地 Tool 工具方法实现大模型能力扩展
前言大模型虽强，但在实时信息获取（如当前日期、天气）、复杂计算等场景下存在局限。而 Tool 工具方法 正是解决这一问题的关键 —— 它让大模型能调用本地代码获取结果，弥补自身能力短板。本文将通过具体代码案例，手把手教你在 Spring AI 中集成本地 Tool，实现大模型与本地逻辑的联动。一、基础准备：Spring AI 调用大模型环境搭建在集成 Tool 前，先搭建 Spring AI 调用大模型的基础环境，确保能正常与大模型交互。1. 添加核心依赖在 pom.xml 中引入 Spring AI OpenAI 适配器依赖（兼容主流大模型平台）： <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 的底层实现原理，敬请期待！
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 响应式编程入门（我的另一篇博客）
接入 DeepSeek | Java 调用大模型实战保姆级教程：新手快速上手
接入 DeepSeek | Java 调用大模型实战保姆级教程：新手快速上手最近各大平台接入 DeepSeek 大模型的案例越来越多，作为程序员，你是不是也想亲手实现一次？别担心，这篇教程会用3 种实战方式，带你从零开始用 Java 接入 DeepSeek 大模型，从 SDK 调用到原生 HTTP 请求全覆盖，新手也能轻松上手～前置知识：为什么需要响应式编程？大模型对话为了提升用户体验，通常会采用流式输出（像 ChatGPT 那样逐字显示回复）。而 Java 中实现流式输出需要用到响应式编程（Reactive Programming），核心是通过Flux（Reactor 框架）处理异步数据流。如果你对响应式编程不太熟悉，可以先简单理解：Flux能帮你 “实时接收” 大模型的每一段输出，而不是等全部结果返回后才处理。👉 快速入门参考：Spring 响应式编程官方文档👉 也可参考我的博客：响应式编程学习笔记准备工作：先获取 DeepSeek API Key在开始编码前，需要先申请 DeepSeek 的 API Key：访问 DeepSeek 开放平台：https://platform.deepseek.com/api_keys注册 / 登录账号后，在 “API Keys” 页面点击 “创建 API Key”，保存生成的密钥（后续代码中会用到）。方式一：通过 DeepSeek 官方 SDK 调用（最直接）DeepSeek 官方提供了适配 Spring AI 的 SDK，开箱即用，适合快速集成。1. 添加依赖在pom.xml中引入 Spring AI 对 DeepSeek 的支持（建议使用1.0.0+版本，兼容性更好）： <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-deepseek</artifactId> <version>1.0.0</version> </dependency>2. 编码实现：阻塞式与流式输出import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; public class DeepSeekSdkTest { // 初始化DeepSeek模型（核心对象） private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .apiKey("你的API Key") // 替换为你的DeepSeek API Key .build()) .defaultOptions(DeepSeekChatOptions.builder() .model("deepseek-chat") // 指定模型（支持deepseek-chat、deepseek-coder等） .temperature(0.7) // 控制输出随机性（0-1，值越高越随机） .build()) .build(); // 测试1：阻塞式输出（适合简单场景，等待完整结果返回） @Test public void testBlockingCall() { // 构建对话消息（SystemMessage设定角色，UserMessage是用户提问） List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手，擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好，请问你是谁")); // 调用模型并获取结果 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印完整回复 System.out.println("完整回复：" + response.getResult().getOutput().getText()); } // 测试2：流式输出（适合用户体验场景，逐字显示回复） @Test public void testStreamCall() { // 准备参数 List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手，擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好，请问你是谁")); // 调用 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); // 打印结果 flux.subscribe(resp -> System.out.println(resp.getResult().getOutput().getText())); // 阻塞等待（测试环境用，实际项目无需此操作） try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }代码说明DeepSeekChatModel：DeepSeek SDK 的核心类，通过建造者模式配置 API Key 和模型参数。call()方法：阻塞式调用，适合不需要实时显示的场景（如后端批量处理）,结果如下图。stream()方法：流式调用，返回Flux对象，通过subscribe()实时接收输出（前端可配合 SSE 显示）。模型参数：temperature控制回复随机性（0 = 严谨，1= creative），model可切换为代码模型deepseek-coder。方式二：通过 OpenAI 兼容 SDK 调用（推荐！）大多数主流大模型（包括 DeepSeek）都兼容 OpenAI 的 API 格式，用 OpenAI 的 SDK 调用更通用，后续切换模型（如 GPT、 Claude）几乎不用改代码。1. 添加依赖引入 Spring AI 对 OpenAI 的支持（和方式一的依赖不冲突，可共存）：<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>1.0.0</version> </dependency>2. 编码实现：兼容 OpenAI 格式import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; 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 reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; public class OpenAiCompatibleTest { // 初始化OpenAI兼容模型（核心是替换baseUrl为DeepSeek） private final OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.deepseek.com/") // DeepSeek的OpenAI兼容接口地址 .apiKey("你的API Key") // 同样使用DeepSeek的API Key .build()) .defaultOptions(OpenAiChatOptions.builder() .model("deepseek-chat") // 模型名称与DeepSeek一致 .temperature(0.7) .build()) .build(); // 测试阻塞式输出（和方式一用法几乎一致） @Test public void testBlockingCall() { List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手，擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好，请问你是谁")); ChatResponse response = chatModel.call(new Prompt(messages)); System.out.println("完整回复：" + response.getResult().getOutput().getText()); } // 测试流式输出 @Test public void testStreamCall() { // 准备参数 List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个出色的聊天助手，擅长通过幽默的风格回答用户问题。")); messages.add(new UserMessage("你好，请问你是谁")); // 调用 Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); // 打印结果 flux.subscribe(resp -> System.out.println(resp.getResult().getOutput().getText())); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }为什么推荐这种方式？兼容性强：后续想切换到 GPT-4、Anthropic Claude 等模型，只需修改baseUrl和model参数。生态成熟：OpenAI 的 SDK 文档和社区支持更丰富，遇到问题更容易找到解决方案。代码复用：团队中如果已有 OpenAI 调用逻辑，无需重复开发，直接适配 DeepSeek。方式三：原生 HTTP 请求调用（无 SDK，更灵活）如果不想依赖第三方 SDK，也可以直接通过 HTTP 请求调用 DeepSeek 的 API，适合需要自定义请求 / 响应处理的场景。1. 核心原理DeepSeek 的聊天接口地址为：https://api.deepseek.com/v1/chat/completions，支持 POST 请求，通过stream: true参数开启流式输出。请求头需携带Authorization: Bearer 你的API Key，请求体为 JSON 格式（包含对话消息、模型参数等）。2. 编码实现：用 WebClient 发送请求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 HttpDirectCallTest { // 你的DeepSeek API Key private static final String API_KEY = "你的API Key"; // DeepSeek聊天接口地址 private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; @Test public void testStreamHttpCall() { // 构建请求体JSON（流式输出需设置stream: true） String requestBody = """ { "messages": [ { "role": "system", "content": "你是一个出色的聊天助手，擅长通过幽默的风格回答用户问题。" }, { "role": "user", "content": "如何用Java发送HTTP请求？" } ], "model": "deepseek-chat", "stream": true, "temperature": 0.7, "max_tokens": 2048 } """; // 用WebClient发送POST请求（响应式HTTP客户端） 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); // 流式接收响应 // 订阅并处理响应 responseFlux.subscribe( chunk -> { // 每一段流式输出（SSE格式，需解析data字段） if (chunk.contains("\"data\":")) { String content = chunk.split("\"data\":")[1].replace("}", "").replace("\"", "").trim(); if (!content.equals("[DONE]")) { // 过滤结束标识 System.out.print(content); } } }, error -> System.err.println("HTTP请求错误：" + error.getMessage()), () -> System.out.println("\nHTTP流式输出结束～") ); // 阻塞等待 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }代码说明请求体参数：messages是对话历史（role支持system/user/assistant），stream: true开启流式输出。响应处理：流式响应为 SSE（Server-Sent Events）格式，每一行是data: {内容}，需解析data字段获取实际输出。灵活性：可自定义超时时间、重试机制、请求拦截器等，适合复杂场景（如企业级网关适配）。三种方式对比与选择建议方式优点缺点适合场景DeepSeek 官方 SDK原生支持，参数适配性好仅支持 DeepSeek，切换模型需改代码仅用 DeepSeek，快速集成OpenAI 兼容 SDK跨模型兼容，生态成熟需确认模型兼容性（部分参数可能不同）可能切换模型，追求代码复用原生 HTTP 请求无依赖，高度自定义需手动处理参数、解析响应，开发效率低自定义需求高，避免第三方 SDK 依赖👉 新手推荐方式二，兼顾简单性和灵活性；追求极致自定义用方式三；仅深度集成 DeepSeek 用方式一。避坑指南API Key 安全：不要硬编码 API Key，建议通过配置文件（如application.yml）或环境变量注入，避免泄露。超时设置：大模型响应可能较慢，需在 HTTP 客户端或 SDK 中设置合理超时时间（如 30 秒以上）。流式输出前端适配：如果前端需要显示流式效果，可通过 SSE（Server-Sent Events）或 WebSocket 接收后端的Flux输出。模型参数调试：temperature和top_p控制输出风格，可根据需求调整（技术问答建议temperature=0.3更严谨）。总结通过本文的 3 种方式，介绍了 Java 调用 DeepSeek 大模型的核心方法。无论是快速集成的 SDK 方式，还是灵活的原生 HTTP 请求，都能满足不同场景的需求。接下来可以尝试扩展功能：比如添加对话历史管理、实现多轮对话，或者集成到 Spring Boot 项目中提供 API 接口。如果遇到问题，欢迎在评论区留言， 祝大家开发顺利！ 🚀