ForcePilot/test/gaia_eval/runner.py

273 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""GAIA 评估运行器
核心评估逻辑:加载 agent、执行评估、收集结果。
"""
import asyncio
import time
import uuid
from dataclasses import dataclass
from tqdm import tqdm
from .config import EvalConfig
from .dataset_loader import GaiaTask
from .scorer import GaiaScorer
@dataclass
class EvalResult:
"""单条评估结果"""
task_id: str
level: int
question: str
gold_answer: str
predicted_answer: str
is_correct: bool
error: str | None = None
duration_seconds: float = 0.0
class GaiaEvalRunner:
"""GAIA 评估运行器
通过直接调用 BaseAgent.invoke_messages() 执行评估,
无需启动 Web 服务。
"""
def __init__(self, config: EvalConfig):
self.config = config
self._agent = None
self._agent_config: dict = {}
async def _init_agent(self):
"""初始化 Agent 实例并加载预设配置"""
from src.agents import agent_manager
self._agent = agent_manager.get_agent(self.config.agent_id)
# 如果指定了 agent_config_id从数据库加载预设配置
if self.config.agent_config_id is not None:
await self._load_agent_config_from_db()
else:
# 使用 CLI 参数组装配置
self._agent_config = self.config.build_agent_config()
async def _load_agent_config_from_db(self):
"""从数据库加载 agent 预设配置"""
from src.repositories.agent_config_repository import AgentConfigRepository
from src.storage.postgres.manager import pg_manager
# 评估脚本独立运行时pg_manager 和 mcp_service 未经过 FastAPI 启动事件初始化,需手动初始化
if not pg_manager._initialized:
pg_manager.initialize()
from src.services.mcp_service import init_mcp_servers
await init_mcp_servers()
async with pg_manager.get_async_session_context() as db:
repo = AgentConfigRepository(db)
config_item = await repo.get_by_id(self.config.agent_config_id)
if config_item is None:
raise ValueError(f"Agent 配置 ID {self.config.agent_config_id} 不存在")
# 与线上逻辑保持一致:从 config_json.context 中提取配置
self._agent_config = (config_item.config_json or {}).get("context", {})
def _extract_answer(self, result: dict) -> str:
"""从 agent 返回结果中提取最终答案
Agent 返回格式为 { "messages": [...] },取最后一条 AI 消息的内容。
"""
messages = result.get("messages", [])
if not messages:
return ""
# 从后往前找第一条 AI 消息
for msg in reversed(messages):
msg_type = getattr(msg, "type", None)
if msg_type == "ai":
content = getattr(msg, "content", "")
if isinstance(content, str) and content.strip():
return self._parse_final_answer(content)
return ""
@staticmethod
def _parse_final_answer(content: str) -> str:
"""尝试从 AI 回复中解析出最终答案
如果回复中包含 "FINAL ANSWER:" 标记,则提取标记后面的内容。
否则返回完整回复。
"""
# 常见答案标记格式
markers = [
"FINAL ANSWER:",
"Final Answer:",
"final answer:",
"最终答案:",
"最终答案:",
"答案:",
"答案:",
]
for marker in markers:
if marker in content:
answer = content.split(marker, 1)[1].strip()
# 取第一行(去掉后续的解释)
first_line = answer.split("\n")[0].strip()
return first_line
# 如果只有一行,直接返回
lines = [ln.strip() for ln in content.strip().split("\n") if ln.strip()]
if len(lines) == 1:
return lines[0]
# 多行回复,返回最后一行(通常是最终答案)
return lines[-1] if lines else content.strip()
async def _run_single_task(self, task: GaiaTask) -> EvalResult:
"""执行单条评估任务"""
start_time = time.time()
predicted_answer = ""
error = None
try:
from langchain.messages import HumanMessage
from .file_handler import build_attachments_and_files, build_image_message_content
# 构建系统提示词
gaia_system_suffix = (
"\n\nIMPORTANT: When you have found the answer, "
"state it clearly on a single line prefixed with 'FINAL ANSWER: '. "
"Keep the answer as concise as possible - just the answer itself, "
"no extra explanation."
)
input_context = {
"user_id": "gaia_eval",
"thread_id": str(uuid.uuid4()),
"agent_config": {
**self._agent_config,
"system_prompt": (
self._agent_config.get("system_prompt", "You are a helpful assistant.")
+ gaia_system_suffix
),
},
}
# 处理附件文件
attachments: list[dict] = []
files: dict = {}
image_content: list[dict] | None = None
if task.file_path:
# 尝试构建图片多模态消息
image_content = build_image_message_content(task.file_path, task.question)
if not image_content:
# 非图片文件:通过 attachments + files 注入 state
attachments, files = build_attachments_and_files(
task.file_path, task.file_name
)
# 构建消息
if image_content:
# 图片:使用多模态 HumanMessage
messages = [HumanMessage(content=image_content)]
else:
messages = [HumanMessage(content=task.question)]
# 构建 graph 输入(包含 attachments 和 files
graph_input = {"messages": messages}
if attachments:
graph_input["attachments"] = attachments
if files:
graph_input["files"] = files
# 直接调用 graph.ainvoke 以传递完整的初始 state
graph = await self._agent.get_graph()
context = self._agent.context_schema()
agent_config = input_context.get("agent_config")
if isinstance(agent_config, dict):
context.update(agent_config)
context.update(input_context)
invoke_config = {
"configurable": {
"thread_id": context.thread_id,
"user_id": context.user_id,
},
"recursion_limit": 100,
}
# 设置超时
result = await asyncio.wait_for(
graph.ainvoke(graph_input, context=context, config=invoke_config),
timeout=self.config.timeout,
)
predicted_answer = self._extract_answer(result)
except TimeoutError:
error = f"Timeout after {self.config.timeout}s"
except Exception as e:
error = f"{type(e).__name__}: {e}"
duration = time.time() - start_time
is_correct = GaiaScorer.score(predicted_answer, task.final_answer) if not error else False
return EvalResult(
task_id=task.task_id,
level=task.level,
question=task.question,
gold_answer=task.final_answer,
predicted_answer=predicted_answer,
is_correct=is_correct,
error=error,
duration_seconds=round(duration, 2),
)
async def run(self, tasks: list[GaiaTask]) -> list[EvalResult]:
"""执行批量评估
Args:
tasks: 待评估的 GaiaTask 列表
Returns:
评估结果列表
"""
await self._init_agent()
results: list[EvalResult] = []
if self.config.concurrency <= 1:
# 串行执行
for task in tqdm(tasks, desc="评估进度", unit=""):
result = await self._run_single_task(task)
results.append(result)
status = "" if result.is_correct else ("" if not result.error else "")
tqdm.write(
f" {status} [{result.task_id[:8]}] L{result.level} "
f"| 预测: {result.predicted_answer[:50]!r} "
f"| 标准: {result.gold_answer[:50]!r} "
f"| {result.duration_seconds:.1f}s"
)
else:
# 并发执行(使用信号量控制并发度)
semaphore = asyncio.Semaphore(self.config.concurrency)
pbar = tqdm(total=len(tasks), desc="评估进度", unit="")
async def bounded_run(task: GaiaTask) -> EvalResult:
async with semaphore:
result = await self._run_single_task(task)
pbar.update(1)
return result
results = await asyncio.gather(*[bounded_run(t) for t in tasks])
pbar.close()
return results