فهرست منبع

【功能新增】AI:新增 function call 示例,完成所有模型的测试 = = 累

YunaiV 5 ماه پیش
والد
کامیت
ffe4afaaaf

+ 3 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
@@ -238,7 +239,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         // 2. 构建 ChatOptions 对象
         AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
         ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
-                conversation.getTemperature(), conversation.getMaxTokens());
+                conversation.getTemperature(), conversation.getMaxTokens(),
+                SetUtils.asSet("directory_list", "weather_query"));
         return new Prompt(chatMessages, chatOptions);
     }
 

+ 0 - 95
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java

@@ -1,95 +0,0 @@
-package cn.iocoder.yudao.module.ai.service.tool;
-
-import cn.hutool.core.date.LocalDateTimeUtil;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.StrUtil;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import org.springframework.ai.tool.annotation.Tool;
-import org.springframework.ai.tool.annotation.ToolParam;
-import org.springframework.stereotype.Component;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-
-/**
- * 目录内容列表工具:列出指定目录的内容
- *
- * @author 芋道源码
- */
-@Component
-public class ListDirTool {
-
-    /**
-     * 列出指定目录的内容
-     *
-     * @param relativePath 要列出内容的目录路径,相对于工作区根目录
-     * @return 目录内容列表
-     */
-    @Tool(name = "listDir", description = "列出指定目录的内容")
-    public Response listDir(@ToolParam(description = "要列出内容的目录路径,相对于工作区根目录") String relativePath) {
-        // 校验目录存在
-        String path = StrUtil.blankToDefault(relativePath, ".");
-        Path dirPath = Paths.get(path);
-        if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) {
-            return new Response(Collections.emptyList());
-        }
-        // 列出目录内容
-        File[] files = dirPath.toFile().listFiles();
-        if (ArrayUtil.isEmpty(files)) {
-            return new Response(Collections.emptyList());
-        }
-        return new Response(convertList(Arrays.asList(files), file -> new Response.File()
-                .setDirectory(file.isDirectory()).setName(file.getName())
-                .setSize(file.isFile() ? FileUtil.readableFileSize(file.length()) : null)
-                .setLastModified(
-                        LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN))));
-    }
-
-    @Data
-    @AllArgsConstructor
-    @NoArgsConstructor
-    public static class Response {
-
-        /**
-         * 目录内容列表
-         */
-        private List<File> files;
-
-        @Data
-        public static class File {
-
-            /**
-             * 是否为目录
-             */
-            private Boolean directory;
-
-            /**
-             * 名称
-             */
-            private String name;
-
-            /**
-             * 大小,仅对文件有效
-             */
-            private String size;
-
-            /**
-             * 最后修改时间
-             */
-            private String lastModified;
-
-        }
-
-    }
-
-}

+ 14 - 15
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java → yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java

@@ -1,10 +1,11 @@
-package cn.iocoder.yudao.module.ai.service.tool;
+package cn.iocoder.yudao.module.ai.service.tool.function;
 
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
 import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyDescription;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -12,8 +13,6 @@ import lombok.NoArgsConstructor;
 import org.springframework.stereotype.Component;
 
 import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -23,21 +22,22 @@ import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
 /**
- * 目录内容列表工具:列出指定目录的内容
+ * 工具:列出指定目录的文件列表
  *
  * @author 芋道源码
  */
-@Component("listDir")
-public class ListDirToolB implements Function<ListDirToolB.Request, ListDirToolB.Response> {
+@Component("directory_list")
+public class DirectoryListToolFunction implements Function<DirectoryListToolFunction.Request, DirectoryListToolFunction.Response> {
 
     @Data
-    @JsonClassDescription("列出指定目录的内容")
+    @JsonClassDescription("列出指定目录的文件列表")
     public static class Request {
 
         /**
-         * 要列出内容的目录路径
+         * 目录路径
          */
-        @JsonPropertyDescription("要列出内容的目录路径,例如说:/Users/yunai")
+        @JsonProperty(required = true, value = "path")
+        @JsonPropertyDescription("目录路径,例如说:/Users/yunai")
         private String path;
 
     }
@@ -48,7 +48,7 @@ public class ListDirToolB implements Function<ListDirToolB.Request, ListDirToolB
     public static class Response {
 
         /**
-         * 目录内容列表
+         * 文件列表
          */
         private List<File> files;
 
@@ -82,13 +82,12 @@ public class ListDirToolB implements Function<ListDirToolB.Request, ListDirToolB
     @Override
     public Response apply(Request request) {
         // 校验目录存在
-        String path = StrUtil.blankToDefault(request.getPath(), ".");
-        Path dirPath = Paths.get(path);
-        if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) {
+        String path = StrUtil.blankToDefault(request.getPath(), "/");
+        if (!FileUtil.exist(path) || !FileUtil.isDirectory(path)) {
             return new Response(Collections.emptyList());
         }
-        // 列出目录内容
-        File[] files = dirPath.toFile().listFiles();
+        // 列出目录
+        File[] files = FileUtil.ls(path);
         if (ArrayUtil.isEmpty(files)) {
             return new Response(Collections.emptyList());
         }

+ 118 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/WeatherQueryToolFunction.java

@@ -0,0 +1,118 @@
+package cn.iocoder.yudao.module.ai.service.tool.function;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.function.Function;
+
+import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;
+
+/**
+ * 工具:查询指定城市的天气信息
+ *
+ * @author 芋道源码
+ */
+@Component("weather_query")
+public class WeatherQueryToolFunction
+        implements Function<WeatherQueryToolFunction.Request, WeatherQueryToolFunction.Response> {
+
+    private static final String[] WEATHER_CONDITIONS = { "晴朗", "多云", "阴天", "小雨", "大雨", "雷雨", "小雪", "大雪" };
+
+    @Data
+    @JsonClassDescription("查询指定城市的天气信息")
+    public static class Request {
+
+        /**
+         * 城市名称
+         */
+        @JsonProperty(required = true, value = "city")
+        @JsonPropertyDescription("城市名称,例如:北京、上海、广州")
+        private String city;
+
+    }
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Response {
+
+        /**
+         * 城市名称
+         */
+        private String city;
+
+        /**
+         * 天气信息
+         */
+        private WeatherInfo weatherInfo;
+
+        @Data
+        @AllArgsConstructor
+        @NoArgsConstructor
+        public static class WeatherInfo {
+
+            /**
+             * 温度(摄氏度)
+             */
+            private Integer temperature;
+
+            /**
+             * 天气状况
+             */
+            private String condition;
+
+            /**
+             * 湿度百分比
+             */
+            private Integer humidity;
+
+            /**
+             * 风速(km/h)
+             */
+            private Integer windSpeed;
+
+            /**
+             * 查询时间
+             */
+            private String queryTime;
+
+        }
+
+    }
+
+    @Override
+    public Response apply(Request request) {
+        // 检查城市名称是否为空
+        if (StrUtil.isBlank(request.getCity())) {
+            return new Response("未知城市", null);
+        }
+
+        // 获取天气数据
+        String city = request.getCity();
+        Response.WeatherInfo weatherInfo = generateMockWeatherInfo();
+        return new Response(city, weatherInfo);
+    }
+
+    /**
+     * 生成模拟的天气数据
+     * 在实际应用中,应替换为真实 API 调用
+     */
+    private Response.WeatherInfo generateMockWeatherInfo() {
+        int temperature = RandomUtil.randomInt(-5, 30);
+        int humidity = RandomUtil.randomInt(1, 100);
+        int windSpeed = RandomUtil.randomInt(1, 30);
+        String condition = RandomUtil.randomEle(WEATHER_CONDITIONS);
+        return new Response.WeatherInfo(temperature, condition, humidity, windSpeed,
+                LocalDateTimeUtil.format(LocalDateTime.now(), NORM_DATETIME_PATTERN));
+    }
+
+}

+ 1 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.ai.service.tool;

+ 11 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.ai.config;
 
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
 import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
@@ -17,6 +18,7 @@ import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStore
 import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
 import org.springframework.ai.embedding.BatchingStrategy;
 import org.springframework.ai.embedding.TokenCountBatchingStrategy;
+import org.springframework.ai.model.tool.ToolCallingManager;
 import org.springframework.ai.openai.OpenAiChatModel;
 import org.springframework.ai.openai.OpenAiChatOptions;
 import org.springframework.ai.openai.api.OpenAiApi;
@@ -70,6 +72,7 @@ public class YudaoAiAutoConfiguration {
                         .maxTokens(properties.getMaxTokens())
                         .topP(properties.getTopP())
                         .build())
+                .toolCallingManager(getToolCallingManager())
                 .build();
         return new DeepSeekChatModel(openAiChatModel);
     }
@@ -96,6 +99,7 @@ public class YudaoAiAutoConfiguration {
                         .maxTokens(properties.getMaxTokens())
                         .topP(properties.getTopP())
                         .build())
+                .toolCallingManager(getToolCallingManager())
                 .build();
         return new DouBaoChatModel(openAiChatModel);
     }
@@ -122,6 +126,7 @@ public class YudaoAiAutoConfiguration {
                         .maxTokens(properties.getMaxTokens())
                         .topP(properties.getTopP())
                         .build())
+                .toolCallingManager(getToolCallingManager())
                 .build();
         return new SiliconFlowChatModel(openAiChatModel);
     }
@@ -155,6 +160,7 @@ public class YudaoAiAutoConfiguration {
                         .maxTokens(properties.getMaxTokens())
                         .topP(properties.getTopP())
                         .build())
+                .toolCallingManager(getToolCallingManager())
                 .build();
         return new HunYuanChatModel(openAiChatModel);
     }
@@ -181,6 +187,7 @@ public class YudaoAiAutoConfiguration {
                         .maxTokens(properties.getMaxTokens())
                         .topP(properties.getTopP())
                         .build())
+                .toolCallingManager(getToolCallingManager())
                 .build();
         return new XingHuoChatModel(openAiChatModel);
     }
@@ -210,4 +217,8 @@ public class YudaoAiAutoConfiguration {
         return new TokenCountBatchingStrategy();
     }
 
+    private static ToolCallingManager getToolCallingManager() {
+        return SpringUtil.getBean(ToolCallingManager.class);
+    }
+
 }

+ 20 - 9
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java

@@ -23,6 +23,7 @@ import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration;
 import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
 import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
+import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
 import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
 import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
 import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
@@ -58,11 +59,14 @@ import org.springframework.ai.embedding.BatchingStrategy;
 import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.image.ImageModel;
 import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxChatOptions;
 import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
 import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
 import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
 import org.springframework.ai.model.tool.ToolCallingManager;
 import org.springframework.ai.moonshot.MoonshotChatModel;
+import org.springframework.ai.moonshot.MoonshotChatOptions;
 import org.springframework.ai.moonshot.api.MoonshotApi;
 import org.springframework.ai.ollama.OllamaChatModel;
 import org.springframework.ai.ollama.OllamaEmbeddingModel;
@@ -90,10 +94,7 @@ import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservat
 import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
 import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
 import org.springframework.ai.vectorstore.redis.RedisVectorStore;
-import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
-import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel;
-import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions;
-import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
+import org.springframework.ai.zhipuai.*;
 import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
 import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
 import org.springframework.beans.BeansException;
@@ -110,6 +111,7 @@ import java.util.Timer;
 import java.util.TimerTask;
 
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
 
 /**
  * AI Model 模型工厂的实现类
@@ -308,7 +310,9 @@ public class AiModelFactoryImpl implements AiModelFactory {
      */
     private static DashScopeChatModel buildTongYiChatModel(String key) {
         DashScopeApi dashScopeApi = new DashScopeApi(key);
-        return new DashScopeChatModel(dashScopeApi);
+        DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
+                .withTemperature(0.7).build();
+        return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
     }
 
     /**
@@ -385,7 +389,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
     private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
         ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
                 : new ZhiPuAiApi(url, apiKey);
-        return new ZhiPuAiChatModel(zhiPuAiApi);
+        ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
+        return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
     }
 
     /**
@@ -403,7 +408,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
     private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
         MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
                 : new MiniMaxApi(url, apiKey);
-        return new MiniMaxChatModel(miniMaxApi);
+        MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
+        return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
     }
 
     /**
@@ -412,7 +418,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
     private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
         MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey)
                 : new MoonshotApi(url, apiKey);
-        return new MoonshotChatModel(moonshotApi);
+        MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
+        return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
     }
 
     /**
@@ -449,7 +456,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
         // 获取 AzureOpenAiChatProperties 对象
         AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
         return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties,
-                null, null, null);
+                getToolCallingManager(), null, null);
     }
 
     /**
@@ -704,4 +711,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
         return SpringUtil.getBean(ToolCallingManager.class);
     }
 
+    private static FunctionCallbackResolver getFunctionCallbackResolver() {
+        return SpringUtil.getBean(FunctionCallbackResolver.class);
+    }
+
 }

+ 19 - 15
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java

@@ -13,6 +13,8 @@ import org.springframework.ai.openai.OpenAiChatOptions;
 import org.springframework.ai.qianfan.QianFanChatOptions;
 import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
 
+import java.util.Set;
+
 /**
  * Spring AI 工具类
  *
@@ -21,22 +23,27 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
 public class AiUtils {
 
     public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
+        return buildChatOptions(platform, model, temperature, maxTokens, null);
+    }
+
+    public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
+                                               Set<String> toolNames) {
         // noinspection EnhancedSwitchMigration
         switch (platform) {
             case TONG_YI:
-                // TODO functions
-                return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build();
+                return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
+                        .withFunctions(toolNames).build();
             case YI_YAN:
                 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
             case ZHI_PU:
-                // TODO functions
-                return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
+                return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
+                        .functions(toolNames).build();
             case MINI_MAX:
-                // TODO functions
-                return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
+                return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
+                        .functions(toolNames).build();
             case MOONSHOT:
-                // TODO functions
-                return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
+                return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
+                        .functions(toolNames).build();
             case OPENAI:
             case DEEP_SEEK: // 复用 OpenAI 客户端
             case DOU_BAO: // 复用 OpenAI 客户端
@@ -44,17 +51,14 @@ public class AiUtils {
             case XING_HUO: // 复用 OpenAI 客户端
             case SILICON_FLOW: // 复用 OpenAI 客户端
                 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
-//                        .toolNames() TODO
-                        .toolNames("listDir")
-                        .build();
+                        .toolNames(toolNames).build();
             case AZURE_OPENAI:
                 // TODO 芋艿:貌似没 model 字段???!
-                // TODO 芋艿:.toolNames() TODO
-                return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build();
+                return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
+                        .toolNames(toolNames).build();
             case OLLAMA:
-                // TODO 芋艿:.toolNames() TODO
                 return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
-                        .toolNames("listDir").build();
+                        .toolNames(toolNames).build();
             default:
                 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
         }