公司要求数据不出境,得换模型
四月底法务下了个通知:客服对话数据不能出境,我们正在用的海外模型必须换掉。当时系统基于 Spring AI 写的,第一反应是"换个 base-url 应该就行",实际折腾了两周才稳定上线。
这篇记录接入国内模型的过程,主要是 Spring AI Alibaba 和 OpenAI 兼容层两条路线的对比,以及那些文档里不会写的坑。
两条路线
接入通义千问这类国内模型,有两种方式:
- 路线一:Spring AI Alibaba。阿里官方维护的 Spring AI 扩展,提供
DashScopeChatModel等实现,跟 Spring AI 的抽象(ChatClient、VectorStore、ToolCallback)深度集成; - 路线二:OpenAI 兼容层。大部分国内厂商都提供了 OpenAI 兼容的 endpoint,直接把 Spring AI 的
OpenAiChatModel的base-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 Calling | JSON 模式 | 上下文 | 我们的用途 |
|---|---|---|---|---|
| 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_p 和 temperature 同时设置的问题——大部分国内模型建议只设一个,两个都设行为不可预期。我们统一只设 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 模型 | 召回@5 | MRR |
|---|---|---|
| paraphrase-multilingual(原用) | 0.74 | 0.52 |
| text-embedding-v3 | 0.89 | 0.71 |
这个提升比换对话模型还明显。在 RAG 场景里,embedding 模型的选择往往比 LLM 的选择影响更大,这点很多人会搞反优先级。
成本对比
切换前后一个月的数据(同样的业务量,约 42 万次问答 + 15 万次质检):
| 项目 | 切换前 | 切换后 |
|---|---|---|
| 月度模型成本 | ¥96,000 | ¥31,000 |
| 平均响应 P99 | 2.8s | 2.1s |
| 用户满意度 | 4.1/5 | 4.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 模型,而不是换对话模型。如果你的检索效果不好,先看看这一层。