From c769dd20f7767be08efce324a19990d5dda6654c Mon Sep 17 00:00:00 2001 From: hkdong Date: Wed, 27 May 2026 18:07:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=80=BB=E6=8E=A7?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E5=85=B1=E8=AF=86=E5=90=8E=E4=BB=8D?= =?UTF-8?q?=E8=A2=AB=E9=94=99=E5=88=A4=E4=B8=BA=E7=BB=A7=E7=BB=AD=E6=B4=BE?= =?UTF-8?q?=E6=B4=BB=E7=9A=84=E6=AD=BB=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _find_dispatch_message 原实现会一路向前翻历史,直到撞到任意一条带 tool_calls 的 AIMessage,导致本轮 LLM 即使共识(tool_calls=[]),也会 被上一轮的派活 AI 翻出来再用一次,前端因此把同样的 task 又发给子智能体, 形成"嘴上共识、手上派活"的死循环。 改成只看最新一条 AIMessage(跳过末尾 skill_stop / clarification ack ToolMessage 即可),让 tool_calls 是否为空作为本轮的唯一权威信号。 Co-Authored-By: Claude Opus 4.7 --- .claude/settings.json | 10 ++++++ .../app/gateway/routers/multi_agent.py | 33 +++++++++++-------- offline-backend-20260512/backend/config.yaml | 2 +- 3 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..e02ee52c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "PowerShell(uv --version)", + "PowerShell(netstat -ano)", + "PowerShell(Get-NetTCPConnection -LocalPort 8001 -ErrorAction SilentlyContinue)", + "PowerShell(Get-Process -Id 20560 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path,StartTime; try { \\(Invoke-WebRequest -Uri \"http://127.0.0.1:8001/health\" -UseBasicParsing -TimeoutSec 5\\).Content } catch { \"health check failed: $_\" })" + ] + } +} diff --git a/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py b/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py index 97854298..bcf6bb01 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py +++ b/offline-backend-20260512/backend/app/gateway/routers/multi_agent.py @@ -764,25 +764,30 @@ def _find_trailing_ai_text(messages: list[dict[str, Any]], *, exclude_text: str def _find_dispatch_message(messages: list[dict[str, Any]]) -> dict[str, Any]: - """Locate the latest AIMessage that carries dispatch tool calls. + """Locate the latest AIMessage from THIS turn. - The very last message can be a skill-stop ToolMessage injected by - ``SkillStopMiddleware`` when it intercepts the dispatch, or a clarification - ToolMessage. Either way, the actionable tool_calls live on the AIMessage - *before* it, so we walk backwards until we hit one with ``tool_calls``. - Returns the latest AIMessage when none of them carry tool_calls so the - caller can still extract its text content. + Trailing ToolMessages (skill_stop / clarification ack injected by + ``SkillStopMiddleware`` or ``ClarificationMiddleware``) are skipped because + the actionable tool_calls live on the AIMessage that emitted them. We stop + at the **first AIMessage encountered** — its ``tool_calls`` (or lack + thereof) is the authoritative signal for this turn. + + 历史踩坑:之前实现是"一直往前翻直到撞到带 tool_calls 的 AI",看似优雅, + 实际上让 leader 永远收敛不了:本轮 LLM 不论怎么共识,只要历史上派过活, + 上一轮的派活 AI 就会被翻出来再用一次,前端会把同样的 task 又发给子智能体, + 陷入死循环。现在停在最新一条 AI 上,把判定交给调用方:`tool_calls=[]` → + 共识分支,有 tool_calls → 派活分支。 """ - last_ai: dict[str, Any] = {} for msg in reversed(messages): if not isinstance(msg, dict): continue - if (msg.get("type") or "").lower() in ("ai", "aimessage", "aimessagechunk"): - if not last_ai: - last_ai = msg - if msg.get("tool_calls"): - return msg - return last_ai + msg_type = (msg.get("type") or "").lower() + if msg_type in ("tool", "toolmessage"): + continue # 跳过末尾 skill_stop / clarification ack + if msg_type in ("ai", "aimessage", "aimessagechunk"): + return msg + break # 撞到 Human/其他类型,本轮没 AI(异常路径,留空让上游报错) + return {} async def _leader_run( diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index ac83d28f..fbf628ce 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -14,7 +14,7 @@ models: display_name: DeepSeek Chat use: langchain_openai:ChatOpenAI model: deepseek-chat - api_key: sk-e1c22b7931dc446f8b67e57878edd105 + api_key: sk-1520028668cb4cc388ed8315ff1e856e base_url: https://api.deepseek.com request_timeout: 600.0 max_retries: 2 From aeebf537394af1994cbc36957880ac237bde30e9 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 27 May 2026 19:15:09 +0800 Subject: [PATCH 2/2] api --- offline-backend-20260512/backend/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index ac83d28f..fbf628ce 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -14,7 +14,7 @@ models: display_name: DeepSeek Chat use: langchain_openai:ChatOpenAI model: deepseek-chat - api_key: sk-e1c22b7931dc446f8b67e57878edd105 + api_key: sk-1520028668cb4cc388ed8315ff1e856e base_url: https://api.deepseek.com request_timeout: 600.0 max_retries: 2