fix(llm): 修复 thinking mode 的 reasoning_content 协议 400,默认关闭深度思考
build / build-push (push) Successful in 1m31s

线上现象:用户发消息后助手回
「大模型错误:The `reasoning_content` in the thinking mode must be passed back to the API.」

根因:DeepSeek V4 系列(含 deepseek-flash)默认开启 thinking mode。官方协议规定,
只要请求带 tools 参数,历史里所有 assistant 消息的 reasoning_content 都必须原样回传,
否则接口直接回 HTTP 400。而 ChatMessage.toJson() 只输出 role/content/tool_calls/tool_call_id,
把该字段整个丢掉 —— 于是「第一轮模型调工具成功、第二轮请求必炸」。

改动:
- ChatMessage 增加 reasoningContent 字段并在 toJson 里输出(非空才输出);ChatResult 同步解析响应字段。
- AgentOrchestrator 新增 assistantOf(),把模型每次返回的思维链写回对应的 assistant 消息
  (带 tool_calls 的那条与最终回复那条都要带)。
- DeepSeekClient:
  * 新增 agent.llm.thinking(auto/enabled/disabled),按配置显式发送 thinking 字段;
  * 命中 reasoning_content 协议错误时自动关闭 thinking 重试一次(自愈,线上无需等改配置即可恢复);
  * humanizeApiError 增加该错误的中文话术,避免原样抛出英文协议报错。
- AgentSettings.Llm.thinking 由 boolean 改为三态字符串,默认 disabled:
  本工程属工具密集编排,几乎每轮都带 tools,CoT 收益有限却显著增加延迟与 token,
  且 thinking 模式下 temperature 会被忽略。
- application.yml 补 agent.llm.thinking: disabled 与说明。

新增 ThinkingModeTest(7 例):字段序列化/省略、disabled 与 auto 的请求体、响应解析、
命中协议错误时的自愈重试、以及非协议错误不被误重试。
This commit is contained in:
lijing
2026-10-12 12:13:41 +08:00
parent 0197a9de05
commit 15b3c9da30
7 changed files with 388 additions and 26 deletions
@@ -129,8 +129,25 @@ public class AgentSettings {
/** 单次 HTTP 超时(毫秒)。⚠️ 实际出站超时已由平台统一客户端接管,见 {@code yh.http.read-timeout-ms};本字段保留仅作文档参考 */
private int timeout = 60000;
/** 是否开启深度思考(旧的 deepseek-reasoner 独立模型已下线;V4 系列改为在请求里带 effort 档位,本开关暂未接线) */
private boolean thinking = false;
/**
* 深度思考(thinking mode)开关,三态:{@code auto} / {@code enabled} / {@code disabled}。
*
* <p><b>★ 2026-10-12 必读</b>:DeepSeek V4 系列(含 {@code deepseek-flash})<b>默认开启</b> thinking mode。
* 官方协议规定:<b>只要请求带 {@code tools} 参数,历史里所有 assistant 消息的
* {@code reasoning_content} 都必须原样回传</b>,否则接口直接回
* HTTP 400 {@code The `reasoning_content` in the thinking mode must be passed back to the API}
* —— 表现为「第一轮模型调工具成功、第二轮就报错」,非常容易误判成鉴权/网络问题。
*
* <p>本工程是<b>工具密集型</b>编排(几乎每轮都带 tools),CoT 收益有限,却要付出
* 更高的延迟与 token 成本;且 thinking mode 下 {@code temperature} 被忽略。
* 因此默认 {@code disabled}(请求体显式带 {@code {"thinking":{"type":"disabled"}}})。
*
* <p>{@code auto} = 不发送该字段、由服务端决定(当前服务端默认 = enabled,不推荐);
* {@code enabled} = 显式开启(此时 {@link com.yunhai.agent.llm.dto.ChatMessage#getReasoningContent()}
* 会随 assistant 消息原样回传)。<b>注意</b>:会话消息落库未保存该字段,
* 若开启 thinking,请在同一次请求内完成多轮工具循环(浏览器代执行续跑会另起请求,可能不适用)。
*/
private String thinking = "disabled";
}
@Data
@@ -44,6 +44,12 @@ public class DeepSeekClient implements LlmClient {
private static final String TOOL_CHOICE_AUTO = "auto";
/** thinking mode 显式开启 */
private static final String THINKING_ENABLED = "enabled";
/** thinking mode 显式关闭 */
private static final String THINKING_DISABLED = "disabled";
private final RestTemplate restTemplate;
private final AgentSettings settings;
@@ -88,32 +94,32 @@ public class DeepSeekClient implements LlmClient {
"未配置大模型 api-key(agent.llm.api-key)");
}
JSONObject body = new JSONObject();
body.put("model", llm.getModel());
body.put("max_tokens", llm.getMaxTokens());
JSONArray msgArr = new JSONArray();
for (ChatMessage m : messages) {
msgArr.add(m.toJson());
}
body.put("messages", msgArr);
// 现役 V4 系列(deepseek-flash / deepseek-v4-pro)temperature 与 tools 都支持。
// 保留 isReasoner 判断只为兼容历史上"reasoner 不支持二者"的模型名,正常情况下恒为 false。
if (!isReasoner(llm.getModel())) {
body.put("temperature", llm.getTemperature());
if (tools != null && !tools.isEmpty()) {
body.put("tools", tools);
body.put("tool_choice", TOOL_CHOICE_AUTO);
}
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + llm.getApiKey());
headers.set("Accept", "application/json");
String url = StringUtils.isNotBlank(llm.getHost()) ? llm.getHost() : llm.getFallbackHost();
String thinkingType = thinkingType(llm);
try {
return doChat(url, headers, llm, messages, tools, thinkingType);
} catch (BizException e) {
// ★ 自愈:命中 thinking mode 的 reasoning_content 协议错误(HTTP 400)。
// 根因是历史 assistant 消息缺 reasoning_content,最省事的确定解就是关掉 thinking 重来一次。
// 这样即便线上配置来不及改(默认 auto/enabled),用户侧也不会卡死。
if (isThinkingOn(thinkingType) && isReasoningContentError(e.getMessage())) {
log.warn("[大模型] 命中 reasoning_content 协议错误,自动关闭 thinking 重试一次:{}", e.getMessage());
return doChat(url, headers, llm, messages, tools, THINKING_DISABLED);
}
throw e;
}
}
/** 实际发起一次 chat/completions 并解析响应 */
private ChatResult doChat(String url, HttpHeaders headers, AgentSettings.Llm llm,
List<ChatMessage> messages, JSONArray tools, String thinkingType) {
JSONObject body = buildBody(llm, messages, tools, thinkingType);
String requestBody = JSON.toJSONString(body);
long start = System.currentTimeMillis();
@@ -163,6 +169,13 @@ public class DeepSeekClient implements LlmClient {
} else if (contentObj != null) {
result.setContent(JSON.toJSONString(contentObj));
}
// ★ 思维链:thinking mode 下必须随 assistant 消息一起回传,否则下一次带 tools 的请求会 400
Object reasoningObj = message.get("reasoning_content");
if (reasoningObj instanceof String) {
result.setReasoningContent((String) reasoningObj);
} else if (reasoningObj != null) {
result.setReasoningContent(JSON.toJSONString(reasoningObj));
}
JSONArray toolCalls = message.getJSONArray("tool_calls");
if (toolCalls != null) {
List<ToolCall> list = new ArrayList<>();
@@ -190,12 +203,76 @@ public class DeepSeekClient implements LlmClient {
result.setCompletionTokens(usage.getInteger("completion_tokens"));
}
log.info("大模型调用完成 cost={}ms tools={} tokens={}/{}",
log.info("大模型调用完成 cost={}ms tools={} tokens={}/{} reasoningChars={}",
cost, result.getToolCalls().size(),
result.getPromptTokens(), result.getCompletionTokens());
result.getPromptTokens(), result.getCompletionTokens(),
StringUtils.length(result.getReasoningContent()));
return result;
}
/** 组装 chat/completions 请求体(thinkingType 为 null 时不发 thinking 字段,由服务端决定) */
private JSONObject buildBody(AgentSettings.Llm llm, List<ChatMessage> messages,
JSONArray tools, String thinkingType) {
JSONObject body = new JSONObject();
body.put("model", llm.getModel());
body.put("max_tokens", llm.getMaxTokens());
JSONArray msgArr = new JSONArray();
for (ChatMessage m : messages) {
msgArr.add(m.toJson());
}
body.put("messages", msgArr);
// 现役 V4 系列(deepseek-flash / deepseek-v4-pro)temperature 与 tools 都支持。
// 保留 isReasoner 判断只为兼容历史上"reasoner 不支持二者"的模型名,正常情况下恒为 false。
if (!isReasoner(llm.getModel())) {
body.put("temperature", llm.getTemperature());
if (tools != null && !tools.isEmpty()) {
body.put("tools", tools);
body.put("tool_choice", TOOL_CHOICE_AUTO);
}
}
// ★ thinking mode 显式开关(auto 时不发,交服务端默认)。
// 必须显式按配置发送:DeepSeek V4 系列默认 enabled,不发就会带上那套「reasoning_content 必须回传」的约束。
if (thinkingType != null) {
JSONObject thinking = new JSONObject();
thinking.put("type", thinkingType);
body.put("thinking", thinking);
}
return body;
}
/**
* 解析配置里的 thinking 开关:
* <ul>
* <li>{@code enabled} → 显式开启</li>
* <li>{@code disabled} → 显式关闭</li>
* <li>其它(含 {@code auto}、空)→ 返回 null,请求里不发该字段,由服务端默认决定</li>
* </ul>
*/
private static String thinkingType(AgentSettings.Llm llm) {
String t = llm.getThinking();
if (StringUtils.isBlank(t)) {
return null;
}
t = t.trim().toLowerCase();
if (THINKING_ENABLED.equals(t) || THINKING_DISABLED.equals(t)) {
return t;
}
return null;
}
/** 本次请求是否可能处于 thinking 开启态(enabled,或未指定而由服务端默认开启) */
private static boolean isThinkingOn(String thinkingType) {
return thinkingType == null || THINKING_ENABLED.equals(thinkingType);
}
/** 是否 thinking mode 的 reasoning_content 协议错误(HTTP 400,文案固定含该字段名) */
private static boolean isReasoningContentError(String message) {
return message != null && message.toLowerCase().contains("reasoning_content");
}
/**
* 把底层 HTTP 异常翻译成使用者能看懂的话。
*
@@ -256,6 +333,12 @@ public class DeepSeekClient implements LlmClient {
if (lower.contains("rate limit") || lower.contains("too many requests")) {
return "大模型请求过于频繁(" + m + "),请稍后重试";
}
// thinking mode 协议错误:只要请求带 tools,历史 assistant 消息的 reasoning_content 必须原样回传。
// 本工程已把它按 assistant 消息透传(见 ChatMessage#reasoningContent),并在命中该错误时自动关 thinking 重试。
if (lower.contains("reasoning_content")) {
return "大模型思考模式协议错误(" + m + "):历史消息未回传 reasoning_content。"
+ "已自动降级重试;若仍失败请把 agent.llm.thinking 设为 disabled";
}
if (lower.contains("model not exist") || lower.contains("model_not_found")
|| lower.contains("invalid model") || lower.contains("does not exist")) {
return "大模型模型标识无效(" + m + "):请用官方实时清单 GET https://api.deepseek.com/models "
@@ -39,6 +39,17 @@ public class ChatMessage {
/** role=tool 时必填:对应哪一次工具调用 */
private String toolCallId;
/**
* assistant 消息的思维链(DeepSeek thinking mode 特有字段,与 content 同级)。
*
* <p><b>★ 必须原样回传</b>:官方协议规定,只要请求带 {@code tools} 参数,
* 历史里<b>每一条</b> assistant 消息的 {@code reasoning_content} 都要带回(包括没调工具的那轮),
* 否则 HTTP 400 {@code The `reasoning_content` in the thinking mode must be passed back to the API}。
* 本字段由 {@link com.yunhai.agent.llm.DeepSeekClient} 从响应里解析,
* 由 {@code AgentOrchestrator} 写回对应 assistant 消息。
*/
private String reasoningContent;
public ChatMessage(String role, String content) {
this.role = role;
this.content = content;
@@ -78,6 +89,10 @@ public class ChatMessage {
if (StringUtils.isNotBlank(toolCallId)) {
o.put("tool_call_id", toolCallId);
}
// ★ thinking mode 协议:assistant 的思维链必须原样回传,否则带 tools 的请求会 400
if (StringUtils.isNotBlank(reasoningContent)) {
o.put("reasoning_content", reasoningContent);
}
return o;
}
}
@@ -14,6 +14,12 @@ public class ChatResult {
/** 模型回答正文(可能为空:当它只想调工具时) */
private String content;
/**
* 思维链(DeepSeek thinking mode):与 {@link #content} 同级返回。
* <p><b>★ 必须原样回传到后续请求的 assistant 消息</b>,否则带 tools 的请求会 HTTP 400。
*/
private String reasoningContent;
/** 模型要调用的工具 */
private List<ToolCall> toolCalls = new ArrayList<>();
@@ -166,14 +166,16 @@ public class AgentOrchestrator {
String reply = StringUtils.isNotBlank(result.getContent())
? result.getContent()
: "(模型没有返回内容,请换个说法再试一次)";
messages.add(new ChatMessage(ChatMessage.ROLE_ASSISTANT, reply));
messages.add(assistantOf(reply, result.getReasoningContent()));
resp.setReply(reply);
finish(resp, session, totalPrompt, totalCompletion, out);
return resp;
}
// 记录 assistant 的 tool_calls(后续必须以 role=tool 逐条应答,否则上下文非法)
ChatMessage assistantMsg = new ChatMessage(ChatMessage.ROLE_ASSISTANT, result.getContent());
// ★ 同时带上 reasoning_content:thinking mode 下带 tools 的请求要求历史 assistant 原样回传该字段,
// 否则下一轮请求会被接口判 400(The reasoning_content ... must be passed back to the API)。
ChatMessage assistantMsg = assistantOf(result.getContent(), result.getReasoningContent());
assistantMsg.setToolCalls(result.getToolCalls());
messages.add(assistantMsg);
@@ -324,6 +326,8 @@ public class AgentOrchestrator {
}
String idPrefix = "br_" + System.currentTimeMillis() + "_";
// 注:这条 assistant 是「服务端补造」的(模型当轮的消息在挂起时已撤销),没有真实 reasoning_content。
// thinking mode 关闭时(agent.llm.thinking=disabled,默认)不需要该字段;若改为开启,此处可能被接口判 400。
ChatMessage assistant = new ChatMessage(ChatMessage.ROLE_ASSISTANT, null);
List<ToolCall> calls = new ArrayList<>();
List<String> contents = new ArrayList<>();
@@ -512,6 +516,7 @@ public class AgentOrchestrator {
fn.setName(def.getName());
fn.setArguments(JSON.toJSONString(args));
tc.setFunction(fn);
// 同上:服务端补造的 assistant(无真实 reasoning_content),thinking 关闭时无需该字段
ChatMessage assistant = new ChatMessage(ChatMessage.ROLE_ASSISTANT, null);
assistant.setToolCalls(Collections.singletonList(tc));
messages.add(assistant);
@@ -797,6 +802,19 @@ public class AgentOrchestrator {
return "「" + def.getTitle() + "」执行失败:" + (tr.getError() == null ? "未知原因" : tr.getError());
}
/**
* 构造 assistant 消息,并统一挂上 thinking mode 的思维链。
*
* <p>★ DeepSeek V4 系列默认开启 thinking mode,官方协议要求:只要请求带 {@code tools},
* 历史里所有 assistant 消息的 {@code reasoning_content} 必须原样回传,否则 HTTP 400。
* 因此模型每一次返回的思维链都要落进对应的 assistant 消息里。
*/
private ChatMessage assistantOf(String content, String reasoningContent) {
ChatMessage m = new ChatMessage(ChatMessage.ROLE_ASSISTANT, content);
m.setReasoningContent(reasoningContent);
return m;
}
private void finish(ChatResponse resp, SessionStore.SessionState session,
int promptTokens, int completionTokens, EventSink out) {
resp.setPromptTokens(promptTokens);
+7
View File
@@ -82,6 +82,13 @@ agent:
max-tokens: 2048
temperature: 0.3
timeout: 60000
# ★ 深度思考(thinking mode)开关:auto / enabled / disabled
# DeepSeek V4 系列(含 deepseek-flash)默认就是 enabled;一旦请求带 tools(本应用几乎每轮都带),
# 历史里所有 assistant 消息的 reasoning_content 必须原样回传,否则接口回 HTTP 400
# 「The `reasoning_content` in the thinking mode must be passed back to the API」。
# 本工程属工具密集编排,CoT 收益有限却显著增加延迟与 token(thinking 下 temperature 也会失效),
# 故默认 disabled。若确需推理能力可改 enabled(代码已按 assistant 消息回传该字段)。
thinking: disabled
# --- 编排参数 ---
loop:
@@ -0,0 +1,216 @@
package com.yunhai.agent;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yunhai.agent.config.AgentSettings;
import com.yunhai.agent.llm.DeepSeekClient;
import com.yunhai.agent.llm.dto.ChatMessage;
import com.yunhai.agent.llm.dto.ChatResult;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* thinking mode 协议回归(纯单元,不依赖 Spring)。
*
* <p><b>背景(2026-10-12 线上实测)</b>:用户发消息后助手回
* 「大模型错误:The `reasoning_content` in the thinking mode must be passed back to the API.」。
* 根因是 DeepSeek V4 系列(含 {@code deepseek-flash})<b>默认开启</b> thinking mode,官方协议规定:
* <b>只要请求带 {@code tools},历史里所有 assistant 消息的 {@code reasoning_content} 都必须原样回传</b>,
* 否则接口回 HTTP 400。而原先的 {@link ChatMessage#toJson()} 只输出 role/content/tool_calls,
* 把该字段整个丢掉了 —— 于是「第一轮调工具成功、第二轮必炸」。
*
* <p>本测试锁住四件事:
* <ol>
* <li>assistant 的 reasoning_content 会写进请求 JSON(且为空时不写);</li>
* <li>thinking 配置 <b>disabled</b> 时请求体显式带 {@code {"thinking":{"type":"disabled"}}};</li>
* <li>thinking 配置 <b>auto</b> 时不发该字段(交服务端决定);</li>
* <li>命中 reasoning_content 协议错误时,自动关闭 thinking <b>重试一次</b>(自愈)。</li>
* </ol>
*/
class ThinkingModeTest {
/** ① assistant 的思维链必须原样出现在请求 JSON 里 */
@Test
void assistantReasoningContentIsSerializedBack() {
ChatMessage m = new ChatMessage(ChatMessage.ROLE_ASSISTANT, "好的");
m.setReasoningContent("让我先看看这个用户是谁");
JSONObject json = m.toJson();
assertEquals("assistant", json.getString("role"));
assertEquals("让我先看看这个用户是谁", json.getString("reasoning_content"),
"thinking mode 下 assistant 的 reasoning_content 必须回传");
}
/** ② 没有思维链时不写该字段(避免给非 thinking 场景塞脏字段) */
@Test
void blankReasoningContentIsOmitted() {
ChatMessage m = new ChatMessage(ChatMessage.ROLE_ASSISTANT, "好的");
assertFalse(m.toJson().containsKey("reasoning_content"), "空思维链不应输出该字段");
}
/** ③ 默认配置 = disabled:请求体必须显式带 thinking.type=disabled(否则服务端默认 enabled 会带出协议约束) */
@Test
void disabledThinkingIsSentExplicitly() {
CapturingRestTemplate rt = new CapturingRestTemplate(okResponse("你好", null));
DeepSeekClient client = clientWith(rt, "disabled");
client.chat(oneUserMessage(), sampleTools());
String body = rt.bodies.get(0);
assertTrue(body.contains("\"thinking\""), "必须显式发送 thinking 字段:" + body);
assertTrue(body.contains("\"disabled\""), "thinking.type 应为 disabled:" + body);
}
/** ④ auto:不发 thinking 字段,把决定权交回服务端 */
@Test
void autoThinkingSendsNoField() {
CapturingRestTemplate rt = new CapturingRestTemplate(okResponse("你好", null));
DeepSeekClient client = clientWith(rt, "auto");
client.chat(oneUserMessage(), sampleTools());
assertFalse(rt.bodies.get(0).contains("\"thinking\""),
"auto 不应发送 thinking 字段:" + rt.bodies.get(0));
}
/** ⑤ 响应里的 reasoning_content 要被解析出来(供编排层写回 assistant 消息) */
@Test
void reasoningContentIsParsedFromResponse() {
CapturingRestTemplate rt = new CapturingRestTemplate(okResponse("在的", "先确认用户身份"));
DeepSeekClient client = clientWith(rt, "enabled");
ChatResult result = client.chat(oneUserMessage(), sampleTools());
assertEquals("在的", result.getContent());
assertEquals("先确认用户身份", result.getReasoningContent());
}
/** ⑥ 自愈:首次命中 reasoning_content 400 → 自动关 thinking 重试一次并成功 */
@Test
void reasoningContentErrorTriggersRetryWithoutThinking() {
CapturingRestTemplate rt = new CapturingRestTemplate(
errorResponse("The `reasoning_content` in the thinking mode must be passed back to the API."),
okResponse("重试后的回答", null));
DeepSeekClient client = clientWith(rt, "enabled");
ChatResult result = client.chat(oneUserMessage(), sampleTools());
assertEquals(2, rt.calls, "应在命中协议错误后自动重试一次");
assertTrue(rt.bodies.get(0).contains("\"enabled\""), "首次请求按配置开启 thinking");
assertTrue(rt.bodies.get(1).contains("\"disabled\""), "重试时必须关闭 thinking");
assertEquals("重试后的回答", result.getContent());
}
/** ⑦ 自愈只对 reasoning_content 错误生效:别的错误不该被静默重试 */
@Test
void otherErrorsAreNotRetried() {
CapturingRestTemplate rt = new CapturingRestTemplate(
errorResponse("Insufficient Balance"));
DeepSeekClient client = clientWith(rt, "enabled");
Exception thrown = null;
try {
client.chat(oneUserMessage(), sampleTools());
} catch (Exception e) {
thrown = e;
}
assertNotNull(thrown, "余额不足应当抛出业务异常");
assertEquals(1, rt.calls, "非协议类错误不应重试");
}
/* ==================== 测试脚手架 ==================== */
private static DeepSeekClient clientWith(RestTemplate rt, String thinking) {
AgentSettings settings = new AgentSettings();
settings.getLlm().setApiKey("sk-test-key");
settings.getLlm().setModel("deepseek-flash");
settings.getLlm().setThinking(thinking);
return new DeepSeekClient(rt, settings);
}
private static List<ChatMessage> oneUserMessage() {
return new ArrayList<>(Arrays.asList(ChatMessage.user("我要请假")));
}
private static JSONArray sampleTools() {
JSONObject fn = new JSONObject();
fn.put("name", "leave_list");
JSONObject tool = new JSONObject();
tool.put("type", "function");
tool.put("function", fn);
JSONArray arr = new JSONArray();
arr.add(tool);
return arr;
}
private static String okResponse(String content, String reasoning) {
JSONObject msg = new JSONObject();
msg.put("role", "assistant");
msg.put("content", content);
if (reasoning != null) {
msg.put("reasoning_content", reasoning);
}
JSONObject choice = new JSONObject();
choice.put("message", msg);
choice.put("finish_reason", "stop");
JSONArray choices = new JSONArray();
choices.add(choice);
JSONObject usage = new JSONObject();
usage.put("prompt_tokens", 10);
usage.put("completion_tokens", 5);
JSONObject o = new JSONObject();
o.put("choices", choices);
o.put("usage", usage);
return o.toJSONString();
}
private static String errorResponse(String message) {
JSONObject err = new JSONObject();
err.put("message", message);
err.put("type", "invalid_request_error");
JSONObject o = new JSONObject();
o.put("error", err);
return o.toJSONString();
}
/** 记录每次请求体、按顺序吐出预置响应,用来替换真实 HTTP 调用 */
private static class CapturingRestTemplate extends RestTemplate {
private final List<String> bodies = new ArrayList<>();
private final Deque<String> responses = new ArrayDeque<>();
private int calls = 0;
CapturingRestTemplate(String... presetResponses) {
for (String r : presetResponses) {
responses.add(r);
}
}
@Override
@SuppressWarnings("unchecked")
public <T> ResponseEntity<T> postForEntity(String url, Object request,
Class<T> responseType, Object... uriVariables) {
calls++;
bodies.add((String) ((HttpEntity<?>) request).getBody());
String body = responses.isEmpty() ? okResponse("兜底", null) : responses.poll();
return (ResponseEntity<T>) ResponseEntity.ok(body);
}
}
}