实训5:大模型的统一接入与结构化输出
本次任务的目标是:把项目里用到的多个大模型厂商统一接入,统一管理,并解决一个常见问题--让大模型稳定地返回能被程序直接解析的JSON。
技术栈:
Spring Boot 4.0
Spring AI
阿里云百炼/Kimi/Deepseek/GLM等(OpenAI兼容接口)
一.要解决的两个问题
项目里很多功能都要调用大模型,比如知识库问答。这里有两个问题:
1.我们想支持多个模型厂商(通义,Kimi,Deepseek,GLM等),如果每个地方都自己写一遍接入代码,会很乱,也没法灵活切换。
2.有些地方需要模型返回结构化的JSON,但模型偶尔会多输出一段解释文字,或者引导没转义,导致程序解析失败。
所以我做了两块:一是统一接入和缓存(LlmProviderRegistry),二是结构化输出的重试和修复(StructuredOutputInvoker).
二.核心代码实现
1.统一接入:用一个Registry把不同厂商都按OpenAI兼容的方式接入,并用ConcurrentHashMap缓存,避免重复创建。
private final Map<String, ChatClient> clientCache = new ConcurrentHashMap<>();
// 按 providerId 取 ChatClient,没有就创建并缓存
public ChatClient getChatClient(String providerId) {
return clientCache.computeIfAbsent(providerId, id -> createChatClient(id));
}
// 不指定就用默认 provider(配置在 app.ai.default-provider,默认 dashscope)
public ChatClient getChatClientOrDefault(String providerId) {
return (providerId != null && !providerId.isBlank())
? getChatClient(providerId) : getDefaultChatClient();
}
各厂商的 baseUrl、apiKey、model 都配在 application.yml 的 app.ai.providers 下。因为它们都兼容 OpenAI 协议,所以接入方式统一。Embedding 模型也用同样的方式缓存。
2.结构化输出:把"调用模型 + 解析 JSON + 失败重试"封装在一起。
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
String content = chatClient.prompt().system(sys).user(user).call().content();
return convertWithRepair(content, outputConverter); // 解析,失败先本地修复再解析
} catch (Exception e) {
lastError = e;
// 重试时在 system 提示词里追加"只返回合法 JSON"和上次的错误原因,引导模型改正
}
}
3.本地 JSON 修复:模型有时会在 JSON 字符串里写没转义的引号,导致解析失败。解析报错时先尝试本地修复一遍再解析,能少调用一次模型。
另外,为了让模型更容易输出规范的 JSON,提示词本身也按固定结构写(角色、任务、输出格式、约束,也就是 RISEN 这类结构),并在末尾明确列出 JSON 需要哪些字段。
三.几个关键点
为什么要缓存:ChatClient 和 Embedding 模型构建是有开销的,按 provider 缓存之后,重复调用就不用每次重建。
为什么重试时回灌错误:第一次解析失败时,把错误原因和"只返回 JSON"的要求加进提示词再问一次,模型改正的概率更高。
为什么要本地修复:能在本地修好的(比如漏转义的引号)就不再多花一次模型调用,省时间也更稳。
四.总结
| 多厂商统一接入 | LlmProviderRegistry,OpenAI 兼容方式 |
| 避免重复创建 | ConcurrentHashMap 缓存 ChatClient / Embedding |
| 结构化输出 | StructuredOutputInvoker 封装调用 |
| 解析失败重试 | 重试时回灌错误 + 强调只返回 JSON |
| 减少模型调用 | 本地修复未转义引号后再解析 |
整体下来,模型接入和结构化输出都被收成了通用能力,其他功能要用大模型时直接调用即可
更多推荐



所有评论(0)