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;
}

}`

相关推荐
JaguarJack4 小时前
Clawedbot 完整对接飞书教程 手把手搭建你的专属 AI 助手
ai·clawdbot
大厂技术总监下海6 小时前
从“使用AI服务”到“拥有AI助手”:Clawdbot,你的个人AI基础设施
人工智能·ai·开源
带刺的坐椅7 小时前
论 AI Skills 分布式发展的必然性:从单体智能到“云端大脑”的跃迁
java·ai·llm·mcp·tool-call·skills
Dreams°1238 小时前
进阶实战:Wan2.2-T2V-A5B 实现可点击跳转的互动式教育视频
算法·microsoft·ai·音视频
嵌入式郑工9 小时前
如何用CLAUDECODE重塑嵌入式开发
嵌入式硬件·ai·ai编程
严同学正在努力9 小时前
DataAgent:企业级智能数据分析师,Text-to-SQL+Python 分析 + 自动出报告一站式搞定(开源项目)
python·sql·ai·开源·bigdata
DS随心转APP11 小时前
怎么导出deepseek聊天记录
人工智能·ai·chatgpt·deepseek·ds随心转
梁辰兴11 小时前
DeepSeek-OCR 2如何让AI像人类一样“看懂“复杂文档?
人工智能·ai·大模型·ocr·deepseek·梁辰兴·deepseek-ocr 2
阿杰学AI12 小时前
AI核心知识67——大语言模型之NTP (简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·aigc·ntp·机械学习