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 的安装非常简单:

css 复制代码
npm install -g --ignore-scripts @earendil-works/pi-coding-agent

安装之后,在终端输入 pi 即可看到:

配置 DeepSeek API Key

配置 API Key 也很简单:输入 /loginUse 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 零代码项目」,具体方法如下:

  1. 首先找到相对底层的类 dev.langchain4j.model.openai.OpenAiChatModel
  2. 在项目的同包路径 src/main/java/dev/langchain4j/model/openai/ 下创建同名类 OpenAiChatModel,并把源码内容复制过来
  3. 之后可以让 AI 进行修改,可以使用 Cursor 或者 Codex 之类的,让工具调用时添加上 reasoning_content 这个参数
  4. 下面是我修改好的代码片段,完整版本见 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 字段。

相关推荐
wuhen_n2 小时前
RAG 入门:检索增强生成核心原理
前端·人工智能·typescript·langchain·ai编程
晚笙coding3 小时前
从零讲透 LangChain 提示词模板:不只是 Prompt,而是“可复用的 AI 指令工厂”
人工智能·langchain·prompt
晚笙coding3 小时前
从零讲透 LangChain 输出格式化:让模型真的“能用”
java·开发语言·langchain
颜酱3 小时前
LangChain 调大模型:模板拼接 + invoke / stream / batch
python·langchain
染指11103 小时前
19.LangChain框架7-LangChain1.0版本使用Agent(中间件实例)
人工智能·python·机器学习·langchain·agent·rag
喵叔哟4 小时前
第2周学习笔记
笔记·python·学习·langchain
装不满的克莱因瓶13 小时前
学习 LCEL 表达式:降低 LLM 应用开发难度
人工智能·ai·langchain·agent·智能体·lcel·langgraph
lhxcc_fly13 小时前
6.3.1RAG--存储(嵌入向量)
langchain·llm·embedding
论迹14 小时前
【LangChain】-- 定义聊天模型
langchain