fix(agent): normalize fenced and flat-args LLM outputs in parser

核心算法 #10(Agent Loop):仅加固 _parse_response 解析路径——
剥除 ```json 围栏 + 收拢 action 平铺参数(deepseek 稳定输出变体,
案例 637-3/615-3 三连拒 0 步阵亡)。Thinking+JSON 协议、json_repair
兜底链、hook 时序与步数语义均未改动。
This commit is contained in:
2026-07-11 08:16:28 -04:00
parent 39c6352781
commit 8d84d5e236
2 changed files with 84 additions and 3 deletions
+27 -3
View File
@@ -18,6 +18,7 @@ TRM4 → TRM5 有意变更(非简化):
from __future__ import annotations
import json
import re
from typing import TYPE_CHECKING, Any
import pluggy
@@ -31,6 +32,9 @@ if TYPE_CHECKING:
from core.protocols import LLMProvider
from core.types import LLMResponse
# deepseek 等模型稳定输出变体:```json 围栏包裹 JSON 体
_CODE_FENCE_RE = re.compile(r"^\s*```(?:json)?\s*\n?|\n?\s*```\s*$")
async def _call_hook(hook: Any, **kwargs: Any) -> list[Any]:
"""调用 pluggy hook 并 await 异步返回值。
@@ -267,7 +271,8 @@ class AgentLoop:
) -> tuple[str, dict, dict, str, dict, str] | None:
"""从 LLMResponse 中提取结构化决策数据。
解析流程: content → repair_json → json.loads → 校验 action/tool/args。
解析流程: content → 剥除 ```json 围栏 → repair_json → json.loads
→ 收拢 action 平铺参数 → 校验 action/tool/args。
参数:
response: LLMResponse 实例。
@@ -282,7 +287,7 @@ class AgentLoop:
if not content.strip():
return None
repaired = repair_json(content)
repaired = repair_json(_CODE_FENCE_RE.sub("", content).strip())
try:
data = json.loads(repaired)
except (json.JSONDecodeError, ValueError):
@@ -291,7 +296,7 @@ class AgentLoop:
if not isinstance(data, dict) or "action" not in data:
return None
action = data["action"]
action = self._normalize_action(data["action"])
if not isinstance(action, dict) or "tool" not in action or "args" not in action:
return None
@@ -299,6 +304,25 @@ class AgentLoop:
plan = data.get("plan", {})
return thought, reflect, plan, content, action, response.call_id
@staticmethod
def _normalize_action(action: Any) -> Any:
"""收拢 deepseek 变体的 action 平铺参数。
deepseek 等模型稳定输出变体: 工具参数平铺在 action 下(缺 args
嵌套),确定性收拢为标准 {"tool": ..., "args": {...}} 结构。
标准嵌套结构与非法结构均原样返回,由调用方校验。
参数:
action: 从 LLM 输出解析出的 action 字段(任意类型)。
返回:
归一化后的 action(仅平铺变体被改写,其余原样返回)。
"""
if isinstance(action, dict) and "tool" in action and "args" not in action:
flat_args = {k: v for k, v in action.items() if k != "tool"}
return {"tool": action["tool"], "args": flat_args}
return action
async def _execute_tool(
self,
dispatcher: ToolDispatcher,