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 也很简单:输入 /loginUse 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
作者分享
ARTS 0815: 分隔链表、大删除是加活不是减负与协议栈如何一层层拆信封
4
DeepSeek Harness 缓存命中率太惊人了,有时候竟然能到 99%,看鱼皮哥的视频竟然还出现过 100% 😱 https://www.bilibili.com/video/BV1VkgK6NEZS
7
ARTS 0809: 反转链表、Shopify 如何用 MySQL 解决超卖与 AI 时代程序员的价值
5
译文:《SwiftUI 七年:平庸的故事》 SwiftUI 在 2019 年高调发布,本应成为苹果全平台成熟、可量产的 UI 未来。七年过去,到了 2026 年,它仍像一场永不结束的 beta:布局难预期、性能不稳、数据流混乱,还几乎没有可靠的向后兼容,开发者被迫写一堆 shim 和 workaround。 作者用苹果官方教程(甚至是有问题的)以及与 UIKit 的对比说明:SwiftUI 用“看起来方便”换掉了精确的工程控制。更深一层,他认为这反映了苹果从 Cocoa、Aqua、Auto Layout 那种不妥协的工艺,转向“够用就行”的企业文化。 SwiftUI 为何存在 苹果并非单纯想提供更好工具,而是不得不应对竞争:React、React Native、Flutter 让“一套代码多端跑”变得诱人;Mac 上原生应用又日渐被网页和 Electron 吃掉。SwiftUI 要同时拴住原生生态、并降低移植到 Mac 的成本。卖点是:响应式数据流、声明式布局、跨平台复用。 数据流 “单一数据源”听起来很美,实际却是 @State、@Binding、ObservedObject,再到 Observation / @Observable 的不断换代。你很难确定视图会更新几次、为何更新;它该忽略的变化会反应,该关心的变化又可能忽略 - - 像个黑盒。 布局系统 基于尺寸协商的布局在 Keynote 里很合理,做浮动视图、自定义侧边栏时却极度不稳定。官方教程里一个很普通的侧边栏,多年仍有问题。布局脆弱到最后往往只能上 GeometryReader - - 一旦用了,声明式优势就没了,还要手算坐标,而且下一版布局规则一变,数学还得重写。 API 稳定与功能对等 代码里满是 if #available。滚动收起键盘要到 iOS 16;工具栏定制很晚才来;网络图片 AsyncImage 要到 iOS 15,缓存相关 API 到 2026 年 7 月仍在 beta。旧 API 常被换掉(如 NavigationView → NavigationStack),开发者要维护多套实现,等于替苹果做 QA。对比 Android 的 Jetpack Compose 可作为依赖打包回退到旧设备,SwiftUI 做不到“写最新 API、稳定回退”。 性能 在真实对比里,即便做了后台解码等优化,SwiftUI 图片网格滚动仍明显不如 UIKit。若展示一堆 JPEG 都得靠顶级芯片撑,架构本身就有问题。 跨平台神话 苹果说的是“学一次、到处用”,不是“写一次、到处跑”。iOS 上学到的布局很少直接适用 Mac;同一套 view 跨平台实现也不一致。结果常变成:学一次、再学一次、某处能用、处处要调。 哲学转向 最大的问题是“够用就行”:覆盖 90% 用例就算成功,用 velocity 掩盖质量下降。作者列举系统与一线应用中的各种瑕疵,认为这不是偶然,而是苹果主动降低质量门槛 - - 所以即使过了七年,他仍不信任 SwiftUI。 结论 对构建稳定、高性能、可维护系统真正重要的部分,SwiftUI 几乎都有问题。它不是“极差”,而是平庸 - - 用假便利换真精度,要么你花时间给框架打补丁,要么把半成品发出去。作者更宁愿继续用“遗留”的 UIKit / AppKit。 最后小总结: 最让人不能接受的不是“SwiftUI 还有 bug”,而是它把“看起来很快”当成了工程上的完成态。声明式、预览、跨平台,每一项都在秀高级感,可真正写进业务后,你面对的是难预测的重绘、脆弱的布局、层层 #available,以及把兼容和排错外包给业务方的现实。七年够长了,若还靠“框架还年轻”解释,那更像是对标准的侮辱。技术选型从来不只是语法偏好,而是你选的是可预期性、可维护成本,以及对用户体验的态度。平庸的“成功”往往比明显失败更危险 ,你说它能上线、能 demo、能交差,但是却在细节里一点点磨损信任。工具可以换代,但对质量的要求不该跟着一起降级。
4
ARTS 0802: 合并有序链表、AI 时代的技术断层与 TCP 200ms 延迟之谜
5
下载 APP