feat: add structured output ladder with bounded feedback retries

This commit is contained in:
2026-07-20 07:22:03 -04:00
parent 7608958d0e
commit 936895919c
4 changed files with 370 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
"""JsonRepairStrategy: 阶梯②修复(D7/D14;蓝本 VT core/agent/loop.py:341-377)。
处理链: 围栏剥离 → json_repair → json.loads → 可选 normalize 钩子。
业务 schema 相关的变体归一化(如 VT `_normalize_action` 的 DeepSeek 平铺
收拢)**不入库**——零业务假设铁律;业务侧经 `normalize` 注入自带函数。
"""
from __future__ import annotations
import json
import re
from typing import TYPE_CHECKING, Any
from polygateway.errors import ResultInvalidError
if TYPE_CHECKING:
from collections.abc import Callable
try:
from json_repair import repair_json
except ImportError as _exc: # pragma: no cover - 依赖缺失路径
repair_json = None
_IMPORT_ERROR = _exc
else:
_IMPORT_ERROR = None
# VT loop.py:37 同款围栏正则
_CODE_FENCE_RE = re.compile(r"^\s*```(?:json)?\s*\n?|\n?\s*```\s*$")
class JsonRepairStrategy:
"""prompt 约定 + 事后修复策略;不改请求体(request_overlay 恒空)。"""
def __init__(self, normalize: Callable[[Any], Any] | None = None) -> None:
if repair_json is None:
raise ImportError(
"结构化输出需要 json_repair 包: pip install 'polygateway[structured]'"
) from _IMPORT_ERROR
self._normalize = normalize
def request_overlay(self, schema: dict[str, Any] | None) -> dict[str, Any]:
return {}
def parse(self, text: str) -> Any:
"""修复并解析;失败抛 ResultInvalidError(坏结果 ≠ 坏服务)。"""
stripped = _CODE_FENCE_RE.sub("", text).strip()
if not stripped:
raise ResultInvalidError("结构化输出为空", raw_text=text, repair_error="empty content")
try:
data = json.loads(repair_json(stripped))
except (json.JSONDecodeError, ValueError) as exc:
raise ResultInvalidError(
"JSON 修复失败", raw_text=text, repair_error=str(exc)
) from exc
if self._normalize is not None:
data = self._normalize(data)
return data
@@ -0,0 +1,32 @@
"""NativeSchemaStrategy: 阶梯①预防(D7/D14)——网关支持时用协议级约束。
请求侧注入 response_format(有 schema 用 json_schema 严格模式,无 schema
退为 json_object);响应侧仍复用修复链兜底——原生约束下模型偶发的围栏/
噪声不至于直接判死。
"""
from __future__ import annotations
from typing import Any
from polygateway.structured.json_repair import JsonRepairStrategy
class NativeSchemaStrategy:
"""response_format 注入策略;由装配层按 provider 注册表能力选择。"""
def __init__(self) -> None:
self._repair = JsonRepairStrategy()
def request_overlay(self, schema: dict[str, Any] | None) -> dict[str, Any]:
if schema is None:
return {"response_format": {"type": "json_object"}}
return {
"response_format": {
"type": "json_schema",
"json_schema": {"name": "structured_output", "strict": True, "schema": schema},
}
}
def parse(self, text: str) -> Any:
return self._repair.parse(text)