Spring AI 集成 DeepSeek 原生供应商:告别 OpenAI 兼容层,获取结构化推理过程 背景项目中一直用 Spring AI 的 OpenAI 兼容层调用 DeepSeek API把spring.ai.openai.base-url指向https://api.deepseek.com。这种方式能跑但有几个痛点拿不到reasoning_contentDeepSeek 的推理过程CoT不会以结构化字段返回只能让模型把思考过程包在think标签里前端再用状态机做标签解析极其脆弱缺失 DeepSeek 特有 APIPrefix Completion、DeepSeek 特有的参数等都无法使用配置语义不清晰写着openai实际调的却是 DeepSeek维护成本高Spring AI 在 1.x 版本已经官方支持了 DeepSeek本文记录完整的迁移过程。一、添加依赖在pom.xml中添加 DeepSeek Starter版本由 BOM 1.1.3 统一管理dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-deepseek/artifactId /dependency二、配置供应商在application-dev.yml中添加 DeepSeek 配置块spring: ai: deepseek: api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx chat: options: model: deepseek-v4-flash temperature: 1.0API Key 建议通过环境变量注入避免硬编码。三、注册 Bean在AiConfig.java中注册 DeepSeek 的 ChatModel 和 ChatClientimport org.springframework.ai.deepseek.DeepSeekChatModel; Bean(deepseekChatModel) public ChatModel deepseekChatModel(DeepSeekChatModel deepSeekChatModel) { return deepSeekChatModel; } Bean(deepseekChatClient) public ChatClient deepseekChatClient( Qualifier(deepseekChatModel) ChatModel deepseekChatModel, MessageFormatAdvisor messageFormatAdvisor, LifecycleToolCallAdvisor lifecycleToolCallAdvisor, TaskProgressAdvisor taskProgressAdvisor, RetryAdvisor retryAdvisor) { return ChatClient.builder(deepseekChatModel) .defaultToolContext(new HashMap(Map.of(debug, true))) .defaultAdvisors( messageFormatAdvisor, lifecycleToolCallAdvisor, taskProgressAdvisor, retryAdvisor ) .build(); }四、Controller 改造 — 原生推理流式输出改造前每个 SSE Chunk 拿到的是AssistantMessagethink标签可能被切碎在多个 Chunk 里需要维护复杂的状态机做拼接。改造后使用DeepSeekAssistantMessagereasoningContent和text是两个独立字段import org.springframework.ai.deepseek.DeepSeekAssistantMessage; .concatMap(response - { AssistantMessage output response.getResult().getOutput(); ListServerSentEventChatChunk events new ArrayList(); if (output.getToolCalls() ! null !output.getToolCalls().isEmpty()) { // handle tool calls return Flux.fromIterable(events); } if (output instanceof DeepSeekAssistantMessage dsMsg) { String reasoning dsMsg.getReasoningContent(); if (reasoning ! null !reasoning.isEmpty()) { state.accumulateReasoning(reasoning, events); } } String text output.getText(); if (text ! null !text.isEmpty()) { state.flushReasoning(events); events.add(createEvent(message, state.messageId(), text, null)); } return Flux.fromIterable(events); })五、推理内容缓冲优化reasoningContent以 Token 粒度到达每个 SSE Chunk 可能只有一个字直接推给前端会导致渲染碎片化。需要在服务端按语义边界缓冲private static class StreamState { private static final int REASONING_FLUSH_THRESHOLD 50; private static final Pattern SENTENCE_BOUNDARY Pattern.compile([。.!?\n]); private final StringBuilder reasoningBuffer new StringBuilder(); public void accumulateReasoning(String delta, ListServerSentEventChatChunk target) { reasoningBuffer.append(delta); String buf reasoningBuffer.toString(); var matcher SENTENCE_BOUNDARY.matcher(buf); int lastEnd 0; while (matcher.find()) { String segment buf.substring(lastEnd, matcher.end()).trim(); if (!segment.isEmpty()) { target.add(createEvent(thought, reasoning, segment, null)); } lastEnd matcher.end(); } reasoningBuffer.delete(0, lastEnd); if (reasoningBuffer.length() REASONING_FLUSH_THRESHOLD) { target.add(createEvent(thought, reasoning, reasoningBuffer.toString(), null)); reasoningBuffer.setLength(0); } } }触发策略遇到句号/问号/感叹号/换行 → 按标点切分整句发出缓冲区积累超过 50 字符无标点 → 强制整块发出切换到文本输出或工具调用 → 排空缓存六、效果对比改造前前端收到逐个单词的thought事件event: thought data: {content:The,role:thought} event: thought data: {content:user,role:thought} event: thought data: {content:wants,role:thought}改造后前端收到完整的语义段落event: thought data: {content:The user wants me to add a new feature.,role:thought} event: thought data: {content:Let me think about the best approach.,role:thought}总结Spring AI 官方 DeepSeek Starter 带来的核心收益1.结构化推理内容DeepSeekAssistantMessage.getReasoningContent()直接获取 CoT无需think标签 hack2.服务端缓冲按语义边界批量下发前端零改动即可获得平滑渲染3.配置语义化spring.ai.deepseek.*一目了然4.扩展性无缝使用 DeepSeek 特有功能Prefix Completion、Reasoning 多轮对话等