"""PlanChecker — 计划检查与复盘 每步执行后检查产出质量,全部完成后复盘总结并写入经验库。 核心能力: 1. QualityGate: 每步完成后验证产出(required_fields / min_word_count / 自定义校验) 2. LLMReflector: 使用 LLM 评估步骤质量(可选,回退到规则评估) 3. ReviewReport: 全部完成后生成复盘报告(成功路径、失败原因、耗时分布、优化建议) 4. ExperienceStore: 复盘结果写入经验库(可选依赖) 使用方式: checker = PlanChecker() result = await checker.check_step(step, exec_result) report = await checker.review_plan(plan, plan_result) """ from __future__ import annotations import logging import time from dataclasses import dataclass, field from enum import Enum from typing import Any, Callable, Awaitable from agentkit.core.plan_schema import ExecutionPlan, PlanStep, PlanStepStatus from agentkit.core.plan_executor import PlanExecutionResult, StepExecutionResult from agentkit.skills.base import QualityGateConfig logger = logging.getLogger(__name__) class CheckStatus(str, Enum): """检查结果状态""" PASS = "pass" FAIL = "fail" SKIP = "skip" @dataclass class CheckResult: """单步检查结果 Attributes: step_id: 步骤 ID status: 检查状态(pass/fail/skip) reason: 检查原因说明 quality_score: 质量评分(0.0 ~ 1.0) details: 详细检查项 """ step_id: str status: CheckStatus reason: str = "" quality_score: float = 0.0 details: dict[str, Any] = field(default_factory=dict) @dataclass class ReviewReport: """复盘报告 全部步骤完成后生成,包含成功路径、失败原因、耗时分布和优化建议。 Attributes: plan_id: 计划 ID outcome: 整体结果("success" / "partial" / "failure") success_path: 成功步骤路径(按执行顺序) failure_reasons: 失败原因列表 duration_distribution: 各步骤耗时分布 optimization_tips: 优化建议 quality_scores: 各步骤质量评分 total_duration_ms: 总耗时 success_rate: 成功率 """ plan_id: str outcome: str = "success" success_path: list[str] = field(default_factory=list) failure_reasons: list[str] = field(default_factory=list) duration_distribution: dict[str, float] = field(default_factory=dict) optimization_tips: list[str] = field(default_factory=list) quality_scores: dict[str, float] = field(default_factory=dict) total_duration_ms: float = 0.0 success_rate: float = 1.0 def to_dict(self) -> dict[str, Any]: """序列化为字典""" return { "plan_id": self.plan_id, "outcome": self.outcome, "success_path": self.success_path, "failure_reasons": self.failure_reasons, "duration_distribution": self.duration_distribution, "optimization_tips": self.optimization_tips, "quality_scores": self.quality_scores, "total_duration_ms": self.total_duration_ms, "success_rate": self.success_rate, } # 自定义校验器类型:接收步骤结果,返回 (通过, 原因) CustomValidator = Callable[[dict[str, Any] | None], tuple[bool, str]] class QualityGate: """质量门控 基于 QualityGateConfig 验证步骤产出: 1. required_fields: 结果字典必须包含指定字段 2. min_word_count: 结果文本字段最少字数 3. custom_validator: 自定义校验函数 """ def __init__( self, config: QualityGateConfig | None = None, custom_validator: CustomValidator | None = None, ): self._config = config or QualityGateConfig() self._custom_validator = custom_validator def check(self, step: PlanStep, exec_result: StepExecutionResult) -> CheckResult: """检查步骤产出质量 Args: step: 计划步骤 exec_result: 步骤执行结果 Returns: CheckResult: 检查结果 """ # 跳过非完成步骤 if exec_result.status != PlanStepStatus.COMPLETED: return CheckResult( step_id=step.step_id, status=CheckStatus.SKIP, reason=f"Step status is {exec_result.status.value}, skipping quality check", ) result = exec_result.result details: dict[str, Any] = {} failures: list[str] = [] # 1. 检查 required_fields missing_fields = self._check_required_fields(result) if missing_fields: failures.append(f"Missing required fields: {', '.join(missing_fields)}") details["missing_fields"] = missing_fields # 2. 检查 min_word_count word_count_result = self._check_min_word_count(result) if word_count_result: failures.append(word_count_result) details["word_count_check"] = word_count_result # 3. 自定义校验 custom_result = self._check_custom(result) if custom_result: failures.append(custom_result) details["custom_check"] = custom_result if failures: return CheckResult( step_id=step.step_id, status=CheckStatus.FAIL, reason="; ".join(failures), quality_score=self._compute_quality_score(len(failures)), details=details, ) return CheckResult( step_id=step.step_id, status=CheckStatus.PASS, reason="All quality checks passed", quality_score=1.0, details=details, ) def _check_required_fields(self, result: dict[str, Any] | None) -> list[str]: """检查必填字段""" if not self._config.required_fields: return [] if result is None: return list(self._config.required_fields) return [f for f in self._config.required_fields if f not in result] def _check_min_word_count(self, result: dict[str, Any] | None) -> str: """检查最少字数""" if self._config.min_word_count <= 0: return "" if result is None: return f"Result is None, cannot check min_word_count ({self._config.min_word_count})" total_words = 0 for value in result.values(): if isinstance(value, str): total_words += len(value.split()) if total_words < self._config.min_word_count: return ( f"Word count ({total_words}) is below minimum " f"({self._config.min_word_count})" ) return "" def _check_custom(self, result: dict[str, Any] | None) -> str: """执行自定义校验""" if self._custom_validator is None: return "" try: passed, reason = self._custom_validator(result) if not passed: return reason or "Custom validation failed" except Exception as e: return f"Custom validator error: {e}" return "" @staticmethod def _compute_quality_score(failure_count: int) -> float: """根据失败项数计算质量评分""" if failure_count == 0: return 1.0 if failure_count == 1: return 0.5 if failure_count == 2: return 0.25 return 0.1 class RuleBasedStepReflector: """基于规则的步骤反思器 评估步骤执行质量,生成质量评分和改进建议。 当 LLM 不可用时的回退方案。 """ async def reflect_step( self, step: PlanStep, exec_result: StepExecutionResult, ) -> tuple[float, list[str]]: """对步骤执行结果进行反思 Args: step: 计划步骤 exec_result: 步骤执行结果 Returns: (quality_score, suggestions): 质量评分和改进建议 """ suggestions: list[str] = [] if exec_result.status != PlanStepStatus.COMPLETED: # 失败步骤 score = 0.0 if exec_result.error: if "timed out" in exec_result.error.lower(): suggestions.append( f"Step '{step.name}' timed out: consider increasing timeout or decomposing the task" ) elif "no agent available" in exec_result.error.lower(): suggestions.append( f"Step '{step.name}' had no available agent: check skill registry" ) else: suggestions.append( f"Step '{step.name}' failed: {exec_result.error}" ) return score, suggestions # 成功步骤 score = 0.6 # 基础分 # 有输出数据加分 if exec_result.result and len(exec_result.result) > 0: score += 0.2 # 无重试加分 if exec_result.retry_count == 0: score += 0.1 # 耗时合理加分 if exec_result.duration_ms > 0 and exec_result.duration_ms < 30000: score += 0.1 score = min(score, 1.0) # 生成建议 if exec_result.retry_count > 0: suggestions.append( f"Step '{step.name}' required {exec_result.retry_count} retries: " f"consider improving step reliability" ) if exec_result.duration_ms > 60000: suggestions.append( f"Step '{step.name}' took {exec_result.duration_ms / 1000:.1f}s: " f"consider optimizing for performance" ) return score, suggestions class PlanChecker: """计划检查器 每步执行后检查产出质量,全部完成后复盘总结并写入经验库。 检查环节:每步完成后,QualityGate 验证产出 + Reflector 评估是否达标 复盘环节:全部完成后,生成复盘报告(成功路径、失败原因、耗时分布) 经验写入:复盘结果写入 ExperienceStore(可选) 闭环:检查不通过 → 触发重试或计划调整 使用方式: # 独立使用 checker = PlanChecker() result = await checker.check_step(step, exec_result) report = await checker.review_plan(plan, plan_result) # 与 PlanExecutor 集成 checker = PlanChecker(experience_store=store) executor = PlanExecutor( agent_pool=pool, on_step_complete=checker.make_step_complete_callback(), ) """ def __init__( self, quality_gate: QualityGate | None = None, quality_gate_config: QualityGateConfig | None = None, custom_validator: CustomValidator | None = None, reflector: Any | None = None, experience_store: Any | None = None, max_check_retries: int = 1, quality_threshold: float = 0.5, step_quality_configs: dict[str, QualityGateConfig] | None = None, ): """初始化 PlanChecker Args: quality_gate: 质量门控实例(优先使用) quality_gate_config: 质量门控配置(quality_gate 为 None 时使用) custom_validator: 自定义校验函数 reflector: 步骤反思器(None 时使用 RuleBasedStepReflector) experience_store: 经验存储(None 时不写入经验库) max_check_retries: 检查不通过时最大重试次数 quality_threshold: 质量评分阈值,低于此值视为不通过 step_quality_configs: 每步骤独立的质量门控配置 """ if quality_gate is not None: self._quality_gate = quality_gate else: self._quality_gate = QualityGate( config=quality_gate_config, custom_validator=custom_validator, ) self._reflector = reflector or RuleBasedStepReflector() self._experience_store = experience_store self._max_check_retries = max_check_retries self._quality_threshold = quality_threshold self._step_quality_configs = step_quality_configs or {} # 内部状态:记录每步检查结果 self._check_results: dict[str, CheckResult] = {} self._step_quality_gates: dict[str, QualityGate] = {} # 为有独立配置的步骤创建 QualityGate for step_id, config in self._step_quality_configs.items(): self._step_quality_gates[step_id] = QualityGate(config=config) async def check_step( self, step: PlanStep, exec_result: StepExecutionResult, ) -> CheckResult: """检查单个步骤的产出质量 在每步完成后调用,验证产出是否达标。 Args: step: 计划步骤 exec_result: 步骤执行结果 Returns: CheckResult: 检查结果 """ # 选择步骤专属或默认 QualityGate gate = self._step_quality_gates.get(step.step_id, self._quality_gate) # 1. QualityGate 规则检查 gate_result = gate.check(step, exec_result) # 2. Reflector 评估(仅对已完成步骤) if exec_result.status == PlanStepStatus.COMPLETED: try: reflect_score, suggestions = await self._reflector.reflect_step( step, exec_result ) except Exception as e: logger.warning(f"Reflector failed for step '{step.step_id}': {e}") reflect_score = gate_result.quality_score suggestions = [] # 综合评分:取 QualityGate 和 Reflector 的加权平均 combined_score = 0.4 * gate_result.quality_score + 0.6 * reflect_score # 如果 Reflector 评分低于阈值,标记为不通过 if combined_score < self._quality_threshold and gate_result.status == CheckStatus.PASS: gate_result = CheckResult( step_id=step.step_id, status=CheckStatus.FAIL, reason=f"Quality score ({combined_score:.2f}) below threshold ({self._quality_threshold})", quality_score=combined_score, details={ **gate_result.details, "reflector_score": reflect_score, "reflector_suggestions": suggestions, }, ) elif gate_result.status != CheckStatus.PASS: # 已有不通过结果,更新评分 gate_result = CheckResult( step_id=step.step_id, status=gate_result.status, reason=gate_result.reason, quality_score=combined_score, details={ **gate_result.details, "reflector_score": reflect_score, "reflector_suggestions": suggestions, }, ) else: # 通过,更新评分 gate_result = CheckResult( step_id=step.step_id, status=gate_result.status, reason=gate_result.reason, quality_score=combined_score, details={ **gate_result.details, "reflector_score": reflect_score, "reflector_suggestions": suggestions, }, ) # 记录检查结果 self._check_results[step.step_id] = gate_result logger.info( f"Check step '{step.step_id}': status={gate_result.status.value}, " f"score={gate_result.quality_score:.2f}, reason={gate_result.reason}" ) return gate_result async def review_plan( self, plan: ExecutionPlan, plan_result: PlanExecutionResult, task_type: str = "", goal: str = "", ) -> ReviewReport: """复盘整个计划执行结果 全部步骤完成后调用,生成复盘报告并写入经验库。 Args: plan: 执行计划 plan_result: 计划执行结果 task_type: 任务类型(写入经验库用) goal: 任务目标(写入经验库用) Returns: ReviewReport: 复盘报告 """ # 1. 构建成功路径 success_path = plan_result.completed_steps # 2. 收集失败原因 failure_reasons = self._collect_failure_reasons(plan_result) # 3. 构建耗时分布 duration_distribution = { sid: r.duration_ms for sid, r in plan_result.step_results.items() } # 4. 收集质量评分 quality_scores = { sid: cr.quality_score for sid, cr in self._check_results.items() } # 5. 计算成功率 total_steps = len(plan.steps) completed_count = len(plan_result.completed_steps) success_rate = completed_count / total_steps if total_steps > 0 else 0.0 # 6. 判断整体结果 outcome = self._determine_outcome(plan_result) # 7. 生成优化建议 optimization_tips = self._generate_optimization_tips( plan_result, quality_scores ) report = ReviewReport( plan_id=plan.plan_id, outcome=outcome, success_path=success_path, failure_reasons=failure_reasons, duration_distribution=duration_distribution, optimization_tips=optimization_tips, quality_scores=quality_scores, total_duration_ms=plan_result.total_duration_ms, success_rate=success_rate, ) logger.info( f"Review plan '{plan.plan_id}': outcome={outcome}, " f"success_rate={success_rate:.2f}, " f"failures={len(failure_reasons)}, " f"tips={len(optimization_tips)}" ) # 8. 写入经验库(可选) if self._experience_store is not None: await self._write_experience(report, plan, plan_result, task_type, goal) return report def should_retry(self, check_result: CheckResult, retry_count: int) -> bool: """判断是否应该重试 检查不通过且重试次数未耗尽时返回 True。 Args: check_result: 检查结果 retry_count: 当前重试次数 Returns: 是否应该重试 """ if check_result.status != CheckStatus.FAIL: return False if check_result.status == CheckStatus.SKIP: return False return retry_count < self._max_check_retries def should_request_human(self, check_result: CheckResult, retry_count: int) -> bool: """判断是否应该请求人工介入 检查不通过且重试次数已耗尽时返回 True。 Args: check_result: 检查结果 retry_count: 当前重试次数 Returns: 是否应该请求人工介入 """ if check_result.status != CheckStatus.FAIL: return False return retry_count >= self._max_check_retries def make_step_complete_callback( self, ) -> Callable[[PlanStep, StepExecutionResult], Awaitable[None]]: """创建步骤完成回调,用于与 PlanExecutor 集成 用法: checker = PlanChecker() executor = PlanExecutor( agent_pool=pool, on_step_complete=checker.make_step_complete_callback(), ) Returns: 异步回调函数 """ async def on_step_complete( step: PlanStep, exec_result: StepExecutionResult ) -> None: await self.check_step(step, exec_result) return on_step_complete def _collect_failure_reasons(self, plan_result: PlanExecutionResult) -> list[str]: """收集失败原因""" reasons: list[str] = [] for sid, r in plan_result.step_results.items(): if r.status == PlanStepStatus.FAILED: reason = f"Step '{sid}' failed" if r.error: reason += f": {r.error}" reasons.append(reason) elif r.status == PlanStepStatus.SKIPPED: reason = f"Step '{sid}' skipped" if r.error: reason += f": {r.error}" reasons.append(reason) # 补充检查不通过的原因 for sid, cr in self._check_results.items(): if cr.status == CheckStatus.FAIL: reason = f"Step '{sid}' quality check failed: {cr.reason}" if reason not in reasons: reasons.append(reason) return reasons def _determine_outcome(self, plan_result: PlanExecutionResult) -> str: """判断整体结果""" total = len(plan_result.step_results) if total == 0: return "success" completed = len(plan_result.completed_steps) failed = len(plan_result.failed_steps) skipped = len(plan_result.skipped_steps) if completed == total: return "success" if failed == total or (failed + skipped == total and completed == 0): return "failure" return "partial" def _generate_optimization_tips( self, plan_result: PlanExecutionResult, quality_scores: dict[str, float], ) -> list[str]: """生成优化建议""" tips: list[str] = [] # 基于质量评分 low_quality_steps = [ sid for sid, score in quality_scores.items() if score < self._quality_threshold ] if low_quality_steps: tips.append( f"Steps with low quality scores: {', '.join(low_quality_steps)}. " f"Consider improving input data or step configuration." ) # 基于重试 high_retry_steps = [ (sid, r.retry_count) for sid, r in plan_result.step_results.items() if r.retry_count > 0 ] if high_retry_steps: steps_str = ", ".join( f"'{sid}' ({count} retries)" for sid, count in high_retry_steps ) tips.append( f"Steps requiring retries: {steps_str}. " f"Consider improving step reliability." ) # 基于耗时 slow_steps = [ (sid, r.duration_ms) for sid, r in plan_result.step_results.items() if r.duration_ms > 60000 ] if slow_steps: steps_str = ", ".join( f"'{sid}' ({ms / 1000:.1f}s)" for sid, ms in slow_steps ) tips.append( f"Slow steps detected: {steps_str}. " f"Consider optimizing for performance." ) # 基于跳过步骤 skipped = plan_result.skipped_steps if skipped: tips.append( f"Skipped steps: {', '.join(skipped)}. " f"Review dependency chain and failure handling strategy." ) # 基于检查结果中的 Reflector 建议 for sid, cr in self._check_results.items(): reflector_suggestions = cr.details.get("reflector_suggestions", []) for suggestion in reflector_suggestions: if suggestion not in tips: tips.append(suggestion) return tips async def _write_experience( self, report: ReviewReport, plan: ExecutionPlan, plan_result: PlanExecutionResult, task_type: str, goal: str, ) -> None: """将复盘结果写入经验库""" from agentkit.evolution.experience_schema import TaskExperience # 构建步骤摘要 steps_summary_parts: list[str] = [] for step in plan.steps: r = plan_result.step_results.get(step.step_id) if r: steps_summary_parts.append( f"{step.name}: {r.status.value}" + (f" ({r.duration_ms / 1000:.1f}s)" if r.duration_ms > 0 else "") ) steps_summary = "; ".join(steps_summary_parts) experience = TaskExperience( task_type=task_type or "plan_execution", goal=goal or plan.goal, steps_summary=steps_summary, outcome=report.outcome, duration_seconds=report.total_duration_ms / 1000, success_rate=report.success_rate, failure_reasons=report.failure_reasons, optimization_tips=report.optimization_tips, ) try: exp_id = await self._experience_store.record_experience(experience) logger.info(f"Experience recorded: {exp_id} outcome={report.outcome}") except Exception as e: logger.error(f"Failed to write experience to store: {e}") def reset(self) -> None: """重置内部状态(用于新一轮检查)""" self._check_results.clear()