@@ -66,6 +66,39 @@ class ErrorPatternResponse(BaseModel):
6666# ============================================================
6767
6868
69+ def _validate_pattern_item (item : Any ) -> dict | None :
70+ """校验 LLM 输出的错误模式项:缺失必填字段/类型非法时返回 None(过滤)。
71+
72+ GW-2#3: LLM JSON 输出截断或格式漂移时,PatternItem(**p) 严格构造会抛
73+ ValidationError 导致 500——与 quiz_gen_chain._validate_question 相同的
74+ 逐项校验+过滤模式,非法项丢弃而非崩溃。
75+ """
76+ if not isinstance (item , dict ):
77+ return None
78+ required = {"type" , "keywords" , "explanation" , "suggestion" }
79+ if not all (k in item and item [k ] not in (None , "" ) for k in required ):
80+ return None
81+ keywords = item .get ("keywords" )
82+ if not isinstance (keywords , list ) or not all (isinstance (k , str ) for k in keywords ):
83+ return None
84+ return item
85+
86+
87+ def _validate_top_offender (item : Any ) -> dict | None :
88+ """校验 LLM 输出的高频错误卡片项:缺 flashcardId 时返回 None(过滤)。"""
89+ if not isinstance (item , dict ):
90+ return None
91+ flashcard_id = item .get ("flashcardId" )
92+ if not isinstance (flashcard_id , str ) or not flashcard_id :
93+ return None
94+ try :
95+ count = int (item .get ("count" , 0 ))
96+ except (TypeError , ValueError ):
97+ # GW-2#3: count 非数字时容错为 0,而非让 pydantic 抛 500
98+ count = 0
99+ return {"flashcardId" : flashcard_id , "count" : count }
100+
101+
69102@router .post ("/error-pattern" , response_model = ErrorPatternResponse , summary = "分析黄金错误模式" )
70103async def error_pattern (request : Request , body : ErrorPatternRequest ) -> ErrorPatternResponse :
71104 """
@@ -126,12 +159,27 @@ async def _run_chain(provider, model_name):
126159 await cache .set_ai_cache (cache_key , chain_result , expire = 3600 )
127160
128161 # 构建响应对象
129- patterns = [
130- PatternItem (** p ) for p in patterns_data
131- ]
132- top_offenders = [
133- TopOffender (** o ) for o in top_offenders_data
134- ]
162+ # GW-2#3: LLM 输出逐项校验+过滤,缺字段项不导致 500(截断/格式漂移降级)
163+ patterns = []
164+ dropped_patterns = 0
165+ for p in patterns_data :
166+ validated = _validate_pattern_item (p )
167+ if validated is not None :
168+ patterns .append (PatternItem (** validated ))
169+ else :
170+ dropped_patterns += 1
171+
172+ top_offenders = []
173+ for o in top_offenders_data :
174+ validated = _validate_top_offender (o )
175+ if validated is not None :
176+ top_offenders .append (TopOffender (** validated ))
177+
178+ if dropped_patterns > 0 :
179+ logger .warning (
180+ "错误模式分析: %d 条模式项因缺字段被过滤(LLM 输出质量问题)" ,
181+ dropped_patterns ,
182+ )
135183
136184 return ErrorPatternResponse (
137185 patterns = patterns ,
0 commit comments