以下知识点接通的全部是阿里百炼平台,可能部分是springai alibaba专属功能
SpringAI Alibaba 与 Spring AI 的区别
- Spring AI 是 Spring 团队做的 AI 开发抽象层,提供统一的编程模型/接口,
例如 ChatModel、ChatClient、EmbeddingModel、VectorStore、函数调用/工具调用等
特点:你用同一套 API 写业务代码,底层换不同大模型厂商时,尽量只改配置/依赖。 - Spring AI Alibaba 在 Spring AI 的抽象之上,对阿里系模型与平台能力做适配与 Starter 自动装配(比如 DashScope/百炼、通义系列等)
特点:让你在 Spring AI 的统一 API 下,更顺滑地用阿里云/百炼生态。
结构化输出
//结构化输出,你可以要求大模型按照你想要的数据结构返回数据
@RestController
public class Lesson01EntityReturnController {
// ChatClient:Spring AI 推荐的"高层对话客户端"(比 ChatModel 更易用)
private final ChatClient chatClient;
// 构造器注入:Spring 推荐做法(避免字段注入)
public Lesson01EntityReturnController(ChatClient.Builder builder) {
// builder.build():构建一个 ChatClient 实例(底层由你当前 starter 决定是 DashScope/OpenAI/Ollama)
this.chatClient = builder.build();
}
// GET 接口:演示 entity(Class) ------ 把模型输出映射成 Java 实体
@GetMapping("/lesson/01/entity")
public ActorFilms entity(
// actor:从 URL 参数读取,例如 /lesson/01/entity?actor=Tom%20Hanks
@RequestParam(defaultValue = "Tom Hanks") String actor
) {
// prompt():开始构造一次请求(提示词/消息)
// user(...):设置"用户消息"(User role)
// call():同步调用(等待模型生成完整结果)
// entity(ActorFilms.class):把结果按 JSON 结构映射成 ActorFilms
return this.chatClient.prompt()
.user("请输出 JSON,字段为 actor 和 movies(数组)。actor=" + actor)
.call()
.entity(ActorFilms.class);
}
// GET 接口:演示 entity(ParameterizedTypeReference) ------ 返回泛型实体(例如 List<ActorFilms>)
@GetMapping("/lesson/01/entity/list")
public List<ActorFilms> entityList() {
// ParameterizedTypeReference:解决 Java 的"泛型擦除"问题
// 这样 Spring AI 才知道你的目标类型是 List<ActorFilms>,而不是一个普通的 List,
//如果用.entity(List.class),框架不知道List里面是什么类型,ParameterizedTypeReference:解决就解决了这个问题
ParameterizedTypeReference<List<ActorFilms>> type = new ParameterizedTypeReference<>() {};
return this.chatClient.prompt()
.user("请输出 JSON 数组,每个元素包含 actor 和 movies。随机给 3 个演员。")
.call()
.entity(type);
}
// record:Java 16+ 的数据类语法糖,很适合做 DTO,类似于final class。
//并且替代@data的getxxx功能,但是没有set,比如这里可以直接这样用
//ActorFilms af = new ActorFilms("Tom", List.of("A", "B"));
//String name = af.actor();注意:不是 getActor(),而是 actor()
//List<String> ms = af.movies();
// 要点:字段名要尽量与模型输出 JSON 字段一致(actor/movies)
public record ActorFilms(
// actor:演员名字
String actor,
// movies:电影名称列表
List<String> movies
) {
}
}
对话存储
存到内存中
//conversationStore存储回话记忆,实际上就存在内存中,并指定最近几条存,并不是存所有的
@RestController
public class Lesson04MessageWindowMemoryController {
// ChatClient:调用模型
private final ChatClient chatClient;
// 复用 Lesson03 的内存仓库:把"窗口记忆"作为一种读取策略
private final Lesson03InMemoryConversationStore conversationStore;
public Lesson04MessageWindowMemoryController(ChatClient.Builder builder,
Lesson03InMemoryConversationStore conversationStore) {
this.chatClient = builder.build();
this.conversationStore = conversationStore;
}
// Lesson04:窗口记忆(只带最近 N 条历史),防止上下文无限增长
// 访问:
// 1) GET /lesson/04/memory/window?cid=001&msg=我叫张三&win=6
// 2) GET /lesson/04/memory/window?cid=001&msg=我叫什么&win=6
@GetMapping("/lesson/04/memory/window")
public String windowMemory(
// cid:会话ID
@RequestParam("cid") @NonNull String cid,
// msg:当前用户消息
@RequestParam("msg") @NonNull String msg,
// win:窗口大小(可选),默认 8 条
@RequestParam(value = "win", defaultValue = "8") int windowSize
) {
// 1) 先写入用户消息
conversationStore.append(cid, "user", msg);
// 2) 只取最近 windowSize 条消息(窗口记忆)
String historyText = conversationStore.formatWindowHistoryForPrompt(cid, windowSize);
// 3) 把窗口历史放进 system(让模型"看得到最近的上下文")
String system = "你是一个中文助手。以下是最近 " + windowSize + " 条对话:\n" + historyText;
// 4) 调用模型
String answer = this.chatClient.prompt()
.system(system)
.user(msg)
.call()
.content();
// 5) 写入助手回复
conversationStore.append(cid, "assistant", answer);
return answer;
}
}
//用 Map 存"多个会话"的消息
@Component
public class Lesson03InMemoryConversationStore {
// 用 Map 存"多个会话"的消息:key=cid(会话ID),value=该会话的消息列表
private final Map<String, List<ChatLine>> store = new ConcurrentHashMap<>();
// 追加一条对话消息到某个会话里
public void append(String conversationId, String role, String content) {
// computeIfAbsent:如果该 cid 第一次出现,就创建一个新的 ArrayList 作为消息容器
// 说明:教学示例用 ArrayList 即可;并发极高场景建议加锁或用并发集合
List<ChatLine> lines = store.computeIfAbsent(conversationId, id -> new ArrayList<>());
// 追加一条消息(带时间戳),方便你观察顺序
lines.add(new ChatLine(role, content, Instant.now()));
}
// 把整个会话历史格式化成一段可直接塞进 Prompt 的文本
public String formatHistoryForPrompt(String conversationId) {
// 取出会话消息列表
List<ChatLine> lines = store.get(conversationId);
// 没有历史时,返回空字符串(避免 null)
if (lines == null || lines.isEmpty()) {
return "";
}
// 将消息按时间排序(理论上 add 顺序就是时间顺序,这里为了教学更严谨)
lines.sort(Comparator.comparing(ChatLine::time));
// 把每一条消息拼成一行:role: content
StringBuilder sb = new StringBuilder();
for (ChatLine line : lines) {
sb.append(line.role())
.append(": ")
.append(line.content())
.append("\n");
}
return sb.toString();
}
// 只取最近 windowSize 条历史,用于"窗口记忆"(控制 token)
public String formatWindowHistoryForPrompt(String conversationId, int windowSize) {
// 取出会话消息列表
List<ChatLine> lines = store.get(conversationId);
// 没有历史时,返回空字符串
if (lines == null || lines.isEmpty()) {
return "";
}
// 如果窗口大小不合法,做一次兜底
if (windowSize <= 0) {
windowSize = 1;
}
// 计算要截取的起始位置:只取最后 windowSize 条
int fromIndex = Math.max(lines.size() - windowSize, 0);
// subList:截取最近 windowSize 条(注意:subList 是视图,读即可)
List<ChatLine> window = lines.subList(fromIndex, lines.size());
// 拼接成可放入 Prompt 的文本
StringBuilder sb = new StringBuilder();
for (ChatLine line : window) {
sb.append(line.role())
.append(": ")
.append(line.content())
.append("\n");
}
return sb.toString();
}
// 这里用 record 表示"一条消息记录"
public record ChatLine(
String role,
String content,
Instant time
) {
}
}
存到redis中
@Component
public class Lesson05RedisConversationStore {
// StringRedisTemplate:最简单的 Redis 操作方式(key/value 都是 String)
private final StringRedisTemplate redis;
public Lesson05RedisConversationStore(StringRedisTemplate redis) {
this.redis = redis;
}
// 统一 Redis key 的格式,避免冲突
@NonNull
private String key(@NonNull String conversationId) {
return "lesson05:chat:memory:" + conversationId;
}
// 追加一条消息到 Redis(LPUSH)
public void append(@NonNull String conversationId, @NonNull String role, String content) {
// 教学简化:用"role|content"保存一条消息
// 生产建议:用 JSON + Jackson 序列化,更严谨
String safeContent = content == null ? "" : content;
String line = role + "|" + escape(safeContent);
String redisKey = key(conversationId);
// LPUSH:把新消息插到列表头(最新在最前面)
redis.opsForList().leftPush(redisKey, line);
// 设置过期时间:避免 Redis 里无限增长(这里示例 7 天)
redis.expire(redisKey, 7, TimeUnit.DAYS);
}
// 读取最近 windowSize 条历史
public List<String> latest(@NonNull String conversationId, int windowSize) {
// 防御:窗口大小不合法时,至少取 1 条,避免 range 的 end 为 -1
if (windowSize <= 0) {
windowSize = 1;
}
// LRANGE 0..windowSize-1:取出最近 windowSize 条(因为我们用 LPUSH)
List<String> lines = redis.opsForList().range(key(conversationId), 0, windowSize - 1);
// range(...) 可能返回 null(取不到 key 或底层实现的空返回),这里统一返回空列表
return lines == null ? Collections.emptyList() : lines;
}
// 把历史拼成可放进 Prompt 的文本
public String formatLatestForPrompt(@NonNull String conversationId, int windowSize) {
// 取出最近 windowSize 条(注意:返回顺序是"新->旧")
List<String> lines = latest(conversationId, windowSize);
// 没有数据时返回空字符串
if (lines.isEmpty()) {
return "";
}
// 拼接成多行文本(这里保持"新->旧"的顺序,便于你观察)
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line).append("\n");
}
return sb.toString();
}
// 简单转义:避免内容里包含换行/分隔符影响显示(教学用,不追求完美)
private String escape(String s) {
return s.replace("\n", "\\n").replace("|", "\\|");
}
}
//利用redis存储对话记忆
@RestController
public class Lesson05RedisMemoryController {
private final ChatClient chatClient;
private final Lesson05RedisConversationStore redisStore;
public Lesson05RedisMemoryController(ChatClient.Builder builder,
Lesson05RedisConversationStore redisStore) {
this.chatClient = builder.build();
this.redisStore = redisStore;
}
// Lesson05:Redis 持久化记忆(应用重启不丢)
// 访问:
// 1) GET /lesson/05/memory/redis?cid=001&msg=我叫张三&win=10
// 2) GET /lesson/05/memory/redis?cid=001&msg=我叫什么&win=10
@GetMapping("/lesson/05/memory/redis")
public String redisMemory(
@RequestParam("cid") @NonNull String cid,
@RequestParam("msg") @NonNull String msg,
@RequestParam(value = "win", defaultValue = "10") int windowSize
) {
// 1) 把用户消息写入 Redis
redisStore.append(cid, "user", msg);
// 2) 从 Redis 取最近 N 条历史
String history = redisStore.formatLatestForPrompt(cid, windowSize);
// 3) 把历史拼入 system(作为上下文)
String system = "你是一个中文助手。以下是该会话的 Redis 历史(最新在前):\n" + history;
// 4) 调用模型
String answer = this.chatClient.prompt()
.system(system)
.user(msg)
.call()
.content();
// content() 在部分实现/场景下可能返回 null,这里做一次兜底,避免 Redis 写入时报空安全告警
String safeAnswer = answer == null ? "" : answer;
// 5) 把助手回复写回 Redis
redisStore.append(cid, "assistant", safeAnswer);
return safeAnswer;
}
}
文生图、文生语音功能
此功能更加注重当前你使用的模型有没有这个功能
//使用ImageModel调用文生图的功能
@RestController
public class Lesson07ImageModelController {
// ObjectProvider:可选依赖(有 Bean 就拿,没有就返回 null),避免因为没配置文生图导致项目启动失败
private final ObjectProvider<ImageModel> imageModelProvider;
public Lesson07ImageModelController(ObjectProvider<ImageModel> imageModelProvider) {
this.imageModelProvider = imageModelProvider;
}
// Lesson07:文生图(Text -> Image)
// 访问:GET /lesson/07/image?prompt=一只戴墨镜的猫
@GetMapping("/lesson/07/image")
public Map<String, Object> generate(@RequestParam(value = "prompt", defaultValue = "一只戴墨镜的猫") String prompt) {
ImageModel imageModel = imageModelProvider.getIfAvailable();
// 如果你当前 starter 没启用文生图,这里会提示你去启用对应能力
if (imageModel == null) {
Map<String, Object> r = new LinkedHashMap<>();
r.put("enabled", false);
r.put("message", "当前项目没有注入 ImageModel(可能未启用文生图能力)。");
return r;
}
// ImagePrompt:文生图请求对象(最简构造:直接传 prompt 文本)
ImagePrompt imagePrompt = new ImagePrompt(prompt);
// call(...):同步生成图片
ImageResponse response = imageModel.call(imagePrompt);
// getResult():取第一张生成的图片(也可以用 getResults() 取全部)
ImageGeneration generation = response.getResult();
// getOutput():取图片输出对象(包含 url 或 base64 等信息)
Image image = generation.getOutput();
// 不同 provider 返回可能是 url 或 base64,这里都返回,哪个有值用哪个
Map<String, Object> r = new LinkedHashMap<>();
r.put("enabled", true);
r.put("prompt", prompt);
r.put("url", image.getUrl());
r.put("base64", image.getB64Json());
return r;
}
}
文生语音
`// 调用文生语音功能
@RestController
public class Lesson08AudioController {
// TTS:文本 -> 语音
private final ObjectProvider<TextToSpeechModel> ttsProvider;
// ASR:语音 -> 文本(转写)
private final ObjectProvider<TranscriptionModel> transcriptionProvider;
public Lesson08AudioController(ObjectProvider<TextToSpeechModel> ttsProvider,
ObjectProvider<TranscriptionModel> transcriptionProvider) {
this.ttsProvider = ttsProvider;
this.transcriptionProvider = transcriptionProvider;
}
// Lesson08:查看音频能力是否已启用
// 访问:GET /lesson/08/audio/status
@GetMapping("/lesson/08/audio/status")
public Map<String, Object> status() {
Map<String, Object> r = new LinkedHashMap<>();
r.put("ttsEnabled", ttsProvider.getIfAvailable() != null);
r.put("transcriptionEnabled", transcriptionProvider.getIfAvailable() != null);
r.put("tip", "如果为 false,检查 application.yml 里是否排除了 DashScopeAudio*AutoConfiguration,或是否未引入/未配置对应 starter。 ");
return r;
}
// Lesson08:TTS 文生音(Text -> Speech)
// 访问:GET /lesson/08/tts?text=你好
@GetMapping(value = "/lesson/08/tts", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<?> tts(@RequestParam(value = "text", defaultValue = "你好,我是Spring AI") String text) {
TextToSpeechModel model = ttsProvider.getIfAvailable();
if (model == null) {
byte[] body = "TextToSpeechModel 未启用。".getBytes(StandardCharsets.UTF_8);
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED)
.contentType(MediaType.TEXT_PLAIN)
.body(body);
}
// call(String):最简用法,直接返回音频 bytes(具体格式由 provider 决定)
byte[] audioBytes = model.call(text);
if (audioBytes == null) {
audioBytes = new byte[0];
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=tts.bin")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(audioBytes);
}
// Lesson08:ASR 语音转文字(Speech -> Text)
// 访问:POST /lesson/08/transcribe (form-data: file=@your-audio-file)
@PostMapping("/lesson/08/transcribe")
public Map<String, Object> transcribe(@RequestPart("file") MultipartFile file) throws IOException {
TranscriptionModel model = transcriptionProvider.getIfAvailable();
if (model == null) {
Map<String, Object> r = new LinkedHashMap<>();
r.put("enabled", false);
r.put("message", "TranscriptionModel 未启用。" );
return r;
}
// MultipartFile -> Resource:把上传的音频文件转成 Spring Resource
Resource audio = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
// transcribe(resource):便捷方法,直接得到转写文本
String text = model.transcribe(audio);
Map<String, Object> r = new LinkedHashMap<>();
r.put("enabled", true);
r.put("filename", file.getOriginalFilename());
r.put("text", text);
return r;
}
}`