fix: tool_calls 流中断自愈——400 配对错误时移除不完整段重试一次;加消息结构诊断日志

This commit is contained in:
2026-09-04 13:43:20 +08:00
parent c0c1be05f8
commit 1560a13d86
2 changed files with 61 additions and 23 deletions
+51 -23
View File
@@ -132,6 +132,22 @@ class Agent:
pass pass
return text_result, image_b64 return text_result, image_b64
def _repair_tool_messages(self):
"""修复 tool_calls 配对不完整:从尾部移除「assistant 带 tool_calls 但其后
tool 回应不足」的消息段(流中断可能丢失分片,400 重试前自愈)。"""
for i in range(len(self.messages) - 1, -1, -1):
m = self.messages[i]
if m.get("role") == "assistant" and m.get("tool_calls"):
# 统计其后 tool 消息数是否匹配
need = len(m["tool_calls"])
have = sum(1 for x in self.messages[i + 1:]
if x.get("role") == "tool")
if have < need:
_log.warning("修复不完整 tool_calls 段(need=%d have=%d),回退 %d 条消息",
need, have, len(self.messages) - i)
self.messages = self.messages[:i]
return
# ---------- 主循环(流式) ---------- # ---------- 主循环(流式) ----------
async def run_stream(self, prompt: str, serial: str = "", async def run_stream(self, prompt: str, serial: str = "",
history=None, on_delta=None, on_tool=None, history=None, on_delta=None, on_tool=None,
@@ -166,29 +182,41 @@ class Agent:
content_parts = [] content_parts = []
tool_acc = {} # index -> {id, name, args} tool_acc = {} # index -> {id, name, args}
has_tool = False has_tool = False
async for chunk in self._chat_stream(): retried = False
choice = (chunk.get("choices") or [{}])[0] while True:
delta = choice.get("delta") or {} try:
text = delta.get("content") async for chunk in self._chat_stream():
if text: choice = (chunk.get("choices") or [{}])[0]
content_parts.append(text) delta = choice.get("delta") or {}
if on_delta: text = delta.get("content")
on_delta(text, "content") if text:
rtext = delta.get("reasoning_content") content_parts.append(text)
if rtext: if on_delta:
if on_delta: on_delta(text, "content")
on_delta(rtext, "reasoning") rtext = delta.get("reasoning_content")
for tc in delta.get("tool_calls") or []: if rtext:
has_tool = True if on_delta:
idx = tc.get("index", 0) on_delta(rtext, "reasoning")
acc = tool_acc.setdefault(idx, {"id": "", "name": "", "args": ""}) for tc in delta.get("tool_calls") or []:
if tc.get("id"): has_tool = True
acc["id"] = tc["id"] idx = tc.get("index", 0)
fn = tc.get("function") or {} acc = tool_acc.setdefault(idx, {"id": "", "name": "", "args": ""})
if fn.get("name"): if tc.get("id"):
acc["name"] += fn["name"] acc["id"] = tc["id"]
if fn.get("arguments"): fn = tc.get("function") or {}
acc["args"] += fn["arguments"] if fn.get("name"):
acc["name"] += fn["name"]
if fn.get("arguments"):
acc["args"] += fn["arguments"]
break
except RuntimeError as e:
# 流中断导致 tool_calls 分片丢失:修复后重试一次
if ("tool_calls" in str(e) or "must be followed" in str(e)) and not retried:
_log.warning("tool_calls 消息不完整,自愈重试")
self._repair_tool_messages()
retried = True
continue
raise
full_content = "".join(content_parts) full_content = "".join(content_parts)
+10
View File
@@ -367,6 +367,16 @@ def _agent_thread(run_id, prompt, serial, cfg):
_log.warning(f"经验保存异常: {e}") _log.warning(f"经验保存异常: {e}")
except Exception as e: except Exception as e:
_log.warning(f"Agent 运行异常: {e}") _log.warning(f"Agent 运行异常: {e}")
# 诊断:打印消息结构(tool_calls 与 tool 消息配对检查)
try:
roles = [m.get("role", "?") for m in agent.messages]
tcs = sum(1 for m in agent.messages
if m.get("tool_calls") and isinstance(m.get("tool_calls"), list))
tools_msg = sum(1 for m in agent.messages if m.get("role") == "tool")
_log.warning(f"诊断 messages: {len(agent.messages)} 条 roles={roles[-8:]} "
f"tool_calls消息={tcs} tool回应={tools_msg}")
except Exception:
pass
with _lock: with _lock:
_run["state"] = "error" _run["state"] = "error"
_run["error"] = f"{type(e).__name__}: {str(e)[:200]}" _run["error"] = f"{type(e).__name__}: {str(e)[:200]}"