Administrator
发布于 2025-05-16 / 1898 阅读
41

Spring AI Alibaba 与国内大模型的集成实践

公司要求数据不出境,得换模型

四月底法务下了个通知:客服对话数据不能出境,我们正在用的海外模型必须换掉。当时系统基于 Spring AI 写的,第一反应是"换个 base-url 应该就行",实际折腾了两周才稳定上线。

这篇记录接入国内模型的过程,主要是 Spring AI Alibaba 和 OpenAI 兼容层两条路线的对比,以及那些文档里不会写的坑。

两条路线

接入通义千问这类国内模型,有两种方式:

  • 路线一:Spring AI Alibaba。阿里官方维护的 Spring AI 扩展,提供 DashScopeChatModel 等实现,跟 Spring AI 的抽象(ChatClientVectorStoreToolCallback)深度集成;
  • 路线二:OpenAI 兼容层。大部分国内厂商都提供了 OpenAI 兼容的 endpoint,直接把 Spring AI 的 OpenAiChatModelbase-url 指过去。

我先试了路线二,因为听起来改动最小。实际配置:

spring:
  ai:
    openai:
      api-key: sk-xxxx
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
      chat:
        options:
          model: qwen-plus

改完确实能跑通普通对话。但往下走就遇到问题了:stream 返回的 chunk 里 usage 字段是 null,我们统计 token 的逻辑全废;部分模型的 function calling 返回格式跟 OpenAI 有细微差异,Spring AI 解析时偶尔抛异常;模型列表、计费规则这些得自己维护。

所以最后切到了路线一。

Spring AI Alibaba 的接法

版本上要注意对齐,我用的是 Spring AI 1.0.0-M6 配 Spring AI Alibaba 1.0.0-M6.1。这俩版本号必须匹配,混用会报各种莫名其妙的 NoSuchMethodError

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>1.0.0-M6.1</version>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-store-analyticaldb</artifactId>
    <version>1.0.0-M6.1</version>
</dependency>

业务逻辑代码基本不用改,因为我们本来就是面向 ChatClient 编程的:

@Service
public class QaService {

    private final ChatClient chatClient;

    public QaService(ChatModel chatModel, VectorStore vectorStore) {
        this.chatClient = ChatClient.builder(chatModel)
                .defaultSystem("""
                    你是客服助手,只根据提供的知识库内容回答。
                    如果知识库中没有相关信息,直接说"没有找到相关资料"。
                    """)
                .defaultAdvisors(
                    QuestionAnswerAdvisor.builder(vectorStore)
                        .searchRequest(SearchRequest.builder()
                            .topK(6)
                            .similarityThreshold(0.72)
                            .build())
                        .build())
                .build();
    }

    public Flux<String> stream(String question) {
        return chatClient.prompt().user(question).stream().content();
    }
}

这里有个点值得夸一下:因为用了 Spring AI 的抽象层,换模型的改动量确实很小,我们实际改的就是依赖、配置文件和模型名,业务代码一行没动。这大概是我们当初选 Spring AI 而不是直接调 SDK 的最大回报。

落地时的注意点

模型能力差异比想象的大

这是最需要提前确认的。同为"通义千问",不同型号的能力差异不小:

模型Function CallingJSON 模式上下文我们的用途
qwen-max支持支持32K复杂推理、意图识别
qwen-plus支持支持128K主力问答
qwen-turbo部分支持支持128K简单分类、摘要
qwen-vl-max不支持支持8K截图识别

我们踩的坑是:一开始图便宜全用 qwen-turbo,上了工具调用之后发现它在多工具选择时准确率明显下降(我们实测 74%,qwen-plus 是 93%)。后来改成按任务分级——需要工具调用的走 plus,简单分类走 turbo,成本只涨了 18%,准确率回到正常水平。

参数范围不一致

各家模型的参数取值范围不同,temperature 有的支持 0~1,有的支持 0~2。 OpenAI 的 SDK 会在客户端校验,超范围直接报错。我们有个配置是从 OpenAI 迁过来的,temperature: 1.2 直接调用失败了。

还有 top_ptemperature 同时设置的问题——大部分国内模型建议只设一个,两个都设行为不可预期。我们统一只设 temperature。

token 计费口径

这个必须自己核实。国内模型的 token 计算方式跟 OpenAI 不完全一样,中文的分词粒度不同,同样的文本算出来的 token 数差异不小。我们实测同一段中文客服对话:

  • OpenAI tokenizer 估算:1240 token
  • 通义千问实际计费:1587 token

差了 28%。如果你按 OpenAI 的口径做成本预估和上下文裁剪,会算错。解决办法是调用后用响应里的 usage 字段校正,我们用一周的实际数据反推了一个系数(1.27),上下文裁剪时乘上去。

限流规则

国内厂商的限流一般是 RPM + TPM 双维度,而且不同模型档位的配额独立。我们上线第二天就被限流了,因为压测脚本用的是 qwen-max,把当天的配额用光了,导致线上的 qwen-plus 也受影响(其实是账号级别的 TPM 上限)。

现在我们在网关层按模型维度单独限流,并且给压测单独开了个账号。

结构化输出要验证

Spring AI 的 BeanOutputConverter 在国内模型上能用,但可靠性不如 OpenAI 的 JSON mode。我们的做法是双保险:既用 converter,也在 prompt 里明确给出 JSON 示例,再在代码里做一次 Schema 校验。

Schema schema = JsonSchemaFactory.getInstance()
        .getSchema(qaResultSchema);
var errors = schema.validate(resultJson);
if (!errors.isEmpty()) {
    log.warn("schema validation failed: {}", errors);
    return fallback(resultJson);     // 尽力修复,不行就转人工
}

实测 3 万次调用里,Schema 校验失败 214 次(0.7%),多数出现在输出内容较长的时候(模型会在 JSON 后面追加解释文字)。我们的修复逻辑是截取出第一个完整的 JSON 对象。

向量模型和分析型数据库

检索这块我们用的是 AnalyticDB(阿里自研的向量检索),Spring AI Alibaba 提供了 AnalyticalDatabaseVectorStore。配置很简单:

spring:
  ai:
    vectorstore:
      analyticdb:
        collect-name: kb_customer_service
        dimensions: 768
        index-type: HNSW

用的是阿里自研的 text-embedding-v3(768 维),中文效果比我们之前用的多语言开源模型好不少。同一批 5000 条客服文档、50 个标注 query 的测试:

Embedding 模型召回@5MRR
paraphrase-multilingual(原用)0.740.52
text-embedding-v30.890.71

这个提升比换对话模型还明显。在 RAG 场景里,embedding 模型的选择往往比 LLM 的选择影响更大,这点很多人会搞反优先级。

成本对比

切换前后一个月的数据(同样的业务量,约 42 万次问答 + 15 万次质检):

项目切换前切换后
月度模型成本¥96,000¥31,000
平均响应 P992.8s2.1s
用户满意度4.1/54.0/5
数据合规存在出境风险合规

成本降了 68%,效果基本持平(满意度掉了 0.1,在误差范围内)。响应速度还快了一些,因为国内网络延迟低——这点在做流式输出时体验差异很明显。

几条建议

  • 优先用 Spring AI 的抽象层编程,不要直接依赖厂商 SDK。这次迁移我们业务代码零改动,全靠这层;
  • 用 Spring AI Alibaba 而不是 OpenAI 兼容层,除非你只用最基础的对话功能。兼容层在流式 usage、工具调用这些地方有坑;
  • 版本号要对齐,Spring AI 和 Spring AI Alibaba 的版本是配套的;
  • 按任务分级用模型,别一刀切。全用最贵的是浪费,全用最便宜的效果不达标;
  • token 计费口径自己核实,别按 OpenAI 的估算。

小结

接入国内模型在工程上没有太多技术含量,主要是细节多:模型能力差异、参数范围、限流规则、计费口径,每一项都得自己验证一遍,文档往往不全。

真正让我觉得值的是,当初选择面向 Spring AI 抽象编程而不是直接调 SDK 这个决定。整个迁移过程中,业务代码一行没改,改的全是配置和依赖。这种"可替换性"在 AI 领域特别重要——模型迭代太快了,谁也说不准半年后会不会又要换。

还有个观察:RAG 场景里 embedding 模型的重要性被严重低估了。我们这次最大的效果提升来自换 embedding 模型,而不是换对话模型。如果你的检索效果不好,先看看这一层。

参考