|
7 | 7 | import asyncio |
8 | 8 | import json |
9 | 9 | import logging |
10 | | -from typing import List, Optional, Tuple, Type, TypeVar |
| 10 | +from dataclasses import dataclass |
| 11 | +from typing import Generic, List, Optional, Tuple, Type, TypeVar |
11 | 12 |
|
12 | 13 | from json_repair import repair_json |
13 | 14 | from pydantic import BaseModel, ValidationError |
|
27 | 28 |
|
28 | 29 | T = TypeVar("T", bound=BaseModel) |
29 | 30 |
|
| 31 | + |
| 32 | +@dataclass(frozen=True) |
| 33 | +class StructuredJSONGenerateResult(Generic[T]): |
| 34 | + payload: Optional[T] |
| 35 | + failure_stage: str = "" |
| 36 | + errors: Tuple[str, ...] = () |
| 37 | + |
| 38 | + @property |
| 39 | + def ok(self) -> bool: |
| 40 | + return self.payload is not None |
| 41 | + |
30 | 42 | # 校验或解析失败后的额外重试轮数;总次数 = 1 + 该值,且不超过全局上限。 |
31 | 43 | DEFAULT_MAX_RETRIES = LLM_MAX_TOTAL_ATTEMPTS - 1 |
32 | 44 |
|
@@ -118,6 +130,25 @@ async def structured_json_generate( |
118 | 130 | max_retries: int = DEFAULT_MAX_RETRIES, |
119 | 131 | ) -> Optional[T]: |
120 | 132 | """调用 LLM 获取结构化 JSON 输出,并经过清洗、修复、校验管线。""" |
| 133 | + result = await structured_json_generate_with_status( |
| 134 | + llm=llm, |
| 135 | + prompt=prompt, |
| 136 | + config=config, |
| 137 | + schema_model=schema_model, |
| 138 | + max_retries=max_retries, |
| 139 | + ) |
| 140 | + return result.payload |
| 141 | + |
| 142 | + |
| 143 | +async def structured_json_generate_with_status( |
| 144 | + llm: LLMService, |
| 145 | + prompt: Prompt, |
| 146 | + config: GenerationConfig, |
| 147 | + schema_model: Type[T], |
| 148 | + *, |
| 149 | + max_retries: int = DEFAULT_MAX_RETRIES, |
| 150 | +) -> StructuredJSONGenerateResult[T]: |
| 151 | + """Like structured_json_generate, but preserves failure category.""" |
121 | 152 | current_prompt = prompt |
122 | 153 | last_errors: List[str] = [] |
123 | 154 | total_attempts = min(1 + max(0, max_retries), LLM_MAX_TOTAL_ATTEMPTS) |
@@ -151,7 +182,11 @@ async def structured_json_generate( |
151 | 182 | ) |
152 | 183 | await asyncio.sleep(delay) |
153 | 184 | continue |
154 | | - return None |
| 185 | + return StructuredJSONGenerateResult( |
| 186 | + payload=None, |
| 187 | + failure_stage="llm_generate", |
| 188 | + errors=tuple(last_errors), |
| 189 | + ) |
155 | 190 |
|
156 | 191 | cleaned = sanitize_llm_output(raw) |
157 | 192 | data, parse_errors = parse_and_repair_json(cleaned) |
@@ -187,7 +222,7 @@ async def structured_json_generate( |
187 | 222 | ) |
188 | 223 | if attempt > 0: |
189 | 224 | logger.info("结构化 JSON 管线重试成功 (attempt=%d)", attempt) |
190 | | - return instance |
| 225 | + return StructuredJSONGenerateResult(payload=instance) |
191 | 226 | last_errors = parse_errors + schema_errors |
192 | 227 | recorder.record_span( |
193 | 228 | phase="schema_validated", |
@@ -249,4 +284,8 @@ async def structured_json_generate( |
249 | 284 | "total_attempts": total_attempts, |
250 | 285 | }, |
251 | 286 | ) |
252 | | - return None |
| 287 | + return StructuredJSONGenerateResult( |
| 288 | + payload=None, |
| 289 | + failure_stage="parse_or_schema", |
| 290 | + errors=tuple(last_errors), |
| 291 | + ) |
0 commit comments