fix(llm): prevent "No tool call found" errors by ensuring tool call pair integrity#448
Conversation
…air integrity (dataelement#324) The error "No tool call found for function call output" (or "invalid params, tool result's tool id not found") occurs when the LLM API receives a function_call_output item without a matching function_call. This happens due to multiple compounding issues: 1. Context window truncation can break assistant(tool_calls)+tool pairs, leaving orphaned tool result messages without their assistant message. 2. The OpenAIResponsesClient._messages_to_input() had no validation to detect or remove orphaned function_call_output items before sending to the API. 3. All IM channels (Feishu, Slack, Teams, DingTalk, Discord, WeCom) passed tool_call DB records with role="tool_call" (invalid) instead of converting them to proper assistant+tool message pairs. 4. _messages_to_input() dropped dynamic_content from system messages for the Responses API. Changes: - Add _sanitize_input_items() to OpenAIResponsesClient that removes orphaned function_call/function_call_output items before sending to the API - Fix dynamic_content handling in _messages_to_input() for system messages - Create shared utility convert_chat_messages_to_llm_format() that properly converts tool_call DB records to assistant+tool pairs - Create shared utility truncate_messages_with_pair_integrity() that ensures tool call pair integrity during context window truncation - Update websocket.py to use shared utilities instead of inline logic - Fix all 9 IM channel history loading sites + A2A gateway to use convert_chat_messages_to_llm_format() - Add pair-aware truncation to _call_agent_llm in feishu.py Closes dataelement#324
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58bfa365cf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…gent_background hist_msgs is already sorted oldest→newest by list(reversed(...)), so passing reversed(hist_msgs) to _conv() flips the order again, causing the model to read conversation history in reverse chronological order.
…-integrity # Conflicts: # backend/app/api/feishu.py # backend/app/api/websocket.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9ab60c26bd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| tc_name = tc_data.get("name", "unknown") | ||
| tc_args = tc_data.get("args", {}) |
There was a problem hiding this comment.
Preserve legacy tool-call payload keys
When replaying any persisted tool_call rows that use the legacy/local schema (tool_name/arguments), this shared converter now turns them into a tool call named unknown with empty args because it only reads name/args. The Feishu helper being replaced explicitly accepted both schemas, so existing Feishu histories saved with the older shape lose the tool identity and arguments in subsequent LLM context; keep the same fallback keys here before using this for all channel history.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
根据chatgpt-codex-connector的代码审查来看。
在 backend/app/services/llm/utils.py 第 102-103 行,convert_chat_messages_to_llm_format() 函数中:
tc_name = tc_data.get("name", "unknown") # ← 只认 name
tc_args = tc_data.get("args", {}) # ← 只认 args
chatgpt-codex-connector 指出:飞书等旧版本的 tool_call 记录里存的键名是 tool_name 和 arguments(不是 name 和 args)。统一改用这个新函数后,所有历史数据库里用旧格式存的工具调用记录,都会变成 name="unknown"、args={},导致 AI 在历史上下文中完全看不懂原本的工具操作。
chatgpt-codex-connector 建议加上 fallback:同时兼容两种键名,比如:
tc_name = tc_data.get("name") or tc_data.get("tool_name", "unknown")
tc_args = tc_data.get("args") or tc_data.get("arguments", {})
@yaojin3616 你需要我chatgpt-codex-connector提出的问题解决并且提交修复到主线吗?
…air integrity (#324)
The error "No tool call found for function call output" (or "invalid params, tool result's tool id not found") occurs when the LLM API receives a function_call_output item without a matching function_call. This happens due to multiple compounding issues:
Context window truncation can break assistant(tool_calls)+tool pairs, leaving orphaned tool result messages without their assistant message.
The OpenAIResponsesClient._messages_to_input() had no validation to detect or remove orphaned function_call_output items before sending to the API.
All IM channels (Feishu, Slack, Teams, DingTalk, Discord, WeCom) passed tool_call DB records with role="tool_call" (invalid) instead of converting them to proper assistant+tool message pairs.
_messages_to_input() dropped dynamic_content from system messages for the Responses API.
Changes:
Closes #324
Summary
Checklist
Fixes #324
@Apache012 @dataelement-dev
问题
对话轮次多了之后,Clawith 会频繁报错:"No tool call found for function call output...",导致对话无法继续。
原因
Clawith 在调用工具(如搜索、代码执行等)时,需要把"工具调用"和"工具结果"成对发送给 AI 模型。但之前有两处场景会把这对关系拆散:
当对话历史太长需要裁剪时,可能只保留了"结果"而丢掉了"调用",AI 就找不到对应的调用记录了;
飞书、钉钉等 IM 渠道在加载历史消息时,没有正确处理工具相关的消息格式,也会导致配对丢失。
修复
在发送给 AI 之前,自动检查并移除没有配对的工具消息,从根源避免报错;
修复了所有 IM 渠道(飞书、钉钉、Teams、Slack、Discord、企微等)的历史消息加载逻辑,确保工具调用和结果始终成对;
改进了对话历史裁剪逻辑,裁剪时保证不会把一对工具消息拆开。
涉及文件
backend/app/services/llm/client.py — 发送前自动校验并清理孤立消息
backend/app/services/llm/utils.py — 新增共享的消息转换和裁剪工具函数
backend/app/api/websocket.py — 使用共享工具函数
backend/app/api/feishu.py, dingtalk.py, teams.py, slack.py, discord_bot.py, wecom.py, gateway.py — 修复 IM 渠道历史消息处理
backend/app/services/discord_gateway.py, wecom_stream.py — 同上