背景
使用 LangChain4j 搭配 OpenAI 的 starter 进行工具调用时,会出现 400 Bad Request:
json
{"error":{"message":"The `reasoning_content` in the thinking mode must be passed back to the API",...}}
问题分析
出现这个问题,是因为发送的 request body 不符合 DeepSeek 的规范。那么缺少的是哪一个字段?光靠猜并不靠谱,这里我们用一个非常简易的 Agent 框架 pi 来抓真实请求体。
安装 pi
pi 的安装非常简单:
css
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
安装之后,在终端输入 pi 即可看到:

配置 DeepSeek API Key
配置 API Key 也很简单:输入 /login → Use an API Key → 选择 DeepSeek,再输入密钥。

输入密钥后回车确认即可:

用扩展抓取真实请求体
下面是我让 pi 生成 ai-request-logger 扩展用的 Prompt,它会把所有 AI provider 请求/响应落盘到 .pi/ai-request-logger/ 下。
markdown
# Prompt: 生成 ai-request-logger 扩展
为 pi coding agent 创建扩展,拦截并记录所有 AI provider 请求/响应到 `.pi/ai-request-logger/` 目录下按日期分 `.jsonl` 文件。
**核心功能:**
1. `before_provider_request` → 记录请求 ID、模型、消息数、payload 大小
2. `after_provider_response` → 追加状态码、延迟
3. `message_end` → 追加 token 用量、费用
4. `turn_end` → 汇总本轮统计,footer 显示
5. 注册 `/ai-log` 命令 → custom UI 面板查看日志(滚动/展开)
6. 注册 `query_logs` 工具 → LLM 可查询统计/历史
7. 注册 `log_level` 工具 → LLM 调整日志级别
**实现要求:**
- 内存维护 `RequestLog[]` 和 `TurnSummary[]`,上限 1000 条
- 文件写用 `fs.promises.appendFile`,不阻塞
- 完整 payload 仅 verbose 模式存储
- 参考示例:`provider-payload.ts`、`todo.ts`、`summarize.ts`、`model-status.ts`
- 所有 I/O try-catch 包裹,不抛异常阻塞主流程
通过 pi 进行工具调用时,他就会吧日志信息记录到 .pi 文件夹下面:

我们可以看到工具调用会添加一个 reasoning_content 字段,并且这个 content 字段为 null 也不影响:

定位根因
我们查看自己的请求体,发现没有这个参数,所以报错就是因为缺少 reasoning_content。知道原因后,修改就容易了。
修复 Bug
通过同包名覆盖 dev.langchain4j.model.openai.OpenAiChatModel,在 DeepSeek 模型分支下回传 reasoning_content 字段。我们使用同包名覆盖源码的方式实现:JVM 类加载时,工程内同包同名类会优先于依赖包中的版本。这种方式虽然不利于版本升级,但最直接有效。灵感来源于「AI 零代码项目」,具体方法如下:
- 首先找到相对底层的类
dev.langchain4j.model.openai.OpenAiChatModel - 在项目的同包路径
src/main/java/dev/langchain4j/model/openai/下创建同名类OpenAiChatModel,并把源码内容复制过来 - 之后可以让 AI 进行修改,可以使用 Cursor 或者 Codex 之类的,让工具调用时添加上
reasoning_content这个参数 - 下面是我修改好的代码片段,完整版本见 GitHub 上的
OpenAiChatModel.java:
scss
@Override
public ChatResponse doChat(ChatRequest chatRequest) {
OpenAiChatRequestParameters parameters = (OpenAiChatRequestParameters) chatRequest.parameters();
validate(parameters);
String modelName = parameters.modelName();
List<Message> messages = isDeepSeekModel(modelName)
? toOpenAiMessages(chatRequest.messages(), sendThinking, thinkingFieldName)
: OpenAiUtils.toOpenAiMessages(chatRequest.messages(), sendThinking, thinkingFieldName);
ChatCompletionRequest openAiRequest = toOpenAiChatRequest(
chatRequest, parameters, sendThinking, thinkingFieldName, strictTools, strictJsonSchema)
.messages(messages)
.build();
....
....
}
private static boolean isDeepSeekModel(String modelName) {
return modelName != null && modelName.toLowerCase().contains("deepseek");
}
/**
* DeepSeek V4 thinking + tool_calls:含 tool_calls 的 assistant 必须回传 reasoning_content(无则 "")。
*/
private static List<Message> toOpenAiMessages(
List<ChatMessage> messages, boolean sendThinking, String thinkingFieldName) {
return messages.stream()
.map(message -> toOpenAiMessage(message, sendThinking, thinkingFieldName))
.collect(toList());
}
private static Message toOpenAiAssistantWithToolReasoning(AiMessage aiMessage, String thinkingFieldName) {
String reasoning = aiMessage.thinking();
if (reasoning == null) {
reasoning = "";
}
ToolExecutionRequest first = aiMessage.toolExecutionRequests().get(0);
if (first.id() == null) {
FunctionCall functionCall = FunctionCall.builder()
.name(first.name())
.arguments(first.arguments())
.build();
return AssistantMessage.builder()
.functionCall(functionCall)
.customParameter(thinkingFieldName, reasoning)
.build();
}
List<ToolCall> toolCalls = aiMessage.toolExecutionRequests().stream()
.map(it -> ToolCall.builder()
.id(it.id())
.type(FUNCTION)
.function(FunctionCall.builder()
.name(it.name())
.arguments(isNullOrBlank(it.arguments()) ? "{}" : it.arguments())
.build())
.build())
.collect(toList());
return AssistantMessage.builder()
.content(aiMessage.text())
.toolCalls(toolCalls)
.customParameter(thinkingFieldName, reasoning)
.build();
}
测试
Github 上面提供了一个简单的 demo,测试发现是可以的非常成功!通过日志可以看到正确携带了 reasoning_content 字段。
