280 lines
8.3 KiB
Python
280 lines
8.3 KiB
Python
"""MultiObjectiveFitness - 多目标适应度评估
|
||
|
||
支持准确率+延迟+成本的综合评估,Pareto 前沿维护。
|
||
扩展 StrategyTuner 到多维参数空间。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import math
|
||
import random
|
||
from dataclasses import dataclass, field
|
||
from typing import Any
|
||
|
||
from agentkit.evolution.genetic import FitnessScore
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class FitnessWeights:
|
||
"""适应度权重配置"""
|
||
|
||
accuracy: float = 0.6
|
||
latency: float = 0.2
|
||
cost: float = 0.2
|
||
|
||
def __post_init__(self):
|
||
total = self.accuracy + self.latency + self.cost
|
||
if abs(total - 1.0) > 0.01:
|
||
# Normalize to sum=1
|
||
self.accuracy /= total
|
||
self.latency /= total
|
||
self.cost /= total
|
||
|
||
|
||
class MultiObjectiveFitness:
|
||
"""多目标适应度评估器
|
||
|
||
将多个维度的指标综合为加权适应度分数,
|
||
并支持 Pareto 前沿维护。
|
||
|
||
使用方式:
|
||
evaluator = MultiObjectiveFitness(weights=FitnessWeights(accuracy=0.6, latency=0.2, cost=0.2))
|
||
score = evaluator.evaluate(accuracy=0.9, latency_ms=500, cost_tokens=2000)
|
||
weighted = evaluator.weighted_score(score)
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
weights: FitnessWeights | None = None,
|
||
max_latency_ms: float = 10000.0,
|
||
max_cost_tokens: float = 10000.0,
|
||
):
|
||
self._weights = weights or FitnessWeights()
|
||
self._max_latency_ms = max_latency_ms
|
||
self._max_cost_tokens = max_cost_tokens
|
||
|
||
def evaluate(
|
||
self,
|
||
accuracy: float = 0.0,
|
||
latency_ms: float = 0.0,
|
||
cost_tokens: float = 0.0,
|
||
custom: float = 0.0,
|
||
) -> FitnessScore:
|
||
"""评估多目标适应度"""
|
||
return FitnessScore(
|
||
accuracy=min(max(accuracy, 0.0), 1.0),
|
||
latency_ms=latency_ms,
|
||
cost_tokens=cost_tokens,
|
||
custom=custom,
|
||
)
|
||
|
||
def weighted_score(self, score: FitnessScore) -> float:
|
||
"""计算加权综合分数"""
|
||
n = score.normalized
|
||
return (
|
||
n["accuracy"] * self._weights.accuracy
|
||
+ n["latency"] * self._weights.latency
|
||
+ n["cost"] * self._weights.cost
|
||
)
|
||
|
||
def pareto_rank(self, scores: list[FitnessScore]) -> list[int]:
|
||
"""计算 Pareto 等级
|
||
|
||
返回每个个体的 Pareto 等级(0 = 前沿,1 = 第二层,...)
|
||
|
||
使用非支配排序算法 (NSGA-II)。
|
||
"""
|
||
n = len(scores)
|
||
if n == 0:
|
||
return []
|
||
|
||
ranks = [0] * n
|
||
domination_count = [0] * n # 被多少个体支配
|
||
dominated_set: list[list[int]] = [[] for _ in range(n)] # 支配哪些个体
|
||
|
||
# Build domination relationships
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
if scores[i].dominates(scores[j]):
|
||
dominated_set[i].append(j)
|
||
domination_count[j] += 1
|
||
elif scores[j].dominates(scores[i]):
|
||
dominated_set[j].append(i)
|
||
domination_count[i] += 1
|
||
|
||
# Assign ranks level by level
|
||
current_front = [i for i in range(n) if domination_count[i] == 0]
|
||
rank = 0
|
||
|
||
while current_front:
|
||
for idx in current_front:
|
||
ranks[idx] = rank
|
||
|
||
next_front = []
|
||
for idx in current_front:
|
||
for dominated_idx in dominated_set[idx]:
|
||
domination_count[dominated_idx] -= 1
|
||
if domination_count[dominated_idx] == 0:
|
||
next_front.append(dominated_idx)
|
||
|
||
current_front = next_front
|
||
rank += 1
|
||
|
||
return ranks
|
||
|
||
def crowding_distance(self, scores: list[FitnessScore]) -> list[float]:
|
||
"""计算拥挤度距离(同一 Pareto 等级内的多样性指标)"""
|
||
n = len(scores)
|
||
if n <= 2:
|
||
return [float("inf")] * n
|
||
|
||
distances = [0.0] * n
|
||
dimensions = ["accuracy", "latency", "cost"]
|
||
|
||
for dim in dimensions:
|
||
# Sort by this dimension
|
||
indices = list(range(n))
|
||
get_val = lambda i: scores[i].normalized[dim]
|
||
indices.sort(key=get_val)
|
||
|
||
# Boundary points get infinite distance
|
||
distances[indices[0]] = float("inf")
|
||
distances[indices[-1]] = float("inf")
|
||
|
||
# Compute range
|
||
vals = [get_val(i) for i in indices]
|
||
val_range = vals[-1] - vals[0]
|
||
if val_range == 0:
|
||
continue
|
||
|
||
# Add normalized distance
|
||
for k in range(1, n - 1):
|
||
i = indices[k]
|
||
distances[i] += (vals[k + 1] - vals[k - 1]) / val_range
|
||
|
||
return distances
|
||
|
||
|
||
@dataclass
|
||
class ExtendedStrategyConfig:
|
||
"""扩展的策略配置"""
|
||
|
||
temperature: float = 0.5
|
||
max_iterations: int = 5
|
||
top_k: int = 5
|
||
retrieval_mode: str = "enhanced" # "standard", "enhanced"
|
||
timeout_seconds: int = 300
|
||
tool_weights: dict[str, float] = field(default_factory=dict)
|
||
|
||
|
||
class ExtendedStrategyTuner:
|
||
"""多维策略调优器
|
||
|
||
扩展 StrategyTuner 到多维参数空间:
|
||
- temperature, max_iterations, top_k, retrieval_mode
|
||
- 支持参数范围约束
|
||
- Bayesian-inspired 多维优化
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
param_ranges: dict[str, tuple[float, float]] | None = None,
|
||
):
|
||
self._param_ranges = param_ranges or {
|
||
"temperature": (0.0, 2.0),
|
||
"max_iterations": (1, 10),
|
||
"top_k": (1, 20),
|
||
}
|
||
self._history: list[dict[str, Any]] = []
|
||
|
||
def record(self, config: ExtendedStrategyConfig, metric: float) -> None:
|
||
"""记录配置和效果指标"""
|
||
self._history.append({
|
||
"config": config,
|
||
"metric": metric,
|
||
})
|
||
|
||
async def suggest(
|
||
self, current: ExtendedStrategyConfig
|
||
) -> ExtendedStrategyConfig:
|
||
"""基于历史数据建议新策略
|
||
|
||
使用多维 Bayesian-inspired 优化:
|
||
1. 在历史中找到 Pareto 最优配置
|
||
2. 在最优配置附近添加高斯噪声探索
|
||
"""
|
||
if len(self._history) < 3:
|
||
return current
|
||
|
||
best = max(self._history, key=lambda x: x["metric"])
|
||
best_config = best["config"]
|
||
|
||
suggested_temperature = self._optimize_param(
|
||
"temperature",
|
||
best_config.temperature,
|
||
noise_std=0.1,
|
||
)
|
||
|
||
suggested_max_iterations = int(self._optimize_param(
|
||
"max_iterations",
|
||
best_config.max_iterations,
|
||
noise_std=1.0,
|
||
))
|
||
|
||
suggested_top_k = int(self._optimize_param(
|
||
"top_k",
|
||
best_config.top_k,
|
||
noise_std=2.0,
|
||
))
|
||
|
||
# Retrieval mode: switch if >50% of top performers use the other mode
|
||
suggested_mode = self._suggest_retrieval_mode(best_config.retrieval_mode)
|
||
|
||
return ExtendedStrategyConfig(
|
||
temperature=suggested_temperature,
|
||
max_iterations=suggested_max_iterations,
|
||
top_k=suggested_top_k,
|
||
retrieval_mode=suggested_mode,
|
||
timeout_seconds=current.timeout_seconds,
|
||
tool_weights=dict(best_config.tool_weights),
|
||
)
|
||
|
||
def _optimize_param(
|
||
self,
|
||
param_name: str,
|
||
best_value: float,
|
||
noise_std: float,
|
||
) -> float:
|
||
"""多维 Bayesian-inspired 参数优化"""
|
||
decay = 1.0 / (1.0 + len(self._history) / 10.0)
|
||
effective_noise = noise_std * decay
|
||
perturbation = random.gauss(0, effective_noise)
|
||
new_value = best_value + perturbation
|
||
|
||
min_val, max_val = self._param_ranges.get(param_name, (0.0, 1.0))
|
||
return max(min_val, min(max_val, new_value))
|
||
|
||
def _suggest_retrieval_mode(self, current_mode: str) -> str:
|
||
"""建议检索模式"""
|
||
if len(self._history) < 5:
|
||
return current_mode
|
||
|
||
# Check top performers
|
||
top = sorted(self._history, key=lambda x: x["metric"], reverse=True)[:5]
|
||
enhanced_count = sum(
|
||
1 for h in top if h["config"].retrieval_mode == "enhanced"
|
||
)
|
||
|
||
if enhanced_count >= 3:
|
||
return "enhanced"
|
||
elif enhanced_count <= 1:
|
||
return "standard"
|
||
return current_mode
|
||
|
||
@property
|
||
def history_size(self) -> int:
|
||
return len(self._history)
|