Spring AI Alibaba Playground 是 Spring AI Alibaba 社區以 Spring AI Alibaba 和 Spring AI 為框架搭建的 AI 應用。包含完善的前端 UI + 后端實現,具備對話,圖片生成,工具調用,RAG,MCP 等眾多 AI 相關功能。在 playground 的基礎之上,您可以快速復刻出一個屬于自己的 AI 應用。其中工具調用,MCP 集成,聊天模型切換等功能亦可為您搭建自己的 AI 應用提供參考。
Playground 代碼地址:https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-playground
項目首頁預覽:
image-20250607164742879
本篇文章中,內容較多。涉及運行,項目介紹,配置介紹等。分為三個部分介紹 Spring AI Alibaba Playground,您可以根據自己的需要跳轉到不同章節瀏覽。
目錄
本地運行
配置介紹
項目介紹
1. 本地運行
本章節中,將主要介紹如何在本地啟動 Playground 項目。
1. 1 下載源碼
Playground 代碼地址:https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-playground
playground 項目位于 spring-ai-alibaba-example 倉庫下,被設計為一個獨立的項目,不依賴于 spring-ai-alibaba-example pom 管理。這意味著您需要使用 IDEA 單獨打開 playground 項目目錄。而不是在 example 根目錄下直接啟動。
PS: 如果直接啟動需要配置 IDEA 的運行工作目錄,請參考 README 描述:https://github.com/springaialibaba/spring-ai-alibaba-examples/blob/main/spring-ai-alibaba-playground/README.md
1.2 配置變更
1.2.1 MCP 配置變更
因為 playground 項目引入了 mcp stdio 的方式來演示 Spring AI 如何接入 MCP 服務。因此當您的啟動環境為 windows 時,需要安裝并配置啟動 MCP Server 需要的環境。
以下面的 MCP Server json 配置文件為例:
{
"mcpServers": {
"github": {
"command": "npx",
"args": [
"/c",
"npx",
"-y",
"@modelcontextprotocol/server-github"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "your_github_token"
}
}
}
}
您需要安裝 NPX 工具,如果在 Windows 系統啟動,需要變更 command 為 npx.cmd。否則會啟動失敗。
1.2.2 數據庫文件變更
Playground 使用 SQLite 作為 Chat Memory 的存儲數據庫。在項目啟動時,會自動在 src/main/resources 目錄下創建 saa.db 的數據庫文件,如果啟動時沒有自動創建,出現啟動失敗錯誤時。您需要手動創建此文件。
1.2.3 前端打包
playground 前端項目采用打包編譯到 jar 包中一起啟動方式運行,因此在啟動后端時,需要運行 mvn clean package。確保前端項目正確打包編譯,您可以在 target/classes/static 路徑下看到前端資源文件。
1.2.4 可觀測集成
Playground 項目中集成了 Spring AI 的可觀測功能,如果您不想觀察 AI 應用運行中的一些指標數據,您可以忽略此步驟。
PS: 因為 AI 大模型應用的觀測數據中包含用戶的輸入等信息,在生產部署時,請確保敏感信息選項關閉。
在 spring-ai-alibaba-example 倉庫 docker-compose 目錄準備了 AI 應用常用的工具 docker-compose 啟動文件,您可以參考啟動 zipkin。
可觀測實現參考:https://java2ai.com/blog/spring-ai-alibaba-observability-arms/?spm=5176.29160081.0.0.2856aa5cenvkmu
1.2.5 apiKey 配置
Playground 項目集成了 RAG,向量數據庫和 Function Call 等功能,因此在啟動之初您應該配置對應的 ak。
PS: playgrond 中所有的 key 都通過 env 的方式注入,如果配置了 env 之后,項目仍然獲取不到 ak,請重啟 IDEA
DashScope 大模型 ak:AI 應用使用
阿里云 IQS 信息檢索服務 ak:模塊化 RAG 示例,web search 使用
阿里云 Analytic 項目數據庫 ak:RAG 使用;
百度翻譯和百度地圖 ak:Function Call 調用使用;
Github 個人 secret:MCP Server 演示使用。
關于 AK 的獲取方式自行搜索,這里不在過多贅述。
1.3 啟動并訪問
如果上面的配置步驟全部完成,在 playground 項目啟動之后,在瀏覽器輸入 http://localhost:8080 您將會看到文章開始時的首頁頁面。
image-20250607171739807
PS: 如果體驗對應的 Function Call 或者 MCP 功能時,請確保配置了對應的服務 AK 且 AK 有效。
此項目僅作為演示使用,一些功能初具形狀,尚不完善。歡迎貢獻代碼并完善項目!🚀
2. 配置介紹
playground 項目作為一個較完善的 AI 應用項目,涉及較多的配置文件,在此章節中將一一說明。
2.1 resources 配置
resource 目錄配置文件如下:
resources
├── application-dev.yml
├── application-prod.yml
├── application.yml
├── banner.txt
├── db
├── logback-spring.xml
├── mcp-config.yml
├── mcp-libs
├── models.yaml
└── rag
db 為 saa.db 目錄,主要為 playground 的 chat memory 提供存儲支持;
mcp-libs:MCP Stdio 的服務 jar 目錄;
rag:RAG 功能的知識庫文檔目錄,在項目啟動時,將自動向量化文檔并存入向量數據庫;
mcp-config.yaml:palyground 項目增強的 mcp-server 配置;
application-*.yml:項目啟動配置。
2.1.1 MCP Config 增強
解決的問題:在 playground 中使用 MCP Stdio 的方式來集成和演示 MCP 功能,在涉及到本地服務時,例如以下配置:
{
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"D:\\open_sources\\spring-ai-alibaba-examples\\spring-ai-alibaba-mcp-example\\spring-ai-alibaba-mcp-build-example\\mcp-stdio-server-example\\target\\mcp-stdio-server-example-1.0.0.jar"
],
"env": {}
}
}
}
在二進制文件配置時,必須要求使用絕對路徑配置且 json 配置較難理解。因此 playground 在配置做了增強,將 json 轉為了語義清晰的 yml 方式定義。細節請參考 MCP
2.2 pom.xml 配置
此章節部分將主要介紹核心依賴,其他依賴請參考:https://github.com/springaialibaba/spring-ai-alibaba-examples/blob/main/spring-ai-alibaba-playground/pom.xml
<dependencies>
? ? <!-- Chat Memory 功能實現時需要此依賴項 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
? ? <!-- playground 文本總結功能依賴 tika 對輸入的各類文本進行解析 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
? ? <!-- Spring AI MCP client 相關依賴-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
<version>${spring-ai.version}</version>
</dependency>
? ? <dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-mcp-client</artifactId>
<version>${spring-ai.version}</version>
</dependency>
? <!-- Spring AI OpenAI Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
? ? <!-- Spring AI RAG markdown 文本讀入解析 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
? ? <!-- Spring AI 向量數據庫 Advisors -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>
? ? <!-- Spring AI Alibaba DashScope starter -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
? ? <!-- Spring AI Alibaba Memory 實現 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
? ? <!-- Spring AI Alibaba analyticdb 向量數據庫集成 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-store-analyticdb</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
? ? <!-- DB,為 ChatMemory 和 playground 提供存儲支持 -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>${sqlite-jdbc.version}</version>
</dependency>
? ? <dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>${hibernate.version}</version>
</dependency>
? ? <!-- Playground 可觀測集成 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
<version>${micrometr.version}</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!-- Spring AI 和 Spring AI Alibaba 依賴管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. 項目介紹
在 playground 中集成了較多功能,RAG,MCP,Function Call 等。此章節中將主要對 RAG Web Search,MCP 調用,Function Call 拆分介紹。以便能夠基于此 playground 搭建符合自己需求的 AI 應用。
3.1 RAG 實現
RAG 仍然是當今最流行的 AI 應用結合私有知識庫的方式。通過 RAG 能夠構建問答機器人,專業領域助手等。
在 playground 項目中,使用的向量數據庫是 analyticdb 和基于內存的 SimpleVectorStore。您可以替換為任意您想使用的向量數據庫。
3.1.1 向量數據庫初始化
初始化配置代碼位于:com/alibaba/cloud/ai/application/config/rag
@Bean
CommandLineRunner ingestTermOfServiceToVectorStore(VectorStoreDelegate vectorStoreDelegate) {
return args -> {
String type = System.getenv("VECTOR_STORE_TYPE");
VectorStoreInitializer initializer = new VectorStoreInitializer();
initializer.init(vectorStoreDelegate.getVectorStore(type));
};
}
通過 VECTOR_STORE_TYPE 的方式來選擇使用那種類型的向量數據庫,VectorStoreDelegate 代碼如下:其作用是通過 type 的值返回向量數據庫的實例 bean。
PS:在這里您可以替換為您所使用的向量數據庫來構建 RAG 功能。
public class VectorStoreDelegate {
? ? private VectorStore simpleVectorStore;
? ? private VectorStore analyticdbVectorStore;
? ? public VectorStoreDelegate(VectorStore simpleVectorStore, VectorStore analyticdbVectorStore) {
this.simpleVectorStore = simpleVectorStore;
this.analyticdbVectorStore = analyticdbVectorStore;
}
? ? public VectorStore getVectorStore(String vectorStoreType) {
? ? ? ?if (Objects.equals(vectorStoreType, "analyticdb") && analyticdbVectorStore != null) {
return analyticdbVectorStore;
}
? ? ? ?return simpleVectorStore;
}
}
3.1.2 RAG 文檔初始化
在 VectorStoreInitializer 中將 resources/rag 下的 md 文檔向量化并加載到向量數據庫中:
public void init(VectorStore vectorStore) throws Exception {
List<MarkdownDocumentReader> markdownDocumentReaderList = loadMarkdownDocuments();
? ? int size = 0;
if (markdownDocumentReaderList.isEmpty()) {
logger.warn("No markdown documents found in the directory.");
return;
}
? ? logger.debug("Start to load markdown documents into vector store......");
for (MarkdownDocumentReader markdownDocumentReader : markdownDocumentReaderList) {
List<Document> documents = new TokenTextSplitter(2000, 1024, 10, 10000, true).transform(markdownDocumentReader.get());
size += documents.size();
? ? ? ? // 拆分 documents 列表為最大 25 個元素的子列表
for (int i = 0; i < documents.size(); i += 25) {
int end = Math.min(i + 25, documents.size());
List<Document> subList = documents.subList(i, end);
vectorStore.add(subList);
}
}
logger.debug("Load markdown documents into vector store successfully. Load {} documents.", size);
}
3.1.3 構建 Service
在業務代碼中注入向量數據庫 bean,即可完成 RAG 功能的實現。
@Service
public class SAARAGService {
? ? private final ChatClient client;
? ? private final VectorStoreDelegate vectorStoreDelegate;
? ? private String vectorStoreType;
? ? public SAARAGService(
VectorStoreDelegate vectorStoreDelegate,
SimpleLoggerAdvisor simpleLoggerAdvisor,
MessageChatMemoryAdvisor messageChatMemoryAdvisor,
@Qualifier("dashscopeChatModel") ChatModel chatModel,
@Qualifier("systemPromptTemplate") PromptTemplate systemPromptTemplate
) {
this.vectorStoreType = System.getenv("VECTOR_STORE_TYPE");
this.vectorStoreDelegate = vectorStoreDelegate;
this.client = ChatClient.builder(chatModel)
.defaultSystem(
systemPromptTemplate.getTemplate()
).defaultAdvisors(
messageChatMemoryAdvisor,
simpleLoggerAdvisor
).build();
}
? ? public Flux<String> ragChat(String chatId, String prompt) {
? ? ? ?return client.prompt()
.user(prompt)
.advisors(memoryAdvisor -> memoryAdvisor
.param(ChatMemory.CONVERSATION_ID, chatId)
).advisors(
QuestionAnswerAdvisor
.builder(vectorStoreDelegate.getVectorStore(vectorStoreType))
.searchRequest(
SearchRequest.builder()
// TODO all documents retrieved from ADB are under 0.1
// ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .similarityThreshold(0.6d)
.topK(6)
.build()
)
.build()
).stream()
.content();
}
}
RAG 實現文章參考:https://java2ai.com/blog/spring-ai-alibaba-rag-ollama/?spm=5176.29160081.0.0.2856aa5cenvkmu
3.2 Web Search 功能實現
在 Qwen 模型中,可以通過 enable_search 開啟模型的聯網搜索能力。在 playground 通過 Module RAG 的方式來集成聯網搜索功能。
3.2.1 Module RAG 介紹
Spring AI 實現了模塊化 RAG 架構,架構的靈感來自于論文“模塊化 RAG:將 RAG 系統轉變為類似樂高的可重構框架”中詳述的模塊化概念。將 RAG 分為三步:
Pre-Retrieval
增強和轉換用戶輸入,使其更有效地執行檢索任務,解決格式不正確的查詢、query 語義不清晰、或不受支持的語言等。
QueryAugmenter 查詢增強:使用附加的上下文數據信息增強用戶 query,提供大模型回答問題時的必要上下文信息;
QueryTransformer 查詢改寫:因為用戶的輸入通常是片面的,關鍵信息較少,不便于大模型理解和回答問題。因此需要使用 prompt 調優手段或者大模型改寫用戶 query;
QueryExpander 查詢擴展:將用戶 query 擴展為多個語義不同的變體以獲得不同視角,有助于檢索額外的上下文信息并增加找到相關結果的機會。
Retrieval
負責查詢向量存儲等數據系統并檢索和用戶 query 相關性最高的 Document。
DocumentRetriever:檢索器,根據 QueryExpander 使用不同的數據源進行檢索,例如 搜索引擎、向量存儲、數據庫或知識圖等;
DocumentJoiner:將從多個 query 和從多個數據源檢索到的 Document 合并為一個 Document 集合;
Post-Retrieval
負責處理檢索到的 Document 以獲得最佳的輸出結果,解決模型中的中間丟失和上下文長度限制等。
PS:Spring AI 在 1.0.0 中棄用了 DocumentRanker。您可以實現 DocumentPostProcessor 接口來實現此功能。Playground 待補充。
生成
生成用戶 Query 對應的大模型輸出。
3.2.2 數據來源
聯網搜索,顧名思義。就是將網絡上的數據通過實時搜索的方式獲取到并交給大模型來獲得最新得消息咨詢。playground 項目中使用了阿里云的 IQS,信息檢索服務作為聯網搜索的數據源。您可以使用搜索引擎服務替換 IQS。
IIQS 搜索實現如下:其本質為請求服務接口或調用 SDK。
public GenericSearchResult search(String query) {
? ? // String encodeQ = URLEncoder.encode(query, StandardCharsets.UTF_8);
ResponseEntity<GenericSearchResult> resultResponseEntity = run(query);
? ? return genericSearchResult(resultResponseEntity);
}
private ResponseEntity<GenericSearchResult> run(String query) {
? ? return this.restClient.get()
.uri(
"/search/genericSearch?query={query}&timeRange={timeRange}",
query,
TIME_RANGE
).retrieve()
.toEntity(GenericSearchResult.class);
}
}
3.2.3 數據加工
在這一步中,將搜索引擎獲取到的數據進行清洗并轉為 Spring AI 的 Document 文檔。
public List<Document> getData(GenericSearchResult respData) throws URISyntaxException {
? ? List<Document> documents = new ArrayList<>();
? ? Map<String, Object> metadata = getQueryMetadata(respData);
? ? for (ScorePageItem pageItem : respData.getPageItems()) {
? ? ? Map<String, Object> pageItemMetadata = getPageItemMetadata(pageItem);
Double score = getScore(pageItem);
String text = getText(pageItem);
? ? ? if (Objects.equals("", text)) {
? ? ? ? Media media = getMedia(pageItem);
Document document = new Document.Builder()
.metadata(metadata)
.metadata(pageItemMetadata)
.media(media)
.score(score)
.build();
? ? ? ? documents.add(document);
break;
}
? ? ? Document document = new Document.Builder()
.metadata(metadata)
.metadata(pageItemMetadata)
.text(text)
.score(score)
.build();
? ? ? documents.add(document);
}
? ? return documents;
}
? private Double getScore(ScorePageItem pageItem) {
? ? return pageItem.getScore();
}
? // .... 省略數據清洗代碼
? // 限制聯網搜索的文檔數,提高聯網搜索響應速度
public List<Document> limitResults(List<Document> documents, int minResults) {
? ? int limit = Math.min(documents.size(), minResults);
? ? return documents.subList(0, limit);
}
}
3.2.4 Module RAG 流程
接下來,便是使用 Module RAG API 處理用戶 Prompt。使其更符合大模型的輸入輸出,獲得更好的效果
具體代碼參考:https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-playground/src/main/java/com/alibaba/cloud/ai/application/rag
3.2.5 Web Search 服務類
在構造方法中注入相關 Bean;
在 ChatClient 中通過 RetrievalAugmentationAdvisor 引入 advisor 實現模塊化 RAG 的聯網搜索功能。
public SAAWebSearchService(
DataClean dataCleaner,
QueryExpander queryExpander,
IQSSearchEngine searchEngine,
QueryTransformer queryTransformer,
SimpleLoggerAdvisor simpleLoggerAdvisor,
@Qualifier("dashscopeChatModel") ChatModel chatModel,
@Qualifier("queryArgumentPromptTemplate") PromptTemplate queryArgumentPromptTemplate
) {
? ? this.queryTransformer = queryTransformer;
this.queryExpander = queryExpander;
this.queryArgumentPromptTemplate = queryArgumentPromptTemplate;
? ? // reasoning content for DeepSeek-r1 is integrated into the output
this.reasoningContentAdvisor = new ReasoningContentAdvisor(1);
? ? // Build chatClient
this.chatClient = ChatClient.builder(chatModel)
.defaultOptions(
DashScopeChatOptions.builder()
.withModel(DEFAULT_WEB_SEARCH_MODEL)
// stream 模式下是否開啟增量輸出
.withIncrementalOutput(true)
.build())
.build();
? ? // 日志
this.simpleLoggerAdvisor = simpleLoggerAdvisor;
? ? this.webSearchRetriever = WebSearchRetriever.builder()
.searchEngine(searchEngine)
.dataCleaner(dataCleaner)
.maxResults(2)
.build();
}
//Handle user input
public Flux<String> chat(String prompt) {
? ? return chatClient.prompt()
.advisors(
createRetrievalAugmentationAdvisor(),
reasoningContentAdvisor,
simpleLoggerAdvisor
).user(prompt)
.stream()
.content();
}
private RetrievalAugmentationAdvisor createRetrievalAugmentationAdvisor() {
? ? return RetrievalAugmentationAdvisor.builder()
.documentRetriever(webSearchRetriever)
.queryTransformers(queryTransformer)
.queryAugmenter(
new CustomContextQueryAugmenter(
queryArgumentPromptTemplate,
null,
true)
).queryExpander(queryExpander)
.documentJoiner(new ConcatenationDocumentJoiner())
.build();
}
Web Search 實現文章參考:https://java2ai.com/blog/spring-ai-alibaba-module-rag/?spm=5176.29160081.0.0.2856aa5cenvkmu&source=blog/
Spring AI RAG:https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html#_advisors
3.3 MCP ?集成
3.3.1 MCP Config 增強
為了解決 MCP Stdio json 配置文件難以理解和本地 MCP Server 二進制文件需要絕對路徑的問題。在 playground 中對 MCP Stdio 配置做了增強處理。
其主要步驟是將 McpStdioClientProperties 屬性配置重寫,以便在后續 MCP Client 初始化使用增強的 MCP 配置。
@Component
public class CustomMcpStdioTransportConfigurationBeanPostProcessor implements BeanPostProcessor {
? private static final Logger logger = LoggerFactory.getLogger(CustomMcpStdioTransportConfigurationBeanPostProcessor.class);
? private final ObjectMapper objectMapper;
? private final McpStdioClientProperties mcpStdioClientProperties;
? public CustomMcpStdioTransportConfigurationBeanPostProcessor(
ObjectMapper objectMapper,
McpStdioClientProperties mcpStdioClientProperties
) {
this.objectMapper = objectMapper;
this.mcpStdioClientProperties = mcpStdioClientProperties;
}
? @NotNull
@Override
public Object postProcessAfterInitialization(@NotNull Object bean, @NotNull String beanName) throws BeansException {
? ? if (bean instanceof StdioTransportAutoConfiguration) {
? ? ? logger.debug("增強 McpStdioTransportConfiguration bean start: {}", beanName);
? ? ? McpServerConfig mcpServerConfig;
try {
mcpServerConfig = McpServerUtils.getMcpServerConfig();
? ? ? ? // Handle the jar relative path issue in the configuration file.
for (Map.Entry<String, McpStdioClientProperties.Parameters> entry : mcpServerConfig.getMcpServers()
.entrySet()) {
? ? ? ? ? if (entry.getValue() != null && entry.getValue().command().startsWith("java")) {
? ? ? ? ? ? McpStdioClientProperties.Parameters serverConfig = entry.getValue();
String oldMcpLibsPath = McpServerUtils.getLibsPath(serverConfig.args());
String rewriteMcpLibsAbsPath = getMcpLibsAbsPath(McpServerUtils.getLibsPath(serverConfig.args()));
if (rewriteMcpLibsAbsPath != null) {
serverConfig.args().remove(oldMcpLibsPath);
serverConfig.args().add(rewriteMcpLibsAbsPath);
}
}
}
? ? ? ? String msc = objectMapper.writeValueAsString(mcpServerConfig);
logger.debug("Registry McpServer config: {}", msc);
? ? ? ? // write mcp client
mcpStdioClientProperties.setServersConfiguration(new ByteArrayResource(msc.getBytes()));
((StdioTransportAutoConfiguration) bean).stdioTransports(this.mcpStdioClientProperties);
}
catch (IOException e) {
throw new SAAAppException(e.getMessage());
}
? ? ? logger.debug("增強 McpStdioTransportConfiguration bean end: {}", beanName);
}
? ? return bean;
}
}
在 MCPServerUtils 中讀取 mcp-config.yaml 配置并轉為 McpServerConfig。
public static McpServerConfig getMcpServerConfig() throws IOException {
? ? ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
InputStream resourceAsStream = ModelsUtils.class.getClassLoader().getResourceAsStream(MCP_CONFIG_FILE_PATH);
? ? McpServerConfig mcpServerConfig = mapper.readValue(resourceAsStream, McpServerConfig.class);
mcpServerConfig.getMcpServers().forEach((key, parameters) -> {
Map<String, String> env = parameters.env();
if (Objects.nonNull(env)) {
env.entrySet().stream()
.filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty() &&
entry.getValue().startsWith("${") && entry.getValue().endsWith("}"))
.forEach(entry -> {
String envKey = entry.getValue().substring(2, entry.getValue().length() - 1);
String envValue = System.getenv(envKey);
// allow env is null.
if (envValue != null && !envValue.isEmpty()) {
env.put(entry.getKey(), envValue);
}
});
}
});
return mcpServerConfig;
}
3.3.2 MCP Server 工具回顯
為了便于展示 MCP Client 如何調用 MCP Server 的 tools 和展示 MCP Server 中有哪些 Tools,playground 中做了特殊處理。
自定義 MCP Server 存放 MCP Server 的 tools 信息用于瀏覽器顯示:
public class McpServer {
? private String id;
? private String name;
? private String desc;
? private Map<String, String> env;
? private List<Tools> toolList;
}
因為 Spring AI 的 SyncMcpToolCallback 中的 MCPClient 沒有對外暴露獲取 MCP Server 的相關屬性,只有 Tools 定義。playground 對 SyncMcpToolCallback 做了包裝處理:
public class SyncMcpToolCallbackWrapper {
? private final SyncMcpToolCallback callback;
? public SyncMcpToolCallbackWrapper(SyncMcpToolCallback callback) {
this.callback = callback;
}
? public McpSyncClient getMcpClient() {
? ? try {
Field field = SyncMcpToolCallback.class.getDeclaredField("mcpClient");
field.setAccessible(true);
return (McpSyncClient) field.get(callback);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
在 MCPServerUtils 中做了 MCP Server 容器的初始化操作:
public static void initMcpServerContainer(ToolCallbackProvider toolCallbackProvider) throws IOException {
? ? McpServerConfig mcpServerConfig = McpServerUtils.getMcpServerConfig();
Map<String, String> mcpServerDescMap = initMcpServerDescMap();
? ? mcpServerConfig.getMcpServers().forEach((key, parameters) -> {
? ? ? ? List<McpServer.Tools> toolsList = new ArrayList<>();
for (ToolCallback toolCallback : toolCallbackProvider.getToolCallbacks()) {
? ? ? ? ? ? // todo: 拿不到 mcp client, 先用包裝器拿吧
SyncMcpToolCallback mcpToolCallback = (SyncMcpToolCallback) toolCallback;
SyncMcpToolCallbackWrapper syncMcpToolCallbackWrapper = new SyncMcpToolCallbackWrapper(mcpToolCallback);
String currentMcpServerName = syncMcpToolCallbackWrapper.getMcpClient().getServerInfo().name();
? ? ? ? ? ? // 按照 mcp server name 聚合 mcp server tools
if (Objects.equals(key, currentMcpServerName)) {
McpServer.Tools tool = new McpServer.Tools();
tool.setDesc(toolCallback.getToolDefinition().description());
tool.setName(toolCallback.getToolDefinition().name());
tool.setParams(toolCallback.getToolDefinition().inputSchema());
? ? ? ? ? ? ? ? toolsList.add(tool);
}
}
? ? ? ? McpServerContainer.addServer(McpServer.builder()
.id(getId())
.name(key)
.env(parameters.env())
.desc(mcpServerDescMap.get(key))
.toolList(toolsList)
.build()
);
});
}
MCP Server 工具回顯效果如下:您可以在 resource 下的 mcp-config.yaml 中添加更多 MCP Server。
image-20250607190058038
3.3.3 MCP 工具調用
完成上面一系列的初始化操作之后,接下來便是編寫 MCP Service 類:為了能夠獲取 MCP Server Tools 的執行信息,這里使用了 Spring AI Tools 的 internalToolExecutionEnabled API。來收集大模型的工具入參和執行結果等,在前端做調用展示。
為了收集 MCP Tools 調用過程中的信息。Playground 項目編寫了 ToolCallResp 類來收集一些信息:
public class ToolCallResp {
? ? /**
* Tool 的執行狀態
*/
private ToolState status;
? ? /**
* Tool Name
*/
private String toolName;
? ? /**
* Tool 執行參數
*/
private String toolParameters;
? ? /**
* Tool 執行結果
*/
private String toolResult;
? ? /**
* 工具執行開始的時間戳
*/
private LocalDateTime toolStartTime;
? ? /**
* 工具執行完成的時間戳
*/
private LocalDateTime toolEndTime;
? ? /**
* 工具執行的錯誤信息
*/
private String errorMessage;
? ? /**
* 工具執行輸入
*/
private String toolInput;
? ? /**
* 工具執行耗時
*/
private Long toolCostTime;
/**
* Tool 記錄tool返回的中間結果
*/
private String toolResponse;
}
MCP Service 實現:
@Service
public class SAAMcpService {
? private final ChatClient chatClient;
? private final ObjectMapper objectMapper;
? private final ToolCallbackProvider tools;
? private final ToolCallingManager toolCallingManager;
? private final McpStdioClientProperties mcpStdioClientProperties;
? private static final Logger logger = LoggerFactory.getLogger(SAAMcpService.class);
? public SAAMcpService(
ObjectMapper objectMapper,
ToolCallbackProvider tools,
SimpleLoggerAdvisor simpleLoggerAdvisor,
ToolCallingManager toolCallingManager,
McpStdioClientProperties mcpStdioClientProperties,
@Qualifier("openAiChatModel") ChatModel chatModel
) throws IOException {
? ? this.objectMapper = objectMapper;
this.mcpStdioClientProperties = mcpStdioClientProperties;
? ? // Initialize chat client with non-blocking configuration
this.chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
simpleLoggerAdvisor
).defaultToolCallbacks(tools)
.build();
this.tools = tools;
this.toolCallingManager = toolCallingManager;
? ? McpServerUtils.initMcpServerContainer(tools);
}
? public ToolCallResp chat(String prompt) {
? ? // manual run tools flag
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(tools.getToolCallbacks())
.internalToolExecutionEnabled(false)
.build();
? ? ChatResponse response = chatClient.prompt(new Prompt(prompt, chatOptions))
.call().chatResponse();
? ? logger.debug("ChatResponse: {}", response);
assert response != null;
List<AssistantMessage.ToolCall> toolCalls = response.getResult().getOutput().getToolCalls();
logger.debug("ToolCalls: {}", toolCalls);
String responseByLLm = response.getResult().getOutput().getText();
logger.debug("Response by LLM: {}", responseByLLm);
? ? // execute tools with no chat memory messages.
var tcr = ToolCallResp.TCR();
if (!toolCalls.isEmpty()) {
? ? ? tcr = ToolCallResp.startExecute(
responseByLLm,
toolCalls.get(0).name(),
toolCalls.get(0).arguments()
);
tcr.setToolParameters(toolCalls.get(0).arguments());
logger.debug("Start ToolCallResp: {}", tcr);
ToolExecutionResult toolExecutionResult = null;
? ? ? try {
toolExecutionResult = toolCallingManager.executeToolCalls(new Prompt(prompt, chatOptions), response);
? ? ? ? tcr.setToolEndTime(LocalDateTime.now());
}
catch (Exception e) {
? ? ? ? tcr.setStatus(ToolCallResp.ToolState.FAILURE);
tcr.setErrorMessage(e.getMessage());
tcr.setToolEndTime(LocalDateTime.now());
tcr.setToolCostTime((long) (tcr.getToolEndTime().getNano() - tcr.getToolStartTime().getNano()));
logger.error("Error ToolCallResp: {}, msg: {}", tcr, e.getMessage());
// throw new RuntimeException("Tool execution failed, please check the logs for details.");
}
? ? ? String llmCallResponse = "";
if (Objects.nonNull(toolExecutionResult)) {
ChatResponse finalResponse = chatClient.prompt().messages(toolExecutionResult.conversationHistory())
.call().chatResponse();
if (finalResponse != null) {
llmCallResponse = finalResponse.getResult().getOutput().getText();
}
? ? ? ? StringBuilder sb = new StringBuilder();
toolExecutionResult.conversationHistory().stream()
.filter(message -> message instanceof ToolResponseMessage)
.forEach(message -> {
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) message;
toolResponseMessage.getResponses().forEach(tooResponse -> {
sb.append(tooResponse.responseData());
});
});
tcr.setToolResponse(sb.toString());
}
? ? ? tcr.setStatus(ToolCallResp.ToolState.SUCCESS);
tcr.setToolResult(llmCallResponse);
tcr.setToolCostTime((long) (tcr.getToolEndTime().getNano() - tcr.getToolStartTime().getNano()));
logger.debug("End ToolCallResp: {}", tcr);
}
else {
logger.debug("ToolCalls is empty, no tool execution needed.");
tcr.setToolResult(responseByLLm);
}
? ? return tcr;
}
? public ToolCallResp run(String id, Map<String, String> envs, String prompt) throws IOException {
? ? Optional<McpServer> runMcpServer = McpServerContainer.getServerById(id);
if (runMcpServer.isEmpty()) {
logger.error("McpServer not found, id: {}", id);
return ToolCallResp.TCR();
}
? ? String runMcpServerName = runMcpServer.get().getName();
var mcpServerConfig = McpServerUtils.getMcpServerConfig();
McpStdioClientProperties.Parameters parameters = new McpStdioClientProperties.Parameters(
mcpServerConfig.getMcpServers().get(runMcpServerName).command(),
mcpServerConfig.getMcpServers().get(runMcpServerName).args(),
envs
);
? ? if (parameters.command().startsWith("java")) {
String oldMcpLibsPath = McpServerUtils.getLibsPath(parameters.args());
String rewriteMcpLibsAbsPath = getMcpLibsAbsPath(McpServerUtils.getLibsPath(parameters.args()));
? ? ? parameters.args().remove(oldMcpLibsPath);
parameters.args().add(rewriteMcpLibsAbsPath);
}
? ? String mcpServerConfigJSON = objectMapper.writeValueAsString(mcpServerConfig);
mcpStdioClientProperties.setServersConfiguration(new ByteArrayResource(mcpServerConfigJSON.getBytes()));
? ? return chat(prompt);
}
}
Function Call 參考:https://docs.spring.io/spring-ai/reference/api/tools.html
MCP Server 文章參考:https://java2ai.com/blog/spring-ai-alibaba-mcp/?spm=5176.29160081.0.0.2856aa5cenvkmu
3.4 Function Call 集成
playground 中實現了 Function Call 的功能,和 MCP 一樣,支持調用狀態顯示。工具瀏覽器回顯同理。
3.4.1 Function Tools 初始化
您可以通過使用 Spring AI Alibaba 的提供的 Tool Calling Starter 來引入工具,也可以像 Playground 一樣,通過 FunctionToolCallback 來自定義工具。
Playground Tools: https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-playground/src/main/java/com/alibaba/cloud/ai/application/tools
Tools 初始化代碼如下:
public List<ToolCallback> getTools() {
? ? return List.of(buildBaiduTranslateTools(), buildBaiduMapTools());
}
private ToolCallback buildBaiduTranslateTools() {
? ? return FunctionToolCallback
.builder(
"BaiduTranslateService",
new BaiduTranslateTools(ak, sk, restClientbuilder, responseErrorHandler)
).description("Baidu translation function for general text translation.")
.inputSchema(
"""
{
"type": "object",
"properties": {
"Request": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "Content that needs to be translated."
},
"from": {
"type": "string",
"description": "Source language that needs to be translated."
},
"to": {
"type": "string",
"description": "Target language to translate into."
}
},
"required": ["q", "from", "to"],
"description": "Request object to translate text to a target language."
},
"Response": {
"type": "object",
"properties": {
"translatedText": {
"type": "string",
"description": "The translated text."
}
},
"required": ["translatedText"],
"description": "Response object for the translation function, containing the translated text."
}
},
"required": ["Request", "Response"]
}
"""
).inputType(BaiduTranslateTools.BaiduTranslateToolRequest.class)
.toolMetadata(ToolMetadata.builder().returnDirect(false).build())
.build();
}
3.4.2 Function Tools 調用
完成了工具引入或定義之后。接下來,便可以在 service 中使用這些 Tools 來增強大模型的能力。工具調用代碼和 MCP Server Tools 類似。
public class SAAToolsService {
? private static final Logger logger = LoggerFactory.getLogger(SAAToolsService.class);
? private final ChatClient chatClient;
? private final ToolCallingManager toolCallingManager;
? private final ToolsInit toolsInit;
? public SAAToolsService(
ToolsInit toolsInit,
ToolCallingManager toolCallingManager,
SimpleLoggerAdvisor simpleLoggerAdvisor,
MessageChatMemoryAdvisor messageChatMemoryAdvisor,
@Qualifier("openAiChatModel") ChatModel chatModel
) {
? ? this.toolsInit = toolsInit;
this.toolCallingManager = toolCallingManager;
? ? this.chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
simpleLoggerAdvisor
// ? ? ? ? ? ?messageChatMemoryAdvisor
).build();
}
? public ToolCallResp chat(String prompt) {
? ? // manual run tools flag
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(toolsInit.getTools())
.internalToolExecutionEnabled(false)
.build();
Prompt userPrompt = new Prompt(prompt, chatOptions);
? ? ChatResponse response = chatClient.prompt(userPrompt)
.call().chatResponse();
? ? logger.debug("ChatResponse: {}", response);
assert response != null;
List<AssistantMessage.ToolCall> toolCalls = response.getResult().getOutput().getToolCalls();
logger.debug("ToolCalls: {}", toolCalls);
String responseByLLm = response.getResult().getOutput().getText();
logger.debug("Response by LLM: {}", responseByLLm);
? ? // execute tools with no chat memory messages.
var tcr = ToolCallResp.TCR();
if (!toolCalls.isEmpty()) {
? ? ? tcr = ToolCallResp.startExecute(
responseByLLm,
toolCalls.get(0).name(),
toolCalls.get(0).arguments()
);
logger.debug("Start ToolCallResp: {}", tcr);
ToolExecutionResult toolExecutionResult = null;
? ? ? try {
toolExecutionResult = toolCallingManager.executeToolCalls(new Prompt(prompt, chatOptions), response);
? ? ? ? tcr.setToolEndTime(LocalDateTime.now());
}
catch (Exception e) {
? ? ? ? tcr.setStatus(ToolCallResp.ToolState.FAILURE);
tcr.setErrorMessage(e.getMessage());
tcr.setToolEndTime(LocalDateTime.now());
tcr.setToolCostTime((long) (tcr.getToolEndTime().getNano() - tcr.getToolStartTime().getNano()));
logger.error("Error ToolCallResp: {}, msg: {}", tcr, e.getMessage());
// throw new RuntimeException("Tool execution failed, please check the logs for details.");
}
? ? ? String llmCallResponse = "";
if (Objects.nonNull(toolExecutionResult)) {
// ? ? ? ?ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolExecutionResult.conversationHistory()
// ? ? ? ? ? ?.get(toolExecutionResult.conversationHistory().size() - 1);
// ? ? ? ?llmCallResponse = toolResponseMessage.getResponses().get(0).responseData();
ChatResponse finalResponse = chatClient.prompt().messages(toolExecutionResult.conversationHistory()).call().chatResponse();
llmCallResponse = finalResponse.getResult().getOutput().getText();
}
? ? ? tcr.setStatus(ToolCallResp.ToolState.SUCCESS);
tcr.setToolResult(llmCallResponse);
tcr.setToolCostTime((long) (tcr.getToolEndTime().getNano() - tcr.getToolStartTime().getNano()));
logger.debug("End ToolCallResp: {}", tcr);
}
else {
logger.debug("ToolCalls is empty, no tool execution needed.");
tcr.setToolResult(responseByLLm);
}
? ? return tcr;
}
}
Function Tools 集成文章參考:https://java2ai.com/blog/spring-ai-toolcalling/?spm=5176.29160081.0.0.2856aa5cenvkmu
3.5 前端頁面
Playground 前端頁面使用 React 實現,為上述功能提供了一套基本的前端界面,您可以參考以下關鍵信息做一些自定義修改.
3.5.1 數據持久化
部分業務場景需要將數據持久化以便于查詢歷史記錄,生產環境使用時推薦使用服務端存儲記錄,但 Playground 出于效果演示目的,目前默認將所有歷史數據保存到客戶端本地,相關實現參考如下代碼:
/**
* 處理消息發送的完整流程
* @param text - 消息文本
* @param sendRequest - 發送請求的函數
* @param createMessage - 創建消息對象的函數
*/
const processSendMessage = async <T extends BaseMessage>({
text,
sendRequest,
createMessage,
}: {
text: string;
sendRequest: (text: string, timestamp: number, message: T) => Promise<void>;
createMessage: (text: string, timestamp: number) => T;
}) => {
if (!text.trim() || !activeConversation) return;
? ? const userTimestamp = Date.now();
const userMessage = createMessage(text, userTimestamp);
? ? // 存儲用戶數據到客戶端本地,生產環境可省略該步驟由服務端存儲
updateActiveConversation({
...activeConversation,
messages: [
...activeConversation.messages,
userMessage,
] as T[],
});
? ? try {
await sendRequest(text, userTimestamp, userMessage);
} catch (error) {
console.error("error:", error);
}
};
3.5.2 消息文本樣式自定義渲染
對話氣泡組件支持富文本樣式渲染,并且具備較好的拓展性,您可以參考如下代碼對任意標簽做自定義樣式拓展:
/**
* 獲取自定義渲染配置
* @param style - css 樣式
*/
const getMarkdownRenderConfig = (styles: Record<string, string>) => {
? return {
div: ({ children }) => {
return <pre className={styles.codeBlock}>{children}</pre>
},
code: ({ children, className }) => {
return <code className={styles.codeInline}>{children}</code>
},
think: ({ children }) => {
return ?<div className={styles.thinkTag}>{children}</div>;
},
tool: ({ children }) => {
return <div className={styles.toolTag}>{children}</div>;
},
};
};
您也可以通過正則匹配等方式進行語法分析,將特定格式的字符串修改為由自定義標簽包裹的形式,再由上述方式設置其渲染樣式,實現可交互表單、圖表等復雜樣式。
4. 總結
Spring AI Alibaba 官方社區開發了一個包含完整 前端UI+后端實現 的智能體 Playground 示例。未來社區會持續更新維護。以此來演示 Spring AI 和 Spring AI Alibaba 的最新功能。