Spring Boot 3 + Qwen 3.5 最佳实践:从接口调用到 RAG 向量检索一站式开发

文章目录

    • 前言
    • [一、先整点废话:为什么非得是 Qwen 3.5?](#一、先整点废话:为什么非得是 Qwen 3.5?)
    • [二、环境准备:工欲善其事,先装好 Maven](#二、环境准备:工欲善其事,先装好 Maven)
    • [三、基础对话:先让 AI 说上话](#三、基础对话:先让 AI 说上话)
    • 四、流式输出:让文字像打字机一样蹦出来
    • [五、RAG 实战:给大模型装上"外接大脑"](#五、RAG 实战:给大模型装上"外接大脑")
      • [5.1 向量存储选型](#5.1 向量存储选型)
      • [5.2 文档入库流程](#5.2 文档入库流程)
      • [5.3 结合上下文的对话](#5.3 结合上下文的对话)
    • [六、函数调用:让 AI 能"动手"不只是"动嘴"](#六、函数调用:让 AI 能"动手"不只是"动嘴")
    • 七、生产环境踩坑实录
      • [7.1 超时设置](#7.1 超时设置)
      • [7.2 Token 计算](#7.2 Token 计算)
      • [7.3 并发控制](#7.3 并发控制)
    • 八、总结

无意间发现了一个CSDN大神的人工智能教程,忍不住分享一下给大家。很通俗易懂,重点是还非常风趣幽默,像看小说一样。床送门放这了👉 http://blog.csdn.net/jiangjunshow

前言

TL;DR

本文手把手教你用 Spring Boot 3 对接阿里最新发布的 Qwen 3.5 大模型,从最简单的"你问我答"聊到企业级 RAG 向量检索。全程代码可抄,踩坑点已标红,读完直接就能在生产环境跑起来。

一、先整点废话:为什么非得是 Qwen 3.5?

前阵子帮朋友公司做内部知识库,试了七八个模型,最后锚定了 Qwen 3.5。不是 GPT 用不起,而是这玩意儿在中文场景下真的有"母语优势"------理解"领导的意思"比老外模型准多了。

而且最关键的,阿里云那个 DashScope 平台对 Java 程序员特别友好,SDK 写得比某些开源项目还良心,Maven 依赖一拉,五分钟就能跑起来 Hello World。不像某些模型,光签名算法就能把你折腾到怀疑人生。

今天这篇就当你我的"结对编程",我把这段时间踩的坑、调的优,全倒出来。

二、环境准备:工欲善其事,先装好 Maven

先整一个 Spring Boot 3.2+ 的项目,别用 2.x 了,都 2026 年了,该跟 Jakarta EE 命名空间和解了。

xml 复制代码
    org.springframework.boot
    spring-boot-starter-parent
    3.2.5




    org.springframework.boot
    spring-boot-starter-web




    org.springframework.boot
    spring-boot-starter-data-redis

注意啊,dashscope-sdk-java 这包更新挺勤的,写代码的时候去 Maven Central 瞅一眼最新版,别复制粘贴了过时的版本号。

然后 application.yml 里塞上你的 Key:

yaml 复制代码
dashscope:
  api-key: ${DASHSCOPE_API_KEY:sk-xxxxxxxxxxxx}
  model: qwen-turbo-latest  # 默认用 turbo,便宜够用

这个 Key 去阿里云百炼平台申请,新用户送几百万 Token,够你开发阶段挥霍了。

三、基础对话:先让 AI 说上话

最简单的场景就是"用户发个消息,AI 回段文字"。这玩意儿看起来简单,但要是想写得优雅,得用 Spring 的异步特性。

先整一个配置类,把 DashScope 客户端注入容器:

java 复制代码
@Configuration
public class DashScopeConfig {
    @Value("${dashscope.api-key}")
    private String apiKey;

    @Bean
    public Generation generation() {
        return new Generation(apiKey);
    }
}

然后写个 Service,封装一下调用逻辑。这里有个坑:Qwen 3.5 的接口参数跟早期版本有变化,resultFormat 必须显式指定为 "message",不然返回的格式跟预期对不上。

java 复制代码
@Service
@Slf4j
public class QwenChatService {
    @Autowired
    private Generation generation;

    public String chat(String userMessage) {
        try {
            Message userMsg = Message.builder()
                    .role(Role.USER.getValue())
                    .content(userMessage)
                    .build();

            GenerationParam param = GenerationParam.builder()
                    .model("qwen-turbo-latest")  // 或 qwen-max-latest
                    .messages(Arrays.asList(userMsg))
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .build();

            GenerationResult result = generation.call(param);
            return result.getOutput().getChoices().get(0).getMessage().getContent();

        } catch (Exception e) {
            log.error("调用失败", e);
            return "服务开小差了,稍后再试~";
        }
    }
}

Controller 层就简单了:

java 复制代码
@RestController
@RequestMapping("/api/chat")
public class ChatController {
    @Autowired
    private QwenChatService chatService;

    @PostMapping
    public String chat(@RequestBody String message) {
        return chatService.chat(message);
    }
}

这时候你 Postman 一测,应该就能看到返回了。如果报错 InvalidApiKey,检查下你的 Key 是不是复制的时候带了空格------这坑我踩过三次。

四、流式输出:让文字像打字机一样蹦出来

上面那个是"憋大招"模式,等 AI 想完了才一次性吐给你。要是回答很长,用户盯着空白页面干等,体验跟断网了似的。

Qwen 3.5 支持 SSE(Server-Sent Events)流式输出,咱们配合 Spring 的 SseEmitter 整一个"打字机效果":

java 复制代码
@GetMapping("/stream")
public SseEmitter streamChat(@RequestParam String message) {
    SseEmitter emitter = new SseEmitter(120_000L); // 2分钟超时
    new Thread(() -> {
        try {
            Message userMsg = Message.builder()
                    .role(Role.USER.getValue())
                    .content(message)
                    .build();

            GenerationParam param = GenerationParam.builder()
                    .model("qwen-turbo-latest")
                    .messages(Arrays.asList(userMsg))
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .incrementalOutput(true)  // 关键参数:开启增量输出
                    .build();

            // 流式调用
            Flowable flowable = generation.streamCall(param);

            flowable.subscribe(
                result -> {
                    String content = result.getOutput().getChoices().get(0).getMessage().getContent();
                    if (content != null) {
                        emitter.send(SseEmitter.event().data(content));
                    }
                },
                error -> {
                    log.error("流式调用异常", error);
                    emitter.completeWithError(error);
                },
                () -> emitter.complete()
            );

        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    }).start();

    return emitter;
}

前端用 EventSource 接收,文字就会一个字一个字往外蹦,跟 ChatGPT 网页版那个感觉一模一样。注意 incrementalOutput(true) 这个参数,不开的话它还是会攒一块儿给你。

五、RAG 实战:给大模型装上"外接大脑"

基础对话玩明白了,整点硬核的------RAG(检索增强生成)。简单说就是:用户问问题的时候,咱们先去知识库里搜相关资料,把搜到的内容塞进 Prompt 里,让 AI 基于这些资料回答。

这相当于给模型装了个"外接硬盘",能解决两个问题:

  1. 模型不知道的私域知识(比如你们公司的内部文档)
  2. 胡编乱造(有了参考资料做约束,准确率飙升)

5.1 向量存储选型

RAG 核心是向量检索,得把文本转成向量(Embedding)存起来。这里我用 Redis Stack,因为大部分项目本来就有 Redis,不用再引入新组件。

首先得有个 Embedding 模型把文字转成向量。DashScope 提供了现成的文本嵌入模型 text-embedding-v3:

java 复制代码
@Service
public class EmbeddingService {
    @Autowired
    private Generation generation;

    public List embed(String text) throws Exception {
        TextEmbeddingParam param = TextEmbeddingParam.builder()
                .model(TextEmbeddingConst.V3)  // 用 v3 模型,效果比较好
                .text(text)
                .build();
        TextEmbeddingResult result = generation.textEmbedding(param);
        return result.getOutput().getEmbeddings().get(0).getEmbedding();
    }
}

5.2 文档入库流程

假设我们有一堆公司内部文档,要塞进向量库。流程是:读取文档 → 切分 chunk → 转向量 → 存 Redis。

java 复制代码
@Component
public class VectorStoreService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private EmbeddingService embeddingService;

    // 存文档
    public void storeDocument(String docId, String content) throws Exception {
        // 简单按 500 字切分,实际生产用语义切分效果更好
        List chunks = splitIntoChunks(content, 500);

        for (int i = 0; i < chunks.size(); i++) {
            String chunk = chunks.get(i);
            List vector = embeddingService.embed(chunk);

            // Redis JSON 格式存储
            String key = "doc:" + docId + ":chunk:" + i;
            Map data = new HashMap<>();
            data.put("content", chunk);
            data.put("vector", vector);
            data.put("docId", docId);

            redisTemplate.opsForValue().set(key, JSON.toJSONString(data));
        }
    }

    // 向量检索 - 这里用简单遍历,生产环境用 Redis Search 或专门向量数据库
    public List searchSimilar(String query, int topK) throws Exception {
        List queryVector = embeddingService.embed(query);

        // 扫描所有文档块(演示用,生产别这么干,性能爆炸)
        Set keys = redisTemplate.keys("doc:*:chunk:*");
        List scoredChunks = new ArrayList<>();

        for (String key : keys) {
            String json = redisTemplate.opsForValue().get(key);
            JSONObject obj = JSON.parseObject(json);
            List vector = obj.getJSONArray("vector").toJavaList(Float.class);

            // 计算余弦相似度
            double score = cosineSimilarity(queryVector, vector);
            scoredChunks.add(new ScoredChunk(obj.getString("content"), score));
        }

        // 取 TopK
        return scoredChunks.stream()
                .sorted(Comparator.comparing(ScoredChunk::getScore).reversed())
                .limit(topK)
                .map(ScoredChunk::getContent)
                .collect(Collectors.toList());
    }

    private double cosineSimilarity(List a, List b) {
        double dot = 0.0, normA = 0.0, normB = 0.0;
        for (int i = 0; i < a.size(); i++) {
            dot += a.get(i) * b.get(i);
            normA += Math.pow(a.get(i), 2);
            normB += Math.pow(b.get(i), 2);
        }
        return dot / (Math.sqrt(normA) * Math.sqrt(normB));
    }
}

5.3 结合上下文的对话

有了检索能力,对话 Service 要改一改,把搜到的资料塞进 Prompt:

java 复制代码
public String chatWithRag(String userMessage) {
    try {
        // 1. 先检索相关知识
        List contexts = vectorStoreService.searchSimilar(userMessage, 3);
        String contextStr = String.join("\n---\n", contexts);

        // 2. 构建增强 Prompt
        String systemPrompt = "你是公司智能助手。请基于以下参考资料回答用户问题:" +
                "\n\n参考资料:\n" + contextStr +
                "\n\n如果参考资料无法回答问题,请说'根据现有资料无法回答'。";

        Message systemMsg = Message.builder()
                .role(Role.SYSTEM.getValue())
                .content(systemPrompt)
                .build();

        Message userMsg = Message.builder()
                .role(Role.USER.getValue())
                .content(userMessage)
                .build();

        GenerationParam param = GenerationParam.builder()
                .model("qwen-max-latest")  // RAG 建议用 max 版本,逻辑能力更强
                .messages(Arrays.asList(systemMsg, userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();

        GenerationResult result = generation.call(param);
        return result.getOutput().getChoices().get(0).getMessage().getContent();

    } catch (Exception e) {
        log.error("RAG 调用失败", e);
        return "检索服务异常";
    }
}

这时候你问"咱们公司的年假制度是啥",它就能从你之前存的 HR 文档里找答案,而不是瞎编一套劳动法条文。

六、函数调用:让 AI 能"动手"不只是"动嘴"

Qwen 3.5 支持 Function Calling,就是让模型根据对话判断要不要调用你的 Java 方法。比如用户说"查一下订单 AAA123 的状态",AI 能自动识别出要调用 queryOrderStatus 方法,并把订单号传进去。

先定义一个工具类:

java 复制代码
public class OrderService {
    @FunctionDefinition(
        name = "queryOrderStatus",
        description = "查询订单状态",
        parameters = {
            @Parameter(name = "orderId", description = "订单编号", required = true, type = "string")
        }
    )
    public String queryOrderStatus(String orderId) {
        // 实际查数据库
        return "订单 " + orderId + " 状态:已发货,预计明天送达";
    }
}

然后对话时把工具列表传给模型:

java 复制代码
public String chatWithTools(String userMessage) {
    try {
        List functions = Arrays.asList(
            FunctionDefinition.builder()
                .name("queryOrderStatus")
                .description("查询订单状态")
                .parameters(JsonUtils.toJsonObject(OrderQueryParam.class))
                .build()
        );

        Message userMsg = Message.builder()
                .role(Role.USER.getValue())
                .content(userMessage)
                .build();

        GenerationParam param = GenerationParam.builder()
                .model("qwen-max-latest")
                .messages(Arrays.asList(userMsg))
                .tools(functions)
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();

        GenerationResult result = generation.call(param);
        Message message = result.getOutput().getChoices().get(0).getMessage();

        // 检查是否需要调用函数
        if (message.getToolCalls() != null && !message.getToolCalls().isEmpty()) {
            ToolCall toolCall = message.getToolCalls().get(0);
            String functionName = toolCall.getFunction().getName();
            String arguments = toolCall.getFunction().getArguments();

            // 执行本地方法
            if ("queryOrderStatus".equals(functionName)) {
                Map args = JSON.parseObject(arguments, Map.class);
                String orderId = args.get("orderId");
                String toolResult = orderService.queryOrderStatus(orderId);

                // 把结果再传给 AI,让它组织语言回复
                Message toolMsg = Message.builder()
                        .role(Role.TOOL.getValue())
                        .content(toolResult)
                        .build();

                // 第二次调用获取最终回复...
                return finalReply;
            }
        }

        return message.getContent();

    } catch (Exception e) {
        log.error("工具调用失败", e);
        return "处理异常";
    }
}

这玩意儿相当于给 AI 长了手,能操作你的业务系统。但注意权限控制,别让 AI 能随意调用敏感操作,比如"删除订单"这种得加白名单。

七、生产环境踩坑实录

7.1 超时设置

大模型推理有时候慢,特别是用 max 版本或者长文本。Spring Boot 默认超时可能不够,得改:

yaml 复制代码
spring:
  mvc:
    async:
      request-timeout: 120000  # 2分钟

或者用 WebClient 玩异步非阻塞。

7.2 Token 计算

Qwen 3.5 按 Token 计费,Prompt 太长会烧钱。RAG 的时候别塞太多参考资料,一般 Top3 最相关的 chunk 就够了,省得上下文窗口爆了。

7.3 并发控制

如果是对外服务,记得加限流。阿里云的接口有 QPS 限制,免费版一般 10-20 QPS,超了会报错。可以用 Guava RateLimiter 或者 Sentinel 挡一下。

java 复制代码
RateLimiter limiter = RateLimiter.create(10); // 每秒10个
public String chat(String message) {
    if (!limiter.tryAcquire()) {
        return "系统繁忙,请稍后再试";
    }
    // ... 调用逻辑
}

八、总结

整套玩下来,Spring Boot 3 对接 Qwen 3.5 其实就三板斧:

  1. 基础对话:SDK 直接调用,注意流式输出提升体验
  2. RAG 检索:向量数据库 + 文本嵌入,解决私域知识问题
  3. 函数调用:给 AI 开放业务接口,从聊天升级成智能助手

这套方案我已经在生产环境跑了两个月,稳定性没问题。Qwen 3.5 的中文理解能力确实能打,特别是处理一些本土化的表述(比如"这个需求很急"背后的优先级判断),比国外模型接地气。

代码都在上面了,对着抄基本不会出错。如果遇到诡异问题,先去 DashScope 控制台看日志,那儿的错误提示比 SDK 抛的异常详细十倍。

最后提醒一句,模型版本迭代快,过几个月可能又有新特性,关注下阿里云的更新公告,别守着旧版 SDK 错过新功能。就这样,散会。

相关推荐
kisshuan123962 小时前
[特殊字符] MangaLens:AI精准识别漫画气泡,对话内容一目了然
人工智能
电子科技圈2 小时前
从工具到平台:如何化解跨架构时代的工程开发和管理难题
人工智能·设计模式·架构·编辑器·软件工程·软件构建·设计规范
zhangshuang-peta2 小时前
加密MCP保险库:人工智能系统中安全凭证管理的关键
人工智能·安全·chatgpt·ai agent·mcp·peta
零雲2 小时前
java面试:Spring是如何解决循环依赖问题的
java·spring·面试
yuhaiqiang2 小时前
太牛了🐂,再也没有被AI 骗过,自从用了这个外挂 !必须装上
javascript·人工智能·后端
GISer_Jing2 小时前
Agent技术深度解析:LLM增强智能体架构与优化
前端·人工智能·架构·aigc
冬奇Lab2 小时前
一天一个开源项目(第48篇):Agent-Reach - 给 AI Agent 装上互联网能力,零 API 费用支持 Twitter、Reddit、YouTub
人工智能·开源·资讯
星爷AG I2 小时前
14-3 开环控制和闭环控制(AGI基础理论)
人工智能·agi
总有刁民想爱朕ha2 小时前
OpenClaw + 钉钉:打造企业级AI智能助手,让工作更高效
人工智能·钉钉·openclaw