目錄
Spring AI 介紹
Spring AI 組件介紹
Spring AI 結構化輸出
Srping AI 多模態
Spring AI 本地Ollama
Spring AI 源碼
Spring AI Advisor機制
Spring AI Tool Calling
Spring AI MCP
Spring AI RAG
Spring AI Agent
Spring AI 是一個用于 AI 工程的應用程序框架。 其目標是將 Spring 生態系統設計原則(如可移植性和模塊化設計)應用于 AI 領域,并將使用 POJO 作為應用程序的構建塊推廣到 AI 領域。
Spring AI 的核心是解決了 AI 集成的根本挑戰:將您的企業數據和 API 與 AI 模型連接起來。
AI 模型
AI Embedding
嵌入是文本、圖像或視頻的數字表示形式,用于捕獲輸入之間的關系。
嵌入的工作原理是將文本、圖像和視頻轉換為浮點數數組(稱為向量)。 這些矢量旨在捕獲文本、圖像和視頻的含義。 嵌入數組的長度稱為向量的維數。
結構化輸出
RAG
ETL
AI TOOL
代碼包
spring-ai-commons
spring-ai-commons 是 Spring AI 框架中的基礎通用模塊,主要提供跨功能模塊的公共組件和工具類支持。
一、模塊定位
?1. 基礎支撐層?
- 作為 Spring AI 的底層通用包,包含不適合單獨劃分到特定模塊的公共實現
- 為其他模塊(如模型交互、向量存儲等)提供標準化工具類和接口定義
2?. 代碼復用中心?
- 集中管理重復使用的常量、異常處理、類型轉換等基礎邏輯23
二、核心功能
?1. 通用工具類?
- 提供字符串處理、JSON 序列化等公共方法
- 包含跨模塊的配置解析和驗證工具
2?. 基礎抽象接口?
- 定義模型加載、數據轉換等通用接口規范
- 支持模塊間的標準化交互協議(如 MCP 相關基礎)
3?. 異常處理體系?
- 統一封裝 AI 模型調用、數據處理等場景的異常類
spring-ai-template-st
spring-ai-template-st 是 Spring AI 框架中的字符串模板渲染組件,主要用于動態生成 AI 模型交互所需的提示詞(Prompt)。
一、核心功能
?1. 動態模板渲染?
- 支持通過占位符(如 {input})動態替換模板變量,生成結構化提示詞
- 示例模板:
"請用中文回答關于 {topic} 的問題,回答需包含以下要點:{requirements}"
2?. 多角色提示支持?
- 可定義不同角色的消息模板(如 system、user 角色),適配多輪對話場景
?3. 與 Prompt 類深度集成?
- 渲染結果可直接轉換為 Prompt 對象,用于 ChatClient 調用
二、技術實現
- 底層依賴?:基于 StringTemplate 引擎實現變量替換8
- 配置方式?:通常與 spring-ai-model 模塊配合使用,通過 @Configuration 注入模板實例
spring-ai-model
spring-ai-model 是 Spring AI 框架的核心模塊之一,負責統一管理各類 AI 模型的交互邏輯和抽象接口。
一、模塊定位
?1. 模型抽象層?
- 提供跨模型服務商(如 OpenAI、DeepSeek、Hugging Face 等)的標準化調用接口
- 通過 Model 抽象類定義通用交互協議,屏蔽不同 API 的底層差異37
- 多模態支持?
- 覆蓋語言模型(如 ChatGPT)、圖像生成(如 Stable Diffusion)、嵌入模型(Embedding)等 AI 能力
二、核心類與接口
?1. 基礎抽象類?
- Model:頂級抽象接口,定義 call() 方法處理 ModelRequest 并返回 ModelResponse
- ModelRequest:封裝輸入指令(instructions)和模型參數(ChatOptions)
- ModelResponse:包含輸出結果(List)和元數據(ResponseMetadata)
2?. 衍生模型接口?
- ChatModel:面向對話場景的擴展接口(如 ZhiPuAiChatModel 實現)38
- EmbeddingModel:處理文本/圖像向量化任務7
spring-ai-vector-store
spring-ai-vector-store 是 Spring AI 框架中專門用于處理向量數據庫交互的核心模塊,提供標準化的向量存儲與檢索能力。
一、核心功能
?1. 統一接口抽象?
- 通過 VectorStore 接口定義標準化操作,包括文檔寫入(add)、刪除(delete)和相似性搜索(similaritySearch)
- 支持通過 Filter.Expression 實現條件化數據操作
- 多數據庫適配?
- 內置支持 20+ 向量數據庫(如 Pinecone、PgVector、Milvus 等),通過統一 API 屏蔽底層差異
- 提供 SimpleVectorStore 作為輕量級內存實現,適用于開發測試
3?. RAG 流程集成?
- 與嵌入模型(EmbeddingModel)協同,自動將文檔轉換為向量后存儲
- 支持檢索增強生成(RAG)場景下的上下文檢索
二、核心類與接口
1?. 關鍵接口?
- VectorStore:頂層接口,繼承 DocumentWriter 實現文檔寫入能力12
- SearchRequest:封裝相似性搜索的請求參數(如返回數量、相似度閾值)
?2. 實現類示例?
- SimpleVectorStore:基于內存的并發安全實現,使用 ConcurrentHashMap 存儲向量數據
- PgVectorStore:集成 PostgreSQL 的 pgvector 擴展,支持自動建表和索引
?3. 構建器模式?
- 通過 AbstractVectorStoreBuilder 抽象基類實現鏈式配置,子類覆蓋具體數據庫邏輯6
spring-ai-rag
spring-ai-rag 是 Spring AI 框架中實現檢索增強生成(RAG)的核心模塊,通過整合向量檢索與大模型生成能力,解決傳統大語言模型的靜態知識局限性和幻覺問題。
一、核心架構
1?. 模塊化流程設計?
- 預檢索階段?:支持查詢轉換(QueryTransformer)和擴展(如 RewriteQueryTransformer)
- 檢索階段?:通過 VectorStoreDocumentRetriever 實現相似性搜索,支持閾值過濾(similarityThreshold)
?后處理階段?:提供文檔重排、壓縮和上下文增強(ContextualQueryAugmenter)
2?. 關鍵組件?
- QuestionAnswerAdvisor:開箱即用的 RAG 流程封裝,支持動態過濾表達式
- RetrievalAugmentationAdvisor(孵化中):支持自定義順序式 RAG 流程
二、技術特性
1?. 動態知識整合?
- 通過外部知識庫(如 Milvus、Elasticsearch)實時擴展模型知識,避免重新訓練
?2. 混合檢索支持?
- 結合語義搜索與傳統關鍵詞檢索,提升結果相關性
?3. Spring 生態集成?
- 與 spring-ai-vector-store 深度協同,支持自動配置向量數據庫連接
spring-ai-advisors-vector-store
spring-ai-advisors-vector-store 是 Spring AI 框架中專門用于結合向量存儲(Vector Store)和攔截器(Advisors)技術的模塊,旨在簡化檢索增強生成(RAG)流程的實現。
一、模塊定位
1?. RAG 流程封裝?
- 提供開箱即用的 QuestionAnswerAdvisor,自動將向量存儲檢索結果注入大模型請求上下文,實現檢索增強生成
- 支持通過 RetrievalAugmentationAdvisor 動態擴展用戶查詢的上下文信息
2?. 攔截器架構?
- 基于 Spring AOP 思想,通過 CallAroundAdvisor 攔截模型調用,在請求前后插入向量檢索邏輯
- 支持非流式(CallAroundAdvisorChain)和流式(StreamAroundAdvisorChain)兩種處理模式
二、核心組件
?1. 關鍵類與接口?
QuestionAnswerAdvisor:內置 RAG 攔截器,自動關聯 VectorStore 與 ChatModel
AdvisedRequest:封裝原始請求和共享上下文(如檢索到的文檔列表)
AdvisedResponse:包含模型響應及增強后的元數據
2?. 配置方式?
// 構建時注冊默認攔截器
ChatClient.builder(chatModel).defaultAdvisors(new QuestionAnswerAdvisor(vectorStore) // 綁定向量存儲).build();:ml-citation{ref="1,5" data="citationList"}
spring-ai-retry
spring-ai-retry 是 Spring AI 框架中專門處理 AI 模型調用重試機制的模塊,通過標準化策略增強系統容錯能力。
一、核心功能
1?. 異常分類機制?
- 定義 TransientAiException(可重試異常)和 NonTransientAiException(不可重試異常)兩類異常,明確重試邊界
- 自動識別網絡超時、速率限制等臨時性故障
- 策略配置?
- 支持最大重試次數(maxAttempts)、退避間隔(backoff)等參數定制
內置指數退避算法避免雪崩效應
二、關鍵組件
?1. 核心類?
- RetryUtils:提供重試邏輯的靜態工具方法,支持同步/異步調用
- RetryTemplate(擴展):與 Spring Retry 模塊集成,支持復雜策略組合
?
- 配置示例?
# application.properties
spring.ai.retry.max-attempts=3
spring.ai.retry.initial-interval=1000ms
spring.ai.retry.multiplier=2.0
spring-ai-client-chat
spring-ai-client-chat 是 Spring AI 框架中用于與大語言模型(LLM)交互的核心模塊,提供標準化的聊天式 API 接口和高級功能封裝。
一、核心組件
?1. ChatClient 接口?
-? 功能定位?:統一多模型(如 OpenAI、Gemini、本地 LLM)的聊天交互,支持同步/流式響應、上下文管理和結構化輸出
- 方法特性?:
// 鏈式構建請求
chatClient.prompt().system("你是一名Java專家") // 系統提示.user("解釋Spring AOP原理") // 用戶輸入.call() // 同步執行.content(); // 獲取文本響應
支持流式處理(stream())、實體映射(entity(Class))和參數動態注入(param())
2?. ChatClientRequestBuilder?
- 提供 Fluent API 設計,支持溫度(temperature)、最大 Token 數(maxTokens)等模型參數配置
二、關鍵特性
?1. 多模型支持?
- 通過 ChatModel 抽象層兼容 20+ 模型提供商(如 Anthropic、智譜 AI),僅需更換依賴即可切換模型實現?
- 上下文管理?
- 內置對話記憶(ChatMemory),自動維護多輪對話歷史
- 支持通過 Prompt 對象傳遞歷史消息實現連續對話
3?. 結構化輸出?
- 自動將 JSON 響應映射為 Java 對象:
record Joke(String setup, String punchline) {}
Joke joke = chatClient.prompt().user("講個冷笑話").call().entity(Joke.class); // 自動反序列化
適用于需要強類型響應的場景
spring-ai-mcp
spring-ai-mcp 是 Spring AI 框架中實現 ?模型上下文協議(Model Context Protocol, MCP)? 的核心模塊,提供標準化接口實現大語言模型(LLM)與外部工具、數據源的動態交互能力。
一、協議定位
1?. 核心目標?
- 標準化 LLM 與外部資源(數據庫、API、文件等)的交互協議,類似 AI 領域的 “HTTP 協議”
- 支持動態工具發現、上下文感知和資源安全訪問,解決傳統 Function Calling 的碎片化問題
2?. 架構分層?
- 傳輸層?:支持 STDIO(進程間通信)和 HTTP SSE(事件流)兩種傳輸模式
- 會話層?:通過 McpSession 管理通信狀態與協議版本協商
- 服務層?:提供工具調用、資源管理和提示模板注入等標準化服務
二、核心組件
?1. 服務端(McpServer)?
- 通過 @EnableMcpServer 注解快速啟動,支持同步/異步操作模式
- 關鍵能力:
// 示例:天氣預報服務端
@McpTool(name = "weather", description = "查詢城市天氣")
public String getWeather(@Param("city") String city) {return weatherService.fetch(city);
}
支持工具自動注冊、資源 URI 映射和結構化日志
2?. 客戶端(McpClient)?
- 內置協議協商機制,自動處理兼容性和能力發現
- 多傳輸協議支持:
// 配置 SSE 客戶端
McpClient client = new SseMcpClient("http://localhost:8080/mcp");
支持動態上下文注入和批量操作
spring-ai-spring-cloud-bindings
spring-ai-spring-cloud-bindings 是 Spring AI 框架中實現云服務自動化集成的核心模塊,通過標準化綁定機制簡化 AI 組件(如向量存儲、模型服務)與云平臺的連接配置。
一、核心功能
?1. 云憑證自動注入?
- 自動從云平臺(AWS/Azure/阿里云等)獲取 API 密鑰、訪問令牌等憑證,通過環境變量或 application.yml 動態注入
- 支持多環境隔離配置,如開發/生產環境使用不同云賬號
?
- 服務端點發現?
- 自動解析云服務的 API 端點(如 OpenAI 的 regional endpoint),避免硬編碼
- 與 Spring Cloud 服務發現組件(如 Nacos)集成,實現動態路由
- 資源綁定?
- 將云存儲(如 S3 Bucket)、向量數據庫(如 Azure AI Search)等資源映射為 Spring Bean,直接注入業務代碼
二、技術實現
1?. 綁定協議?
- 基于 Spring Cloud Bindings 規范,擴展支持 AI 領域特有資源類型(如 VectorStore、EmbeddingModel)
- 通過 @EnableAiCloudBindings 注解激活模塊
?
- 配置示例?
# application.yml
spring:cloud:bindings:openai-api:type: ai-modelprovider: azure # 自動使用AZURE_OPENAI_KEY環境變量pinecone-store:type: vector-storeprovider: aws # 自動綁定AWS Bedrock的Pinecone服務
支持通過 spring.cloud.bindings.* 前綴覆蓋默認行為
spring-ai-model-chat-memory
spring-ai-model-chat-memory 是 Spring AI 框架中實現對話記憶(Chat Memory)功能的核心模塊,用于解決大語言模型(LLM)無狀態問題,支持多輪對話的上下文管理。
一、核心功能
?1. 上下文維護?
- 通過 ChatMemory 接口自動存儲和檢索歷史消息(包括 UserMessage、SystemMessage、AssistantMessage),解決 LLM 單次請求無記憶的問題
- 默認實現 MessageWindowChatMemory 采用滑動窗口機制,保留最近 20 條消息(可配置)
2?. 存儲擴展?
- 支持多種持久化方案:
? - 內存存儲?:默認 InMemoryChatMemoryRepository(開發環境適用)
? - JDBC 存儲?:通過 spring-ai-starter-model-chat-memory-repository-jdbc 將對話記錄保存到 MySQL 等關系型數據庫
-? NoSQL 存儲?:兼容 Cassandra、Neo4j 等
3?. 高級特性?
- 向量搜索增強:VectorStoreChatMemoryAdvisor 支持基于語義相似度的歷史消息檢索
- 動態清理策略:可配置按消息數量、時間窗口或 Token 限制自動清理舊消息
二、技術實現
1?. 核心接口?
public interface ChatMemory {void add(Message message); // 添加消息List<Message> get(); // 獲取當前對話上下文void clear(); // 清空記憶
}
開發者可通過 @Autowired 直接注入使用
2?. 配置示例?
Copy Code
# application.yml
spring:ai:chat:memory:type: message-window # 使用滑動窗口策略size: 10 # 窗口大小storage: jdbc # 持久化方式
支持通過屬性文件靈活調整記憶策略
源代碼研究
1. BOM
<dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0-SNAPSHOT</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
2. Spring AI Configure
spring.ai.anthropic
spring.ai.azure.openai
spring.ai.bedrock.aws
spring.ai.chat
org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration
spring.ai.huggingface.chat
spring.ai.image.observations
spring.ai.minimax
spring.ai.mistralai
spring.ai.moonshot
spring.ai.oci.genai
spring.ai.ollama
spring.ai.openai
spring.ai.postgresml
spring.ai.qianfan
spring.ai.retry
spring.ai.stabilityai
spring.ai.embedding.transformer
spring.ai.vectorstore
spring.ai.vertex
spring.ai.watsonx
spring.ai.zhipuai
3. ChatModel API
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {default String call(String message) {Prompt prompt = new Prompt(new UserMessage(message));Generation generation = call(prompt).getResult();return (generation != null) ? generation.getOutput().getText() : "";}default String call(Message... messages) {Prompt prompt = new Prompt(Arrays.asList(messages));Generation generation = call(prompt).getResult();return (generation != null) ? generation.getOutput().getText() : "";}@OverrideChatResponse call(Prompt prompt);default ChatOptions getDefaultOptions() {return ChatOptions.builder().build();}default Flux<ChatResponse> stream(Prompt prompt) {throw new UnsupportedOperationException("streaming is not supported");}}
例子:Ollama Chat
Ollama API客戶端
public class OllamaChatModel implements ChatModel {private static final Logger logger = LoggerFactory.getLogger(OllamaChatModel.class);private static final String DONE = "done";private static final String METADATA_PROMPT_EVAL_COUNT = "prompt-eval-count";private static final String METADATA_EVAL_COUNT = "eval-count";private static final String METADATA_CREATED_AT = "created-at";private static final String METADATA_TOTAL_DURATION = "total-duration";private static final String METADATA_LOAD_DURATION = "load-duration";private static final String METADATA_PROMPT_EVAL_DURATION = "prompt-eval-duration";private static final String METADATA_EVAL_DURATION = "eval-duration";private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();private static final ToolCallingManager DEFAULT_TOOL_CALLING_MANAGER = ToolCallingManager.builder().build();private final OllamaApi chatApi;private final OllamaOptions defaultOptions;private final ObservationRegistry observationRegistry;private final OllamaModelManager modelManager;private final ToolCallingManager toolCallingManager;/*** The tool execution eligibility predicate used to determine if a tool can be* executed.*/private final ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate;private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;public OllamaChatModel(OllamaApi ollamaApi, OllamaOptions defaultOptions, ToolCallingManager toolCallingManager,ObservationRegistry observationRegistry, ModelManagementOptions modelManagementOptions) {this(ollamaApi, defaultOptions, toolCallingManager, observationRegistry, modelManagementOptions,new DefaultToolExecutionEligibilityPredicate());}public OllamaChatModel(OllamaApi ollamaApi, OllamaOptions defaultOptions, ToolCallingManager toolCallingManager,ObservationRegistry observationRegistry, ModelManagementOptions modelManagementOptions,ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate) {Assert.notNull(ollamaApi, "ollamaApi must not be null");Assert.notNull(defaultOptions, "defaultOptions must not be null");Assert.notNull(toolCallingManager, "toolCallingManager must not be null");Assert.notNull(observationRegistry, "observationRegistry must not be null");Assert.notNull(modelManagementOptions, "modelManagementOptions must not be null");Assert.notNull(toolExecutionEligibilityPredicate, "toolExecutionEligibilityPredicate must not be null");this.chatApi = ollamaApi;this.defaultOptions = defaultOptions;this.toolCallingManager = toolCallingManager;this.observationRegistry = observationRegistry;this.modelManager = new OllamaModelManager(this.chatApi, modelManagementOptions);this.toolExecutionEligibilityPredicate = toolExecutionEligibilityPredicate;initializeModel(defaultOptions.getModel(), modelManagementOptions.pullModelStrategy());}public static Builder builder() {return new Builder();}static ChatResponseMetadata from(OllamaApi.ChatResponse response, ChatResponse previousChatResponse) {Assert.notNull(response, "OllamaApi.ChatResponse must not be null");DefaultUsage newUsage = getDefaultUsage(response);Integer promptTokens = newUsage.getPromptTokens();Integer generationTokens = newUsage.getCompletionTokens();int totalTokens = newUsage.getTotalTokens();Duration evalDuration = response.getEvalDuration();Duration promptEvalDuration = response.getPromptEvalDuration();Duration loadDuration = response.getLoadDuration();Duration totalDuration = response.getTotalDuration();if (previousChatResponse != null && previousChatResponse.getMetadata() != null) {if (previousChatResponse.getMetadata().get(METADATA_EVAL_DURATION) != null) {evalDuration = evalDuration.plus(previousChatResponse.getMetadata().get(METADATA_EVAL_DURATION));}if (previousChatResponse.getMetadata().get(METADATA_PROMPT_EVAL_DURATION) != null) {promptEvalDuration = promptEvalDuration.plus(previousChatResponse.getMetadata().get(METADATA_PROMPT_EVAL_DURATION));}if (previousChatResponse.getMetadata().get(METADATA_LOAD_DURATION) != null) {loadDuration = loadDuration.plus(previousChatResponse.getMetadata().get(METADATA_LOAD_DURATION));}if (previousChatResponse.getMetadata().get(METADATA_TOTAL_DURATION) != null) {totalDuration = totalDuration.plus(previousChatResponse.getMetadata().get(METADATA_TOTAL_DURATION));}if (previousChatResponse.getMetadata().getUsage() != null) {promptTokens += previousChatResponse.getMetadata().getUsage().getPromptTokens();generationTokens += previousChatResponse.getMetadata().getUsage().getCompletionTokens();totalTokens += previousChatResponse.getMetadata().getUsage().getTotalTokens();}}DefaultUsage aggregatedUsage = new DefaultUsage(promptTokens, generationTokens, totalTokens);return ChatResponseMetadata.builder().usage(aggregatedUsage).model(response.model()).keyValue(METADATA_CREATED_AT, response.createdAt()).keyValue(METADATA_EVAL_DURATION, evalDuration).keyValue(METADATA_EVAL_COUNT, aggregatedUsage.getCompletionTokens().intValue()).keyValue(METADATA_LOAD_DURATION, loadDuration).keyValue(METADATA_PROMPT_EVAL_DURATION, promptEvalDuration).keyValue(METADATA_PROMPT_EVAL_COUNT, aggregatedUsage.getPromptTokens().intValue()).keyValue(METADATA_TOTAL_DURATION, totalDuration).keyValue(DONE, response.done()).build();}private static DefaultUsage getDefaultUsage(OllamaApi.ChatResponse response) {return new DefaultUsage(Optional.ofNullable(response.promptEvalCount()).orElse(0),Optional.ofNullable(response.evalCount()).orElse(0));}@Overridepublic ChatResponse call(Prompt prompt) {// Before moving any further, build the final request Prompt,// merging runtime and default options.Prompt requestPrompt = buildRequestPrompt(prompt);return this.internalCall(requestPrompt, null);}private ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {OllamaApi.ChatRequest request = ollamaChatRequest(prompt, false);ChatModelObservationContext observationContext = ChatModelObservationContext.builder().prompt(prompt).provider(OllamaApiConstants.PROVIDER_NAME).build();ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry).observe(() -> {OllamaApi.ChatResponse ollamaResponse = this.chatApi.chat(request);List<AssistantMessage.ToolCall> toolCalls = ollamaResponse.message().toolCalls() == null ? List.of(): ollamaResponse.message().toolCalls().stream().map(toolCall -> new AssistantMessage.ToolCall("", "function", toolCall.function().name(),ModelOptionsUtils.toJsonString(toolCall.function().arguments()))).toList();var assistantMessage = new AssistantMessage(ollamaResponse.message().content(), Map.of(), toolCalls);ChatGenerationMetadata generationMetadata = ChatGenerationMetadata.NULL;if (ollamaResponse.promptEvalCount() != null && ollamaResponse.evalCount() != null) {generationMetadata = ChatGenerationMetadata.builder().finishReason(ollamaResponse.doneReason()).build();}var generator = new Generation(assistantMessage, generationMetadata);ChatResponse chatResponse = new ChatResponse(List.of(generator),from(ollamaResponse, previousChatResponse));observationContext.setResponse(chatResponse);return chatResponse;});if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) {var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);if (toolExecutionResult.returnDirect()) {// Return tool execution result directly to the client.return ChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build();}else {// Send the tool execution result back to the model.return this.internalCall(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),response);}}return response;}@Overridepublic Flux<ChatResponse> stream(Prompt prompt) {// Before moving any further, build the final request Prompt,// merging runtime and default options.Prompt requestPrompt = buildRequestPrompt(prompt);return this.internalStream(requestPrompt, null);}private Flux<ChatResponse> internalStream(Prompt prompt, ChatResponse previousChatResponse) {return Flux.deferContextual(contextView -> {OllamaApi.ChatRequest request = ollamaChatRequest(prompt, true);final ChatModelObservationContext observationContext = ChatModelObservationContext.builder().prompt(prompt).provider(OllamaApiConstants.PROVIDER_NAME).build();Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry);observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();Flux<OllamaApi.ChatResponse> ollamaResponse = this.chatApi.streamingChat(request);Flux<ChatResponse> chatResponse = ollamaResponse.map(chunk -> {String content = (chunk.message() != null) ? chunk.message().content() : "";List<AssistantMessage.ToolCall> toolCalls = List.of();// Added null checks to prevent NPE when accessing tool callsif (chunk.message() != null && chunk.message().toolCalls() != null) {toolCalls = chunk.message().toolCalls().stream().map(toolCall -> new AssistantMessage.ToolCall("", "function", toolCall.function().name(),ModelOptionsUtils.toJsonString(toolCall.function().arguments()))).toList();}var assistantMessage = new AssistantMessage(content, Map.of(), toolCalls);ChatGenerationMetadata generationMetadata = ChatGenerationMetadata.NULL;if (chunk.promptEvalCount() != null && chunk.evalCount() != null) {generationMetadata = ChatGenerationMetadata.builder().finishReason(chunk.doneReason()).build();}var generator = new Generation(assistantMessage, generationMetadata);return new ChatResponse(List.of(generator), from(chunk, previousChatResponse));});// @formatter:offFlux<ChatResponse> chatResponseFlux = chatResponse.flatMap(response -> {if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) {// FIXME: bounded elastic needs to be used since tool calling// is currently only synchronousreturn Flux.defer(() -> {var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);if (toolExecutionResult.returnDirect()) {// Return tool execution result directly to the client.return Flux.just(ChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build());}else {// Send the tool execution result back to the model.return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),response);}}).subscribeOn(Schedulers.boundedElastic());}else {return Flux.just(response);}}).doOnError(observation::error).doFinally(s ->observation.stop()).contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));// @formatter:onreturn new MessageAggregator().aggregate(chatResponseFlux, observationContext::setResponse);});}Prompt buildRequestPrompt(Prompt prompt) {// Process runtime optionsOllamaOptions runtimeOptions = null;if (prompt.getOptions() != null) {if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,OllamaOptions.class);}else {runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,OllamaOptions.class);}}// Define request options by merging runtime options and default optionsOllamaOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,OllamaOptions.class);// Merge @JsonIgnore-annotated options explicitly since they are ignored by// Jackson, used by ModelOptionsUtils.if (runtimeOptions != null) {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.setInternalToolExecutionEnabled(this.defaultOptions.getInternalToolExecutionEnabled());requestOptions.setToolNames(this.defaultOptions.getToolNames());requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());requestOptions.setToolContext(this.defaultOptions.getToolContext());}// Validate request optionsif (!StringUtils.hasText(requestOptions.getModel())) {throw new IllegalArgumentException("model cannot be null or empty");}ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks());return new Prompt(prompt.getInstructions(), requestOptions);}/*** Package access for testing.*/OllamaApi.ChatRequest ollamaChatRequest(Prompt prompt, boolean stream) {List<OllamaApi.Message> ollamaMessages = prompt.getInstructions().stream().map(message -> {if (message instanceof UserMessage userMessage) {var messageBuilder = OllamaApi.Message.builder(Role.USER).content(message.getText());if (!CollectionUtils.isEmpty(userMessage.getMedia())) {messageBuilder.images(userMessage.getMedia().stream().map(media -> this.fromMediaData(media.getData())).toList());}return List.of(messageBuilder.build());}else if (message instanceof SystemMessage systemMessage) {return List.of(OllamaApi.Message.builder(Role.SYSTEM).content(systemMessage.getText()).build());}else if (message instanceof AssistantMessage assistantMessage) {List<ToolCall> toolCalls = null;if (!CollectionUtils.isEmpty(assistantMessage.getToolCalls())) {toolCalls = assistantMessage.getToolCalls().stream().map(toolCall -> {var function = new ToolCallFunction(toolCall.name(),JsonParser.fromJson(toolCall.arguments(), new TypeReference<>() {}));return new ToolCall(function);}).toList();}return List.of(OllamaApi.Message.builder(Role.ASSISTANT).content(assistantMessage.getText()).toolCalls(toolCalls).build());}else if (message instanceof ToolResponseMessage toolMessage) {return toolMessage.getResponses().stream().map(tr -> OllamaApi.Message.builder(Role.TOOL).content(tr.responseData()).build()).toList();}throw new IllegalArgumentException("Unsupported message type: " + message.getMessageType());}).flatMap(List::stream).toList();OllamaOptions requestOptions = (OllamaOptions) prompt.getOptions();OllamaApi.ChatRequest.Builder requestBuilder = OllamaApi.ChatRequest.builder(requestOptions.getModel()).stream(stream).messages(ollamaMessages).options(requestOptions);if (requestOptions.getFormat() != null) {requestBuilder.format(requestOptions.getFormat());}if (requestOptions.getKeepAlive() != null) {requestBuilder.keepAlive(requestOptions.getKeepAlive());}List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);if (!CollectionUtils.isEmpty(toolDefinitions)) {requestBuilder.tools(this.getTools(toolDefinitions));}return requestBuilder.build();}private String fromMediaData(Object mediaData) {if (mediaData instanceof byte[] bytes) {return Base64.getEncoder().encodeToString(bytes);}else if (mediaData instanceof String text) {return text;}else {throw new IllegalArgumentException("Unsupported media data type: " + mediaData.getClass().getSimpleName());}}private List<ChatRequest.Tool> getTools(List<ToolDefinition> toolDefinitions) {return toolDefinitions.stream().map(toolDefinition -> {var tool = new ChatRequest.Tool.Function(toolDefinition.name(), toolDefinition.description(),toolDefinition.inputSchema());return new ChatRequest.Tool(tool);}).toList();}@Overridepublic ChatOptions getDefaultOptions() {return OllamaOptions.fromOptions(this.defaultOptions);}/*** Pull the given model into Ollama based on the specified strategy.*/private void initializeModel(String model, PullModelStrategy pullModelStrategy) {if (pullModelStrategy != null && !PullModelStrategy.NEVER.equals(pullModelStrategy)) {this.modelManager.pullModel(model, pullModelStrategy);}}/*** Use the provided convention for reporting observation data* @param observationConvention The provided convention*/public void setObservationConvention(ChatModelObservationConvention observationConvention) {Assert.notNull(observationConvention, "observationConvention cannot be null");this.observationConvention = observationConvention;}public static final class Builder {private OllamaApi ollamaApi;private OllamaOptions defaultOptions = OllamaOptions.builder().model(OllamaModel.MISTRAL.id()).build();private ToolCallingManager toolCallingManager;private ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate();private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;private ModelManagementOptions modelManagementOptions = ModelManagementOptions.defaults();private Builder() {}public Builder ollamaApi(OllamaApi ollamaApi) {this.ollamaApi = ollamaApi;return this;}public Builder defaultOptions(OllamaOptions defaultOptions) {this.defaultOptions = defaultOptions;return this;}public Builder toolCallingManager(ToolCallingManager toolCallingManager) {this.toolCallingManager = toolCallingManager;return this;}public Builder toolExecutionEligibilityPredicate(ToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate) {this.toolExecutionEligibilityPredicate = toolExecutionEligibilityPredicate;return this;}public Builder observationRegistry(ObservationRegistry observationRegistry) {this.observationRegistry = observationRegistry;return this;}public Builder modelManagementOptions(ModelManagementOptions modelManagementOptions) {this.modelManagementOptions = modelManagementOptions;return this;}public OllamaChatModel build() {if (this.toolCallingManager != null) {return new OllamaChatModel(this.ollamaApi, this.defaultOptions, this.toolCallingManager,this.observationRegistry, this.modelManagementOptions, this.toolExecutionEligibilityPredicate);}return new OllamaChatModel(this.ollamaApi, this.defaultOptions, DEFAULT_TOOL_CALLING_MANAGER,this.observationRegistry, this.modelManagementOptions, this.toolExecutionEligibilityPredicate);}}}
4. Embeddings Model API
Embeddings是文本、圖像或視頻的數字表示形式,用于捕獲輸入之間的關系。
Embeddings的工作原理是將文本、圖像和視頻轉換為浮點數數組(稱為向量)。 這些矢量旨在捕獲文本、圖像和視頻的含義。 嵌入數組的長度稱為向量的維數。
public interface EmbeddingModel extends Model<EmbeddingRequest, EmbeddingResponse> {@OverrideEmbeddingResponse call(EmbeddingRequest request);/*** Embeds the given text into a vector.* @param text the text to embed.* @return the embedded vector.*/default float[] embed(String text) {Assert.notNull(text, "Text must not be null");List<float[]> response = this.embed(List.of(text));return response.iterator().next();}/*** Embeds the given document's content into a vector.* @param document the document to embed.* @return the embedded vector.*/float[] embed(Document document);/*** Embeds a batch of texts into vectors.* @param texts list of texts to embed.* @return list of embedded vectors.*/default List<float[]> embed(List<String> texts) {Assert.notNull(texts, "Texts must not be null");return this.call(new EmbeddingRequest(texts, EmbeddingOptionsBuilder.builder().build())).getResults().stream().map(Embedding::getOutput).toList();}/*** Embeds a batch of {@link Document}s into vectors based on a* {@link BatchingStrategy}.* @param documents list of {@link Document}s.* @param options {@link EmbeddingOptions}.* @param batchingStrategy {@link BatchingStrategy}.* @return a list of float[] that represents the vectors for the incoming* {@link Document}s. The returned list is expected to be in the same order of the* {@link Document} list.*/default List<float[]> embed(List<Document> documents, EmbeddingOptions options, BatchingStrategy batchingStrategy) {Assert.notNull(documents, "Documents must not be null");List<float[]> embeddings = new ArrayList<>(documents.size());List<List<Document>> batch = batchingStrategy.batch(documents);for (List<Document> subBatch : batch) {List<String> texts = subBatch.stream().map(Document::getText).toList();EmbeddingRequest request = new EmbeddingRequest(texts, options);EmbeddingResponse response = this.call(request);for (int i = 0; i < subBatch.size(); i++) {embeddings.add(response.getResults().get(i).getOutput());}}Assert.isTrue(embeddings.size() == documents.size(),"Embeddings must have the same number as that of the documents");return embeddings;}/*** Embeds a batch of texts into vectors and returns the {@link EmbeddingResponse}.* @param texts list of texts to embed.* @return the embedding response.*/default EmbeddingResponse embedForResponse(List<String> texts) {Assert.notNull(texts, "Texts must not be null");return this.call(new EmbeddingRequest(texts, EmbeddingOptionsBuilder.builder().build()));}/*** Get the number of dimensions of the embedded vectors. Note that by default, this* method will call the remote Embedding endpoint to get the dimensions of the* embedded vectors. If the dimensions are known ahead of time, it is recommended to* override this method.* @return the number of dimensions of the embedded vectors.*/default int dimensions() {return embed("Test String").length;}}
樣例:OllamaEmbeddingModel
OllamaEmbeddingModel 是 Spring AI 框架中用于與 Ollama 嵌入模型交互的組件實現,主要功能是將文本轉換為語義向量(embedding)用于各類NLP任務。
- 核心功能
?1. 文本向量化?- 將輸入文本轉換為高維浮點數向量(如768維),用于衡量文本間的語義相關性
- 支持通過Ollama原生API或OpenAI兼容API調用,例如:
2?. 模型支持?curl http://localhost:11434/api/embed
- 兼容多種嵌入模型如 bge-m3(多語言/多粒度)、nomic-embed-text(長文本優化)等
- 需通過 ollama pull <模型名> 預先下載模型
public class OllamaEmbeddingModel extends AbstractEmbeddingModel {private static final EmbeddingModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultEmbeddingModelObservationConvention();private final OllamaApi ollamaApi;private final OllamaOptions defaultOptions;private final ObservationRegistry observationRegistry;private final OllamaModelManager modelManager;private EmbeddingModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;public OllamaEmbeddingModel(OllamaApi ollamaApi, OllamaOptions defaultOptions,ObservationRegistry observationRegistry, ModelManagementOptions modelManagementOptions) {Assert.notNull(ollamaApi, "ollamaApi must not be null");Assert.notNull(defaultOptions, "options must not be null");Assert.notNull(observationRegistry, "observationRegistry must not be null");Assert.notNull(modelManagementOptions, "modelManagementOptions must not be null");this.ollamaApi = ollamaApi;this.defaultOptions = defaultOptions;this.observationRegistry = observationRegistry;this.modelManager = new OllamaModelManager(ollamaApi, modelManagementOptions);initializeModel(defaultOptions.getModel(), modelManagementOptions.pullModelStrategy());}public static Builder builder() {return new Builder();}@Overridepublic float[] embed(Document document) {return embed(document.getText());}@Overridepublic EmbeddingResponse call(EmbeddingRequest request) {Assert.notEmpty(request.getInstructions(), "At least one text is required!");// Before moving any further, build the final request EmbeddingRequest,// merging runtime and default options.EmbeddingRequest embeddingRequest = buildEmbeddingRequest(request);OllamaApi.EmbeddingsRequest ollamaEmbeddingRequest = ollamaEmbeddingRequest(embeddingRequest);var observationContext = EmbeddingModelObservationContext.builder().embeddingRequest(request).provider(OllamaApiConstants.PROVIDER_NAME).build();return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry).observe(() -> {EmbeddingsResponse response = this.ollamaApi.embed(ollamaEmbeddingRequest);AtomicInteger indexCounter = new AtomicInteger(0);List<Embedding> embeddings = response.embeddings().stream().map(e -> new Embedding(e, indexCounter.getAndIncrement())).toList();EmbeddingResponseMetadata embeddingResponseMetadata = new EmbeddingResponseMetadata(response.model(),getDefaultUsage(response));EmbeddingResponse embeddingResponse = new EmbeddingResponse(embeddings, embeddingResponseMetadata);observationContext.setResponse(embeddingResponse);return embeddingResponse;});}private DefaultUsage getDefaultUsage(OllamaApi.EmbeddingsResponse response) {return new DefaultUsage(Optional.ofNullable(response.promptEvalCount()).orElse(0), 0);}EmbeddingRequest buildEmbeddingRequest(EmbeddingRequest embeddingRequest) {// Process runtime optionsOllamaOptions runtimeOptions = null;if (embeddingRequest.getOptions() != null) {runtimeOptions = ModelOptionsUtils.copyToTarget(embeddingRequest.getOptions(), EmbeddingOptions.class,OllamaOptions.class);}// Define request options by merging runtime options and default optionsOllamaOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,OllamaOptions.class);// Validate request optionsif (!StringUtils.hasText(requestOptions.getModel())) {throw new IllegalArgumentException("model cannot be null or empty");}return new EmbeddingRequest(embeddingRequest.getInstructions(), requestOptions);}/*** Package access for testing.*/OllamaApi.EmbeddingsRequest ollamaEmbeddingRequest(EmbeddingRequest embeddingRequest) {OllamaOptions requestOptions = (OllamaOptions) embeddingRequest.getOptions();return new OllamaApi.EmbeddingsRequest(requestOptions.getModel(), embeddingRequest.getInstructions(),DurationParser.parse(requestOptions.getKeepAlive()),OllamaOptions.filterNonSupportedFields(requestOptions.toMap()), requestOptions.getTruncate());}/*** Pull the given model into Ollama based on the specified strategy.*/private void initializeModel(String model, PullModelStrategy pullModelStrategy) {if (pullModelStrategy != null && !PullModelStrategy.NEVER.equals(pullModelStrategy)) {this.modelManager.pullModel(model, pullModelStrategy);}}/*** Use the provided convention for reporting observation data* @param observationConvention The provided convention*/public void setObservationConvention(EmbeddingModelObservationConvention observationConvention) {Assert.notNull(observationConvention, "observationConvention cannot be null");this.observationConvention = observationConvention;}public static class DurationParser {private static final Pattern PATTERN = Pattern.compile("(-?\\d+)(ms|s|m|h)");public static Duration parse(String input) {if (!StringUtils.hasText(input)) {return null;}Matcher matcher = PATTERN.matcher(input);if (matcher.matches()) {long value = Long.parseLong(matcher.group(1));String unit = matcher.group(2);return switch (unit) {case "ms" -> Duration.ofMillis(value);case "s" -> Duration.ofSeconds(value);case "m" -> Duration.ofMinutes(value);case "h" -> Duration.ofHours(value);default -> throw new IllegalArgumentException("Unsupported time unit: " + unit);};}else {throw new IllegalArgumentException("Invalid duration format: " + input);}}}public static final class Builder {private OllamaApi ollamaApi;private OllamaOptions defaultOptions = OllamaOptions.builder().model(OllamaModel.MXBAI_EMBED_LARGE.id()).build();private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;private ModelManagementOptions modelManagementOptions = ModelManagementOptions.defaults();private Builder() {}public Builder ollamaApi(OllamaApi ollamaApi) {this.ollamaApi = ollamaApi;return this;}public Builder defaultOptions(OllamaOptions defaultOptions) {this.defaultOptions = defaultOptions;return this;}public Builder observationRegistry(ObservationRegistry observationRegistry) {this.observationRegistry = observationRegistry;return this;}public Builder modelManagementOptions(ModelManagementOptions modelManagementOptions) {this.modelManagementOptions = modelManagementOptions;return this;}public OllamaEmbeddingModel build() {return new OllamaEmbeddingModel(this.ollamaApi, this.defaultOptions, this.observationRegistry,this.modelManagementOptions);}}}
5. Image Model
@FunctionalInterface
public interface ImageModel extends Model<ImagePrompt, ImageResponse> {ImageResponse call(ImagePrompt request);
}
6. Audio Model
7. Chat Memory
public interface ChatMemory {String DEFAULT_CONVERSATION_ID = "default";/*** The key to retrieve the chat memory conversation id from the context.*/String CONVERSATION_ID = "chat_memory_conversation_id";/*** Save the specified message in the chat memory for the specified conversation.*/default void add(String conversationId, Message message) {Assert.hasText(conversationId, "conversationId cannot be null or empty");Assert.notNull(message, "message cannot be null");this.add(conversationId, List.of(message));}/*** Save the specified messages in the chat memory for the specified conversation.*/void add(String conversationId, List<Message> messages);/*** Get the messages in the chat memory for the specified conversation.*/List<Message> get(String conversationId);/*** Clear the chat memory for the specified conversation.*/void clear(String conversationId);}
樣例:
public final class MessageWindowChatMemory implements ChatMemory {private static final int DEFAULT_MAX_MESSAGES = 20;private final ChatMemoryRepository chatMemoryRepository;private final int maxMessages;private MessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) {Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null");Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0");this.chatMemoryRepository = chatMemoryRepository;this.maxMessages = maxMessages;}@Overridepublic void add(String conversationId, List<Message> messages) {Assert.hasText(conversationId, "conversationId cannot be null or empty");Assert.notNull(messages, "messages cannot be null");Assert.noNullElements(messages, "messages cannot contain null elements");List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);List<Message> processedMessages = process(memoryMessages, messages);this.chatMemoryRepository.saveAll(conversationId, processedMessages);}@Overridepublic List<Message> get(String conversationId) {Assert.hasText(conversationId, "conversationId cannot be null or empty");return this.chatMemoryRepository.findByConversationId(conversationId);}@Overridepublic void clear(String conversationId) {Assert.hasText(conversationId, "conversationId cannot be null or empty");this.chatMemoryRepository.deleteByConversationId(conversationId);}private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) {List<Message> processedMessages = new ArrayList<>();Set<Message> memoryMessagesSet = new HashSet<>(memoryMessages);boolean hasNewSystemMessage = newMessages.stream().filter(SystemMessage.class::isInstance).anyMatch(message -> !memoryMessagesSet.contains(message));memoryMessages.stream().filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage)).forEach(processedMessages::add);processedMessages.addAll(newMessages);if (processedMessages.size() <= this.maxMessages) {return processedMessages;}int messagesToRemove = processedMessages.size() - this.maxMessages;List<Message> trimmedMessages = new ArrayList<>();int removed = 0;for (Message message : processedMessages) {if (message instanceof SystemMessage || removed >= messagesToRemove) {trimmedMessages.add(message);}else {removed++;}}return trimmedMessages;}public static Builder builder() {return new Builder();}public static final class Builder {private ChatMemoryRepository chatMemoryRepository;private int maxMessages = DEFAULT_MAX_MESSAGES;private Builder() {}public Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) {this.chatMemoryRepository = chatMemoryRepository;return this;}public Builder maxMessages(int maxMessages) {this.maxMessages = maxMessages;return this;}public MessageWindowChatMemory build() {if (this.chatMemoryRepository == null) {this.chatMemoryRepository = new InMemoryChatMemoryRepository();}return new MessageWindowChatMemory(this.chatMemoryRepository, this.maxMessages);}}}
8. Tool Calling
public interface ToolCallback {/*** Definition used by the AI model to determine when and how to call the tool.*/ToolDefinition getToolDefinition();/*** Metadata providing additional information on how to handle the tool.*/default ToolMetadata getToolMetadata() {return ToolMetadata.builder().build();}/*** Execute tool with the given input and return the result to send back to the AI* model.*/String call(String toolInput);/*** Execute tool with the given input and context, and return the result to send back* to the AI model.*/default String call(String toolInput, @Nullable ToolContext tooContext) {if (tooContext != null && !tooContext.getContext().isEmpty()) {throw new UnsupportedOperationException("Tool context is not supported!");}return call(toolInput);}}