AI应用(8)- 实战演练之SpringAI架构知识点

以下知识点接通的全部是阿里百炼平台,可能部分是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;
}

}`

相关推荐
美酒没故事°1 天前
Open WebUI安装指南。搭建自己的自托管 AI 平台
人工智能·windows·ai
鸿乃江边鸟1 天前
Nanobot 从onboard启动命令来看个人助理Agent的实现
人工智能·ai
本旺1 天前
【Openclaw 】完美解决 Codex 认证失败
ai·codex·openclaw·小龙虾·gpt5.4
张張4081 天前
(域格)环境搭建和编译
c语言·开发语言·python·ai
乐鑫科技 Espressif1 天前
使用 MCP 服务器,把乐鑫文档接入 AI 工作流
人工智能·ai·esp32·乐鑫科技
语戚1 天前
Stable Diffusion 入门:架构、空间与生成流程概览
人工智能·ai·stable diffusion·aigc·模型
俊哥V1 天前
每日 AI 研究简报 · 2026-04-08
人工智能·ai
rrrjqy1 天前
什么是RAG?
ai
Flittly1 天前
【SpringAIAlibaba新手村系列】(15)MCP Client 调用本地服务
java·笔记·spring·ai·springboot
Flittly1 天前
【SpringAIAlibaba新手村系列】(14)MCP 本地服务与工具集成
java·spring boot·笔记·spring·ai