流式对话 1 2 3 4 5 6 7 8 9 10 11 12 13 spring: ai: ollama: base-url: http://127.0.0.1:11434 embedding: options: model: text-embedding-ada-002 openai: base-url: https://api.vveai.com api-key: sk-vArxm5LJ7SY8vUIM343869E1813544C5B80cEb7226BaB806 embedding: options: model: text-embedding-ada-002
1 2 3 4 5 6 7 @Bean public OpenAiApi openAiApi (@Value("${spring.ai.openai.base-url}") String baseUrl, @Value("${spring.ai.openai.api-key}") String apikey) { return OpenAiApi.builder() .baseUrl(baseUrl) .apiKey(apikey) .build(); }
注入 OpenAiApi,OpenAiApi 是真正“干活”与 OpenAI 服务器对话的工具,后续 SpringAI 的封装层,比如说 OpenAiChatClient,其内部持有 OpenAiApi 的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @RestController @RequestMapping("/api/v1/openai/") @CrossOrigin("*") public class OpenAiController implements IAiService { @Resource private OpenAiChatClient chatClient; @Resource private PgVectorStore pgVectorStore; @RequestMapping(value = "generate", method = RequestMethod.GET) @Override public ChatResponse generate (@RequestParam String model, @RequestParam String message) { return chatClient.call(new Prompt ( message, OpenAiChatOptions.builder() .withModel(model) .build() )); } @RequestMapping(value = "generate_stream", method = RequestMethod.GET) public Flux<ChatResponse> generateStream (@RequestParam String model, @RequestParam String message) { return chatClient.stream(new Prompt ( message, OpenAiChatOptions.builder() .withModel(model) .build() )); } }
generateStream() 方法产生流式对话
静态知识库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean public OpenAiEmbeddingModel openAiEmbeddingModel (OpenAiApi openAiApi) { return new OpenAiEmbeddingModel (openAiApi); } @Bean("openAiPgVectorStore") public PgVectorStore pgVectorStore (OpenAiApi openAiApi, JdbcTemplate jdbcTemplate, OpenAiEmbeddingModel embeddingModel) { return PgVectorStore.builder(jdbcTemplate, embeddingModel) .vectorTableName("vector_store_openai" ) .build(); } @Bean public TokenTextSplitter tokenTextSplitter () { return new TokenTextSplitter (); }
OpenAiEmbeddingModel :“文本向量化”工具 调用 OpenAI 的 API 来生成“词嵌入”(Embeddings)。词嵌入是一种将文本(如单词、句子)转换为数值向量(一串数字)的技术,这些向量能捕捉文本的语义含义。
PgVectorStore :连接到 PostgreSQL 的“向量数据库” 存储、管理和检索由上一个 Bean(embeddingModel)生成的向量。这通常用于实现语义搜索(即“查找含义相似的文本”)
TokenTextSplitter :创建一个文本分割器 大语言模型处理的文本长度是有限的。当你需要处理一篇长文章时,这个工具可以把它智能地分割成模型可以接受的小块,同时确保语义的完整性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void upload () { TikaDocumentReader documentReader = new TikaDocumentReader ("./data/file.txt" ); List<Document> documents = documentReader.get(); List<Document> documentList = tokenTextSplitter.apply(documents); documents.forEach(doc -> doc.getMetadata().put("knowledge" , "v1" )); documentList.forEach(doc -> doc.getMetadata().put("knowledge" , "v1" )); pgVectorStore.accept(documentList); log.info("上传完成" ); }
1 2 documents.forEach(doc -> doc.getMetadata().put("knowledge" , "v1" )); documentList.forEach(doc -> doc.getMetadata().put("knowledge" , "v1" ));
功能: 添加元数据(Metadata)。
doc.getMetadata().put(...): Document 对象不仅包含文本内容,还可以携带“元数据”(描述数据的数据)。
“knowledge”, “v1”: 在这里,开发者给原始文档(documents)和分割后的文档块(documentList)都打上了一个标签,键是 “knowledge”,值是 “v1”。
用途: 以后可以根据这个元数据来查询(“只搜索 v1 版本的知识”)或进行更新/删除(“删除所有 v1 的数据”)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Test public void chat () { String message = "王大瓜今年几岁" ; String SYSTEM_PROMPT = """ Use the information from the DOCUMENTS section to provide accurate answers but act as if you knew this information innately. If unsure, simply state that you don't know. Another thing you need to note is that your reply must be in Chinese! DOCUMENTS: {documents} """ ; SearchRequest request = SearchRequest.builder() .query(message) .topK(5 ) .filterExpression("knowledge == 'v1'" ) .build(); List<Document> documents = pgVectorStore.similaritySearch(request); String documentsCollectors = documents == null ? "" : documents.stream().map(Document::getText).collect(Collectors.joining()); Message ragMessage = new SystemPromptTemplate (SYSTEM_PROMPT).createMessage(Map.of("documents" , documentsCollectors)); ArrayList<Message> messages = new ArrayList <>(); messages.add(ragMessage); messages.add(new UserMessage (message)); ChatResponse chatResponse = openAiChatModel.call(new Prompt (messages, OpenAiChatOptions.builder().model("gpt-3.5-turbo" ).build())); log.info("测试结果:{}" , JSON.toJSONString(chatResponse)); }
这段代码展示了根据一个特定的问题,先从向量数据库中检索相关知识,然后将这些知识连同问题一起交给 AI 模型,最后由 AI 模型生成一个基于所提供知识的回答
1 2 3 4 5 SearchRequest request = SearchRequest.builder() .query(message) .topK(5 ) .filterExpression("knowledge == 'v1'" ) .build();
query(message): 使用用户的问题 “王大瓜今年几岁” 作为语义搜索的查询词。
topK(5): 指示数据库返回最相似的 5 个文档块。
filterExpression(“knowledge == ‘v1’”): 这是一个元数据过滤器。它要求数据库只在那些元数据中包含 knowledge == ‘v1’ 的文档中进行搜索。这与上一个 upload 方法中设置的元数据 doc.getMetadata().put(“knowledge”, “v1”) 对应。
pgVectorStore.similaritySearch(request): 在 PostgreSQL 向量数据库中执行搜索,返回一个包含 5 个最相关 Document 对象的列表。
MCP mcp:ai 能理解的协议 ai 可以通过这个“协议”调用一些开发者写好的功能,比如说查询数据库 mcp 分为客户端和服务端
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 { "mcpServers" : { "mcp-server-computer" : { "command" : "java" , "args" : [ "-Dfile.encoding=utf-8" , "-Dspring.ai.mcp.server.stdio=true" , "-jar" , "D:\\project\\xiaofuge\\ai-mcp-knowledge\\mcp-server-computer\\target\\mcp-server-computer-1.0.0.jar" ] } } }
1 2 3 4 5 6 7 spring: ai: mcp: client: request-timeout: 360s stdio: servers-configuration: classpath:/config/mcp-servers-config.json
1 2 @Autowired private ToolCallbackProvider tools;
工作流程如下:
加载客户端配置 (YAML) :
Spring Boot 启动时,会读取 application.yml (或 .properties) 文件。
它找到了 spring.ai.mcp.client 配置,并得知 MCP 客户端需要去 classpath:/config/mcp-servers-config.json 这个路径下查找服务器的定义。
加载服务器定义 (JSON) :
Spring AI 的 MCP 客户端会读取 mcp-servers-config.json 文件。
它发现了一个名为 mcp-server-computer 的服务器。
启动外部服务 (子进程) :
MCP 客户端会根据 JSON 中定义的 command 和 args,在本地启动一个新的 Java 进程 (即 java -jar ... mcp-server-computer-1.0.0.jar)。
Dspring.ai.mcp.server.stdio=true 这个参数告诉那个被启动的 jar 包,它应该通过标准输入/输出 (Stdio) 来与父进程(即您的主 Spring Boot 应用)进行 MCP 协议通信。
工具发现与注册 :
一旦子进程(mcp-server-computer)启动,它会通过 Stdio 告诉主应用(MCP 客户端):“你好,我已经启动了,我提供了以下工具(Tool/Function):[工具A, 工具B, …]”。
Spring AI 的 MCP 客户端接收到这个工具列表后,会自动 为 mcp-server-computer 提供的每一个工具 动态地创建一个 ToolCallback Bean。
注入 ToolCallbackProvider :
ToolCallbackProvider 是 Spring AI 中用于管理和提供所有 可用 ToolCallback 的核心接口。
当您使用 @Autowired private ToolCallbackProvider tools; 时,Spring 注入的是一个包含了所有已注册工具 的提供者实例。
这其中就包括了 在第 4 步中从 mcp-server-computer 子进程动态发现并注册的那些工具。
总结一下:
不需要 手动创建 ToolCallback。只需要通过 JSON 和 YAML 声明 MCP 服务的存在和启动方式,Spring AI 的自动配置就会负责启动它、发现它提供的工具,并将这些工具自动注册到 ToolCallbackProvider 中,使其在应用中立即可用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class MCPTest { @Resource private ChatClient.Builder chatClientBuilder; @Autowired private ToolCallbackProvider tools; @Test public void test_tool () { String userInput = "有哪些工具可以使用" ; var chatClient = chatClientBuilder .defaultTools(tools) .defaultOptions(OpenAiChatOptions.builder() .model("gpt-3.5-turbo" ) .build()) .build(); System.out.println("\n>>> QUESTION: " + userInput); System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content()); } @Test public void test () { String userInput = "获取电脑配置" ; userInput = "获取电脑配置 在 D:\\project\\xiaofuge\\ai-rag-knowledge\\lbwxxc-ai-rag-knowledge\\xfg-dev-tech-app\\cloned-repo 文件夹下,创建 电脑.txt 把电脑配置写入 电脑.txt" ; String mysqlInput = "获取所有订单" ; var chatClient = chatClientBuilder .defaultTools(tools) .defaultOptions(OpenAiChatOptions.builder() .model("gpt-4o" ) .build()) .build(); System.out.println("\n>>> QUESTION: " + mysqlInput); System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(mysqlInput).call().content()); } }
服务端 创建两个 Bean,Bean 内部想想要对 ai 开放的方法加上 @Tool 注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Slf4j @Service public class ComputerService { @Tool(description = "获取电脑配置") public ComputerFunctionResponse queryConfig (ComputerFunctionRequest request) { log.info("获取电脑配置信息 {}" , request.getComputer()); Properties properties = System.getProperties(); String osName = properties.getProperty("os.name" ); String osVersion = properties.getProperty("os.version" ); String osArch = properties.getProperty("os.arch" ); String userName = properties.getProperty("user.name" ); String userHome = properties.getProperty("user.home" ); String userDir = properties.getProperty("user.dir" ); String javaVersion = properties.getProperty("java.version" ); String osInfo = "" ; if (osName.toLowerCase().contains("win" )) { osInfo = getWindowsSpecificInfo(); } else if (osName.toLowerCase().contains("mac" )) { osInfo = getMacSpecificInfo(); } else if (osName.toLowerCase().contains("nix" ) || osName.toLowerCase().contains("nux" )) { osInfo = getLinuxSpecificInfo(); } ComputerFunctionResponse response = new ComputerFunctionResponse (); response.setOsName(osName); response.setOsVersion(osVersion); response.setOsArch(osArch); response.setUserName(userName); response.setUserHome(userHome); response.setUserDir(userDir); response.setJavaVersion(javaVersion); response.setOsInfo(osInfo); return response; } ...... }
1 2 3 4 5 6 7 8 9 @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class ComputerFunctionRequest { @JsonProperty(required = true, value = "computer") @JsonPropertyDescription("电脑名称") private String computer; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class ComputerFunctionResponse { @JsonProperty(required = true, value = "osName") @JsonPropertyDescription("操作系统名称") private String osName; @JsonProperty(required = true, value = "osVersion") @JsonPropertyDescription("操作系统版本") private String osVersion; @JsonProperty(required = true, value = "osArch") @JsonPropertyDescription("操作系统架构") private String osArch; @JsonProperty(required = true, value = "userName") @JsonPropertyDescription("用户的账户名称") private String userName; @JsonProperty(required = true, value = "userHome") @JsonPropertyDescription("用户的主目录") private String userHome; @JsonProperty(required = true, value = "userDir") @JsonPropertyDescription("用户的当前工作目录") private String userDir; @JsonProperty(required = true, value = "javaVersion") @JsonPropertyDescription("Java 运行时环境版本") private String javaVersion; @JsonProperty(required = true, value = "osInfo") @JsonPropertyDescription("系统信息") private String osInfo; }
服务端的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j @SpringBootApplication @Configurable public class McpServerComputerApplication implements CommandLineRunner { public static void main (String[] args) { SpringApplication.run(McpServerComputerApplication.class, args); } @Bean public ToolCallbackProvider computerTools (ComputerService computerService, OrderService orderService) { return MethodToolCallbackProvider.builder().toolObjects(computerService, orderService).build(); } @Override public void run (String... args) throws Exception { log.info("mcp server computer success!" ); } }
这段代码是 MCP 服务器(mcp-server-computer)的 Spring Boot 启动类 。
这正是您上一段 JSON 配置中 java -jar ... mcp-server-computer-1.0.0.jar 命令所运行的那个 .jar 文件的主程序 。
它的核心用途 是:定义并向 Spring AI 框架注册一组可供 AI 调用的工具(Tools) 。
1. 核心功能:@Bean computerTools
1 2 3 4 @Bean public ToolCallbackProvider computerTools (ComputerService computerService, OrderService orderService) { return MethodToolCallbackProvider.builder().toolObjects(computerService, orderService).build(); }
@Bean : 告诉 Spring 框架,这是一个需要被管理的 Bean(组件)。
ToolCallbackProvider : 这是 Spring AI 用来管理“工具回调”的核心接口。
MethodToolCallbackProvider.builder() : 这是一个具体的实现,它能自动扫描 一个或多个对象(Object)中被特定注解(如 @Tool 或 @Description)标记的方法 ,并将这些方法转换为 AI 可调用的工具。
toolObjects(computerService, orderService) : 这是关键所在。它告诉 Spring AI:
请自动注入 ComputerService 和 OrderService 这两个服务。
请扫描 这两个服务里的所有公开方法。
将那些符合规范(被注解)的方法注册为 AI 工具。
简而言之:这个 Bean 的作用就是向 AI 声明:“我这里有两套工具,一套是 ComputerService 里的,一套是 OrderService 里的,你可以调用它们”。
2. 启动和确认:CommandLineRunner
1 2 3 4 @Override public void run (String... args) throws Exception { log.info("mcp server computer success!" ); }
implements CommandLineRunner : 这个接口让 run 方法会在 Spring Boot 应用完全启动并加载完所有 Bean (包括上面那个 ToolCallbackProvider)之后被自动执行。
log.info(...) : 它会在控制台打印一条日志 “mcp server computer success!“。
这条日志有两个重要作用:
给开发者看 :表明这个服务(.jar 包)已经成功启动,没有报错。
给 MCP 客户端(父进程)看 :在 stdio(标准输入输出)通信模式下,父进程会监视这个子进程的输出。看到这条 “success” 日志,父进程就知道这个 MCP 服务器已经准备就绪,可以开始向它请求工具列表了。
AI Agent - SpringAI 1.0.0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 spring: datasource: username: password: url: driver-class-name: org.postgresql.Driver type: com.zaxxer.hikari.HikariDataSource hikari: pool-name: Retail_HikariCP minimum-idle: 15 idle-timeout: 180000 maximum-pool-size: 25 auto-commit: true max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 ai: openai: base-url: api-key:
构建 MCP 工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public McpSyncClient mcpAsyncClientSystem () { ServerParameters stdioParams = ServerParameters.builder("java" ) .args("-Dfile.encoding=utf-8" , "-Dspring.ai.mcp.server.stdio=true" , "-jar" , "D:\\project\\xiaofuge\\ai-mcp-knowledge\\mcp-server-computer\\target\\mcp-server-computer-1.0.0.jar" ) .build(); McpSyncClient mcpSyncClient = McpClient.sync(new StdioClientTransport (stdioParams)).requestTimeout(Duration.ofSeconds(10 )).build(); McpSchema.InitializeResult initialize = mcpSyncClient.initialize(); System.out.println("Stdio MCP Initialized: " + initialize); return mcpSyncClient; }
初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 private ChatModel chatModel;private ChatClient chatClient;@Resource private PgVectorStore vectorStore;@Before public void init () { OpenAiApi openAiApi = OpenAiApi.builder() .baseUrl("https://api.vveai.com" ) .apiKey("sk-vArxm5LJ7SY8vUIM343869E1813544C5B80cEb7226BaB806" ) .completionsPath("v1/chat/completions" ) .embeddingsPath("v1/embeddings" ) .build(); chatModel = OpenAiChatModel.builder() .openAiApi(openAiApi) .defaultOptions(OpenAiChatOptions.builder() .model("gpt-4.1-mini" ) .toolCallbacks(new SyncMcpToolCallbackProvider (mcpAsyncClientSystem()).getToolCallbacks()) .build()) .build(); chatClient = ChatClient.builder(chatModel) .defaultSystem(""" 你是一个 AI Agent 智能体,,今天是 {current_date}。 你可以分析系统配置和查询订单记录 """ ) .defaultAdvisors( PromptChatMemoryAdvisor.builder( MessageWindowChatMemory.builder() .maxMessages(100 ) .build() ).build(), new RagAnswerAdvisor (vectorStore, SearchRequest.builder() .topK(5 ) .filterExpression("knowledge == 'article-prompt-words'" ) .build()), SimpleLoggerAdvisor.builder().build() ) .build(); }
多数据源和 Mapper 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 spring: datasource: mysql: username: password: url: driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource hikari: pool-name: Retail_HikariCP minimum-idle: 15 idle-timeout: 180000 maximum-pool-size: 25 auto-commit: true max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 pgvector: username: password: url: driver-class-name: org.postgresql.Driver type: com.zaxxer.hikari.HikariDataSource hikari: pool-name: Retail_HikariCP minimum-idle: 15 idle-timeout: 180000 maximum-pool-size: 25 auto-commit: true max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 @Configuration public class DataSourceConfig { @Bean("mysqlDataSource") @Primary public DataSource mysqlDataSource (@Value("${spring.datasource.mysql.driver-class-name}") String driverClassName, @Value("${spring.datasource.mysql.url}") String url, @Value("${spring.datasource.mysql.username}") String username, @Value("${spring.datasource.mysql.password}") String password, @Value("${spring.datasource.mysql.hikari.maximum-pool-size:10}") int maximumPoolSize, @Value("${spring.datasource.mysql.hikari.minimum-idle:5}") int minimumIdle, @Value("${spring.datasource.mysql.hikari.idle-timeout:30000}") long idleTimeout, @Value("${spring.datasource.mysql.hikari.connection-timeout:30000}") long connectionTimeout, @Value("${spring.datasource.mysql.hikari.max-lifetime:1800000}") long maxLifetime) { HikariDataSource dataSource = new HikariDataSource (); dataSource.setDriverClassName(driverClassName); dataSource.setJdbcUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setMaximumPoolSize(maximumPoolSize); dataSource.setMinimumIdle(minimumIdle); dataSource.setIdleTimeout(idleTimeout); dataSource.setConnectionTimeout(connectionTimeout); dataSource.setMaxLifetime(maxLifetime); dataSource.setPoolName("MainHikariPool" ); return dataSource; } @Bean("sqlSessionFactoryBean") public SqlSessionFactoryBean sqlSessionFactory (@Qualifier("mysqlDataSource") DataSource mysqlDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(mysqlDataSource); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver (); sqlSessionFactoryBean.setConfigLocation(resolver.getResource("classpath:/mybatis/config/mybatis-config.xml" )); sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/mapper/*.xml" )); return sqlSessionFactoryBean; } @Bean("sqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate (@Qualifier("sqlSessionFactoryBean") SqlSessionFactoryBean sqlSessionFactory) throws Exception { return new SqlSessionTemplate (Objects.requireNonNull(sqlSessionFactory.getObject())); } @Bean("pgVectorDataSource") public DataSource pgVectorDataSource (@Value("${spring.datasource.pgvector.driver-class-name}") String driverClassName, @Value("${spring.datasource.pgvector.url}") String url, @Value("${spring.datasource.pgvector.username}") String username, @Value("${spring.datasource.pgvector.password}") String password, @Value("${spring.datasource.pgvector.hikari.maximum-pool-size:5}") int maximumPoolSize, @Value("${spring.datasource.pgvector.hikari.minimum-idle:2}") int minimumIdle, @Value("${spring.datasource.pgvector.hikari.idle-timeout:30000}") long idleTimeout, @Value("${spring.datasource.pgvector.hikari.connection-timeout:30000}") long connectionTimeout) { HikariDataSource dataSource = new HikariDataSource (); dataSource.setDriverClassName(driverClassName); dataSource.setJdbcUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setMaximumPoolSize(maximumPoolSize); dataSource.setMinimumIdle(minimumIdle); dataSource.setIdleTimeout(idleTimeout); dataSource.setConnectionTimeout(connectionTimeout); dataSource.setInitializationFailTimeout(1 ); dataSource.setConnectionTestQuery("SELECT 1" ); dataSource.setAutoCommit(true ); dataSource.setPoolName("PgVectorHikariPool" ); return dataSource; } @Bean("pgVectorJdbcTemplate") public JdbcTemplate pgVectorJdbcTemplate (@Qualifier("pgVectorDataSource") DataSource dataSource) { return new JdbcTemplate (dataSource); } }
动态实例化对话模型、实例化对话客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 protected String doApply (ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { log.info("Ai Agent 构建,API 构建节点 {}" , JSON.toJSONString(requestParameter)); List<AiClientApiVO> aiClientApiList = dynamicContext.getValue(AiAgentEnumVO.AI_CLIENT_API.getDataName()); if (aiClientApiList == null || aiClientApiList.isEmpty()) { log.warn("没有需要被初始化的 ai client api" ); return null ; } for (AiClientApiVO aiClientApiVO : aiClientApiList) { OpenAiApi openAiApi = OpenAiApi.builder() .baseUrl(aiClientApiVO.getBaseUrl()) .apiKey(aiClientApiVO.getApiKey()) .completionsPath(aiClientApiVO.getCompletionsPath()) .embeddingsPath(aiClientApiVO.getEmbeddingsPath()) .build(); registerBean(AiAgentEnumVO.AI_CLIENT_API.getBeanName(aiClientApiVO.getApiId()), OpenAiApi.class, openAiApi); } return router(requestParameter, dynamicContext); }
实例化 OpenAiApi,并注册为 Bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 @Override protected String doApply (ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { log.info("Ai Agent 构建节点,Tool MCP 工具配置{}" , JSON.toJSONString(requestParameter)); List<AiClientToolMcpVO> aiClientToolMcpList = dynamicContext.getValue(dataName()); if (aiClientToolMcpList == null || aiClientToolMcpList.isEmpty()) { log.warn("没有需要被初始化的 ai client tool mcp" ); return router(requestParameter, dynamicContext); } for (AiClientToolMcpVO mcpVO : aiClientToolMcpList) { McpSyncClient mcpSyncClient = createMcpSyncClient(mcpVO); registerBean(beanName(mcpVO.getMcpId()), McpSyncClient.class, mcpSyncClient); } return router(requestParameter, dynamicContext); } @Override public StrategyHandler<ArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String> get(ArmoryCommandEntity armoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { return aiClientModelNode; } private McpSyncClient createMcpSyncClient (AiClientToolMcpVO aiClientToolMcpVO) { String transportType = aiClientToolMcpVO.getTransportType(); switch (transportType) { case "sse" -> { AiClientToolMcpVO.TransportConfigSse transportConfigSse = aiClientToolMcpVO.getTransportConfigSse(); String originalBaseUri = transportConfigSse.getBaseUri(); String baseUri; String sseEndpoint; int queryParamStartIndex = originalBaseUri.indexOf("sse" ); if (queryParamStartIndex != -1 ) { baseUri = originalBaseUri.substring(0 , queryParamStartIndex - 1 ); sseEndpoint = originalBaseUri.substring(queryParamStartIndex - 1 ); } else { baseUri = originalBaseUri; sseEndpoint = transportConfigSse.getSseEndpoint(); } sseEndpoint = StringUtils.isBlank(sseEndpoint) ? "/sse" : sseEndpoint; HttpClientSseClientTransport sseClientTransport = HttpClientSseClientTransport .builder(baseUri) .sseEndpoint(sseEndpoint) .build(); McpSyncClient mcpSyncClient = McpClient.sync(sseClientTransport).requestTimeout(Duration.ofMinutes(aiClientToolMcpVO.getRequestTimeout())).build(); var init_sse = mcpSyncClient.initialize(); log.info("Tool SSE MCP Initialized {}" , init_sse); return mcpSyncClient; } case "stdio" -> { AiClientToolMcpVO.TransportConfigStdio transportConfigStdio = aiClientToolMcpVO.getTransportConfigStdio(); Map<String, AiClientToolMcpVO.TransportConfigStdio.Stdio> stdioMap = transportConfigStdio.getStdio(); AiClientToolMcpVO.TransportConfigStdio.Stdio stdio = stdioMap.get(aiClientToolMcpVO.getMcpName()); var stdioParams = ServerParameters.builder(stdio.getCommand()) .args(stdio.getArgs()) .env(stdio.getEnv()) .build(); var mcpClient = McpClient.sync(new StdioClientTransport (stdioParams)) .requestTimeout(Duration.ofSeconds(aiClientToolMcpVO.getRequestTimeout())).build(); var init_stdio = mcpClient.initialize(); log.info("Tool Stdio MCP Initialized {}" , init_stdio); return mcpClient; } } throw new RuntimeException ("err! transportType " + transportType + " not exist!" ); }
实例化MCP 并注册为 Bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Override protected String doApply (ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { log.info("Ai Agent 构建节点,Node 对话模型{}" , JSON.toJSONString(requestParameter)); List<AiClientModelVO> aiClientModelList = dynamicContext.getValue(dataName()); if (aiClientModelList == null || aiClientModelList.isEmpty()) { log.warn("没有需要被初始化的 ai client model" ); return router(requestParameter, dynamicContext); } for (AiClientModelVO modelVO : aiClientModelList) { OpenAiApi openAiApi = getBean(AiAgentEnumVO.AI_CLIENT_API.getBeanName(modelVO.getApiId())); if (null == openAiApi) { throw new RuntimeException ("mode 2 api is null" ); } List<McpSyncClient> mcpSyncClients = new ArrayList <>(); for (String toolMcpId : modelVO.getToolMcpIds()) { McpSyncClient mcpSyncClient = getBean(AiAgentEnumVO.AI_CLIENT_TOOL_MCP.getBeanName(toolMcpId)); mcpSyncClients.add(mcpSyncClient); } OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(openAiApi) .defaultOptions( OpenAiChatOptions.builder() .model(modelVO.getModelName()) .toolCallbacks(new SyncMcpToolCallbackProvider (mcpSyncClients).getToolCallbacks()) .build()) .build(); registerBean(beanName(modelVO.getModelId()), OpenAiChatModel.class, chatModel); } return router(requestParameter, dynamicContext); }
根据 openAiApi、MCP 的 Bean,实例化 OpenAiChatModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 @Slf4j @Service public class AiClientNode extends AbstractArmorySupport { @Override protected String doApply (ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { log.info("Ai Agent 构建节点,客户端{}" , JSON.toJSONString(requestParameter)); List<AiClientVO> aiClientList = dynamicContext.getValue(dataName()); if (null == aiClientList || aiClientList.isEmpty()) { return router(requestParameter, dynamicContext); } Map<String, AiClientSystemPromptVO> systemPromptMap = dynamicContext.getValue(AiAgentEnumVO.AI_CLIENT_SYSTEM_PROMPT.getDataName()); for (AiClientVO aiClientVO : aiClientList) { StringBuilder defaultSystem = new StringBuilder ("Ai 智能体 \r\n" ); List<String> promptIdList = aiClientVO.getPromptIdList(); for (String promptId : promptIdList) { AiClientSystemPromptVO aiClientSystemPromptVO = systemPromptMap.get(promptId); defaultSystem.append(aiClientSystemPromptVO.getPromptContent()); } OpenAiChatModel chatModel = getBean(aiClientVO.getModelBeanName()); List<McpSyncClient> mcpSyncClients = new ArrayList <>(); List<String> mcpBeanNameList = aiClientVO.getMcpBeanNameList(); for (String mcpBeanName : mcpBeanNameList) { mcpSyncClients.add(getBean(mcpBeanName)); } List<Advisor> advisors = new ArrayList <>(); List<String> advisorBeanNameList = aiClientVO.getAdvisorBeanNameList(); for (String advisorBeanName : advisorBeanNameList) { advisors.add(getBean(advisorBeanName)); } Advisor[] advisorArray = advisors.toArray(new Advisor []{}); ChatClient chatClient = ChatClient.builder(chatModel) .defaultSystem(defaultSystem.toString()) .defaultToolCallbacks(new SyncMcpToolCallbackProvider (mcpSyncClients.toArray(new McpSyncClient []{}))) .defaultAdvisors(advisorArray) .build(); registerBean(beanName(aiClientVO.getClientId()), ChatClient.class, chatClient); } return router(requestParameter, dynamicContext); } @Override public StrategyHandler<ArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String> get(ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { return defaultStrategyHandler; } @Override protected String beanName (String id) { return AiAgentEnumVO.AI_CLIENT.getBeanName(id); } @Override protected String dataName () { return AiAgentEnumVO.AI_CLIENT.getDataName(); } }