issue: a tool call whose id an earlier round reused is dropped, orphaning its result

当 provider 在同一个 turn 的多轮工具调用中重复使用同一个 tool call id(例如每轮都从 :0 重新编号)时,Open WebUI 的工具循环会丢掉这一轮的 function_call ,却保留它的 function_call_output ,导致持久化的 turn 里出现没

快速结论:当 provider 在同一个 turn 的多轮工具调用中重复使用同一个 tool call id(例如每轮都从 :0 重新编号)时,Open WebUI 的工具循环会丢掉这一轮的 function_call,却保留它的 function_call_output,导致持久化的 turn 里出现没有对应 call 的孤儿 result;下一次请求会被 provider 拒绝,报 tool_call_id ... does not match any tool call in the preceding assistant messages。优先排查你使用的 provider 是否按轮次而非按整个 turn 编号。

适用环境:Issue 已确认环境为 Open WebUI v0.11.0(pip 安装,Windows),模型 moonshotai/kimi-k3 经 OpenRouter 连接,使用原生 function calling(默认)且工具通过 MCP 提供。Issue 中提到 dev HEAD 为 c1c81f8,受影响逻辑仍未改动。

最快修复方案:暂无已发布版本的确认一步修复方案。Issue 中验证通过的候选方案是在去重(dedup)运行之前,对“已被复用且已有对应 function_call_output 的 id”重命名,使每一轮的 call 与 result 重新配对;该方案的经验证参考实现位于 https://github.com/omrgpt/open-webui/tree/fix/tool-call-id-reuse,尚未合入主分支。

注意事项:另一个更小的改法——把 existing_call_ids 限定为“尚未得到结果的 call”——虽然能恢复配对,但会留下两个共享同一 id 的 function_call,而状态更新处约 L5095 会在第一个匹配处 break,导致第二轮的 call 永远停留在 in_progress。此外,坏掉的 turn 已写入聊天记录,单纯重试不会成功,必须先删除该 turn。

问题场景

在 Open WebUI v0.11.0 中,挂载工具并通过 OpenRouter 使用 moonshotai/kimi-k3 等模型进行原生 function calling 时,如果某个提问需要 5–6 轮并行工具调用,就可能触发该问题。具体条件是:provider 按“每轮”而非“每个 turn”给工具调用编号,例如每轮都从 search_web:0、search_web:1 重新开始,于是第二轮的 id 与第一轮完全重复。此时工具循环会丢弃第二轮的 function_call,但为这轮每个调用都追加了 function_call_output。

相同的模型也可能由使用全 turn 统一计数器(形如 <function_name>_<n>)的上游提供,这种上游不会产生 id 冲突。因此同一个聊天可能这一轮正常、下一轮报错,用户侧看不出差异。

报错原文

tool message tool_call_id 'search_web:1' does not match any tool call in the preceding assistant messages

坏掉的 turn 在存储中的形态大致如下:

function_call         add_memory:0
function_call         search_web:1 … :4
function_call_output  add_memory:0, search_web:1 … :4     <- round one, fine
message
function_call         search_web:0                        <- round two; :1-:4 suppressed
function_call_output  search_web:0, :1, :2, :3, :4        <- four results with no call

在另一轮观测中,第二轮的每个 id 都发生冲突,于是 assistant 消息里完全没有 tool_calls,五个结果全部成为孤儿。

原因分析

问题出在 backend/open_webui/utils/middleware.py。output 每次请求只构建一次(约 L4088),但工具循环会在一次请求内跑很多轮(约 L4916)。每一轮判断是否追加 function_call 时,检查的是 id 是否出现在整个 output 里,包括已经完成的轮次:

existing_call_ids = {item.get('call_id') for item in output if item.get('type') == 'function_call'}
for tc in response_tool_calls:
    call_id = tc.get('id', '')
    if call_id not in existing_call_ids:
        output.append({'type': 'function_call', ...})

与此同时,results 是每轮重建的(约 L4952),并且会为该轮的每个调用都追加一个 function_call_output(约 L5117)。因此 id 被复用的那一轮,call 被去重逻辑跳过,result 却照常写入。

sanitize_tool_pairs 本应拦住这种不配对,但没有:它是在整个会话范围内匹配 id,而不是按 assistant block 分别匹配。孤儿 result 在更早的 block 下确实能找到同名 call,所以被判定为合法。Issue 中已通过真实的 convert_output_to_messages + reconcile_tool_pairs 路径复现:assistant block 只声明了 search_web:0,后面却跟着五个 tool 消息(:0–:4),严格校验的 provider 会直接拒绝。reconcile_tool_pairs 因为 id 在全局层面能配对上而原样放行。

相关问题 #28570 还报告了两个衍生缺陷,同一分支已验证覆盖:一是早轮 function_call 的参数被晚轮覆盖(复用 id 在去重和状态更新之前被重命名后,每轮的 item 只匹配自身,参数与状态各自保留,两轮都到达 completed);二是第二个守卫同样失效,即上述“全会话范围 vs 按 block”的差距,已另开 #28937 提议对 sanitize_tool_pairs 和 reconcile_tool_pairs 都改为按 block 重写。

环境排查

  • 确认 Open WebUI 版本是否为 v0.11.0,以及是否为 pip 安装(Issue 环境为 Windows)。
  • 确认模型与接入方式:moonshotai/kimi-k3 经 OpenRouter;确认使用的上游是按轮编号还是按整个 turn 编号。
  • 确认使用的是原生 function calling(默认)且工具经 MCP 提供。
  • 检查触发场景是否包含多轮并行工具调用(Issue 中 5–6 个并行调用可稳定复现)。
  • 确认 backend/open_webui/utils/middleware.py 中相关逻辑是否仍为 Issue 所述形态;Issue 称受影响代码在 dev HEAD(c1c81f8)上未改动。

解决步骤

  1. 先确认现象:检查该聊天已持久化的 turn 中,是否存在 function_call_output 的 id 没有同轮对应的 function_call,或某个 id 在多个轮次中重复出现。
  2. 删除已损坏的 turn。因为坏掉的 turn 已写入聊天记录,重试不会成功,且该聊天的每条后续消息都会以同样方式失败,直到该 turn 被删除。
  3. 临时规避:切换到使用整个 turn 统一计数器编号的上游/provider(Issue 提到同一模型的另一上游使用 <function_name>_<n>,不会碰撞,因此不会触发)。这条属于环境层面的规避,不是代码修复。
  4. 若需要根治,参考已验证的候选修复:在去重逻辑运行之前,对“已被复用且已有对应 function_call_output 的 id”重命名。作用域严格限定为已得到答复的 id,从而不影响去重原本要处理的场景——即已经流式写入 output 但仍在等待结果的 Responses API item 不会被重命名,仍会被跳过。由于重命名发生在执行之前,追加的 function_call、该轮的结果以及状态更新都会使用新 id,配对与状态无需其他改动即可保持一致。参考实现见 https://github.com/omrgpt/open-webui/tree/fix/tool-call-id-reuse(作者表示按首次贡献指引需维护者同意后再开 PR)。
  5. 不要采用只把 existing_call_ids 限定为未答复调用的“小改法”:它能恢复配对,但会留下两个共享同一 id 的 function_call,而状态更新处约 L5095 会在第一个匹配处 break,第二轮的 call 将永远停在 in_progress。
  6. 如果关注第二个守卫(sanitize_tool_pairs / reconcile_tool_pairs 的全会话匹配问题),见 #28937 提出的按 block 重写;Issue 中已验证:格式正确的多轮历史输出与当前行为逐字节一致,损坏的历史 turn 会被修复为按 block 合法的形态,而不是在后续每次请求中被上游拒绝。

验证方法

用真实转换路径回放 Issue 场景:修复后每一轮的 function_call 都应只与自己那一轮的 result 配对,不再出现某一轮 call 被抑制而其 result 仍被写入的情况;reconcile_tool_pairs 对格式正确的 turn 仍应为 no-op。非回归检查方面,一个尚未得到答复的流式重复项仍应被正确去重(单个 function_call、单个 result)。确认该聊天中后续消息不再触发 tool_call_id ... does not match any tool call in the preceding assistant messages,且第二轮及之后的调用状态能到达 completed 而不是长期停留在 in_progress。

参考来源

open-webui/open-webui #28305

相关 Issue:#24758、#12214、#27363、#28570(重复)、#28937。已验证的候选修复分支:omrgpt/open-webui fix/tool-call-id-reuse

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 27356

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注