第1972篇:DeepSeek API的Spring AI集成——R1模型的推理能力工程化使用
第1972篇:DeepSeek API的Spring AI集成——R1模型的推理能力工程化使用
去年底 DeepSeek R1 一出来,我们团队第一时间就测了一遍。不夸张地说,那个推理链的质量让我惊了一下——某些复杂逻辑题,R1 给出的思考过程比我组里一些工程师想得还清楚。但要把 R1 的推理能力真正用到生产,坑比表面上看起来多多了。今天把这块讲清楚。
DeepSeek R1 到底特殊在哪
大多数语言模型是"问了就答",R1 多了一个显式的"思考过程"。模型会先输出一段用 <think>...</think> 包裹的推理链,然后再给出最终答案。
这对工程化有几个影响:
一、响应结构变了。 原来 content 里就是最终答案,现在可能是思考过程加答案的混合体,你不处理就直接返给用户会很难看。
二、Token 消耗翻倍甚至更多。 推理链本身也算 token,一个复杂问题 R1 的思考过程可能有 2000+ token,答案才几百 token。不注意的话费用会超预期。
三、延迟变长。 思考链要先生成,然后才是答案。首字节延迟相对高,流式输出是必须的,不然用户会以为卡住了。
四、temperature 要调低。 R1 做推理任务时,temperature 设高了会让思考链发散,效果变差。官方建议推理场景用 0.6 以下,甚至 0.0。
搞清楚这四点,接入才不会犯方向性错误。
Spring AI 接入 DeepSeek 的两种路径
DeepSeek 没有官方的 Spring AI Starter,但它的 API 完全兼容 OpenAI 格式,所以有两条路:
路径一:用 Spring AI OpenAI Starter,改 baseUrl
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-reasoner # R1 的 API 模型名
temperature: 0.6
max-tokens: 8192这条路配置简单,但有个问题:DeepSeek R1 的响应里有 reasoning_content 字段(包含思考链),而 OpenAI Starter 的响应解析默认不包含这个扩展字段,思考过程就丢了。
路径二:自定义 RestClient,完整解析 R1 响应
这是我推荐的方式,稍微麻烦一点,但能完整利用 R1 的能力:
@Configuration
public class DeepSeekConfig {
@Value("${deepseek.api-key}")
private String apiKey;
@Value("${deepseek.base-url:https://api.deepseek.com}")
private String baseUrl;
@Bean("deepSeekChatModel")
public OpenAiChatModel deepSeekChatModel() {
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl(baseUrl)
.apiKey(apiKey)
.build();
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model("deepseek-reasoner")
.temperature(0.6)
.maxTokens(8192)
.build();
return new OpenAiChatModel(openAiApi, options);
}
@Bean("deepSeekChatClient")
public ChatClient deepSeekChatClient(
@Qualifier("deepSeekChatModel") OpenAiChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是一位严谨的推理助手,善于分步骤分析复杂问题。")
.build();
}
}R1 推理链的解析与提取
R1 的完整响应结构:
{
"choices": [{
"message": {
"role": "assistant",
"content": "最终答案内容",
"reasoning_content": "这里是完整的推理过程..."
}
}]
}Spring AI 的标准 ChatResponse 不直接暴露 reasoning_content,需要从元数据里取:
@Service
public class DeepSeekR1Service {
@Autowired
@Qualifier("deepSeekChatClient")
private ChatClient chatClient;
@Autowired
@Qualifier("deepSeekChatModel")
private OpenAiChatModel chatModel;
/**
* 获取答案和推理链
*/
public R1Response chatWithReasoning(String question) {
// 直接调用 ChatModel 获取完整响应
Prompt prompt = new Prompt(
List.of(new UserMessage(question)),
OpenAiChatOptions.builder()
.model("deepseek-reasoner")
.temperature(0.6)
.build()
);
ChatResponse response = chatModel.call(prompt);
AssistantMessage message = response.getResult().getOutput();
// 从元数据提取推理链
Map<String, Object> metadata = message.getMetadata();
String reasoningContent = (String) metadata.getOrDefault(
"reasoning_content", ""
);
String finalAnswer = message.getContent();
return R1Response.builder()
.question(question)
.reasoningProcess(reasoningContent)
.finalAnswer(finalAnswer)
.promptTokens(response.getMetadata().getUsage().getPromptTokens())
.completionTokens(response.getMetadata().getUsage().getGenerationTokens())
.build();
}
@Data
@Builder
public static class R1Response {
private String question;
private String reasoningProcess;
private String finalAnswer;
private Integer promptTokens;
private Integer completionTokens;
}
}流式输出处理推理链
R1 的流式输出更复杂一点:推理链和最终答案是分段输出的,客户端需要区分处理:
@Service
public class DeepSeekStreamService {
@Autowired
@Qualifier("deepSeekChatModel")
private OpenAiChatModel chatModel;
/**
* 流式输出,区分推理链和最终答案
*/
public Flux<StreamChunk> streamWithReasoning(String question) {
Prompt prompt = new Prompt(
List.of(new UserMessage(question)),
OpenAiChatOptions.builder()
.model("deepseek-reasoner")
.temperature(0.6)
.build()
);
return chatModel.stream(prompt)
.map(response -> {
AssistantMessage message = response.getResult().getOutput();
Map<String, Object> metadata = message.getMetadata();
String reasoningChunk = (String) metadata.getOrDefault(
"reasoning_content", null
);
String contentChunk = message.getContent();
if (reasoningChunk != null && !reasoningChunk.isEmpty()) {
return StreamChunk.reasoning(reasoningChunk);
} else if (contentChunk != null && !contentChunk.isEmpty()) {
return StreamChunk.answer(contentChunk);
}
return StreamChunk.empty();
})
.filter(chunk -> !chunk.isEmpty());
}
@Data
@AllArgsConstructor
public static class StreamChunk {
private ChunkType type;
private String content;
public enum ChunkType { REASONING, ANSWER, EMPTY }
public static StreamChunk reasoning(String content) {
return new StreamChunk(ChunkType.REASONING, content);
}
public static StreamChunk answer(String content) {
return new StreamChunk(ChunkType.ANSWER, content);
}
public static StreamChunk empty() {
return new StreamChunk(ChunkType.EMPTY, "");
}
public boolean isEmpty() {
return type == ChunkType.EMPTY;
}
}
}配套的 SSE 端点:
@RestController
@RequestMapping("/api/deepseek")
public class DeepSeekController {
@Autowired
private DeepSeekStreamService streamService;
@GetMapping(value = "/reason", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> reason(@RequestParam String question) {
return streamService.streamWithReasoning(question)
.map(chunk -> ServerSentEvent.<String>builder()
.event(chunk.getType().name().toLowerCase()) // "reasoning" 或 "answer"
.data(chunk.getContent())
.build()
);
}
}前端可以根据 SSE event 类型区分:reasoning 事件显示思考过程(可折叠),answer 事件显示最终答案。这样用户体验好很多,能看到模型在"思考"的过程。
架构设计:R1 用在哪儿最值
R1 适合的场景:
- 多步骤数学计算
- 逻辑推理和因果分析
- 代码 Bug 分析(不只给答案,要知道为什么)
- 复杂业务规则判断
R1 不适合的场景:
- 简单问答和闲聊(性价比太低)
- 需要极低延迟的场景(推理链生成时间长)
- 创意写作(temperature 要高,R1 反而受限)
任务路由:根据复杂度自动选模型
@Service
public class DeepSeekRoutingService {
@Autowired
@Qualifier("deepSeekChatClient")
private ChatClient r1Client;
@Autowired
private ChatClient defaultChatClient; // deepseek-chat
private static final int COMPLEXITY_THRESHOLD = 50; // 问题字数阈值
public String chat(String message) {
if (isComplexQuery(message)) {
log.info("复杂查询,使用 R1 模型: {}", message.substring(0, Math.min(50, message.length())));
return r1Client.prompt()
.user(message)
.call()
.content();
} else {
return defaultChatClient.prompt()
.user(message)
.call()
.content();
}
}
private boolean isComplexQuery(String message) {
// 简单规则:字数超阈值,或包含推理关键词
if (message.length() > COMPLEXITY_THRESHOLD) return true;
List<String> reasoningKeywords = List.of(
"为什么", "分析", "推导", "证明", "比较", "评估",
"原因", "如何判断", "有什么区别", "利弊"
);
return reasoningKeywords.stream().anyMatch(message::contains);
}
}实际项目里这个路由规则可以做得更精细,比如用一个轻量模型先判断任务类型,再决定用哪个模型处理。但要注意别把路由本身的延迟搞得太高。
推理链缓存:降低重复推理成本
对于相似问题,R1 每次都完整推理一遍太浪费了。可以把推理链缓存起来复用:
@Service
public class CachedR1Service {
@Autowired
private DeepSeekR1Service r1Service;
@Autowired
private EmbeddingModel embeddingModel;
@Autowired
private VectorStore vectorStore;
private static final double SIMILARITY_THRESHOLD = 0.92;
public DeepSeekR1Service.R1Response queryWithCache(String question) {
// 1. 查缓存
List<Document> similar = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(1)
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
);
if (!similar.isEmpty()) {
Document cached = similar.get(0);
log.info("命中推理缓存,问题相似度达标");
return deserializeFromDoc(cached, question);
}
// 2. 缓存未命中,调用 R1
DeepSeekR1Service.R1Response response = r1Service.chatWithReasoning(question);
// 3. 存入缓存
Document doc = new Document(
question,
Map.of(
"reasoning", response.getReasoningProcess(),
"answer", response.getFinalAnswer(),
"original_question", question
)
);
vectorStore.add(List.of(doc));
return response;
}
private DeepSeekR1Service.R1Response deserializeFromDoc(Document doc, String question) {
return DeepSeekR1Service.R1Response.builder()
.question(question)
.reasoningProcess((String) doc.getMetadata().get("reasoning"))
.finalAnswer((String) doc.getMetadata().get("answer"))
.build();
}
}这个方案在知识库问答类场景效果显著,相同或相似问题的 API 成本能降 60% 以上。
deepseek-chat 和 deepseek-reasoner 的协同
有时候我们希望既有 R1 的推理深度,又有快速响应。一个常见模式是"预推理+快速执行":
@Service
public class TwoStageService {
@Autowired
@Qualifier("deepSeekChatClient")
private ChatClient r1Client;
@Autowired
private ChatClient chatClient;
/**
* 两阶段处理:先用 R1 制定方案,再用 chat 执行
*/
public String twoStageProcess(String complexTask) {
// 第一阶段:用 R1 制定执行方案
String plan = r1Client.prompt()
.system("你是一位方案规划专家。请为以下任务制定详细的执行步骤,只输出步骤,不要执行。")
.user(complexTask)
.call()
.content();
log.info("R1 制定的执行方案: {}", plan);
// 第二阶段:用 chat 模型按方案执行(更快更便宜)
return chatClient.prompt()
.system("你是一位执行专家。请严格按照以下方案执行任务,给出具体的执行结果。")
.user("执行方案:\n" + plan + "\n\n原始任务:" + complexTask)
.call()
.content();
}
}这种模式在代码重构、文档生成等任务里用处很大。R1 负责"想清楚",chat 负责"干快点"。
异常处理的坑
DeepSeek API 偶尔会出现推理超时,特别是问题很复杂的时候。需要处理这种情况:
@Service
public class RobustDeepSeekService {
@Autowired
@Qualifier("deepSeekChatModel")
private OpenAiChatModel r1Model;
@Autowired
private ChatClient fallbackClient; // 本地或其他模型
@CircuitBreaker(name = "deepseek-r1", fallbackMethod = "fallbackReason")
@TimeLimiter(name = "deepseek-r1")
public CompletableFuture<String> reason(String question) {
return CompletableFuture.supplyAsync(() -> {
Prompt prompt = new Prompt(List.of(new UserMessage(question)));
return r1Model.call(prompt).getResult().getOutput().getContent();
});
}
public CompletableFuture<String> fallbackReason(String question, Exception e) {
log.warn("R1 模型不可用,降级到普通对话模型: {}", e.getMessage());
return CompletableFuture.supplyAsync(() ->
fallbackClient.prompt()
.user(question)
.call()
.content()
);
}
}resilience4j:
circuitbreaker:
instances:
deepseek-r1:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
timelimiter:
instances:
deepseek-r1:
timeout-duration: 60s # R1 推理可能需要较长时间成本控制:token 消耗监控
R1 的 token 消耗必须监控,不然费用很容易失控:
@Component
@Slf4j
public class TokenCostMonitor {
// DeepSeek 价格(仅供参考,以官方为准)
private static final double R1_INPUT_PRICE_PER_1K = 0.004; // 元/1K tokens
private static final double R1_OUTPUT_PRICE_PER_1K = 0.016; // 推理 token 价格更高
@EventListener
public void onChatCompletion(ChatCompletionEvent event) {
Usage usage = event.getUsage();
double inputCost = usage.getPromptTokens() / 1000.0 * R1_INPUT_PRICE_PER_1K;
double outputCost = usage.getGenerationTokens() / 1000.0 * R1_OUTPUT_PRICE_PER_1K;
double totalCost = inputCost + outputCost;
log.info("R1调用成本 - 输入:{}tokens({:.4f}元) 输出:{}tokens({:.4f}元) 合计:{:.4f}元",
usage.getPromptTokens(), inputCost,
usage.getGenerationTokens(), outputCost,
totalCost);
// 超过阈值告警
if (totalCost > 0.5) {
log.warn("单次调用费用超过0.5元,请检查是否合理: {:.4f}元", totalCost);
}
}
}小结
DeepSeek R1 的推理能力是真的强,但工程化使用有几个关键点:
第一,推理链是 R1 的核心价值,要完整提取而不是丢弃。
第二,根据任务复杂度路由,不要让 R1 干所有事,成本会很难看。
第三,流式输出是必须的,推理链生成时间长,不流式用户体验差。
第四,做好熔断降级,R1 偶尔超时,必须有 plan B。
第五,推理链可以缓存,相似问题复用推理结果,成本大幅下降。
下一篇我们做一个横向评测,把国内主流 AI 云服务的 API 差异比较一下。
