skill-self-evolution 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,471 @@
1
+ """
2
+ SkillExecutor — Skill 执行引擎主类。
3
+
4
+ 流程:
5
+ 1. Pydantic 输入校验
6
+ 2. trace_id 生成 + contextvars 注入
7
+ 3. 加载 evolve.toml(获取 ai_role)
8
+ 4. 加载 skill.md + run.py(磁盘)
9
+ 5. 加载 rules_config + prompt(MySQL)
10
+ 6. execute() → AI 常识判断 → 合理则过 / 不合理则 AI 从原始数据重选
11
+ 7. 写 JSONL 日志(根据 ai_role 自动计算 is_failure)
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from skill_self_evolution.context import set_trace_id
21
+ from skill_self_evolution.deepseek import CircuitBreaker, DeepSeekClient
22
+ from skill_self_evolution.fallback import FallbackConfig, FallbackStrategy
23
+ from skill_self_evolution.loader import SkillLoader, SkillModule
24
+ from skill_self_evolution.logger import SkillLogger
25
+ from skill_self_evolution.models import (
26
+ AiReselectionResult,
27
+ AiValidationResult,
28
+ FallbackConfigModel,
29
+ SkillInput,
30
+ SkillOutput,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class SkillExecutor:
37
+ """Skill 执行引擎。
38
+
39
+ 使用方式:
40
+ executor = SkillExecutor(db_config=...)
41
+ output = await executor.run("nickname-selector", input_data)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ skill_base_dir: Path | None = None,
47
+ deepseek_api_key: str = "",
48
+ deepseek_api_base: str = "https://api.deepseek.com/v1",
49
+ deepseek_model: str = "deepseek-v4-flash",
50
+ ):
51
+ """
52
+ Args:
53
+ skill_base_dir: Skill 根目录(默认 backend/config/services/skill/)
54
+ deepseek_api_key: DeepSeek API Key
55
+ deepseek_api_base: DeepSeek API 基础地址
56
+ deepseek_model: 模型名称
57
+ """
58
+ self._loader = SkillLoader(skill_base_dir)
59
+ self._deepseek = DeepSeekClient(
60
+ api_key=deepseek_api_key,
61
+ api_base=deepseek_api_base,
62
+ model=deepseek_model,
63
+ )
64
+
65
+ @property
66
+ def circuit_breaker(self) -> CircuitBreaker:
67
+ """获取全局熔断器,可在外部调整阈值。"""
68
+ return self._deepseek.circuit_breaker
69
+
70
+ def load_skill(self, skill_name: str) -> SkillModule:
71
+ """预加载 Skill(可选,run() 会自动加载)。"""
72
+ return self._loader.load(skill_name)
73
+
74
+ async def run(
75
+ self,
76
+ skill_name: str,
77
+ input_data: Any,
78
+ *,
79
+ rules_config: dict | None = None,
80
+ prompt_config: dict | None = None,
81
+ trace_id: str | None = None,
82
+ ) -> SkillOutput:
83
+ """执行 Skill 主流程。
84
+
85
+ Args:
86
+ skill_name: Skill 名称(如 "nickname-selector")
87
+ input_data: 业务输入数据(dict 或 Pydantic model)
88
+ rules_config: rules_config.yaml 解析后的 dict(可选,默认从 MySQL 加载)
89
+ prompt_config: prompt.yaml 解析后的 dict(可选,默认从 MySQL 加载)
90
+ trace_id: 外部传入的 trace_id(可选,未传入则使用 input_data 中的或自动生成)
91
+
92
+ Returns:
93
+ SkillOutput: 执行结果
94
+ """
95
+ start_time = time.monotonic()
96
+
97
+ # 1. 加载 Skill 模块
98
+ skill = self._loader.load(skill_name)
99
+
100
+ # 2. 确定 trace_id
101
+ effective_trace_id = (
102
+ trace_id
103
+ or (getattr(input_data, "trace_id", None))
104
+ or str(__import__("uuid").uuid4())
105
+ )
106
+ set_trace_id(effective_trace_id)
107
+
108
+ # 3. 构建 SkillInput(Pydantic 校验入口)
109
+ if isinstance(input_data, dict):
110
+ skill_input = SkillInput(trace_id=effective_trace_id, input_data=input_data)
111
+ else:
112
+ skill_input = SkillInput(trace_id=effective_trace_id, input_data=input_data)
113
+
114
+ warnings: list[str] = []
115
+
116
+ # 4. 加载降级配置(Pydantic 校验)
117
+ fallback_cfg = self._build_fallback_config(rules_config or {})
118
+ fallback = FallbackStrategy(fallback_cfg, self._deepseek.circuit_breaker)
119
+
120
+ # 5. 合并配置传给 execute()
121
+ merged_config = {
122
+ "rules_config": rules_config or {},
123
+ "prompt_config": prompt_config or {},
124
+ }
125
+
126
+ # 6. 执行规则阶段
127
+ try:
128
+ rule_output = skill.execute(skill_input, merged_config)
129
+ if not isinstance(rule_output, SkillOutput):
130
+ rule_output = SkillOutput(
131
+ source="rule",
132
+ result=rule_output if isinstance(rule_output, dict) else {"value": rule_output},
133
+ )
134
+ except Exception as e:
135
+ logger.exception("Skill [%s] 规则执行异常", skill_name)
136
+ elapsed = (time.monotonic() - start_time) * 1000
137
+ output = SkillOutput(
138
+ source="rule",
139
+ result={"error": str(e)},
140
+ warnings=[f"规则执行异常: {e}"],
141
+ )
142
+ self._log(skill, effective_trace_id, True, skill_input, output, None, None, output, warnings, elapsed)
143
+ return output
144
+
145
+ ai_validation: AiValidationResult | None = None
146
+ ai_reselection: AiReselectionResult | None = None
147
+
148
+ # 7. AI 处理阶段
149
+ if skill.ai_role == "correction":
150
+ # 纠错型:AI 常识判断 → 不合理则重选
151
+ fb_check = fallback.check_before_ai()
152
+ if fb_check.skip_ai:
153
+ warnings.extend(fb_check.warnings)
154
+ rule_output.ai_validated = False
155
+ rule_output.warnings = warnings
156
+ elapsed = (time.monotonic() - start_time) * 1000
157
+ is_failure = self._compute_is_failure(skill.ai_role, rule_output, None, None)
158
+ self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
159
+ return rule_output
160
+
161
+ # 7a. AI 验证(Pydantic 输出)
162
+ try:
163
+ ai_validation = await self._ai_validate(skill, rule_output, prompt_config)
164
+ rule_output.ai_validated = True
165
+ except Exception as e:
166
+ logger.warning("Skill [%s] AI 验证异常: %s", skill_name, e)
167
+ fb_result = fallback.on_validate_failure(e)
168
+ warnings.extend(fb_result.warnings)
169
+ if fb_result.skip_ai:
170
+ elapsed = (time.monotonic() - start_time) * 1000
171
+ rule_output.ai_validated = False
172
+ rule_output.warnings = warnings
173
+ is_failure = self._compute_is_failure(skill.ai_role, rule_output, None, None)
174
+ self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
175
+ return rule_output
176
+
177
+ # 7b. 若验证不合理 → AI 重选
178
+ if ai_validation and ai_validation.result == "不合理":
179
+ fb_reselect = fallback.check_before_ai()
180
+ if fb_reselect.skip_ai:
181
+ warnings.extend(fb_reselect.warnings)
182
+ elapsed = (time.monotonic() - start_time) * 1000
183
+ is_failure = self._compute_is_failure(skill.ai_role, rule_output, ai_validation, None)
184
+ self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, ai_validation, None, rule_output, warnings, elapsed)
185
+ return rule_output
186
+
187
+ try:
188
+ ai_reselection = await self._ai_reselect(skill, skill_input, rule_output, prompt_config)
189
+ if ai_reselection and ai_reselection.result != "不合理":
190
+ # 包装 AI 重选结果 — 确保 result 是 dict
191
+ selected_nickname = ai_reselection.result
192
+ if isinstance(selected_nickname, dict):
193
+ reselected_dict = selected_nickname
194
+ else:
195
+ reselected_dict = {
196
+ **rule_output.result,
197
+ "nickname": str(selected_nickname),
198
+ "source": "ai",
199
+ }
200
+ final_output = SkillOutput(
201
+ source="ai",
202
+ result=reselected_dict,
203
+ ai_validated=True,
204
+ ai_reselected=True,
205
+ warnings=warnings,
206
+ )
207
+ elapsed = (time.monotonic() - start_time) * 1000
208
+ is_failure = self._compute_is_failure(skill.ai_role, rule_output, ai_validation, ai_reselection)
209
+ self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, ai_validation, ai_reselection, final_output, warnings, elapsed)
210
+ return final_output
211
+ else:
212
+ # 重选仍不合理
213
+ rule_output.ai_reselected = True
214
+ rule_output.warnings = warnings
215
+ rule_output.warnings.append("AI 重选后仍不合理")
216
+ except Exception as e:
217
+ logger.warning("Skill [%s] AI 重选异常: %s", skill_name, e)
218
+ fb_result2 = fallback.on_reselect_failure(e)
219
+ warnings.extend(fb_result2.warnings)
220
+
221
+ # 验证合理或重选失败 → 返回规则结果
222
+ rule_output.warnings = warnings
223
+ elapsed = (time.monotonic() - start_time) * 1000
224
+ is_failure = self._compute_is_failure(skill.ai_role, rule_output, ai_validation, ai_reselection)
225
+ self._log(skill, effective_trace_id, is_failure, skill_input, rule_output, ai_validation, ai_reselection, rule_output, warnings, elapsed)
226
+ return rule_output
227
+
228
+ elif skill.ai_role == "enhancement":
229
+ # 加分型:AI 增强(如语义评分)
230
+ fb_check = fallback.check_before_ai()
231
+ if fb_check.skip_ai:
232
+ warnings.extend(fb_check.warnings)
233
+ rule_output.warnings = warnings
234
+ elapsed = (time.monotonic() - start_time) * 1000
235
+ self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
236
+ return rule_output
237
+
238
+ try:
239
+ ai_result = await self._ai_enhance(skill, skill_input, rule_output, prompt_config)
240
+ merged_result = {**rule_output.result}
241
+ if ai_result:
242
+ merged_result.update(ai_result)
243
+ final_output = SkillOutput(
244
+ source=rule_output.source,
245
+ result=merged_result,
246
+ ai_validated=True,
247
+ ai_reselected=False,
248
+ warnings=warnings,
249
+ )
250
+ elapsed = (time.monotonic() - start_time) * 1000
251
+ self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, final_output, warnings, elapsed)
252
+ return final_output
253
+ except Exception as e:
254
+ logger.warning("Skill [%s] AI 增强异常: %s", skill_name, e)
255
+ rule_output.warnings = warnings
256
+ elapsed = (time.monotonic() - start_time) * 1000
257
+ self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
258
+ return rule_output
259
+
260
+ else:
261
+ # 未知 ai_role → 纯规则返回
262
+ logger.warning("Skill [%s] 未知 ai_role=%s,纯规则输出", skill_name, skill.ai_role)
263
+ rule_output.warnings = warnings
264
+ elapsed = (time.monotonic() - start_time) * 1000
265
+ self._log(skill, effective_trace_id, False, skill_input, rule_output, None, None, rule_output, warnings, elapsed)
266
+ return rule_output
267
+
268
+ # ── 私有方法 ──
269
+
270
+ def _build_fallback_config(self, rules_config: dict) -> FallbackConfig:
271
+ """从 rules_config 的 ai_fallback 段构建降级配置(Pydantic 校验)。"""
272
+ af = rules_config.get("ai_fallback", {})
273
+ validated = FallbackConfigModel(
274
+ validate_timeout_seconds=float(af.get("validate_timeout_seconds", 3)),
275
+ reselect_timeout_seconds=float(af.get("reselect_timeout_seconds", 5)),
276
+ max_retries=int(af.get("max_retries", 1)),
277
+ circuit_breaker_threshold=int(af.get("circuit_breaker_threshold", 3)),
278
+ circuit_breaker_cooldown_seconds=float(af.get("circuit_breaker_cooldown_seconds", 60)),
279
+ conservative_mode=bool(af.get("conservative_mode", False)),
280
+ )
281
+ return FallbackConfig(
282
+ validate_timeout_seconds=validated.validate_timeout_seconds,
283
+ reselect_timeout_seconds=validated.reselect_timeout_seconds,
284
+ max_retries=validated.max_retries,
285
+ circuit_breaker_threshold=validated.circuit_breaker_threshold,
286
+ circuit_breaker_cooldown_seconds=validated.circuit_breaker_cooldown_seconds,
287
+ conservative_mode=validated.conservative_mode,
288
+ )
289
+
290
+ @staticmethod
291
+ def _compute_is_failure(
292
+ ai_role: str,
293
+ rule_output: SkillOutput,
294
+ ai_validation: AiValidationResult | None,
295
+ ai_reselection: AiReselectionResult | None,
296
+ ) -> bool:
297
+ """根据 ai_role 计算 is_failure 标记。"""
298
+ if ai_role == "enhancement":
299
+ return False
300
+
301
+ if ai_role == "correction":
302
+ if ai_validation and ai_validation.result == "不合理":
303
+ return True
304
+ if ai_reselection and ai_reselection.result == "不合理":
305
+ return True
306
+ result = rule_output.result
307
+ if not result or result.get("error"):
308
+ return True
309
+
310
+ return False
311
+
312
+ async def _ai_validate(
313
+ self,
314
+ skill: SkillModule,
315
+ rule_output: SkillOutput,
316
+ prompt_config: dict | None,
317
+ ) -> AiValidationResult:
318
+ """调用 AI 进行常识验证。返回 Pydantic 模型。"""
319
+ import json as _json, re as _re
320
+
321
+ system_prompt = (prompt_config or {}).get("system_prompt", "你是合理性判断专家。")
322
+ user_template = (prompt_config or {}).get("user_template_validate", "请判断以下结果是否合理:{{result}}")
323
+
324
+ nick = rule_output.result.get("nickname", "")
325
+ candidates = _json.dumps(rule_output.result.get("candidates", [])[:5], ensure_ascii=False)
326
+ user_message = self._render_template(user_template, {"result": nick, "candidates": candidates})
327
+
328
+ resp = await self._deepseek.chat(
329
+ messages=[
330
+ {"role": "system", "content": system_prompt},
331
+ {"role": "user", "content": user_message},
332
+ ],
333
+ temperature=0.1,
334
+ max_tokens=256,
335
+ )
336
+ content = resp.content.strip()
337
+
338
+ # 尝试 JSON 解析
339
+ for candidate in [content]:
340
+ if candidate.startswith("```"):
341
+ lines = candidate.split("\n")
342
+ end = -1 if lines[-1].strip() == "```" else len(lines)
343
+ start = 1 if lines[0].startswith("```json") or lines[0].startswith("```") else 0
344
+ candidate = "\n".join(lines[start:end])
345
+ try:
346
+ parsed = _json.loads(candidate)
347
+ return AiValidationResult(
348
+ result=parsed.get("result", "合理"),
349
+ reason=parsed.get("reason", ""),
350
+ )
351
+ except (_json.JSONDecodeError, ValueError):
352
+ continue
353
+
354
+ # 非 JSON 回退:先检查"不合理",避免"不合理"中的"合理"被误匹配
355
+ if _re.search(r"(不合理|unreasonable|invalid|不是)", content, _re.IGNORECASE):
356
+ return AiValidationResult(result="不合理", reason=content[:120])
357
+ if _re.search(r"(合理|reasonable|valid)", content, _re.IGNORECASE):
358
+ return AiValidationResult(result="合理", reason=content[:120])
359
+ return AiValidationResult(result="合理", reason="no explicit judgement")
360
+
361
+ async def _ai_reselect(
362
+ self,
363
+ skill: SkillModule,
364
+ skill_input: SkillInput,
365
+ rule_output: SkillOutput,
366
+ prompt_config: dict | None,
367
+ ) -> AiReselectionResult:
368
+ """调用 AI 重新选择/提取。返回 Pydantic 模型。"""
369
+ import json as _json2
370
+
371
+ system_prompt = (prompt_config or {}).get("system_prompt", "你是信息提取专家。")
372
+ user_template = (prompt_config or {}).get("user_template_reselect", "请从以下数据中重新选择:{{candidates}}")
373
+
374
+ candidates_list = rule_output.result.get("candidates", []) if isinstance(rule_output.result, dict) else []
375
+ candidates_json = _json2.dumps(candidates_list, ensure_ascii=False)
376
+
377
+ user_message = self._render_template(
378
+ user_template,
379
+ {"candidates": candidates_json},
380
+ )
381
+
382
+ response = await self._deepseek.chat_json(
383
+ messages=[
384
+ {"role": "system", "content": system_prompt},
385
+ {"role": "user", "content": user_message},
386
+ ],
387
+ temperature=0.2,
388
+ max_tokens=1024,
389
+ )
390
+
391
+ return AiReselectionResult(
392
+ result=str(response.get("result", "")),
393
+ reason=str(response.get("reason", "")),
394
+ )
395
+
396
+ async def _ai_enhance(
397
+ self,
398
+ skill: SkillModule,
399
+ skill_input: SkillInput,
400
+ rule_output: SkillOutput,
401
+ prompt_config: dict | None,
402
+ ) -> dict | None:
403
+ """调用 AI 增强规则结果(enhancement 角色)。"""
404
+ system_prompt = (prompt_config or {}).get("system_prompt", "你是评分增强专家。")
405
+ user_template = (prompt_config or {}).get("user_template", "请根据以下信息评分:{{input_data}}")
406
+
407
+ user_message = self._render_template(user_template, {"input_data": skill_input.input_data})
408
+
409
+ response = await self._deepseek.chat_json(
410
+ messages=[
411
+ {"role": "system", "content": system_prompt},
412
+ {"role": "user", "content": user_message},
413
+ ],
414
+ temperature=0.3,
415
+ max_tokens=2048,
416
+ )
417
+ return response
418
+
419
+ @staticmethod
420
+ def _render_template(template: str, context: dict) -> str:
421
+ """简单 Jinja2 风格模板渲染(仅支持 {{var}})。"""
422
+ result = template
423
+ for key, value in context.items():
424
+ placeholder = "{{" + key + "}}"
425
+ if isinstance(value, dict):
426
+ import json
427
+ result = result.replace(placeholder, json.dumps(value, ensure_ascii=False))
428
+ else:
429
+ result = result.replace(placeholder, str(value))
430
+ return result
431
+
432
+ def _log(
433
+ self,
434
+ skill: SkillModule,
435
+ trace_id: str,
436
+ is_failure: bool,
437
+ skill_input: SkillInput,
438
+ rule_output: SkillOutput,
439
+ ai_validation: AiValidationResult | None,
440
+ ai_reselection: AiReselectionResult | None,
441
+ final_output: SkillOutput,
442
+ warnings: list[str],
443
+ elapsed_ms: float,
444
+ ) -> None:
445
+ """写入 JSONL 日志(Pydantic LogEntry 校验)。"""
446
+ try:
447
+ log_writer = SkillLogger(skill.skill_name)
448
+
449
+ # 生成 input_summary
450
+ if skill.summarize_input:
451
+ input_summary = skill.summarize_input(skill_input.input_data)
452
+ elif isinstance(skill_input.input_data, dict):
453
+ keys = list(skill_input.input_data.keys())[:5]
454
+ input_summary = {k: str(skill_input.input_data[k])[:100] for k in keys}
455
+ else:
456
+ input_summary = {"type": type(skill_input.input_data).__name__}
457
+
458
+ # 通过 log_execution 统一校验(LogEntry Pydantic 模型)后写入
459
+ log_writer.log_execution(
460
+ trace_id=trace_id,
461
+ is_failure=is_failure,
462
+ input_summary=input_summary,
463
+ rule_output=rule_output.result,
464
+ ai_validation=ai_validation.model_dump() if ai_validation else None,
465
+ ai_reselection=ai_reselection.model_dump() if ai_reselection else None,
466
+ final_output=final_output.result,
467
+ warnings=warnings,
468
+ elapsed_ms=round(elapsed_ms, 1),
469
+ )
470
+ except Exception as e:
471
+ logger.warning("Skill 日志记录失败: %s", e)
@@ -0,0 +1,129 @@
1
+ """
2
+ AI 降级策略 — 乐观/保守模式 + 熔断检查。
3
+
4
+ 统一降级总原则:
5
+ - 输入非法 → 框架层直接返回 400,不执行业务逻辑
6
+ - AI 验证失败/超时 → 标记跳过,采信规则结果
7
+ - AI 重选失败/超时 → 直接返回规则原始结果
8
+ - AI 全局熔断 → 全链路跳过 AI,纯走规则
9
+ - 保守降级模式 → 可选开关:AI 不可用时标记「需人工复核」而非直接通过
10
+ - warnings → 仅用于日志和监控,不阻断流程
11
+ """
12
+
13
+ import logging
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ from skill_self_evolution.deepseek import CircuitBreaker
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class FallbackMode(str, Enum):
25
+ OPTIMISTIC = "optimistic"
26
+ CONSERVATIVE = "conservative"
27
+
28
+
29
+ @dataclass
30
+ class FallbackConfig:
31
+ """降级配置,来源 rules_config.yaml 的 ai_fallback 段。
32
+
33
+ 注:此结构保持 @dataclass(非 Pydantic),原因:
34
+ - 构造来源已通过 FallbackConfigModel(Pydantic)校验
35
+ - 内嵌在 FallbackStrategy 中,无独立序列化需求
36
+ """
37
+
38
+ validate_timeout_seconds: float = 3.0
39
+ reselect_timeout_seconds: float = 5.0
40
+ max_retries: int = 1
41
+ circuit_breaker_threshold: int = 3
42
+ circuit_breaker_cooldown_seconds: float = 60.0
43
+ conservative_mode: bool = False
44
+ enabled: bool = True
45
+
46
+
47
+ class FallbackResult(BaseModel):
48
+ """降级处理结果(Pydantic 校验)。"""
49
+
50
+ skip_ai: bool = Field(default=False, description="是否应跳过 AI 步骤")
51
+ reason: str = Field(default="", description="降级原因")
52
+ warnings: list[str] = Field(default_factory=list, description="降级时的警告信息")
53
+ needs_review: bool = Field(default=False, description="AI 不可用时是否标记需人工复核")
54
+
55
+
56
+ class FallbackStrategy:
57
+ """AI 降级策略管理器。
58
+
59
+ 使用方式:
60
+ strategy = FallbackStrategy(config, circuit_breaker)
61
+ result = strategy.on_validate_failure(error)
62
+ if result.skip_ai:
63
+ return fallback_output
64
+ """
65
+
66
+ def __init__(self, config: FallbackConfig, circuit_breaker: CircuitBreaker):
67
+ self.config = config
68
+ self.circuit_breaker = circuit_breaker
69
+
70
+ @property
71
+ def mode(self) -> FallbackMode:
72
+ return FallbackMode.CONSERVATIVE if self.config.conservative_mode else FallbackMode.OPTIMISTIC
73
+
74
+ @property
75
+ def is_circuit_open(self) -> bool:
76
+ return self.circuit_breaker.is_open
77
+
78
+ def check_before_ai(self) -> FallbackResult:
79
+ """在调用 AI 之前检查是否应跳过。"""
80
+ warnings: list[str] = []
81
+
82
+ if not self.config.enabled:
83
+ return FallbackResult(skip_ai=True, reason="AI 全局已禁用", warnings=warnings)
84
+
85
+ if self.circuit_breaker.is_open:
86
+ msg = "AI 熔断中,跳过本步骤"
87
+ warnings.append(msg)
88
+ return FallbackResult(
89
+ skip_ai=True,
90
+ reason=msg,
91
+ warnings=warnings,
92
+ needs_review=self.mode == FallbackMode.CONSERVATIVE,
93
+ )
94
+
95
+ return FallbackResult(skip_ai=False)
96
+
97
+ def on_validate_failure(self, error: Exception | None = None) -> FallbackResult:
98
+ """AI 验证失败/超时时的降级处理。"""
99
+ warnings: list[str] = []
100
+ reason = f"AI 验证不可用: {error}" if error else "AI 验证不可用"
101
+
102
+ if self.mode == FallbackMode.CONSERVATIVE:
103
+ warnings.append("需人工复核: AI验证不可用")
104
+ return FallbackResult(
105
+ skip_ai=True,
106
+ reason=reason,
107
+ warnings=warnings,
108
+ needs_review=True,
109
+ )
110
+
111
+ # 乐观模式:默认"验证通过"
112
+ return FallbackResult(skip_ai=True, reason=reason, warnings=warnings)
113
+
114
+ def on_reselect_failure(self, error: Exception | None = None) -> FallbackResult:
115
+ """AI 重选失败/超时时的降级处理。"""
116
+ warnings: list[str] = []
117
+ reason = f"AI 重选不可用: {error}" if error else "AI 重选不可用"
118
+
119
+ if self.mode == FallbackMode.CONSERVATIVE:
120
+ warnings.append("需人工复核: AI重选不可用")
121
+ return FallbackResult(
122
+ skip_ai=True,
123
+ reason=reason,
124
+ warnings=warnings,
125
+ needs_review=True,
126
+ )
127
+
128
+ # 乐观模式:返回规则原始结果
129
+ return FallbackResult(skip_ai=True, reason=reason, warnings=warnings)