前言
随着大语言模型技术的发展,AI 能力逐渐从单一对话工具转变为可嵌入业务系统的智能服务能力。本项目基于 Spring Boot 与 Spring AI 框架,设计并实现了一套轻量化 AI 网关服务,用于统一管理不同业务场景下的大模型调用流程。
传统应用在接入大语言模型时,通常需要在前端或业务代码中直接调用模型接口,不同功能之间容易出现接口管理混乱、提示词难以维护、模型配置分散等问题。因此,本项目通过构建 AI Gateway 层,对 AI 能力进行统一封装,将业务场景识别、Prompt 模板管理、模型调用以及接口返回等流程进行模块化设计。
系统整体采用前后端分离架构,前端仅需要根据具体业务场景调用后端提供的 API 接口,并配置对应的大模型服务参数,即可使用相关 AI 功能。后端通过 Spring AI 提供的 ChatClient 和 ImageModel 实现与大语言模型和图片生成模型的交互,同时通过场景枚举和 Prompt 模板机制,对不同任务进行统一管理。
目前系统主要实现以下 AI 能力:
(1)文本生成能力
基于 ChatClient 调用大语言模型,实现智能问答、内容生成、文本分析等功能。系统根据不同业务场景加载对应提示词模板,将用户输入转换为结构化 Prompt 后发送至模型。
(2)古籍文本语言转换能力
针对中国古籍食谱等文本内容,通过预设 Prompt 模板引导大模型完成古文理解、现代语言转换以及内容解释,使传统文本能够以更加易理解的形式呈现。
(3)文本生成图片能力
基于 Spring AI ImageModel 接入图片生成模型,根据用户输入的描述生成对应图片结果,并支持返回图片 URL 或 Base64 数据,方便前端直接展示。
(4)流式文本输出能力
针对较长文本生成任务,系统提供 SSE 流式接口,使模型生成内容能够实时返回前端,提升用户交互体验。
需要说明的是,当前系统主要关注 AI 能力接入与业务调用流程设计,并未涉及 RAG(Retrieval-Augmented Generation,检索增强生成)相关技术,例如文档切片、向量数据库、知识库检索等模块。系统当前通过 Prompt Engineering(提示词工程)的方式约束模型输出,后续可以根据业务需求进一步扩展知识库检索能力,实现基于私有数据的增强生成。
本文将围绕该 AI 网关系统的核心代码结构展开介绍,按照AI 场景管理 → 配置管理 → Prompt 模板处理 → AI 服务调用 → 前端接口暴露的流程,对各模块功能和实现方式进行分析。
一、先理解:这套代码到底在做什么
可以把这套代码理解成一个“AI 中转站”。
前端不直接调用大模型,而是先向 Spring Boot 后端发送请求。
整体流程如下:
前端发送问题
↓
AiGatewayController 接收请求
↓
判断当前属于哪个 AI 场景
↓
AiGatewayService 处理业务
↓
AiPrivacyService 对敏感内容脱敏
↓
AiPromptTemplateService 加载提示词模板
↓
ChatClient 或 ImageModel 调用 AI 模型
↓
将文字或图片结果返回给前端
例如,当用户发送:
{ "scene": "ancient-text-translation", "prompt": "请翻译:学而时习之,不亦说乎?" }系统会判断这是“古文翻译”场景,然后找到对应的提示词模板,再调用大模型。
最终发送给大模型的内容可能类似:
你是一名专业的古文翻译老师。 请将下面的古文翻译为通俗易懂的现代汉语: 学而时习之,不亦说乎?这里的“你是一名专业的古文翻译老师”就来自提示词模板。
二、阅读 Java 代码前需要知道的几个概念
1. package 是什么
代码开头经常出现:
package com.example.springboot.ai;package表示当前类所在的包。
可以把“包”理解成文件夹,用来对 Java 类进行分类。
例如:
com.example.springboot ├── ai │ ├── AiScene.java │ └── AiPromptTemplateService.java ├── config │ └── AiGatewayProperties.java ├── controller │ └── AiGatewayController.java ├── service │ ├── AiGatewayService.java │ └── AiPrivacyService.java └── utils └── FileUploadUtils.java不同文件夹负责不同工作:
controller:接收前端请求;service:处理具体业务;config:读取配置文件;ai:保存 AI 相关定义;utils:保存通用工具类。
2. import 是什么
例如:
import org.springframework.stereotype.Service;import表示导入其他类。
因为Service并不是当前文件中定义的,所以需要通过import把它引入进来。
3. 注解是什么
代码中有很多以@开头的内容,例如:
@Service @RestController @PostMapping @ConfigurationProperties这些都叫作注解。
注解可以理解成给 Spring Boot 的“说明书”。
例如:
@Service public class AiGatewayService { }@Service是在告诉 Spring:
这个类是一个业务服务类,请帮我创建并管理它。
4. 构造方法和依赖注入
例如:
public AiGatewayService( ChatClient.Builder chatClientBuilder, AiGatewayProperties properties, AiPrivacyService privacyService) { this.properties = properties; this.privacyService = privacyService; }这段代码是构造方法。
当 Spring 创建AiGatewayService时,会自动把它需要的对象传进来。
这种方式叫作“依赖注入”。
可以简单理解为:
AiGatewayService 需要配置对象和隐私处理对象,Spring 会提前准备好,并自动交给它。
三、AiScene:定义系统支持哪些 AI 场景
代码位置:
package com.example.springboot.ai;核心代码:
public enum AiScene { ANCIENT_TEXT_TRANSLATION( "ancient-text-translation", false, "translator" ), CONSTITUTION_ANALYSIS( "constitution-analysis", false, "suggester" ), DIET_THERAPY_PLAN( "diet-therapy-plan", true, "projecter" ), CONSTITUTION_CHANGE_ANALYSIS( "constitution-change-analysis", true, "changeanalysis" ), DIET_THERAPY_QA( "diet-therapy-qa", true, "laoji" ), CULINARY_STEP_IMAGE( "culinary-step-image", false, "text-to-image", "image" ); }1. enum 是什么
enum是枚举类型。
它适合保存一组固定选项。
一个订单状态只能是:未付款、已付款、已发货、已完成,四种之一
这种固定状态就可以使用枚举。
这里的AiScene用来保存系统支持的 AI 场景。
例如:
ANCIENT_TEXT_TRANSLATION:古文翻译;CONSTITUTION_ANALYSIS:体质分析;DIET_THERAPY_PLAN:食疗方案;DIET_THERAPY_QA:食疗问答;CULINARY_STEP_IMAGE:烹饪步骤图片生成。
2. 每个参数代表什么
以这段代码为例:
ANCIENT_TEXT_TRANSLATION( "ancient-text-translation", false, "translator" )它包含三个部分。
第一个参数:templateKey
"ancient-text-translation"它是提示词模板的名称。
系统默认会寻找:
classpath:/ai/prompts/ancient-text-translation.st也就是在项目中定义的提示词模板:
src/main/resources/ai/prompts/ancient-text-translation.st第二个参数:streamingPreferred
这里的 true or false 表示该场景是否更推荐使用流式输出。
流式输出类似聊天机器人逐字显示内容。
普通返回:
等待一段时间后,一次性看到完整答案。
流式返回:
模型生成一点,前端显示一点。
true 表示更推荐流式输出。
需要注意,目前代码只是保存了这个配置,并没有根据它自动决定调用普通接口还是流式接口。
第三个参数:aliases
"translator"这是场景别名。
因此,下面几种写法都可能识别为古文翻译:
ANCIENT_TEXT_TRANSLATION ancient-text-translation translator这样做通常是为了兼容旧系统或者前端之前使用的名称。
3. from 方法的作用
public static AiScene from(String value)这个方法负责把前端传入的字符串转换成AiScene。
例如:
AiScene scene = AiScene.from("translator");最后得到:
AiScene.ANCIENT_TEXT_TRANSLATION如果用户没有传场景:
if (value == null || value.trim().isEmpty()) { throw new IllegalArgumentException("scene or agent is required"); }程序会抛出异常:
scene or agent is required如果传入不存在的场景:
unknown-scene程序会抛出:
Unknown AI scene: unknown-scene4. normalize 方法的作用
private static String normalize(String value) { return value.trim() .replace("-", "_") .toUpperCase(Locale.ROOT); }这个方法用于统一字符串格式。
例如:
ancient-text-translation经过处理后变成:
ANCIENT_TEXT_TRANSLATION处理步骤是:
trim():删除前后空格;replace("-", "_"):将横线替换为下划线;toUpperCase():转换成大写。
这样可以减少用户输入格式不同造成的问题。
四、AiGatewayProperties:读取配置文件
代码位置:
package com.example.springboot.config;核心注解:
@Data @Component @ConfigurationProperties(prefix = "ai.gateway") public class AiGatewayProperties { }1. @Component
@Component表示这个类由 Spring 管理。
其他类需要使用它时,Spring 可以自动注入。
2. @ConfigurationProperties
@ConfigurationProperties(prefix = "ai.gateway")表示读取配置文件中以ai.gateway开头的内容。
例如,可以在application.yml中配置:
ai: gateway: scenes: ancient-text-translation: template: classpath:/ai/prompts/ancient-text-translation.st constitution-analysis: template: classpath:/ai/prompts/constitution-analysis.st image: count: 1 size: 1024x1024 response-format: url step-delay-millis: 1000 audio: enabled: false model: "" voice: ""Spring 会自动把这些配置放入AiGatewayProperties对象。
3. @Data
@Data这是 Lombok 提供的注解。
它会自动生成:
get方法;set方法;toString方法;equals方法;hashCode方法。
例如代码中虽然没有手动写:
public ImageProperties getImage() { return image; }但因为有@Data,Lombok 会帮助生成。
4. scenes 配置
private Map<String, SceneProperties> scenes = new HashMap<>();它用于保存不同场景的配置。
例如:
scenes: ancient-text-translation: template: classpath:/ai/prompts/ancient-text-translation.st读取后可以理解成:
键:ancient-text-translation 值:对应的 SceneProperties 对象我们要将这个部分与先前的AiScene区分开来:
AiScene只能用于判断前端用的是哪个智能体,而这里则是确定使用模型的类型,运行配置等。
5. 图片配置
private ImageProperties image = new ImageProperties();默认值为:
private Integer count = 1; private String size = "2K"; private String responseFormat = "url"; private Long stepDelayMillis = 1000L;它们分别表示:
count:每次生成几张图片;size:图片尺寸;responseFormat:返回 URL 还是 Base64;stepDelayMillis:步骤间隔时间。
其中stepDelayMillis在当前展示的代码中还没有实际使用。
6. 音频配置
private AudioProperties audio = new AudioProperties();其中包括:
private boolean enabled = false; private String model = ""; private String voice = "";它们表示:
是否开启语音功能;
使用哪个语音模型;
使用哪个声音。
五、AiPromptTemplateService:加载和渲染提示词模板
代码位置:
package com.example.springboot.ai;这个类负责处理提示词模板。
1. 默认模板地址
private static final String DEFAULT_TEMPLATE_PATTERN = "classpath:/ai/prompts/%s.st";其中%s是占位符。
假设:
scene.getTemplateKey()返回:
ancient-text-translation最终模板路径就是:
classpath:/ai/prompts/ancient-text-translation.st2. render 方法
public String render( AiScene scene, String prompt, Map<String, Object> meta)它接收三个参数:
scene:当前 AI 场景;prompt:用户输入;meta:其他附加信息。
例如:
{ "scene": "diet-therapy-plan", "prompt": "请制定一份食疗方案", "meta": { "age": 30, "constitution": "湿热体质" } }其中:
prompt = 请制定一份食疗方案 age = 30 constitution = 湿热体质3. 模板变量
代码先创建一个变量集合:
Map<String, Object> variables = new HashMap<>();然后放入用户问题:
variables.put("prompt", prompt == null ? "" : prompt);如果存在meta:
variables.put("meta", meta); variables.putAll(meta);这意味着模板中既可以通过整体的meta使用数据,也可以直接使用具体字段。
一个模板文件可以写成:
你是一名食疗方案助手。 用户问题: {prompt} 用户年龄: {age} 用户体质: {constitution} 请根据以上信息生成一份清晰、合理的食疗建议。渲染后会变成:
你是一名食疗方案助手。 用户问题: 请制定一份食疗方案 用户年龄: 30 用户体质: 湿热体质 请根据以上信息生成一份清晰、合理的食疗建议。4. resolveTemplate 方法
private Resource resolveTemplate(AiScene scene)这个方法负责确定到底使用哪个模板文件。
它首先检查配置文件中有没有指定模板:
properties.getScenes().get(scene.getTemplateKey());如果配置文件中有模板路径,就使用配置中的路径。
如果没有,就使用默认路径:
String.format( DEFAULT_TEMPLATE_PATTERN, scene.getTemplateKey() )因此,这套设计支持两种方式。
第一种是默认约定:
ai/prompts/场景名称.st第二种是在配置文件中自定义模板路径。
六、AiGatewayService:AI 功能的核心业务层
代码位置:
package com.example.springboot.service;这个类是整套系统的核心。
它负责:
调用聊天模型;
调用流式聊天;
调用图片模型;
渲染提示词模板;
预留语音生成功能。
1. 主要依赖
private final ChatClient chatClient; private final ImageModel imageModel; private final AiGatewayProperties properties; private final AiPromptTemplateService promptTemplateService;它们分别负责:
ChatClient
调用聊天模型。
例如:
用户提问 → ChatClient → 大语言模型 → 返回文字ImageModel
调用图片生成模型。
例如:
图片描述 → ImageModel → 图片模型 → 返回图片AiGatewayProperties
读取系统配置。
AiPromptTemplateService
将用户问题填入提示词模板。
2. 构造方法
public AiGatewayService( ChatClient.Builder chatClientBuilder, ObjectProvider<ImageModel> imageModelProvider, AiGatewayProperties properties, AiPromptTemplateService promptTemplateService)Spring 会自动注入这些依赖。
其中:
this.chatClient = chatClientBuilder.build();表示通过 ChatClient.Builder 创建 ChatClient 对象,
后续用于调用大语言模型。
this.imageModel = imageModelProvider.getIfAvailable();表示尝试获取图片生成模型。
由于 ImageModel 并不是所有环境都必须配置,因此采用可选注入方式。
如果系统没有配置图片模型,则 imageModel 为 null,
调用图片生成接口时会进行检查。
即使项目暂时没有图片生成功能,文字聊天功能仍然可以正常启动。
3. 普通聊天 chat
public String chat( AiScene scene, String prompt, Map<String, Object> meta)核心代码:
return chatClient.prompt() .user(buildPrompt(scene, prompt, meta)) .call() .content();可以分成四步理解。
第一步:创建一次 AI 请求
chatClient.prompt()第二步:设置用户提示词
.user(buildPrompt(scene, prompt, meta))buildPrompt会完成模板渲染。
第三步:调用模型
.call()这是一种普通的同步调用。
程序会等待模型生成完毕。
第四步:获取文字内容
.content()最终返回模型生成的文字。
4. 流式聊天 stream
public Flux<String> stream( AiScene scene, String prompt, Map<String, Object> meta)核心代码:
return chatClient.prompt() .user(buildPrompt(scene, prompt, meta)) .stream() .content() .concatWithValues("[DONE]");它与普通聊天的主要区别是:
普通聊天使用:
.call()流式聊天使用:
.stream()流式返回的类型是:
Flux<String>Flux可以理解成一个不断产生数据的管道。
例如模型正在生成:
春 季 适 合 食 用 ……后端可以一段一段发送给前端,而不需要等待完整答案生成。
最后添加:
.concatWithValues("[DONE]")前端看到这个标记,就知道本次回答结束了。
5. 图片生成 generateImages
public List<String> generateImages( AiScene scene, String prompt, Map<String, Object> meta)首先判断图片模型是否存在:
if (imageModel == null) { throw new IllegalStateException( "Spring AI ImageModel is not configured" ); }如果没有配置图片模型,就直接报错。
然后创建图片参数:
OpenAiImageOptions options = OpenAiImageOptions.builder() .N(properties.getImage().getCount()) .responseFormat(properties.getImage().getResponseFormat()) .build();其中:
.N(...)表示生成图片数量。
.responseFormat(...)表示图片返回格式。
接着设置尺寸:
options.setSize(properties.getImage().getSize());然后调用图片模型:
ImageResponse response = imageModel.call( new ImagePrompt( buildPrompt(scene, prompt, meta), options ) );最后从模型响应中取出图片。
图片可能有两种形式:
图片 URL Base64 图片数据6. toImageValue 方法
private String toImageValue(Image image)如果图片模型返回 URL:
if (image.getUrl() != null && !image.getUrl().isBlank()) { return image.getUrl(); }就直接返回 URL。
如果模型返回 Base64:
return "data:image/png;base64," + image.getB64Json();就给 Base64 内容加上浏览器可以识别的前缀。
前端可以直接这样显示:
<img src="data:image/png;base64,……">7. buildPrompt 方法
private String buildPrompt( AiScene scene, String prompt, Map<String, Object> meta)该方法负责将用户输入转换为完整 Prompt。
return promptTemplateService.render( scene, cleanPrompt, sanitizeMeta(meta) );然后,将处理后的内容放入提示词模板。
8. 语音生成功能
public byte[] synthesizeSpeech( String text, Map<String, Object> meta)当前代码只判断语音功能是否开启:
if (!properties.getAudio().isEnabled()) { throw new UnsupportedOperationException( "AI speech is not enabled" ); }后面直接抛出:
throw new UnsupportedOperationException( "AI speech gateway is not implemented yet" );说明语音配置已经预留,但真正的调用代码还没有完成。
七、AiGatewayController:接收前端请求
代码位置:
package com.example.springboot.controller;控制器相当于系统的“入口”。
前端发送的 HTTP 请求,会先进入这个类。
1. 类上的注解
@Tag( name = "AiGatewayController", description = "Spring AI gateway APIs" )这是 Swagger注解,用于生成接口文档。
@CrossOrigin表示允许跨域请求。
例如:
前端:http://localhost:5173 后端:http://localhost:8080两个地址端口不同,浏览器会认为它们来自不同来源。
@CrossOrigin可以允许前端访问后端接口。
@RestController表示这是一个 REST 接口控制器。
它返回的对象会自动转换为 JSON 或文字。
@RequestMapping("/api")表示当前控制器中所有接口都以/api开头。
2. 普通聊天接口
@PostMapping({ "/ai/chat", "/ai/chat/structured" }) public String chatStructured( @RequestBody ChatPayload payload)这个方法支持两个地址:
POST /api/ai/chat POST /api/ai/chat/structured@RequestBody表示接收前端提交的 JSON。
例如:
{ "scene": "ancient-text-translation", "prompt": "请翻译这段古文", "meta": {} }然后调用:
aiGatewayService.chat( resolveScene(payload), resolvePrompt(payload), payload.getMeta() );3. 流式聊天接口
@PostMapping( value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE )接口地址是:
POST /api/ai/chat/stream其中:
produces = MediaType.TEXT_EVENT_STREAM_VALUE表示使用 SSE 流式响应。
SSE 的全称是:
Server-Sent Events可以理解为服务器不断向浏览器推送文字。
4. 图片生成接口
@PostMapping("/ai/image")接口地址:
POST /api/ai/image代码调用:
List<String> images = aiGatewayService.generateImages( scene, resolvePrompt(payload), payload.getMeta() );然后返回:
result.put("scene", scene.name()); result.put("images", images);最终 JSON 可能是:
{ "scene": "CULINARY_STEP_IMAGE", "images": [ "https://example.com/image1.png" ] }5. 配置状态接口
@GetMapping("/ai/config/status")接口地址:
GET /api/ai/config/status这个接口不会真正调用 AI。
它用于查看系统配置是否完整。
例如返回:
{ "provider": "spring-ai-openai-compatible", "baseUrlConfigured": true, "apiKeyConfigured": true, "apiKeyLength": 32, "chatModelConfigured": true, "scenes": [] }需要注意,它只返回 API Key 是否存在和长度,没有返回真正的 API Key。
这比直接输出密钥安全得多。
不过在正式生产环境中,这类配置状态接口仍然建议限制访问权限。
6. resolveScene 方法
private AiScene resolveScene(ChatPayload payload)它用于获取场景。
首先检查请求是否为空:
if (payload == null)然后优先读取:
payload.getScene()如果没有scene,再读取:
payload.getAgent()这说明代码同时兼容两种字段:
{ "scene": "translator" }或者:
{ "agent": "translator" }最后通过:
AiScene.from(scene)转换成枚举。
如果转换失败,返回 HTTP 400 错误。
7. resolvePrompt 方法
private String resolvePrompt(ChatPayload payload)它负责检查用户问题是否存在。
如果为空,会返回:
HTTP 400 prompt is required这样可以防止空问题被发送给大模型。
8. ChatPayload 是什么
提供的代码中没有展示ChatPayload类,但根据调用方式,可以推测它至少包含以下字段:
public class ChatPayload { private String scene; private String agent; private String prompt; private Map<String, Object> meta; // getter 和 setter }如果使用 Lombok,可以写成:
@Data public class ChatPayload { private String scene; private String agent; private String prompt; private Map<String, Object> meta; }它的作用就是接收前端提交的数据。
八、FileUploadUtils:文件上传路径工具
代码位置:
package com.example.springboot.utils;这是一个工具类。
public final class FileUploadUtilsfinal表示这个类不能被继承。
构造方法:
private FileUploadUtils() { }被设置为private,表示外部不能创建这个类的对象。
因为里面的方法都是静态方法,所以不需要:
new FileUploadUtils()可以直接调用:
FileUploadUtils.uploadRoot("uploads");1. uploadRoot 方法
public static Path uploadRoot(String uploadDir)它用于获取文件上传根目录。
如果没有配置目录:
uploadDir == null || uploadDir.trim().isEmpty()就使用默认目录:
uploads然后:
Paths.get(configured) .toAbsolutePath() .normalize();分别表示:
Paths.get():将字符串转换成路径;toAbsolutePath():转换成绝对路径;normalize():清理路径中的多余部分。
例如:
./uploads/../uploads/images规范化后可能变为:
项目路径/uploads/images2. uploadSubdir 方法
public static Path uploadSubdir( String uploadDir, String subdir)它用于获取上传目录下的子目录。
例如:
FileUploadUtils.uploadSubdir( "uploads", "images" );结果类似:
项目目录/uploads/images这里还需要注意一个安全问题。
如果subdir直接来自用户输入,用户可能提交:
../../other-folder即使调用了normalize(),最终路径仍可能跳出上传根目录。
更安全的写法需要额外检查:
Path root = uploadRoot(uploadDir); Path target = root.resolve(subdir).normalize(); if (!target.startsWith(root)) { throw new IllegalArgumentException( "Invalid upload path" ); } return target;九、总结
这组代码并不是简单地“调用一次 AI 接口”,而是搭建了一个比较清晰的 AI 网关结构。
它完成了以下职责:
AiScene 统一管理 AI 场景 AiGatewayProperties 统一管理系统配置 AiPromptTemplateService 根据业务场景生成完整提示词 AiGatewayService 统一调用聊天、流式和图片模型 AiGatewayController 向前端提供 HTTP 接口 FileUploadUtils 处理文件上传路径作为零基础学习者,不需要一开始就理解每一行代码。
更推荐按照下面的顺序理解:
先看请求从哪里进入 ↓ 再看 Controller 调用了谁 ↓ 再看 Service 做了哪些处理 ↓ 再看提示词怎样生成 ↓ 最后理解配置、枚举和工具类只要记住一条主线,这套代码就会容易很多:
接收请求 → 判断场景 → 检查参数 → 渲染提示词 → 调用 AI → 返回结果这就是这套 Spring Boot AI 网关代码最核心的执行过程。
当前系统的核心目标是完成 AI 能力的工程化接入,而不是构建知识增强型 AI 系统,因此主要关注模型调用、Prompt 管理和接口封装。
学习使用,内容仅供参考