Repository: modelscope/evalscope
Issue: TaskConfig 导出 yaml 文件失败
Root cause: Validation Gap

## 自查清单

在提交 issue 之前，请确保您已完成以下步骤:
- [√] 我已仔细阅读了[相关使用说明文档](https://evalscope.readthedocs.io/zh-cn/latest/get_started/parameters.html)
- [√] 我已查看了[常见问题解答](https://evalscope.readthedocs.io/zh-cn/latest/get_started/faq.html)
- [√] 我已搜索并查看了现有的 issues，确认这不是一个重复的问题

## 问题描述

在 ms-swift 中使用 evalscope 做训练时的评估工具时，发现 TaskConfig 导出 yaml 失败。

## EvalScope 版本（必填）
v1.0.1
（ms-swift version: v3.9.0dev0）

## 使用的工具
- [√] Native / 原生框架
- [ ] Opencompass backend
- [ ] VLMEvalKit backend
- [ ] RAGEval backend
- [ ] Perf / 模型推理压测工具
- [ ] Arena / 竞技场模式

## 执行的代码或指令

```bash
PROJECT_NAME=sft
MODEL_NAME=qwen2_5_05b
MODEL_PATH="Qwen/Qwen2.5-0.5B-Instruct"

EXP_NAME=test

TRAIN_TYPE=full
TRAIN_DATASET_PATH="AI-ModelScope/alpaca-gpt4-data-zh#100"
OUTPUT_DIR=outputs/ms-swift

CUDA_VISIBLE_DEVICES=0 \
swift sft \
    --model $MODEL_PATH \
    --model_type qwen2_5 \
    --train_type $TRAIN_TYPE \
    --dataset $TRAIN_DATASET_PATH \
    --torch_dtype bfloat16 \
    --num_train_epochs 2 \
    --per_device_train_batch_size 8 \
    --per_device_eval_batch_size 8 \
    --learning_rate 1e-4 \
    --gradient_accumulation_steps 2 \
    --eval_strategy "steps" \
    --eval_steps 2 \
    --eval_use_evalscope \
    --eval_dataset "gsm8k" \
    --eval_dataset_args '{"gsm8k": {"few_shot_num": 0}}' \
    --eval_generation_config '{"max_tokens": 512, "temperature": 0}' \
    --eval_limit 10 \
    --extra_eval_args '{"ignore_errors": true}' \
    --save_steps 50 \
    --save_total_limit 100 \
    --logging_steps 1 \
    --max_length 2048 \
    --max_new_tokens 512 \
    --truncation_strategy delete \
    --output_dir $OUTPUT_DIR/$PROJECT_NAME/$EXP_NAME \
    --warmup_ratio 0.05 \
    --dataloader_num_workers 4 \
    --model_author swift \
    --model_name $MODEL_NAME 2>&1 | tee tmp.log
```

请提供您执行的主要代码或指令。

## 错误日志

按照上述指令训练模型，会发现 `output/ms-swift/sft/test/{date}/eval/configs` 文件夹下的 task_config yaml 文件为空。

## 运行环境

- 操作系统：Ubuntu 22.04.3 LTS
- Python版本：3.10.16

## 其他信息

问题出在 TaskConfig 的 `to_dict()` 方法：
```python
# evalscope/config.py
    def to_dict(self):
        result = copy.deepcopy(self.__dict__)
        del result['api_key']  # Do not expose api_key in the config

        if isinstance(self.model, (Model, ModelAPI)):
            result['model'] = self.model.__class__.__name__

        if isinstance(self.generation_config, GenerateConfig):
            result['generation_config'] = self.generation_config.model_dump(exclude_unset=True)

        return result
```

ms-swift 向 TaskConfig 传参的时，在 `model_args` 字段中添加了整个模型的索引，导致 `to_dict()` 方法没能正确解析：
```python
# mixin.py
    @torch.no_grad()
    def _evalscope_eval(self):
        from ..llm.eval.utils import EvalModel  # registry here
        from evalscope import TaskConfig, run_task

        self.model.eval()

        task_config_kwargs = dict(
            model=f'model-step{self.state.global_step}',
            model_args=dict(
                model=self.model,                     # 整个模型索引
                template=self.template,
            ),
            eval_type='swift_custom',
            datasets=self.args.eval_dataset,
            dataset_args=self.args.eval_dataset_args,
            limit=self.args.eval_limit,
            work_dir=os.path.join(self.args.output_dir, 'eval'),
            eval_batch_size=self.args.per_device_eval_batch_size,
            generation_config=self.args.eval_generation_config or {'max_tokens': 512},
        )
        task_config_kwargs.update(self.args.extra_eval_args or {})
        task_config = TaskConfig(**task_config_kwargs)
        # start evaluation
        eval_report = run_task(task_config)
        # convert to dict
        eval_dict = {f'test_{k}': v.score for k, v in eval_report.items()}
        self.log(eval_dict)

        self.model.train()
        return eval_dict
```

如果将 `to_dict()` 方法完善一下，考虑到可能存在的 Python class 对象，问题就解决了：
```python
# evalscope/config.py
    def to_dict(self):
        result = copy.deepcopy(self.__dict__)
        del result['api_key']  # Do not expose api_key in the config

        if isinstance(self.model, (Model, ModelAPI)):
            result['model'] = self.model.__class__.__name__

        if isinstance(self.generation_config, GenerateConfig):
            result['generation_config'] = self.generation_config.model_dump(exclude_unset=True)

        # 定义一个递归函数来处理嵌套的字典
        def process_value(value):
            if isinstance(value, dict):
                return {k: process_value(v) for k, v in value.items()}
            elif isinstance(value, list):
                return [process_value(item) for item in value]
            elif isinstance(value, object) and not isinstance(value, (str, int, float, bool, type(None))):
                try:
                    return str(value)
                except (AttributeError, TypeError):
                    try:
                        return repr(value)
                    except (AttributeError, TypeError):
                        return value.__class__.__name__
            else:
                return value

        processed_result = {key: process_value(value) for key, value in result.items()}
        return processed_result
```

我注意到在 [model_args](https://evalscope.readthedocs.io/en/latest/get_started/parameters.html#model-parameters) 文档中对传入的数据的期望是能被 json 解析，是否是 ms-swift 的传参不合适？还是 evalscope 需要考虑支持更多的情况？

这部分已在ms-swift中修复

> 感谢你的反馈！我们将关闭此问题。如果您有任何疑问，请随时重新打开它。如果EvalScope对您有所帮助，欢迎给我们点个STAR以示支持，谢谢！