LangChain4j NullPointerException 问题报告
问题概述
在使用 LangChain4j 处理大模型 API 的流式响应时,当 API 返回包含 tool_calls 的 SSE 事件且 id 字段为空字符串时,会触发 NullPointerException,导致流式响应中断。
问题详情
错误信息
java.lang.NullPointerException: null
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1693)
at dev.langchain4j.model.openai.OpenAiStreamingResponseBuilder.append(OpenAiStreamingResponseBuilder.java:160)
at dev.langchain4j.model.openai.OpenAiStreamingResponseBuilder.append(OpenAiStreamingResponseBuilder.java:85)
at dev.langchain4j.model.openai.OpenAiStreamingChatModel.lambda$doChat$0(OpenAiStreamingChatModel.java:150)
at dev.langchain4j.model.openai.internal.StreamingRequestExecutor$2.onEvent(StreamingRequestExecutor.java:140)
at dev.langchain4j.http.client.log.LoggingHttpClient$1.onEvent(LoggingHttpClient.java:88)
at dev.langchain4j.http.client.sse.DefaultServerSentEventParser.lambda$parse$0(DefaultServerSentEventParser.java:28)
环境信息
- LangChain4j 版本: 0.34.0 (推测,基于代码结构)
- Java 版本: 17.0.14
- Spring Boot 版本: 3.2.3
- API 提供商: https://apis.iflow.cn/v1
- 模型: qwen3-max
根本原因分析
问题触发场景
当大模型 API 返回包含 tool_calls 的流式响应时,tool_calls 的 id 字段在流式传输过程中被分片。某些 SSE 事件中的 id 字段为空字符串 "",导致 LangChain4j 的 OpenAiStreamingResponseBuilder.append 方法在处理时出现异常。
日志证据
1. 第一个 tool_call 事件(正常)
{
"id":"6b3b4f4b-0278-4d67-ac50-10b4169b253c",
"object":"chat.completion.chunk",
"choices":[{
"index":0,
"delta":{
"role":"assistant",
"content":"",
"tool_calls":[{
"id":"call_dfe56453fbb941e7adfebd27",
"type":"function",
"function":{"arguments":"","name":"maps_geo"}
}]
}
}]
}
关键点: id 字段有完整值: "call_dfe56453fbb941e7adfebd27"
2. 后续 tool_call 事件(异常)
{
"id":"6b3b4f4b-0278-4d67-ac50-10b4169b253c",
"object":"chat.completion.chunk",
"choices":[{
"index":0,
"delta":{
"role":"assistant",
"content":"",
"tool_calls":[{
"id":"",
"type":"function",
"function":{"arguments":"{\"address\": \"杭州"}}
}]
}
}]
}
关键点: id 字段为空字符串: ""
3. 错误发生时间点
从日志可以看到:
- 第 604 行:第一个 tool_call 事件被处理
- 第 605 行:立即触发
NullPointerException - 第 650-654 行:后续的 tool_call 事件继续到达,但
id字段为空字符串
代码位置
错误发生在 dev.langchain4j.model.openai.OpenAiStreamingResponseBuilder.append 方法的第 160 行:
// 推测的代码逻辑(基于错误堆栈)
Map<String, ...> map = ...;
String key = toolCall.getId(); // 可能返回空字符串 ""
map.computeIfAbsent(key, ...); // 当 key 为空字符串或 null 时触发异常
复现步骤
-
配置环境
- 使用 LangChain4j 的
OpenAiStreamingChatModel - 配置支持工具调用的 Agent(如
TravelAgent) - 启用日志记录以查看原始 SSE 数据
- 使用 LangChain4j 的
-
触发条件
- 发送一个会触发工具调用的消息(如:“规划一下杭州青山湖骑行路线”)
- 确保 Agent 会调用工具(如高德地图 MCP 工具)
-
观察结果
- 流式响应开始正常返回
- 当第一个 tool_call 事件到达时,响应继续
- 当后续 tool_call 事件的
id字段为空字符串时,触发NullPointerException - 流式响应中断,前端显示不完整
完整请求示例
// 用户消息
String userMessage = "规划一下杭州青山湖骑行路线,要求:1.起点为青山湖圣鹤码头停车场;2.骑行里程15~20公里;3.骑行到一半后原路返回;";
// 通过 RouterService 路由到 TravelAgent
Flux<String> response = routerService.routeAndStreamChat(userMessage);
影响范围
- 功能影响: 流式响应中断,用户无法看到完整的 AI 回复
- 用户体验: 前端显示不完整,可能显示错误信息
- 系统稳定性: 虽然应用不会崩溃(已有错误处理),但功能不完整
解决方案建议
API 提供商修复(推荐)
问题: API 返回的 tool_calls 的 id 字段在流式传输中被分片,导致某些事件中 id 为空字符串。
建议: 联系 API 提供商(apis.iflow.cn),要求:
- 确保 tool_calls 的
id字段在第一个包含该 tool_call 的事件中完整提供 - 后续事件中,如果
id已存在,应保持原值,而不是返回空字符串 - 遵循 OpenAI 的流式响应格式规范
参考: OpenAI 的流式响应格式要求 tool_calls 的 id 应该在第一个包含该 tool_call 的事件中完整提供。
从日志中提取的关键 SSE 事件:
-
正常文本响应事件:
data: {"id":"...","choices":[{"delta":{"content":"您好"}}]} -
第一个 tool_call 事件(正常):
data: {"id":"...","choices":[{"delta":{"tool_calls":[{"id":"call_dfe56453fbb941e7adfebd27","type":"function","function":{"name":"maps_geo"}}]}}]} -
后续 tool_call 事件(异常,id 为空):
data: {"id":"...","choices":[{"delta":{"tool_calls":[{"id":"","type":"function","function":{"arguments":"{\"address\": \"杭州"}}]}}]}