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 即可看到:

image.png

配置 DeepSeek API Key

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

image.png

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

image.png

用扩展抓取真实请求体

下面是我让 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 文件夹下面:

image.png

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

image.png

定位根因

我们查看自己的请求体,发现没有这个参数,所以报错就是因为缺少 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:
▼
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 字段。

image.png

相关专栏

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
leikooo
作者分享
最近看了一个视频叫「被 Vibe Coding 抚平的大脑褶皱,还能救回来吗?」,聊的是 AI 时代学编程的困境,感觉说的挺好的,给鱼友们分享一下。 视频中提到现在学编程和以前最大的区别是,以前卡住了你只能自己想、查文档、翻 Stack Overflow,这个过程虽然痛苦,但你的脑子确实在转。现在有了 AI,卡住的第一反应就是打开 ChatGPT 问一句,代码瞬间就出来了,跑通了,感觉自己搞定了。但问题是,你的大脑在这个过程中几乎没有参与。视频里提到一个实验,有个学生读完题 10 秒钟就放弃思考去问 AI 了,事后还觉得是自己独立完成的。这就是 AI 带来的最大陷阱——你以为自己在学,其实只是在看AI 表演。 视频基于一项研究,总结了学编程时容易掉进去的 8 种思维陷阱。研究表明光是知道这些陷阱的存在,就能明显提升学习效果。前 5 种是编程学习中一直存在的,后 3 种是 AI 时代新出现的: 1)Forming(构建错误):你理解了问题,但用了错误的方法去解决。比如题目要你判断正数多还是负数多,你写了个求和的逻辑,方向对了路走偏了。 2)Dislodging(思维固着):你已经意识到方法不对,但就是转不过弯来换思路,反复在错误的方向上修修补补。 3)Assumption(假设偏差):你完美地解决了一个问题,但不是题目要求的那个问题。比如题目要处理任意个数字,你只处理了四个。4)Location(定位缺失):跳过了关键步骤就开始写代码,感觉快写完了,测试的时候才发现漏了循环或数据结构这种核心东西,得大改。 5)Achievement(成就幻觉):写了一大堆代码,明知道有问题但不愿意推倒重来,总想着再改改就好了,结果越改越乱。 6)Progression(进度错觉):AI 帮你写出了超出你水平的代码,作业都能交,但基础可能已经落后好几周了,自己完全不知道。这个是最危险的,等到面试或者独立写代码的时候才发现脑子里是空的。 7)Interruption(思维中断):你正在集中精力思考,AI 自动补全突然弹出来一段代码,思路直接被打断。有意思的是实验中表现好的学生大多直接忽略了 AI 的补全建议。 8)Mislead(误导跟随):信了 AI 给的一个看似合理但方向错误的建议,白白浪费时间走弯路。 大佬给出的建议是,遇到问题先别急着问 AI,给自己至少五分钟独立思考。卡住、沮丧、想摔键盘,这些不是你学不会的信号,这就是解决问题时的正常感受。AI 生成的代码跑通之后,试着关掉 AI 自己从零写一遍,能写出来才算真的会了。最重要的是分清场景,工作赶进度可以用 AI 提效,但练习和学习的时候请把「拐杖」放下,自己走。别让 AI 替你长脑子。
7
ARTS 0927: 单栈逐层展开嵌套字符串、包管理器与 Agent 沙箱本质同源与单次前向传播复刻极速决策模型
7
祝编程导航的鱼友们中秋快乐,愿代码顺利、生活如意,月圆人团圆,所愿皆可期!
13
ARTS 0913: 双栈互补实现队列、CIDR 聚合化解路由膨胀与 AI 作弊串通绝非偶然 Bug
8
微信爆出 P0 级漏洞😱,接到来电就中招,碰都不用碰手机。 怎么中招的? 黑客只需要是你的微信好友,然后给你打一个语音或视频通话,好了已经搞定了,你已经被黑了。 有多离谱? 完全不需要接这个电话。手机锁着屏扔在桌上,你看都没看一眼,只要响了几秒钟,恶意代码就已经在后台跑起来了。 黑客瞬间拿下你整个微信:翻你聊天记录、看你隐私、用你的身份给别人发消息。 更恐怖的是,你的微信紧接着会变成"丧尸",黑客可以自动给通讯录里所有好友挨个打电话,把他们也感染一遍。每个受害者又立刻变成新的传播者,像病毒一样指数级扩散...,不敢想象如果被利用了,理想情况下那需要多久就能扩散到全国的用户呢?30 分钟左右 好消息是这个 bug 微信团队已经知道了这个 bug ,坏消息这个 bug 好像不是微信团队发现的 文章来源:https://calif.io/research/weworm
7
下载 APP