Spring Boot 实战(四十二):集成 Spring AI 六大场景

目录

    • 官方资源链接
    • 写在前面
    • 一、环境准备
    • [二、后端:Spring Boot 模拟 Spring AI 六大场景](#二、后端:Spring Boot 模拟 Spring AI 六大场景)
      • [2.1 后端架构原理](#2.1 后端架构原理)
      • [2.2 初始化 Maven 工程](#2.2 初始化 Maven 工程)
      • [2.3 启动类与配置](#2.3 启动类与配置)
      • [2.4 CORS 配置](#2.4 CORS 配置)
      • [2.5 请求 DTO](#2.5 请求 DTO)
      • [2.6 模拟 AI 客户端:六大场景实现](#2.6 模拟 AI 客户端:六大场景实现)
      • [2.7 REST 控制器](#2.7 REST 控制器)
      • [2.8 启动并冒烟后端](#2.8 启动并冒烟后端)
    • [三、前端:Vue 3 场景调试控制台](#三、前端:Vue 3 场景调试控制台)
      • [3.1 前端分层原理](#3.1 前端分层原理)
      • [3.2 用 Vite 创建 Vue 工程](#3.2 用 Vite 创建 Vue 工程)
      • [3.3 Vite 代理](#3.3 Vite 代理)
      • [3.4 Axios API 封装](#3.4 Axios API 封装)
      • [3.5 页面入口与样式](#3.5 页面入口与样式)
      • [3.6 核心页面:`App.vue`](#3.6 核心页面:App.vue)
    • 四、联调与验收
      • [4.1 端到端请求链路](#4.1 端到端请求链路)
      • [4.2 建议验收清单](#4.2 建议验收清单)
    • 五、生产与演进建议
      • [5.1 从模拟切到真实 Spring AI](#5.1 从模拟切到真实 Spring AI)
      • [5.2 前端与部署](#5.2 前端与部署)
      • [5.3 可继续扩展的方向](#5.3 可继续扩展的方向)

后端 :Spring Boot 3.2 · Java 17 · REST + CORS · 模拟 Spring AI 客户端(Chat / 多模态 / 结构化输出 / Memory / RAG / Tools / MCP)

前端 :Vue 3 · Vite 5 · Axios · 场景化调试控制台

目标:无需下载任何仓库,按本文章节顺序即可搭出同等效果的前后端联调工程。


官方资源链接

后端

Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/3.2.5/reference/html/

Spring Boot GitHub:https://github.com/spring-projects/spring-boot

Spring AI 官方文档:https://docs.spring.io/spring-ai/reference/

Spring AI GitHub:https://github.com/spring-projects/spring-ai

前端

Vue 官方文档:https://vuejs.org/guide/introduction.html

Vue GitHub:https://github.com/vuejs/core

Vite 官方文档:https://vitejs.dev/guide/

Vite GitHub:https://github.com/vitejs/vite

Axios 官方文档:https://axios-http.com/docs/intro

Axios GitHub:https://github.com/axios/axios


写在前面

做智能体或问答产品时,后端往往要同时暴露「对话、记忆、RAG、工具、多模态」等多类能力;前端则需要一页就能切换场景、填参数、看响应。如果一上来就接入真实模型供应商和向量库,环境、密钥、鉴权容易把学习路径打断。

本文采用「先用可运行的模拟 AI 客户端把 API 契约和前端控制台搭通,后续再替换为真实 Spring AI 」的路径:后端提供统一前缀 /ai 的 REST;前端用 Vue 控制台调用;开发期由 Vite 代理转发,避免跨域卡顿。

整体调用链路如下:

  1. 用户在 Vue 页面选择场景并提交;
  2. Axios 请求 /ai/*
  3. Vite Dev Server 代理到 http://localhost:8080
  4. Spring Boot 经 CORS 过滤器进入 AIController
  5. SimpleAIClient 按场景返回模拟结果;
  6. 前端结果面板展示文本或 JSON。

最终工程清单

模块 目录建议 职责 默认端口
后端 spring-ai REST API、CORS、模拟 AI 能力 8080
前端 spring-ai-ui Vue 场景控制台、Axios 封装、Vite 代理 5173
文档 docs 本实战文章与配图 ---

你最终应得到这样的顶层结构:

text 复制代码
your-workspace/
├── spring-ai/          # Maven 后端
└── spring-ai-ui/       # Vue 前端

后端选型说明

本项目要的不是「再造一个 Agent 框架」,而是:用 Spring Boot 把 AI 核心场景固化为清晰、可替换的 HTTP API。市面常见做法对比如下。

方案 特点 学习/接入成本 与本文目标契合度 本文是否选用
自研模拟客户端 + Spring MVC 无外网/无密钥也能演示全链路,API 契约稳定 高(先打通契约再换实现) 选用
真实 Spring AI + 通义千问等 官方抽象好,可直接对接模型 中(依赖与密钥) 高(生产演进方向) 文末给出替换思路
LangChain4j Java 侧链式编排能力强 中(偏编排而非本教程范围)
直接调用厂商 HTTP SDK 灵活,但业务代码易与厂商强耦合 低~中 低(不利于后续换 Spring AI)

本文先用自研 SimpleAIClient 模拟 ChatClient / Memory / RAG / Tool / MCP 等行为;方法签名与返回形态按「真实接入时尽量少改 Controller」来设计。

前端选型说明

目标是单页场景调试台,不需要路由、状态管理或组件库全家桶。

方案 特点 开发体验 本文是否选用
Vue 3 + Vite + Axios 轻量、代理方便、模板直观 优秀 选用
React + Vite + fetch 生态大,但对简单表单页略「重」 优秀
纯 HTML + fetch 零构建,但可维护性差 一般
Element Plus / Ant Design Vue UI 组件齐全,适合复杂后台 好,但有额外学习成本 否(本教程手写简洁样式)

下方正文按「环境 → 后端 → 前端 → 联调验收 → 生产建议」展开。


一、环境准备

请确认本机具备:

工具 建议版本 用途
JDK 17+ 编译运行后端
Maven 3.8+ 依赖与启动
Node.js 18 LTS(示例以 18.x 验证;Vite 5 可运行) 前端工具链
npm 随 Node 安装 安装前端依赖
curl 系统自带 接口冒烟

可选检查命令:

bash 复制代码
java -version
mvn -v
node -v
npm -v

创建工作区:

bash 复制代码
mkdir -p ai-demo && cd ai-demo
mkdir spring-ai spring-ai-ui

二、后端:Spring Boot 模拟 Spring AI 六大场景

2.1 后端架构原理

图 1:后端分层。CORS 过滤跨域;AIController 负责协议与入参;SimpleAIClient 承担「拟态引擎」职责,后续可替换为真实 Spring AI ChatClient

组件 职责
CorsFilter 允许前端源访问 /ai/**
AIController REST 入口、参数绑定、媒体类型(含 multipart)
SimpleAIClient 六大场景业务逻辑(当前为内存模拟)
application.yml 端口与预留的通义/向量库配置位

数据流(以 Memory 为例):POST /ai/memory → 反序列化为 MemoryRequestchatWithMemory(sessionId, message) → 读写 ConcurrentHashMap 会话历史 → 返回字符串。

2.2 初始化 Maven 工程

spring-ai 下创建 pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>springboot-spring-ai-qwen</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

建议的源码树:

text 复制代码
spring-ai/
└── src/main/
    ├── java/com/demo/
    │   ├── SpringbootDemoApplication.java
    │   ├── config/CorsConfig.java
    │   └── ai/
    │       ├── AIController.java
    │       ├── SimpleAIClient.java
    │       └── model/
    │           ├── PromptRequest.java
    │           ├── MemoryRequest.java
    │           ├── RagRequest.java
    │           ├── ToolRequest.java
    │           └── McpRequest.java
    └── resources/application.yml

2.3 启动类与配置

启动类:

java 复制代码
package com.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }
}

application.yml(端口 + 预留通义与向量库配置,便于后续切换真实实现):

yaml 复制代码
server:
  port: 8080

spring:
  ai:
    tongyi:
      api-key: ${TONGYI_API_KEY:your-tongyi-api-key-here}
      chat:
        options:
          model: qwen-plus
          temperature: 0.7
          max-tokens: 2000
      embedding:
        options:
          model: text-embedding-v2

chromadb:
  host: localhost
  port: 8000
  collection-name: spring-ai-docs

logging:
  level:
    org.springframework.ai: DEBUG
    com.demo: DEBUG

2.4 CORS 配置

前端开发服务器端口为 5173,后端打开 CORS:

java 复制代码
package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.List;

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOriginPatterns(List.of("*"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

生产环境请把 allowedOriginPatterns 收紧为真实前端域名,并配合网关鉴权。

2.5 请求 DTO

五个 JSON Body 模型写法相同,字段如下(均使用 Lombok @Data):

java 复制代码
package com.demo.ai.model;

import lombok.Data;

@Data
public class PromptRequest {
    private String prompt;
}
java 复制代码
package com.demo.ai.model;

import lombok.Data;

@Data
public class MemoryRequest {
    private String sessionId;
    private String message;
}
java 复制代码
package com.demo.ai.model;

import lombok.Data;

@Data
public class RagRequest {
    private String question;
    private String context;
}
java 复制代码
package com.demo.ai.model;

import lombok.Data;

@Data
public class ToolRequest {
    private String functionName;
    private String arguments;
}
java 复制代码
package com.demo.ai.model;

import lombok.Data;

@Data
public class McpRequest {
    private String query;
}

2.6 模拟 AI 客户端:六大场景实现

java 复制代码
package com.demo.ai;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 简化版 AI 客户端,模拟 Spring AI 核心能力。
 * 后续可将方法体替换为真实 ChatClient / Advisor / Tool 调用。
 */
@Component
public class SimpleAIClient {

    private final WebClient webClient;
    private final Map<String, String> conversationMemory = new ConcurrentHashMap<>();

    public SimpleAIClient() {
        this.webClient = WebClient.builder()
                .baseUrl("https://api.example.com")
                .build();
    }

    /** 场景一:基础对话 */
    public String chat(String message) {
        return "AI响应: 您说 '" + message + "',我理解您的意思。";
    }

    /** 场景二:结构化输出 */
    public String generateStructuredResponse(String prompt) {
        return "{\n  \"title\": \"结构化响应\",\n  \"description\": \"基于提示: "
                + prompt + "\",\n  \"keyPoints\": [\"要点1\", \"要点2\", \"要点3\"]\n}";
    }

    /** 场景三:会话记忆 */
    public String chatWithMemory(String sessionId, String message) {
        String history = conversationMemory.getOrDefault(sessionId, "");
        String response = "AI响应: 基于历史对话 '" + history + "',对您的新消息 '"
                + message + "' 的回复。";
        conversationMemory.put(sessionId, history + " 用户: " + message + " AI: " + response);
        return response;
    }

    /** 场景四:RAG */
    public String ragQuery(String question, String context) {
        return "基于提供的上下文信息,对问题 '" + question + "' 的回答是:这是一个RAG增强的回答。";
    }

    /** 场景五:工具调用 */
    public String callTool(String functionName, String arguments) {
        switch (functionName.toLowerCase()) {
            case "weather":
                return "天气查询结果:北京,晴朗,25°C";
            case "time":
                return "当前时间:" + java.time.LocalDateTime.now();
            default:
                return "未知工具: " + functionName;
        }
    }

    /** 场景六:MCP */
    public String mcpIntegration(String query) {
        return "通过MCP协议处理查询: " + query + ",返回标准化结果。";
    }

    /** 多模态 */
    public String multimodalChat(String text, byte[] imageData) {
        return "多模态AI处理:文本 '" + text + "' 和图像数据("
                + imageData.length + "字节)的分析结果。";
    }
}

原理要点:

  • Chat / Structured / RAG / MCP:无状态字符串回显,便于前端校验协议。
  • Memory :按 sessionId 在 JVM 内累加历史(单机演示即可;生产应换 Redis / DB)。
  • Tools :用 switch 模拟 Function Calling;真实 Spring AI 应注册 @Tool 方法。
  • Multimodal:只消费图片字节长度,验证 multipart 上传链路。
  • WebClient 预留了出站 HTTP,目前未对真实 API 发请求,避免依赖外网与密钥。

2.7 REST 控制器

java 复制代码
package com.demo.ai;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import com.demo.ai.model.*;
import java.io.IOException;

@RestController
@RequestMapping("/ai")
public class AIController {

    @Autowired
    private SimpleAIClient aiClient;

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return aiClient.chat(message);
    }

    @PostMapping(value = "/multimodal", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String multimodalChat(
            @RequestParam String text,
            @RequestParam MultipartFile image) throws IOException {
        return aiClient.multimodalChat(text, image.getBytes());
    }

    @PostMapping("/structured")
    public String structuredOutput(@RequestBody PromptRequest request) {
        return aiClient.generateStructuredResponse(request.getPrompt());
    }

    @PostMapping("/memory")
    public String chatWithMemory(@RequestBody MemoryRequest request) {
        return aiClient.chatWithMemory(request.getSessionId(), request.getMessage());
    }

    @PostMapping("/rag")
    public String ragQuery(@RequestBody RagRequest request) {
        return aiClient.ragQuery(request.getQuestion(), request.getContext());
    }

    @PostMapping("/tools")
    public String callTool(@RequestBody ToolRequest request) {
        return aiClient.callTool(request.getFunctionName(), request.getArguments());
    }

    @PostMapping("/mcp")
    public String mcpIntegration(@RequestBody McpRequest request) {
        return aiClient.mcpIntegration(request.getQuery());
    }
}

API 一览:

场景 方法 路径 入参要点
基础对话 GET /ai/chat message
多模态 POST /ai/multimodal text + image(form-data)
结构化输出 POST /ai/structured {"prompt":"..."}
会话记忆 POST /ai/memory sessionId + message
RAG POST /ai/rag question + context
工具调用 POST /ai/tools functionName + arguments
MCP POST /ai/mcp {"query":"..."}

2.8 启动并冒烟后端

bash 复制代码
cd spring-ai
mvn spring-boot:run

另开终端:

bash 复制代码
curl "http://localhost:8080/ai/chat?message=你好Spring%20AI"

curl -X POST http://localhost:8080/ai/structured \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Spring Boot优势"}'

curl -X POST http://localhost:8080/ai/memory \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"test123","message":"我喜欢编程"}'

若返回带「AI响应」或 JSON 字符串,后端基础链路已通。


三、前端:Vue 3 场景调试控制台

3.1 前端分层原理

图 2:UI(App.vue)只关心交互;api/ai.js 封装与后端契约;Vite server.proxy/ai 转到 Spring Boot,开发态即可避免浏览器跨域。

相对常见「页面直接拼 http://localhost:8080」的做法,本分层有两个好处:

  1. 前端代码始终写相对路径 /ai/...,上线时可改同源网关;
  2. 业务组件不感知 multipart / JSON 细节差异(API 层统一处理)。

3.2 用 Vite 创建 Vue 工程

若本机为 Node 18,建议固定 create-vite 大版本,避免最新脚手架要求 Node 20+:

bash 复制代码
cd spring-ai-ui
# 若目录由脚手架自动创建,可在上级执行:
# npm create vite@5.5.0 . -- --template vue

npm install
npm install axios
npm install -D sass

也可手动保证 package.json 接近:

json 复制代码
{
  "name": "spring-ai-ui",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.18.1",
    "vue": "^3.4.35"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.1.2",
    "sass": "^1.99.0",
    "vite": "^5.4.0"
  }
}

3.3 Vite 代理

js 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    proxy: {
      '/ai': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
})

3.4 Axios API 封装

js 复制代码
import axios from 'axios'

const http = axios.create({
  baseURL: '/ai',
  timeout: 30000,
})

export function chat(message) {
  return http.get('/chat', { params: { message } })
}

export function multimodalChat(text, imageFile) {
  const formData = new FormData()
  formData.append('text', text)
  formData.append('image', imageFile)
  return http.post('/multimodal', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  })
}

export function structuredOutput(prompt) {
  return http.post('/structured', { prompt })
}

export function chatWithMemory(sessionId, message) {
  return http.post('/memory', { sessionId, message })
}

export function ragQuery(question, context) {
  return http.post('/rag', { question, context })
}

export function callTool(functionName, argumentsText) {
  return http.post('/tools', { functionName, arguments: argumentsText })
}

export function mcpIntegration(query) {
  return http.post('/mcp', { query })
}

3.5 页面入口与样式

index.html

html 复制代码
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Spring AI 演示控制台</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

main.js

js 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

style.css

css 复制代码
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color: #213547;
  background-color: #f8fafc;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

#app {
  min-height: 100vh;
}

button,
input,
textarea,
select {
  font: inherit;
}

3.6 核心页面:App.vue

完整可复制实现(Tab 切换七个场景 + 统一提交 + 结果面板):

vue 复制代码
<script setup>
import { ref } from 'vue'
import {
  chat,
  multimodalChat,
  structuredOutput,
  chatWithMemory,
  ragQuery,
  callTool,
  mcpIntegration,
} from './api/ai'

const tabs = [
  { key: 'chat', label: '基础对话' },
  { key: 'multimodal', label: '多模态' },
  { key: 'structured', label: '结构化输出' },
  { key: 'memory', label: '会话记忆' },
  { key: 'rag', label: 'RAG问答' },
  { key: 'tools', label: '工具调用' },
  { key: 'mcp', label: 'MCP协议' },
]

const activeTab = ref('chat')
const loading = ref(false)
const result = ref('')
const error = ref('')

const chatMessage = ref('你好,Spring AI')
const multimodalText = ref('请描述这张图片')
const imageFile = ref(null)
const structuredPrompt = ref('Spring Boot介绍')
const sessionId = ref('user123')
const memoryMessage = ref('记住我喜欢Java')
const ragQuestion = ref('什么是Spring?')
const ragContext = ref('Spring是Java企业级应用开发框架。')
const toolName = ref('weather')
const toolArgs = ref('北京')
const mcpQuery = ref('查询用户信息')

function onImageChange(event) {
  imageFile.value = event.target.files?.[0] || null
}

async function run(action) {
  loading.value = true
  error.value = ''
  result.value = ''
  try {
    const response = await action()
    result.value = typeof response.data === 'string'
      ? response.data
      : JSON.stringify(response.data, null, 2)
  } catch (e) {
    error.value = e.response?.data?.message || e.message || '请求失败'
  } finally {
    loading.value = false
  }
}

function submit() {
  switch (activeTab.value) {
    case 'chat':
      return run(() => chat(chatMessage.value))
    case 'multimodal':
      if (!imageFile.value) {
        error.value = '请先选择图片文件'
        return
      }
      return run(() => multimodalChat(multimodalText.value, imageFile.value))
    case 'structured':
      return run(() => structuredOutput(structuredPrompt.value))
    case 'memory':
      return run(() => chatWithMemory(sessionId.value, memoryMessage.value))
    case 'rag':
      return run(() => ragQuery(ragQuestion.value, ragContext.value))
    case 'tools':
      return run(() => callTool(toolName.value, toolArgs.value))
    case 'mcp':
      return run(() => mcpIntegration(mcpQuery.value))
  }
}
</script>

<template>
  <div class="page">
    <header class="header">
      <h1>Spring AI 演示控制台</h1>
      <p>调用后端 <code>spring-ai</code> 的六大核心场景接口</p>
    </header>

    <nav class="tabs">
      <button
        v-for="tab in tabs"
        :key="tab.key"
        class="tab"
        :class="{ active: activeTab === tab.key }"
        @click="activeTab = tab.key; result = ''; error = ''"
      >
        {{ tab.label }}
      </button>
    </nav>

    <section class="panel">
      <div v-if="activeTab === 'chat'" class="form">
        <label>消息内容</label>
        <input v-model="chatMessage" type="text" placeholder="输入对话内容" />
      </div>

      <div v-else-if="activeTab === 'multimodal'" class="form">
        <label>文本描述</label>
        <input v-model="multimodalText" type="text" placeholder="输入文本" />
        <label>图像文件</label>
        <input type="file" accept="image/*" @change="onImageChange" />
      </div>

      <div v-else-if="activeTab === 'structured'" class="form">
        <label>提示词</label>
        <textarea v-model="structuredPrompt" rows="4" placeholder="输入提示词" />
      </div>

      <div v-else-if="activeTab === 'memory'" class="form">
        <label>会话 ID</label>
        <input v-model="sessionId" type="text" placeholder="sessionId" />
        <label>消息</label>
        <input v-model="memoryMessage" type="text" placeholder="输入消息" />
      </div>

      <div v-else-if="activeTab === 'rag'" class="form">
        <label>问题</label>
        <input v-model="ragQuestion" type="text" placeholder="输入问题" />
        <label>上下文</label>
        <textarea v-model="ragContext" rows="4" placeholder="输入上下文" />
      </div>

      <div v-else-if="activeTab === 'tools'" class="form">
        <label>工具名称</label>
        <select v-model="toolName">
          <option value="weather">weather</option>
          <option value="time">time</option>
        </select>
        <label>参数</label>
        <input v-model="toolArgs" type="text" placeholder="工具参数" />
      </div>

      <div v-else class="form">
        <label>MCP 查询</label>
        <input v-model="mcpQuery" type="text" placeholder="输入查询" />
      </div>

      <button class="submit" :disabled="loading" @click="submit">
        {{ loading ? '请求中...' : '发送请求' }}
      </button>
    </section>

    <section class="result" v-if="result || error">
      <h2>响应结果</h2>
      <pre v-if="error" class="error">{{ error }}</pre>
      <pre v-else>{{ result }}</pre>
    </section>
  </div>
</template>

<style scoped lang="scss">
.page {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 20px 48px;
  color: #1f2937;
}

.header {
  margin-bottom: 24px;

  h1 {
    margin: 0 0 8px;
    font-size: 28px;
  }

  p {
    margin: 0;
    color: #6b7280;
  }

  code {
    background: #f3f4f6;
    padding: 2px 6px;
    border-radius: 4px;
  }
}

.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 20px;
}

.tab {
  border: 1px solid #d1d5db;
  background: #fff;
  color: #374151;
  border-radius: 999px;
  padding: 8px 14px;
  cursor: pointer;

  &.active {
    background: #2563eb;
    border-color: #2563eb;
    color: #fff;
  }
}

.panel {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}

.form {
  display: grid;
  gap: 10px;
  margin-bottom: 16px;

  label {
    font-size: 14px;
    font-weight: 600;
    color: #4b5563;
  }

  input,
  textarea,
  select {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid #d1d5db;
    border-radius: 8px;
    padding: 10px 12px;
    font: inherit;
  }
}

.submit {
  background: #111827;
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 10px 18px;
  cursor: pointer;

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

.result {
  margin-top: 20px;

  h2 {
    margin: 0 0 10px;
    font-size: 18px;
  }

  pre {
    margin: 0;
    background: #0f172a;
    color: #e2e8f0;
    border-radius: 12px;
    padding: 16px;
    white-space: pre-wrap;
    word-break: break-word;
  }

  .error {
    background: #7f1d1d;
  }
}
</style>

界面效果示意:

图 4:控制台包含场景 Tab、表单区、结果区;与真实页面布局一致,便于验收对照。

启动前端:

bash 复制代码
cd spring-ai-ui
npm run dev

浏览器打开:http://localhost:5173


四、联调与验收

4.1 端到端请求链路

图 3:用户操作 → Axios → Vite 代理 → CorsFilter → AIController → SimpleAIClient → 结果面板。

4.2 建议验收清单

编号 操作 期望
1 后端 mvn spring-boot:run 8080 监听成功
2 curl /ai/chat?message=hi 返回含「AI响应」的文本
3 前端 npm run dev 5173 可打开控制台
4 Tab「基础对话」发送 结果区出现后端文本,Network 为 /ai/chat
5 「会话记忆」同 sessionId 连发两次 第二次响应内容体现更长历史
6 「结构化输出」 结果可被解析为 JSON(字符串形式)
7 「工具调用」选 weather 返回天气类描述
8 「多模态」上传任意图片 响应中含图片字节长度

若浏览器 Network 里请求打到了 8080 的 CORS 错误,请确认:

  • 是否仍在走 Vite 代理(URL 应为相对路径 /ai/...);
  • 后端 CorsConfig 是否已生效。

五、生产与演进建议

5.1 从模拟切到真实 Spring AI

路线可收敛为四步:

  1. pom.xml 增加与 Boot 3.2 兼容的 Spring AI BOM / 通义(或 OpenAI 兼容)starter;

  2. 用正式 ChatClient / EmbeddingModel 替换 SimpleAIClient 内部实现,尽量保持现有 Controller 路径不变;

  3. Memory 接 Redis 或 JDBC Chat Memory;RAG 接向量库(配置中已预留 Chroma 位置);

  4. Tools 改为声明式工具注册;MCP 按官方 MCP Client 对接外部资源。

    密钥仅通过环境变量注入,例如:

bash 复制代码
export TONGYI_API_KEY=sk-xxxxx

切勿把真实密钥写进仓库。

5.2 前端与部署

  • 开发:继续 Vite proxy;
  • 生产:npm run build 后由 Nginx / Gateway 把 /ai 反代到后端,前端静态资源同域托管;
  • 限流、超时、错误码统一为 JSON,前端再升级 toast / Loading;
  • 多模态注意上传大小与文件类型白名单。

5.3 可继续扩展的方向

  • 流式输出(SSE / WebFlux Flux)并在前端做打字机渲染;

  • 统一响应体 { code, data, message }

  • 按用户鉴权隔离 sessionId

  • 为 Memory / RAG 增加自动化测试(当前可用 MockMvc 覆盖 Controller)。

    按本文完成搭建后,你已经拥有一条「可演示、可替换、前后端分离」的 Spring AI 学习骨架:先把契约与体验跑通,再逐步把模拟实现换成真实模型与向量检索,学习与交付不会被环境阻塞。

相关推荐
lkshop1 小时前
跨境电商 GEO 实战:如何让独立站被 AI 搜索优先推荐
人工智能·chatgpt·geo
西柚小萌新1 小时前
【人工智能:Agent】--零基础上手 Codex 教程
人工智能
欣慰的猫咪1 小时前
GitHub Copilot 正式成为 JetBrains AI Assistant 的原生默认 Agent 之一
人工智能·github·copilot
蓝速科技1 小时前
蓝速科技视觉 3D 全息仓 AI 数字人一体机深度评测
人工智能·科技·3d
山东云弈创峰科技9 小时前
山东云弈创峰:基于多模态AI的跨境供应链数字化重构
人工智能·重构
智慧景区与市集主理人10 小时前
巨有科技乡村农文旅智慧建设|适配乡村短板,打造轻量长效数字业态
人工智能·科技
甲维斯10 小时前
马斯克Grok4.5太会了!狙击GPT5.6,贴脸Opus4.8!
人工智能·ai编程
雪碧聊技术10 小时前
Badge 应用场景与落地实践指南
大数据·人工智能
ai产品老杨10 小时前
NVIDIA GPU部署AI视频分析项目实战记录
人工智能·音视频