LangChain4j 调用 DeepSeek 工具时报 400?用 pi 抓包定位,同包覆盖修复 reasoning_content
背景
使用 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 的安装非常简单:
▼bash复制代码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:
▼java复制代码@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 字段。


