"""EvolutionMixin - 将进化引擎集成到 Agent 生命周期 在任务完成后自动触发反思 → 优化 → A/B 测试 → 应用/回滚的进化流程。 """ import logging from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any from sqlalchemy.exc import DBAPIError from agentkit.core.protocol import EvolutionEvent, TaskMessage, TaskResult from agentkit.evolution.ab_tester import ABTestConfig, ABTestResult, ABTester from agentkit.evolution.evolution_store import EvolutionStore from agentkit.evolution.llm_reflector import LLMReflector from agentkit.evolution.prompt_optimizer import ( Module, PromptOptimizer, ) from agentkit.evolution.reflector import Reflection, Reflector, RuleBasedReflector from agentkit.evolution.strategy_tuner import StrategyConfig, StrategyTuner from agentkit.memory.profile import MemoryStore logger = logging.getLogger(__name__) @dataclass class SoulEvolutionConfig: """Soul 进化多维触发配置""" min_reflections: int = 3 reflection_window_seconds: int = 3600 time_decay_factor: float = 0.5 task_type_weights: dict[str, float] = field(default_factory=dict) quality_gradient_threshold: float = -0.15 @dataclass class EvolutionLogEntry: """进化日志条目""" task_id: str reflection: Reflection | None = None optimized_module: Module | None = None ab_test_result: ABTestResult | None = None applied: bool = False rolled_back: bool = False event_id: str | None = None created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) class EvolutionMixin: """进化混入类,将进化引擎集成到 Agent 生命周期。 用法: class MyAgent(BaseAgent, EvolutionMixin): def __init__(self, ...): BaseAgent.__init__(self, ...) EvolutionMixin.__init__(self, reflector=..., ...) """ _UNSET = object() # 用于区分"未传入"和"显式传入 None" def __init__( self, reflector: Any = _UNSET, prompt_optimizer: PromptOptimizer | None = None, strategy_tuner: StrategyTuner | None = None, ab_tester: ABTester | None = None, evolution_store: EvolutionStore | None = None, reflector_type: str | None = None, llm_gateway: Any | None = None, auxiliary_model: str | None = None, strategy_tuning_enabled: bool = False, evolution_config: SoulEvolutionConfig | None = None, ): if reflector is not EvolutionMixin._UNSET: # 显式传入了 reflector 参数(包括 None) self._reflector = reflector elif reflector_type is not None: # 未传入 reflector,但指定了 reflector_type → 自动创建 self._reflector = self._create_reflector( reflector_type, llm_gateway, auxiliary_model ) else: # 都未指定:保持向后兼容,reflector 为 None self._reflector = None self._prompt_optimizer = prompt_optimizer self._strategy_tuner = strategy_tuner self._ab_tester = ab_tester self._evolution_store = evolution_store self._evolution_log: list[EvolutionLogEntry] = [] self._current_module: Module | None = None self._strategy_tuning_enabled = strategy_tuning_enabled self._evolution_config = evolution_config self.pending_soul_updates: dict[str, list] = {} @staticmethod def _create_reflector( reflector_type: str, llm_gateway: Any | None = None, auxiliary_model: str | None = None, ) -> Reflector | None: """根据 reflector_type 创建对应的反思器 Args: reflector_type: "llm" / "rule" / "auto" llm_gateway: LLMGateway 实例,llm/auto 模式需要 auxiliary_model: LLM 反思使用的模型名称 """ if reflector_type == "llm": if llm_gateway is None: logger.warning( "reflector_type='llm' but no llm_gateway provided, " "falling back to RuleBasedReflector" ) return RuleBasedReflector() model = auxiliary_model or "default" return LLMReflector(llm_gateway=llm_gateway, model=model) if reflector_type == "rule": return RuleBasedReflector() # "auto" 模式:优先 LLM,降级到规则 if llm_gateway is not None: model = auxiliary_model or "default" return LLMReflector(llm_gateway=llm_gateway, model=model) return RuleBasedReflector() async def evolve_after_task( self, task: TaskMessage, result: TaskResult, memory_store: MemoryStore | None = None, ) -> EvolutionLogEntry: """任务完成后执行进化流程。 流程: 1. Reflector 反思 → 得到 Reflection 2. Soul 进化检查(如果 memory_store 可用) 3. 如果 Reflection 有改进建议 → PromptOptimizer 优化 4. 如果优化产生了新 Prompt → ABTester 验证 5. 如果 AB 测试通过 → EvolutionStore 应用变更 6. 如果 AB 测试失败 → 回滚 7. 如果策略调优启用 → StrategyTuner 调优 """ log_entry = EvolutionLogEntry(task_id=task.task_id) # Step 1: 反思 if self._reflector is None: logger.debug("No reflector configured, skipping evolution") self._evolution_log.append(log_entry) return log_entry reflection = await self._reflector.reflect(task, result) log_entry.reflection = reflection logger.info( f"Evolution reflection for task {task.task_id}: " f"outcome={reflection.outcome}, quality={reflection.quality_score:.2f}, " f"suggestions={len(reflection.suggestions)}" ) # Step 2: Soul 进化检查 if memory_store is not None: await self.evolve_soul(task, result, memory_store, reflection=reflection) # Step 3: 如果有改进建议,触发 Prompt 优化 if not reflection.suggestions: logger.debug("No improvement suggestions, skipping optimization") self._evolution_log.append(log_entry) return log_entry if self._prompt_optimizer is None or self._current_module is None: logger.debug("No prompt optimizer or current module configured, skipping optimization") self._evolution_log.append(log_entry) return log_entry # 将反思结果作为训练样本 self._prompt_optimizer.add_example( input_data=task.input_data, output_data=result.output_data or {}, quality_score=reflection.quality_score, ) # Pass trace and reflection to LLMPromptOptimizer if available optimized = await self._optimize_with_context(self._current_module, reflection) # 检查是否真正产生了变化 if optimized.name == self._current_module.name and not optimized.demos: logger.debug("Optimization produced no meaningful changes") self._evolution_log.append(log_entry) return log_entry log_entry.optimized_module = optimized # Step 3: A/B 测试验证 if self._ab_tester is None: logger.debug("No AB tester configured, applying change directly") applied = await self._apply_change(task, result, optimized, reflection) log_entry.applied = applied # Strategy tuning (if enabled) if self._strategy_tuning_enabled and self._strategy_tuner is not None: await self._run_strategy_tuning(task, result, reflection) self._evolution_log.append(log_entry) return log_entry # Run A/B test ab_result = await self._run_ab_test(task, result, optimized, reflection) log_entry.ab_test_result = ab_result if ab_result is None or not ab_result.is_significant: # Insufficient samples or inconclusive if ab_result is None: logger.info("Insufficient data for A/B test, keeping current prompt") else: logger.info( f"A/B test inconclusive (p={ab_result.p_value}), keeping current prompt" ) # Don't apply the change, don't rollback either — just keep current self._evolution_log.append(log_entry) return log_entry if ab_result.winner == "experiment": # Treatment wins → apply optimized prompt logger.info("A/B test significant: treatment wins, applying optimized prompt") applied = await self._apply_change(task, result, optimized, reflection) log_entry.applied = applied else: # Control wins → rollback, keep original logger.info("A/B test significant: control wins, keeping original prompt") rolled_back = await self._rollback_change(log_entry) log_entry.rolled_back = rolled_back # Step 4: Strategy tuning (if enabled) if self._strategy_tuning_enabled and self._strategy_tuner is not None: await self._run_strategy_tuning(task, result, reflection) self._evolution_log.append(log_entry) return log_entry async def _optimize_with_context( self, module: Module, reflection: Reflection ) -> Module: """Run optimization, passing reflection context if optimizer supports it""" from agentkit.evolution.prompt_optimizer import LLMPromptOptimizer if isinstance(self._prompt_optimizer, LLMPromptOptimizer): return await self._prompt_optimizer.optimize(module, trace=None, reflection=reflection) return await self._prompt_optimizer.optimize(module) async def _run_ab_test( self, task: TaskMessage, result: TaskResult, optimized: Module, reflection: Reflection, ) -> ABTestResult | None: """Run A/B test: assign group → record result → evaluate""" test_id = f"evolve_{task.task_id}" # Create test if not exists if test_id not in self._ab_tester._tests: self._ab_tester.create_test(ABTestConfig( test_id=test_id, agent_name=result.agent_name, change_type="prompt", )) # Assign group deterministically based on task_id group = self._ab_tester.assign_group(test_id, task_id=task.task_id) # Record the current task result self._ab_tester.record_result(test_id, group, reflection.quality_score) # Persist results if store is available await self._ab_tester.persist_results(test_id) # Evaluate return await self._ab_tester.evaluate(test_id) async def _run_strategy_tuning( self, task: TaskMessage, result: TaskResult, reflection: Reflection, ) -> None: """Run strategy tuning with trace metrics""" if self._strategy_tuner is None: return # Build current strategy config from result metrics current_config = StrategyConfig( temperature=0.5, max_iterations=5, ) # Record the current result self._strategy_tuner.record(current_config, reflection.quality_score) # Get suggestion suggested = await self._strategy_tuner.suggest(current_config) logger.info( f"Strategy tuning suggestion for task {task.task_id}: " f"temperature={suggested.temperature:.2f}, " f"max_iterations={suggested.max_iterations}" ) def get_evolution_history(self) -> list[dict[str, Any]]: """获取进化历史记录""" history = [] for entry in self._evolution_log: record: dict[str, Any] = { "task_id": entry.task_id, "applied": entry.applied, "rolled_back": entry.rolled_back, "event_id": entry.event_id, "created_at": entry.created_at.isoformat(), } if entry.reflection: record["reflection"] = { "outcome": entry.reflection.outcome, "quality_score": entry.reflection.quality_score, "suggestions": entry.reflection.suggestions, } if entry.optimized_module: record["optimized_module"] = entry.optimized_module.name if entry.ab_test_result: record["ab_test"] = { "winner": entry.ab_test_result.winner, "is_significant": entry.ab_test_result.is_significant, } history.append(record) return history def set_current_module(self, module: Module | None = None) -> None: """设置当前 Prompt 模块 Args: module: Module 实例。如果为 None,子类应自行创建。 """ self._current_module = module async def _apply_change( self, task: TaskMessage, result: TaskResult, optimized: Module, reflection: Reflection, ) -> bool: """应用优化变更""" if self._evolution_store is None: # 无存储时直接更新内存中的模块 self._current_module = optimized return True event = EvolutionEvent( agent_name=result.agent_name, change_type="prompt", before={"module_name": self._current_module.name if self._current_module else ""}, after={"module_name": optimized.name, "demos_count": len(optimized.demos)}, metrics={"quality_score": reflection.quality_score}, ) try: event_id = await self._evolution_store.record(event) self._current_module = optimized # 回写 event_id 到对应的 log entry for entry in reversed(self._evolution_log): if entry.task_id == task.task_id and entry.event_id is None: entry.event_id = event_id break return True except (DBAPIError, RuntimeError, ValueError, KeyError) as e: logger.error(f"Failed to apply evolution change: {e}") return False async def _rollback_change(self, log_entry: EvolutionLogEntry) -> bool: """回滚进化变更""" if self._evolution_store is None or log_entry.event_id is None: return True try: return await self._evolution_store.rollback(log_entry.event_id) except (DBAPIError, RuntimeError, ValueError, KeyError) as e: logger.error(f"Failed to rollback evolution change: {e}") return False def record_reflection( self, pattern: str, reflection: Reflection, task_type: str = "", score: float | None = None, ) -> None: """记录反思到待处理列表,附带时间戳、分数和任务类型。""" if pattern not in self.pending_soul_updates: self.pending_soul_updates[pattern] = [] self.pending_soul_updates[pattern].append( { "reflection": reflection, "timestamp": datetime.now(timezone.utc), "score": score if score is not None else reflection.quality_score, "task_type": task_type, } ) async def evolve_soul( self, task: TaskMessage, result: TaskResult, memory_store: MemoryStore | None = None, reflection: Reflection | None = None, task_type: str = "", score: float | None = None, ) -> bool: """Check if soul should be updated based on accumulated reflections. Multi-dimensional triggers: - Time decay: older reflections contribute less - Quality gradient: declining scores trigger early - Task type weight: different task types have different trigger thresholds - Trigger threshold: effective_count * weight >= min_reflections """ if memory_store is None: return False if reflection is None: if self._reflector is None: return False reflection = await self._reflector.reflect(task, result) # 只关注低质量且有建议的反思 if reflection.quality_score >= 0.5: return False if not reflection.suggestions: return False config = self._evolution_config or SoulEvolutionConfig() # 按 pattern 分类累积反思(patterns为空时使用默认category) categories = reflection.patterns if reflection.patterns else ["default"] for pattern in categories: self.record_reflection( pattern, reflection, task_type=task_type, score=score ) # 检查是否有类别满足触发条件 for category, reflections in list(self.pending_soul_updates.items()): # --- Quality gradient: 3+ declining scores trigger early --- scores = [r["score"] for r in reflections if r["score"] is not None] quality_gradient_triggered = False if len(scores) >= 3: last_3 = scores[-3:] declines = [ last_3[i] - last_3[i - 1] for i in range(1, len(last_3)) ] if all(d <= config.quality_gradient_threshold for d in declines): quality_gradient_triggered = True # --- Time decay: compute effective_count --- now = datetime.now(timezone.utc) effective_count = 0.0 for r in reflections: age_seconds = (now - r["timestamp"]).total_seconds() age_hours = age_seconds / 3600.0 effective_count += config.time_decay_factor ** age_hours # Round to avoid floating-point precision issues # (e.g. 3 recent reflections should yield exactly 3.0) effective_count = round(effective_count, 6) # --- Task type weight --- weight = 1.0 if task_type and task_type in config.task_type_weights: weight = config.task_type_weights[task_type] # --- Trigger threshold: effective_count * weight >= min_reflections --- weighted_count = effective_count * weight if weighted_count >= config.min_reflections or quality_gradient_triggered: # 触发 soul 更新 from agentkit.tools.memory_tool import MemoryTool tool = MemoryTool(memory_store) section = category # 汇总所有累积反思的建议(去重,最多取 5 条) all_suggestions: list[str] = [] seen: set[str] = set() for r in reflections: for suggestion in r["reflection"].suggestions: if suggestion not in seen: seen.add(suggestion) all_suggestions.append(suggestion) content = "; ".join(all_suggestions[:5]) reason = f"连续{len(reflections)}次低质量反思 (category: {category})" update_result = await tool.execute( action="update_soul", file="soul", section=section, content=content, reason=reason, ) if update_result.get("success"): logger.info( f"Soul evolved: category={category}, " f"version={update_result.get('version')}" ) # 清除已处理的类别 del self.pending_soul_updates[category] return True return False