Files
wehub-resource-sync e64161ec32
CI / ci (3.11) (push) Has been cancelled
CI / ci (3.10) (push) Has been cancelled
CI / dependabot (push) Has been cancelled
Release / release_and_publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:15 +08:00

157 lines
6.7 KiB
Python

"""
LLM Fine-tuning Experiment Feedback Generation
Provides feedback analysis for LLM fine-tuning experiments, including
model performance evaluation, training metrics analysis, and improvement suggestions.
"""
import json
from typing import Dict
from rdagent.app.finetune.llm.conf import FT_RD_SETTING
from rdagent.core.proposal import (
Experiment2Feedback,
ExperimentFeedback,
HypothesisFeedback,
)
from rdagent.core.scenario import Scenario
from rdagent.log import rdagent_logger as logger
from rdagent.log.utils import dict_get_with_warning
from rdagent.oai.llm_utils import APIBackend
from rdagent.scenarios.finetune.experiment.experiment import FTExperiment
from rdagent.scenarios.finetune.proposal.proposal import FTHypothesis
from rdagent.scenarios.finetune.proposal.trace import FTTrace
from rdagent.utils import convert2bool
from rdagent.utils.agent.tpl import T
class FTExperiment2Feedback(Experiment2Feedback):
"""Generate feedback for LLM fine-tuning experiments"""
def __init__(self, scen: Scenario, version: str = "exp_feedback") -> None:
super().__init__(scen)
self.version = version
def generate_feedback(
self, exp: FTExperiment, trace: FTTrace | None = None, exception: Exception | None = None
) -> ExperimentFeedback:
"""
Generate comprehensive feedback for LLM fine-tuning experiment.
Args:
exp: The experiment to analyze
trace: Experiment trace (optional)
exception: If provided, indicates experiment failed and contains error details
Note: If exception is None, it means training succeeded and we evaluate quality/effectiveness.
If exception is provided, we analyze the failure cause.
"""
# Get task information
task_desc = exp.sub_tasks[0].get_task_information()
# Initialize for SOTA update logic later
sota_benchmark = None
if exception is not None:
# Error case: use error analysis prompt
version = "exp_feedback_error"
error_info = str(exception)
# Try to get FTRunnerEvaluator's analysis result from workspace
# This contains structured feedback (execution, return_checking, code) instead of raw error string
runner_feedback = None
if exp.sub_workspace_list:
for ws in exp.sub_workspace_list:
if ws and hasattr(ws, "feedback") and ws.feedback:
runner_feedback = ws.feedback
break
if runner_feedback:
# Use FTRunnerEvaluator's structured analysis result
error_info = f"""## Execution Analysis
{runner_feedback.execution}
## Return Checking
{runner_feedback.return_checking}
## Code Analysis
{runner_feedback.code}"""
system_prompt = T(f".prompts:{version}.system").r(
scenario=self.scen.get_scenario_all_desc(),
)
# Get workspace files safely
workspace_files = {}
if hasattr(exp, "experiment_workspace") and exp.experiment_workspace is not None:
workspace_files = exp.experiment_workspace.file_dict
user_prompt = T(f".prompts:{version}.user").r(
hypothesis=exp.hypothesis,
task_desc=task_desc,
workspace_files=workspace_files,
error_info=error_info,
)
else:
# Success case: use normal feedback prompt
version = self.version
# Process experiment result - handle both new and legacy formats
exp_result = exp.experiment_workspace.running_info.result
if isinstance(exp_result, dict) and "benchmark" in exp_result:
# New format: contains benchmark and training_metrics
benchmark = exp_result.get("benchmark", {})
raw_metrics = exp_result.get("training_metrics", {})
# Pass loss_history directly (simpler and preserves full information)
loss_history = raw_metrics.get("loss_history", {"train": [], "eval": []})
# Sample train entries if too many to avoid token bloat
if len(loss_history.get("train", [])) > 60:
loss_history["train"] = loss_history["train"][:30] + loss_history["train"][-30:]
training_metrics = (
{"loss_history": loss_history} if (loss_history.get("train") or loss_history.get("eval")) else {}
)
else:
# Legacy format: exp_result is directly the benchmark result (list of dicts)
benchmark = {"accuracy_summary": exp_result, "error_samples": []}
training_metrics = {}
# Get SOTA experiment's benchmark results for comparison
sota_benchmark = trace.sota_benchmark() if trace else None
# Get baseline benchmark (always exists, computed at scenario init)
baseline_benchmark = getattr(self.scen, "baseline_benchmark_score", None)
system_prompt = T(f".prompts:{version}.system").r(
scenario=self.scen.get_scenario_all_desc(),
has_sota=sota_benchmark is not None,
force_think_token=FT_RD_SETTING.force_think_token,
)
user_prompt = T(f".prompts:{version}.user").r(
hypothesis=exp.hypothesis,
task_desc=task_desc,
workspace_files=exp.experiment_workspace.file_dict,
execution_time=exp.experiment_workspace.running_info.running_time,
benchmark=benchmark,
training_metrics=training_metrics,
sota_benchmark=sota_benchmark,
baseline_benchmark=baseline_benchmark,
)
resp_dict = json.loads(
APIBackend().build_messages_and_create_chat_completion(
user_prompt=user_prompt,
system_prompt=system_prompt,
json_mode=True,
json_target_type=Dict[str, str | bool | int],
)
)
# Extract feedback components
error_type = resp_dict.get("Error Type") if exception is not None else None
hypothesis_feedback = HypothesisFeedback(
code_change_summary=dict_get_with_warning(resp_dict, "Code Summary", "No code summary provided"),
reason=dict_get_with_warning(resp_dict, "Reason", "No reasoning provided"),
decision=convert2bool(dict_get_with_warning(resp_dict, "Decision", "no")),
acceptable=exception is None, # Only acceptable if no error
observations=error_type, # Store error type for history display
)
return hypothesis_feedback