news 2026/9/3 6:16:26

第二章:DeepSeek Function Calling 实战 —— 给 Agent 装上“手“

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第二章:DeepSeek Function Calling 实战 —— 给 Agent 装上“手“

1. 本章目标

第一章我们搭建了一个能聊天的 Agent 控制台,但它只会"动嘴",不会"动手"。这一章我们要给它装上"手"——让它能调用外部工具:

  • 查询当前时间getCurrentTime(timezone)
  • 查询天气getWeather(city)getForecast(city, days)
  • 数学计算calculator(expression)

最终效果:用户在对话框中输入"武汉的天气怎么样?",Agent 会自动调用天气工具查询武汉天气,并将结构化数据转化为自然语言回复。

2.1 本章目标

第一章我们搭建了一个能聊天的 Agent 控制台,但它只会"动嘴",不会"动手"。这一章我们要给它装上"手"——让它能调用外部工具:

  • 查询当前时间getCurrentTime()
  • 查询天气getWeather(city)
  • 数学计算calculator(expression)

最终效果:用户在对话框中输入"武汉明天适合跑步吗",Agent 会自动调用天气工具查询武汉天气,结合天气预报判断是否适合户外运动。


2.2 什么是 Function Calling?

2.2.1 直观理解

没有 Function Calling 的 AI:

用户:武汉今天多少度? AI:抱歉,我的训练数据截止到2025年,无法获取实时天气。

有 Function Calling 的 AI:

用户:武汉今天多少度? AI:(内心:我需要查天气工具) AI:(调用 getWeather("武汉") → 返回 "28°C,晴") AI:武汉今天28°C,天气晴朗,非常适合外出!

2.2.2 工作原理

┌─────────────────────────────────────────────────────────┐ │ Function Calling 流程 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 用户:"武汉天气怎么样?" │ │ │ │ │ ▼ │ │ 1. 发送消息 + 工具描述给 AI │ │ ┌─────────────────────────────────────┐ │ │ │ messages: [{"role":"user", │ │ │ │ "content":"武汉天气?"}] │ │ │ │ tools: [{"name":"getWeather", │ │ │ │ "description":"查询天气", │ │ │ │ "parameters":{...}}] │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 2. AI 决定调用工具,返回工具调用请求 │ │ ┌─────────────────────────────────────┐ │ │ │ finish_reason: "tool_calls" │ │ │ │ tool_calls: [{ │ │ │ │ "function": { │ │ │ │ "name": "getWeather", │ │ │ │ "arguments": {"city": "武汉"} │ │ │ │ } │ │ │ │ }] │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 3. 应用程序执行工具函数 │ │ getWeather("武汉") → "28°C,晴" │ │ │ │ │ ▼ │ │ 4. 将工具结果发送回 AI │ │ ┌─────────────────────────────────────┐ │ │ │ tool_call_id: "call_xxx" │ │ │ │ content: "28°C,晴" │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 5. AI 基于工具结果生成自然语言回复 │ │ "武汉今天28°C,天气晴朗..." │ │ │ └─────────────────────────────────────────────────────────┘

2.2.3 关键概念

概念

说明

类比

Tool Definition

工具的 JSON Schema 描述

函数的"说明书"

Tool Call

AI 决定调用哪个工具

函数调用指令

Arguments

调用工具时的参数

函数实参

Tool Result

工具执行的返回结果

函数返回值


2.3 新增文件结构

agent-console/ ├── pom.xml (不变) └── src/main/ ├── java/com/example/agent/ │ ├── AgentApplication.java (不变) │ ├── config/ │ │ ├── AgentConfig.java (修改:注册工具) │ │ └── ToolConfig.java ★ 新增:工具配置 │ ├── controller/ │ │ └── ChatController.java (修改:支持工具调用) │ ├── service/ │ │ └── ConversationService.java (不变) │ ├── model/ │ │ ├── ChatMessage.java (不变) │ │ ├── Conversation.java (不变) │ │ ├── ChatRequest.java (不变) │ │ └── CreateConversationRequest.java (不变) │ └── tool/ ★ 新增:工具包 │ ├── WeatherTool.java ★ 新增:天气工具 │ ├── TimeTool.java ★ 新增:时间工具 │ └── CalculatorTool.java ★ 新增:计算器工具 └── resources/ ├── application.yml (不变) └── static/ ├── index.html (修改:显示工具调用) └── style.css ★ 新增:工具调用样式

2.4 完整源码

2.4.1 TimeTool.java —— 时间工具

package com.example.agent.tool; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.time.LocalDate; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Set; /** * TimeTool:时间相关的工具集合 * * @Component 让 Spring 管理这个 Bean * Spring AI 会扫描所有带有 @Tool 注解的方法,自动注册为 AI 可调用的工具 * * 注意:@Tool 注解是 Spring AI 2.0 新增的 * 1.x 使用的是 @ToolCallback 或 ToolCallbackProvider * 2.0 简化了工具注册流程 */ @Component public class TimeTool { /** 支持的时区集合,限制常用时区避免无效输入 */ private static final Set<String> SUPPORTED_ZONES = Set.of( "Asia/Shanghai", // 中国标准时间 (UTC+8) "America/New_York", // 美国东部时间 "Europe/London", // 英国伦敦时间 "Asia/Tokyo", // 日本东京时间 "Asia/Singapore" // 新加坡时间 ); /** * getCurrentTime:获取指定时区的当前日期和时间 * * @Tool 注解声明这是一个 AI 可调用的工具 * name:工具名称,AI 通过这个名字来调用 * description:工具描述,AI 根据描述判断何时使用 * * @param timezone 时区名称,例如 "Asia/Shanghai",不传则默认上海时区 * @return 格式化后的时间字符串 */ @Tool(name = "getCurrentTime", description = """ 获取指定时区的当前日期和时间。 支持的中国时区:Asia/Shanghai 如果不指定时区,默认返回北京时间。 返回格式:YYYY-MM-DD HH:mm:ss """) public String getCurrentTime( @ToolParam(description = """ 时区名称,例如: - Asia/Shanghai(北京时间) - America/New_York(纽约时间) - Europe/London(伦敦时间) 如果不传,默认为 Asia/Shanghai """) String timezone) { // 处理默认时区 if (timezone == null || timezone.isBlank()) { timezone = "Asia/Shanghai"; } // 验证时区是否在支持列表中 if (!SUPPORTED_ZONES.contains(timezone)) { return String.format("不支持的时区:%s。支持的时区:%s", timezone, String.join(", ", SUPPORTED_ZONES)); } try { // 获取指定时区的当前时间并格式化输出 ZoneId zone = ZoneId.of(timezone); LocalDate date = LocalDate.now(zone); LocalTime time = LocalTime.now(zone); DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); return String.format("当前时间(%s):%s %s", timezone, date.format(dateFormatter), time.format(timeFormatter)); } catch (Exception e) { return "获取时间失败:" + e.getMessage(); } } }

2.4.2 WeatherTool.java —— 天气工具

package com.example.agent.tool; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; /** * WeatherTool:天气查询工具 * * 这是一个模拟实现,聚焦于演示 Function Calling 的核心机制 * 真实项目中应接入天气 API(如和风天气、OpenWeatherMap) */ @Component public class WeatherTool { /** 模拟城市天气数据:温度、天气状况、湿度、风力 */ private static final Map<String, Map<String, Object>> WEATHER_DATA = Map.ofEntries( Map.entry("Beijing", Map.of("temp", 22, "condition", "Sunny", "humidity", 45, "wind", 3)), Map.entry("Shanghai", Map.of("temp", 26, "condition", "Cloudy", "humidity", 65, "wind", 2)), Map.entry("Guangzhou", Map.of("temp", 30, "condition", "Rainy", "humidity", 80, "wind", 4)), Map.entry("Shenzhen", Map.of("temp", 29, "condition", "Sunny", "humidity", 75, "wind", 3)), Map.entry("Wuhan", Map.of("temp", 28, "condition", "Sunny", "humidity", 60, "wind", 2)), Map.entry("Chengdu", Map.of("temp", 25, "condition", "Overcast", "humidity", 55, "wind", 1)), Map.entry("Hangzhou", Map.of("temp", 27, "condition", "Light Rain", "humidity", 72, "wind", 3)), Map.entry("Nanjing", Map.of("temp", 23, "condition", "Cloudy", "humidity", 58, "wind", 2)) ); /** * getWeather:查询指定城市的当前天气 * * @ToolParam 描述参数的格式和含义,帮助 AI 生成正确的参数值 * required = true 表示该参数为必填 * * @param city 城市英文名称,如 "Beijing"、"Wuhan" * @return 天气信息的字符串描述 */ @Tool(name = "getWeather", description = """ Query the current weather for a specific city. Returns temperature, weather condition, humidity and wind level. Supported cities: Beijing, Shanghai, Guangzhou, Shenzhen, Wuhan, Chengdu, Hangzhou, Nanjing """) public String getWeather( @ToolParam(description = """ City name in English, e.g.: - Beijing - Shanghai - Wuhan - Guangzhou Only major Chinese cities are supported. Must use English name, NOT Chinese characters. """, required = true) String city) { // 查找城市天气数据 Map<String, Object> weather = WEATHER_DATA.get(city); // 城市不存在则返回友好错误提示 if (weather == null) { return String.format("City '%s' not found. Supported cities: %s", city, String.join(", ", WEATHER_DATA.keySet())); } // 组装返回信息 int temp = (int) weather.get("temp"); String condition = (String) weather.get("condition"); int humidity = (int) weather.get("humidity"); int wind = (int) weather.get("wind"); return String.format(""" [Weather for %s] Temperature: %d°C Condition: %s Humidity: %d%% Wind Level: %d """, city, temp, condition, humidity, wind); } /** * getForecast:查询未来几天的天气预报 * * @param city 城市英文名称 * @param days 未来天数(1-7),默认为3天 * @return 预报信息 */ @Tool(name = "getForecast", description = """ Get weather forecast for the next few days. Supports 1 to 7 days forecast. Returns daily weather summary. """) public String getForecast( @ToolParam(description = "City name in English, e.g.: Beijing, Shanghai, Wuhan", required = true) String city, @ToolParam(description = "Number of days to forecast, range 1-7, default is 3") Integer days) { // 默认查询3天 if (days == null || days < 1 || days > 7) { days = 3; } // 检查城市是否存在 if (!WEATHER_DATA.containsKey(city)) { return String.format("City '%s' not found.", city); } // 模拟生成预报数据 StringBuilder forecast = new StringBuilder(); forecast.append(String.format("[%d-Day Forecast for %s]\n", days, city)); String[] conditions = {"Sunny", "Cloudy", "Overcast", "Light Rain", "Sunny", "Cloudy"}; for (int i = 0; i < days; i++) { int temp = ThreadLocalRandom.current().nextInt(20, 35); String cond = conditions[ThreadLocalRandom.current().nextInt(conditions.length)]; forecast.append(String.format("Day %d: %d°C, %s\n", i + 1, temp, cond)); } return forecast.toString(); } }

2.4.3 CalculatorTool.java —— 计算器工具

package com.example.agent.tool; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; /** * CalculatorTool:数学计算工具 * * 大模型在数学计算方面表现不稳定,复杂运算应交由专业工具处理 * 使用 Java ScriptEngine 执行 JavaScript 表达式,比手写解析器更强大 */ @Component public class CalculatorTool { /** JavaScript 脚本引擎,用于执行数学表达式 */ private final ScriptEngine engine; /** 构造器:初始化脚本引擎 */ public CalculatorTool() { ScriptEngineManager manager = new ScriptEngineManager(); this.engine = manager.getEngineByName("JavaScript"); } /** * calculator:执行数学计算 * * @param expression 数学表达式,如 "1 + 2 * 3" * @return 计算结果 */ @Tool(name = "calculator", description = """ Perform mathematical calculations. Supported operations: addition(+), subtraction(-), multiplication(*), division(/), power(^), parentheses Supported functions: sin, cos, tan, sqrt, abs, round Examples: 1 + 2 * 3, (10 + 5) / 3, sqrt(16), 2^10 """) public String calculator( @ToolParam(description = """ Mathematical expression to evaluate, e.g.: - Simple: 1 + 2 * 3 - With parentheses: (10 + 5) / 3 - Functions: sqrt(16), abs(-5) - Mixed: 2 * PI * 5 Note: only numbers, operators, parentheses and math functions allowed """, required = true) String expression) { // 参数校验 if (expression == null || expression.isBlank()) { return "Expression cannot be empty"; } // 安全检查:只允许数学表达式,过滤危险字符 String sanitized = expression.replaceAll("\\s+", ""); if (!sanitized.matches("[0-9+\\-*/.^()a-zA-Z,\\s]+")) { return "Expression contains invalid characters. Only numbers, operators and math functions allowed."; } try { // 执行计算 Object result = engine.eval(expression); // 格式化结果:整数去掉小数点,浮点数保留两位 if (result instanceof Double) { double d = (Double) result; if (d == Math.floor(d) && !Double.isInfinite(d)) { return String.format("%s = %d", expression, (long) d); } return String.format("%s = %.2f", expression, d); } return String.format("%s = %s", expression, result.toString()); } catch (ScriptException e) { return String.format("Calculation error: %s. Please check the expression format.", e.getMessage()); } catch (Exception e) { return "Unknown calculation error, please try again"; } } }

2.4.4 ToolConfig.java —— 工具注册配置

package com.example.agent.config; import com.example.agent.tool.CalculatorTool; import com.example.agent.tool.TimeTool; import com.example.agent.tool.WeatherTool; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbacks; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ToolConfig:工具注册配置类 * * 负责将 @Tool 注解的工具方法注册为 Spring AI 可识别的 ToolCallback * ToolCallbacks.from() 会扫描传入对象的所有方法, * 找出带有 @Tool 注解的方法,解析注解元数据,生成对应的 ToolCallback 对象 * * 注意:@Tool 注解只在 Spring AI 2.0 中有效 */ @Configuration public class ToolConfig { /** 注册天气相关的工具 */ @Bean public ToolCallback[] weatherTools(WeatherTool weatherTool) { return ToolCallbacks.from(weatherTool); } /** 注册时间相关的工具 */ @Bean public ToolCallback[] timeTools(TimeTool timeTool) { return ToolCallbacks.from(timeTool); } /** 注册计算器工具 */ @Bean public ToolCallback[] calculatorTools(CalculatorTool calculatorTool) { return ToolCallbacks.from(calculatorTool); } }

2.4.5 AgentConfig.java —— 修改版

package com.example.agent.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.tool.ToolCallback; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; /** * AgentConfig:AI 客户端配置(修改版) * * 修改内容: * 1. 注入 ToolCallback 列表 * 2. 在 ChatClient 中注册工具 * 3. 系统提示词改为英文以提高 DeepSeek 的遵循度 */ @Configuration public class AgentConfig { /** * chatClient:创建 ChatClient Bean * * Spring 会自动收集所有 ToolCallback[] 类型的 Bean,合并为一个列表注入 * * @param builder ChatClient.Builder * @param toolCallbacks 所有注册的工具回调 * @return 配置好的 ChatClient 实例 */ @Bean public ChatClient chatClient( ChatClient.Builder builder, List<ToolCallback[]> toolCallbacks) { // 将所有工具回调展平为一个数组 ToolCallback[] allTools = toolCallbacks.stream() .flatMap(java.util.Arrays::stream) .toArray(ToolCallback[]::new); return builder .defaultSystem(""" You are an AI Agent assistant built with Java and Spring AI. You have the following capabilities: 1. Time query: Use getCurrentTime tool 2. Weather query: Use getWeather tool 3. Weather forecast: Use getForecast tool 4. Math calculation: Use calculator tool Rules: - When users ask about time, weather, or calculations, prefer using tools - Present tool results in natural language - If a tool returns an error, inform the user honestly - When combining multiple tool results, provide reasonable analysis and suggestions """) .defaultTools(allTools) .build(); } }

2.4.6 ChatController.java —— 修改版

package com.example.agent.controller; import com.example.agent.model.*; import com.example.agent.service.ConversationService; import org.springframework.ai.chat.client.ChatClient; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import java.util.List; import java.util.Map; @RestController @RequestMapping("/ai") public class ChatController { private final ChatClient chatClient; private final ConversationService conversationService; public ChatController( ChatClient.Builder builder, ConversationService conversationService) { this.chatClient = builder.build(); this.conversationService = conversationService; } // ==================== 会话管理接口(不变) ==================== @PostMapping("/conversation") public Conversation createConversation(@RequestBody(required = false) CreateConversationRequest request) { String title = request == null ? null : request.title(); return conversationService.createConversation(title); } @GetMapping("/conversations") public List<Conversation> listConversations() { return conversationService.listConversations(); } @GetMapping("/conversation/{conversationId}/messages") public List<ChatMessage> getMessages(@PathVariable String conversationId) { return conversationService.getHistory(conversationId); } // ==================== 消息收发接口(修改) ==================== /** * 发送消息(非流式,支持工具调用) * * ChatClient 内部会自动处理工具调用的完整流程: * 1. 发送消息给 AI * 2. AI 返回工具调用请求 * 3. ChatClient 自动执行工具 * 4. 将工具结果发回给 AI * 5. AI 生成最终回复 */ @PostMapping("/chat") public Map<String, Object> chat(@RequestBody ChatRequest request) { // 保存用户消息 conversationService.addMessage(request.conversationId(), ChatMessage.user(request.message())); // 调用 AI 模型(自动管理工具调用) var response = chatClient.prompt() .user(request.message()) .call() .chatResponse(); // 提取回复内容并保存 String content = response.getResult().getOutput().getContent(); conversationService.addMessage(request.conversationId(), ChatMessage.assistant(content)); return Map.of( "reply", content, "finishReason", response.getResult().getMetadata().getFinishReason() ); } /** * 发送消息(流式,支持工具调用) * * 流式工具调用的特点: * - 工具调用本身是同步的(执行工具方法) * - 只有最终的文本回复是流式的 * - 前端看不到工具调用的中间过程 */ @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> streamChat(@RequestBody ChatRequest request) { String conversationId = request.conversationId(); String message = request.message(); conversationService.addMessage(conversationId, ChatMessage.user(message)); StringBuilder fullResponse = new StringBuilder(); return chatClient.prompt() .user(message) .stream() .content() .map(chunk -> { fullResponse.append(chunk); return ServerSentEvent.<String>builder(chunk) .event("text") .build(); }) .concatWith(Flux.just( ServerSentEvent.<String>builder("done") .event("end") .build() )) .doOnTerminate(() -> { String completeResponse = fullResponse.toString(); if (!completeResponse.isEmpty()) { conversationService.addMessage( conversationId, ChatMessage.assistant(completeResponse) ); } }); } }

2.4.7 前端 index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Agent 控制台 - 带工具调用</title> <script src="https://cdn.tailwindcss.com"></script> </head> <body class="bg-gray-100"> <div class="flex h-screen"> <!-- 左侧会话列表 --> <div class="w-64 bg-gray-900 text-white p-4 flex flex-col"> <h2 class="text-xl font-bold mb-4">Agent 控制台</h2> <button class="bg-blue-600 hover:bg-blue-700 rounded px-4 py-2 mb-4" onclick="newConversation()"> + 新建会话 </button> <div id="conversation-list" class="flex-1 overflow-y-auto space-y-2"></div> </div> <!-- 右侧聊天区域 --> <div class="flex-1 flex flex-col"> <div id="messages" class="flex-1 overflow-y-auto p-6 space-y-4"></div> <div class="p-4 border-t bg-white"> <div class="flex"> <input id="message-input" type="text" placeholder="试试问:What's the weather like in Wuhan?" class="flex-1 border rounded-l px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> <button onclick="sendMessage()" class="bg-blue-600 hover:bg-blue-700 text-white rounded-r px-6 py-2"> Send </button> </div> </div> </div> </div> <script> let currentConversationId = null; window.onload = () => { loadConversations(); newConversation(); }; async function newConversation() { const res = await fetch('/ai/conversation', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: 'New Session'}) }); const conv = await res.json(); currentConversationId = conv.id; document.getElementById('messages').innerHTML = ''; loadConversations(); } async function loadConversations() { const res = await fetch('/ai/conversations'); const convs = await res.json(); const list = document.getElementById('conversation-list'); list.innerHTML = convs.map(c => ` <div class="p-2 hover:bg-gray-800 cursor-pointer rounded ${c.id === currentConversationId ? 'bg-gray-700' : ''}" onclick="switchConversation('${c.id}')"> <div class="font-medium">${c.title}</div> <div class="text-xs text-gray-500">${new Date(c.createdAt).toLocaleTimeString()}</div> </div> `).join(''); } async function switchConversation(id) { currentConversationId = id; const res = await fetch(`/ai/conversation/${id}/messages`); const messages = await res.json(); const container = document.getElementById('messages'); container.innerHTML = ''; messages.forEach(m => addBubble(m.role, m.content)); loadConversations(); } async function sendMessage() { const input = document.getElementById('message-input'); const message = input.value.trim(); if (!message || !currentConversationId) return; input.value = ''; addBubble('user', message); const thinkingDiv = showThinking(); try { const res = await fetch('/ai/chat', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ conversationId: currentConversationId, message: message }) }); const data = await res.json(); thinkingDiv.remove(); addBubble('assistant', data.reply); } catch (error) { thinkingDiv.remove(); addBubble('assistant', 'Sorry, an error occurred: ' + error.message); } } function showThinking() { const container = document.getElementById('messages'); const div = document.createElement('div'); div.className = 'flex justify-start'; div.innerHTML = ` <div class="bg-white border rounded-lg px-4 py-2"> <div class="flex items-center space-x-2 text-gray-500"> <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-indigo-500"></div> <span>Thinking...</span> </div> </div> `; container.appendChild(div); container.scrollTop = container.scrollHeight; return div; } function addBubble(role, content) { const container = document.getElementById('messages'); const div = document.createElement('div'); div.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`; div.innerHTML = ` <div class="max-w-[70%] rounded-lg px-4 py-2 ${role === 'user' ? 'bg-blue-600 text-white' : 'bg-white text-gray-800 border'}"> <div class="content whitespace-pre-wrap">${content}</div> </div> `; container.appendChild(div); container.scrollTop = container.scrollHeight; return div; } document.getElementById('message-input').addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); }); </script> </body> </html>

2.5 坑点警示

坑点 1:工具名称必须符合 OpenAI API 命名规范

OpenAI API 要求函数名必须匹配正则表达式:^[a-zA-Z0-9_-]+$

❌ 错误示例: name = "查询天气" → 包含中文,报错 name = "get weather" → 包含空格,报错 name = "get.weather" → 包含点号,报错 ✅ 正确示例: name = "getWeather" → 驼峰命名,合法 name = "get_current_time" → 下划线命名,合法 name = "get-weather" → 短横线命名,合法

错误现象

Invalid parameter: tools[0].function.name must match ^[a-zA-Z0-9_-]+$

解读报错中的索引

  • tools[0]表示第一个注册的工具(按 ToolConfig 中 @Bean 方法的定义顺序)
  • tools[1]表示第二个注册的工具
  • 以此类推。如果你的第一个工具是 WeatherTool,报错就指向tools[0]

坑点 2:参数描述要清晰明确

❌ 错误:@ToolParam(description = "城市名称") → AI 可能传入中文、拼音、缩写等各种格式 ✅ 正确:@ToolParam(description = "City name in English, e.g.: Beijing, Shanghai, Wuhan. Must use English name, NOT Chinese characters.") → AI 明确知道要传英文名

参数描述越模糊,AI 传入错误值的概率越高。好的描述应包含:格式说明、示例值、取值范围、是否为必填。


2.6 运行验证

启动命令

export DEEPSEEK_API_KEY=sk-你的key mvn spring-boot:run

测试用例

测试场景

输入示例

预期行为

查询时间

"What time is it?"

调用 getCurrentTime,返回当前时间

查询天气

"What's the weather in Wuhan?"

调用 getWeather,返回武汉天气

天气预报

"How will be the weather in Beijing tomorrow?"

调用 getForecast,返回预报

数学计算

"What is 1024 * 768?"

调用 calculator,返回计算结果

复合查询

"Is it good for running in Wuhan tomorrow?"

先调 getForecast,再综合分析

调试技巧

# application.yml 中开启调试日志 logging: level: org.springframework.ai: DEBUG

开启后可以看到实际发送给 DeepSeek 的请求体,包括 tools 数组的内容,便于排查工具注册和调用的问题。

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

基于PyTorch与CelebA数据集的人脸识别项目实战:从零构建CNN模型

简介&#xff1a;本资源是一个基于CelebA数据集与PyTorch框架实现的人脸识别神经网络完整项目&#xff0c;面向深度学习初学者、计算机视觉方向学生及人脸识别技术实践者&#xff0c;旨在解决人脸检测与属性识别的基础建模问题&#xff0c;适用于课程设计、科研入门与模型复现等…

作者头像 李华
网站建设 2026/9/3 6:12:45

答辩高分秘籍✅告别尬场!OKBIYE一键搞定答辩全流程

谁懂啊&#xff01;论文过了&#xff0c;却栽在答辩上&#xff01; 很多同学熬完开题、写完论文、降完重&#xff0c;最后倒在答辩这一关&#xff1a;PPT简陋杂乱、不知道怎么讲、被老师提问卡壳、全程紧张尬聊&#xff0c;明明论文质量不差&#xff0c;最后只能拿及格分。 其…

作者头像 李华
网站建设 2026/9/3 6:10:57

从本地代码到可开源项目:资料准备与发布完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 6:08:54

论文查重红线避不开?降重+降AIGC双处理帮你化解修改难题

完成论文初稿之后&#xff0c;很多同学会遭遇两大头疼问题&#xff1a;一是重复率超标&#xff0c;大段标红&#xff0c;手动改写效率低下&#xff0c;改完之后语句不通顺&#xff0c;破坏原本论文逻辑&#xff1b;二是AIGC痕迹过高&#xff0c;AI生成特征明显&#xff0c;面对…

作者头像 李华
网站建设 2026/9/3 6:08:44

石头剪刀布YOLOv8数据集:轻量级手势识别工程实践

简介&#xff1a;本资源是专为计算机视觉初学者与YOLOv8模型实践者设计的轻量级手势识别数据集&#xff0c;聚焦于‘石头剪刀布’三类手势的检测任务&#xff0c;适用于模型训练、验证及教学演示等场景。压缩包共83个文件&#xff0c;包含41张标注清晰的JPG图像、41份对应YOLOv…

作者头像 李华