1. 简要描述
在使用 Spring AI 结合 DeepSeek-V3/R1 启用 MCP 或本地工具调用时,工具执行后的下一轮对话常遭遇 HTTP 400 Bad Request。
根本原因:Conversation / MessageWindowConversation 写入历史时缺乏工具链完整性校验。流式聚合产生的异常消息或悬空的 ToolResponseMessage 写入了历史,破坏了 DeepSeek 的严苛格式限制:任何 ToolResponseMessage 前面必须紧邻一个包含匹配 tool_calls 的 AssistantMessage。
2. 错误日志
org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from POST https://api.deepseek.com/v1/chat/completions
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:321)
...
at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:188)
API 报错详情:messages with role "tool" must be a response to a preceding message with "tool_calls"
3. 影响范围 (Impact)
- 大模型:DeepSeek-V3 / DeepSeek-R1 (对工具调用历史顺序校验极严的对象)
- 场景:Spring AI 流式对话 + MCP / 本地 Function Calling
- 后果:当前轮工具调用正常,但下一轮用户发送消息时直接报错 400。一旦历史写脏,会话将持续失败,除非清空历史。
4. 关键缺陷代码 (Code Issue)
① Conversation#add (无条件追加,无校验)
// com/xiaozhi/ai/llm/memory/Conversation.java
public synchronized void add(Message message) {
if(message instanceof UserMessage userMsg){ ... }
if(message instanceof AssistantMessage assistantMessage){ messages.add(assistantMessage); return; }
if(message instanceof ToolResponseMessage toolResponseMessage){ messages.add(toolResponseMessage); }
}
- 问题:不校验
ToolResponseMessage 之前是否存在合法的 AssistantMessage(tool_calls),允许悬空响应直接入库。
② Conversation#addToolCallChain (添加工具链绕过校验)
public synchronized void addToolCallChain(AssistantMessage toolCallMsg, ToolResponseMessage toolResponse) {
messages.add(toolCallMsg);
messages.add(toolResponse);
}
- 问题:直接调用
messages.add 绕过了 add(Message) 的潜在校验。未校验 toolCallMsg 内是否真的有 tool_calls,未校验 toolResponse 的有效性,也未校验两者之间的 id/name 是否匹配。
③ MessageWindowConversation#add (覆写后绕过父类)
// com/xiaozhi/ai/llm/memory/MessageWindowConversation.java
@Override
public synchronized void add(Message message) {
if (message instanceof UserMessage || message instanceof AssistantMessage || message instanceof ToolResponseMessage) {
messages.add(message); // 直接写入,绕过了父类 Conversation 的任何拦截修复逻辑
}
}
5. 预期 vs 实际历史链 (Expected vs Actual)
- 期望的合法历史:
UserMessage -> AssistantMessage(带 tool_calls) -> ToolResponseMessage -> AssistantMessage(最终文本回复,不带 tool_calls)
- 实际写脏的历史(触发 400):
UserMessage -> AssistantMessage(丢失 tool_calls) -> ToolResponseMessage -> AssistantMessage(最终文本)
或者:
UserMessage -> ToolResponseMessage(悬空) -> AssistantMessage(最终文本)
6. 建议修复方案 (设计思路与方向性伪代码)
6.1 设计原则 (Design Principles)
- 统一收敛校验:将所有消息写入逻辑收敛到统一的安全方法中(如
appendValidated(Message)),确保 add()、addToolCallChain() 以及子类 MessageWindowConversation#add 都复用相同的校验。
- 拒绝误杀最终回复:工具调用后最终的
AssistantMessage(final answer) 本身是不带 tool_calls 的。校验时绝不能因为“当前写入消息无 tool_calls 且上一条消息有 tool_calls”就粗暴拦截,必须允许正常流转,避免丢弃最终回复。
- ToolResponse 前置强校验:写入
ToolResponseMessage 之前,必须校验前置消息是否为合法的、携带匹配 tool_calls 的 AssistantMessage。
- 工具链原子性写入校验:
addToolCallChain 写入前,必须验证入参的非空性、tool_calls 存在性,以及 ToolResponse 与 tool_calls 的 id/name 匹配性。
6.2 方向性伪代码 (Directional Pseudo-code)
// com/xiaozhi/ai/llm/memory/Conversation.java
/**
* 统一的消息写入合法性校验(仅作为方向性设计参考,需处理最终回复等边界情况)
*/
protected synchronized boolean appendValidated(Message message) {
if (message instanceof UserMessage) {
return messages.add(message);
}
if (message instanceof AssistantMessage assistantMessage) {
// 实现注意:此处不可粗暴拦截无 tool_calls 的 AssistantMessage,
// 必须允许工具执行完后的 final answer (不带 tool_calls) 正常写入。
// 可主要用于防止完全等价的聚合消息重复写入。
return messages.add(assistantMessage);
}
if (message instanceof ToolResponseMessage toolResponse) {
// 核心防御:ToolResponse 前置强校验
if (messages.isEmpty()) return false;
Message last = messages.get(messages.size() - 1);
if (last instanceof AssistantMessage lastAssistant) {
// 校验前置大模型消息是否声明了工具调用,且响应列表非空
boolean hasCalls = lastAssistant.getToolCalls() != null && !lastAssistant.getToolCalls().isEmpty();
boolean hasResponses = toolResponse.getResponses() != null && !toolResponse.getResponses().isEmpty();
if (hasCalls && hasResponses) {
return messages.add(toolResponse);
}
}
log.warn("拒绝写入悬空或非法的 ToolResponseMessage");
return false;
}
return false;
}
public synchronized void add(Message message) {
appendValidated(message);
}
/**
* 规范 addToolCallChain 的入参校验并复用统一写入逻辑
*/
public synchronized void addToolCallChain(AssistantMessage toolCallMsg, ToolResponseMessage toolResponse) {
if (toolCallMsg == null || toolCallMsg.getToolCalls() == null || toolCallMsg.getToolCalls().isEmpty()) {
log.warn("拒绝写入非法工具链:AssistantMessage 缺少 tool_calls");
return;
}
if (toolResponse == null || toolResponse.getResponses() == null || toolResponse.getResponses().isEmpty()) {
log.warn("拒绝写入非法工具链:ToolResponseMessage 为空");
return;
}
// 校验 toolCallMsg 与 toolResponse 之间的 id / name 匹配性
if (!matchesToolCalls(toolCallMsg, toolResponse)) {
log.warn("拒绝写入非法工具链:ToolResponse 与 tool_calls 不匹配");
return;
}
// 复用 append 逻辑,确保安全写入
if (appendValidated(toolCallMsg)) {
appendValidated(toolResponse);
}
}
6.3 修复 MessageWindowConversation#add
// com/xiaozhi/ai/llm/memory/MessageWindowConversation.java
@Override
public synchronized void add(Message message) {
// 严禁在子类中直接 messages.add(message) 绕过父类校验
super.add(message);
}
7. 验收标准 (Acceptance Criteria)
- 链路自愈与强校验:
Conversation 中绝不允许写入悬空、乱序或与 tool_calls 不匹配的 ToolResponseMessage。
- 确保回复完整:校验逻辑不会误杀工具调用后合法的最终文本回复(即使
addToolCallChain 因异常未能写入,也不能吞掉最终文本)。
- 子类行为一致:
MessageWindowConversation 不再通过覆写绕过父类的安全过滤网。
- 单元测试覆盖:
- 验证正常工具链 + 最终文本回复可以顺利写入。
- 验证悬空的
ToolResponseMessage 无法写入。
- 验证
addToolCallChain 能够有效识别并拦截参数不匹配、空属性的非法调用链。
1. 简要描述
在使用 Spring AI 结合 DeepSeek-V3/R1 启用 MCP 或本地工具调用时,工具执行后的下一轮对话常遭遇 HTTP
400 Bad Request。根本原因:
Conversation/MessageWindowConversation写入历史时缺乏工具链完整性校验。流式聚合产生的异常消息或悬空的ToolResponseMessage写入了历史,破坏了 DeepSeek 的严苛格式限制:任何ToolResponseMessage前面必须紧邻一个包含匹配tool_calls的AssistantMessage。2. 错误日志
API 报错详情:
messages with role "tool" must be a response to a preceding message with "tool_calls"3. 影响范围 (Impact)
4. 关键缺陷代码 (Code Issue)
①
Conversation#add(无条件追加,无校验)ToolResponseMessage之前是否存在合法的AssistantMessage(tool_calls),允许悬空响应直接入库。②
Conversation#addToolCallChain(添加工具链绕过校验)messages.add绕过了add(Message)的潜在校验。未校验toolCallMsg内是否真的有tool_calls,未校验toolResponse的有效性,也未校验两者之间的id/name是否匹配。③
MessageWindowConversation#add(覆写后绕过父类)5. 预期 vs 实际历史链 (Expected vs Actual)
6. 建议修复方案 (设计思路与方向性伪代码)
6.1 设计原则 (Design Principles)
appendValidated(Message)),确保add()、addToolCallChain()以及子类MessageWindowConversation#add都复用相同的校验。AssistantMessage(final answer)本身是不带tool_calls的。校验时绝不能因为“当前写入消息无tool_calls且上一条消息有tool_calls”就粗暴拦截,必须允许正常流转,避免丢弃最终回复。ToolResponseMessage之前,必须校验前置消息是否为合法的、携带匹配tool_calls的AssistantMessage。addToolCallChain写入前,必须验证入参的非空性、tool_calls存在性,以及ToolResponse与tool_calls的id/name匹配性。6.2 方向性伪代码 (Directional Pseudo-code)
6.3 修复
MessageWindowConversation#add7. 验收标准 (Acceptance Criteria)
Conversation中绝不允许写入悬空、乱序或与tool_calls不匹配的ToolResponseMessage。addToolCallChain因异常未能写入,也不能吞掉最终文本)。MessageWindowConversation不再通过覆写绕过父类的安全过滤网。ToolResponseMessage无法写入。addToolCallChain能够有效识别并拦截参数不匹配、空属性的非法调用链。