要让自己的 Agent 接进 LifeVal 这类生活任务基准,输入输出需要按任务描述对齐,但对齐的对象不是任务描述里的自然语言文案,而是两个必须遵守的约定:适配层从任务文件里读取哪些字段、Agent 的结果按什么结构写回结果记录。任务描述里的目标、约束、初始状态,通常对应任务文件中的具体键;结果记录里的任务标识、动作序列、最终状态,通常是评测和复查要读的键。建议先拿一条任务把这两端跑通,确认没有字段断链,再批量跑分;否则分数不好看时,你分不清是 Agent 不行还是适配层漏字段。
接进 LifeVal 的可行做法是先写一层薄的适配:读任务文件时把目标状态与约束拆开,转成结构化输入再交给 Agent,输出统一落成含 task_id、action_sequence、final_state 的结果记录。用单条任务验证三个点:任务标识能对上、最终状态非空、动作条数合理。字段名以你手上的公开任务文件为准,本文代码里的键名都是占位,需要逐个替换后再运行。
读一条任务文件,确认输入字段有哪些
第一步不要写适配逻辑,先随便挑一条任务打印出来看。生活类任务文件通常是 JSON 或 YAML,字段命名各家不一定一样,所以下面的骨架只用占位键,跑之前要按实际文件改。关键是把“目标状态”和“约束”分开读:前者决定 Agent 要达成什么,后者决定哪些动作不能做、哪些资源不能碰。把两者压成一段自然语言,后面就没法单独校验约束是否被遵守。
import json
def load_task(path):
with open(path, encoding="utf-8") as f:
raw = json.load(f)
# 以下键名均为占位,按实际任务文件调整
return {
"task_id": raw.get("task_id") or raw.get("id"),
"instruction": raw.get("instruction"), # 任务的自然语言描述
"target_state": raw.get("target_state"), # 期望达成的最终状态
"initial_state": raw.get("initial_state"), # 起始状态
"constraints": raw.get("constraints", []), # 时间/预算/禁用资源等
"allowed_actions": raw.get("actions"), # 可执行动作集合,若有
}
打印两三条后,你会看到哪些字段是每条任务都有的、哪些是部分任务才出现的。前者是硬性依赖,缺了就应当拒收;后者只能当可选项处理,不能靠默认值填空。
写一个适配层把任务转成 agent 输入
适配函数的职责是“校验 + 重排”,不是“解释任务”。它输出结构化对象,Agent 侧再决定怎么拼提示词。字段缺失时要分支处理:缺 instruction 或 target_state 的任务,建议直接标记为无效任务,不送进 Agent,避免把解析失败记成 Agent 失败;缺 constraints 或 initial_state 的,可以补空列表或 null,同时在日志里留一条记录,别用编造的默认约束冒充真实约束。
def build_agent_input(task):
missing = [k for k in ("instruction", "target_state") if not task.get(k)]
if missing:
return {"ok": False, "reason": "missing_required", "fields": missing}, None
payload = {
"task": task["instruction"],
"goal": task["target_state"],
"constraints": task.get("constraints") or [],
"initial_state": task.get("initial_state"),
"action_space": task.get("allowed_actions") or [],
}
return {"ok": True}, payload
# 调用侧:替换成你自己的 agent 入口
meta, payload = build_agent_input(task)
if not meta["ok"]:
record_invalid(task, meta) # 单独落一条无效记录,不进 agent
else:
raw_output = my_agent.run(payload)
这样做的收益是可定位:无效任务、Agent 报错、结果不符是三类不同的问题,从这一步就开始分开记录,后面批量跑时不用回头猜。
把 agent 的输出写回结果记录
结果记录的用途是复查和比对,所以结构要稳定,字段宁可少也不要随意增删。下面是一个最小骨架,字段为空时的处理规则写在代码注释里:动作序列即使为空也写空数组,不要写 null;最终状态拿不到时写 null 并把状态标成不完整,不要用空对象假装有状态,否则后面的比对脚本分不清“确实没产生状态”和“字段漏写”。
{
"task_id": "TASK-0001",
"agent": "my-agent-v0",
"status": "done",
"action_sequence": [
{"step": 1, "action": "<动作名>", "args": {}},
{"step": 2, "action": "<动作名>", "args": {}}
],
"final_state": {},
"error": null
}
status 建议至少区分 done、incomplete、agent_error 三种;error 正常时写 null,异常时写简短原因,便于后面按原因分组。如果 Agent 返回的是自由文本,写回前先做一层解析,解析失败就落到 agent_error,不要把原文塞进 final_state。
用单条任务验证链路是否打通
批量之前,固定用同一条任务反复跑,直到下面命令能稳定产出结果文件。命令是占位写法,参数名按你自己的脚本调整:
python adapt.py `--task` tasks/sample.json `--dry-run` # 只打印解析后的输入
python run_one.py `--task` tasks/sample.json `--out` runs/sample.jsonl
验证点就三条,逐条核对:结果里的 task_id 与输入任务的 task_id 完全一致;final_state 非空且结构符合预期;action_sequence 条数在合理区间,既不是 0,也不是同一动作被反复堆到几十上百条。另外回看日志,确认没有出现 missing_required,也没有把无效任务混进正常结果。可以用几行断言把这三条固定下来:
rec = json.loads(line)
assert rec["task_id"] == task["task_id"]
assert rec["final_state"], "final_state 为空"
assert 0 < len(rec["action_sequence"]) <= 50
三条都过,说明输入侧和输出侧的字段没有断链;任何一条不过,先修适配层,不要动 Agent 的策略或提示词,否则变量太多,问题会被掩盖。
打通之后按失败原因给批量结果分组
跑通一条不等于能跑批量。批量跑完先别急着看总分,按失败原因分组统计,每组对应一个明确的下一步改动。常见的分组维度和处理方向如下:
| 分组 | 判断依据 | 下一步改动 |
|---|---|---|
| 输入解析失败 | 日志出现 missing_required | 改字段映射,不改 Agent |
| Agent 无输出或超时 | status 为 agent_error,error 有记录 | 查调用参数与超时配置 |
| 动作不可执行 | 动作名不在任务给出的动作集合内 | 收紧动作空间或加前置校验 |
| 动作可执行但状态不符 | final_state 非空但目标未达成 | 这才是需要调策略的部分 |
| 写回字段缺失 | final_state 为 null 或结构异常 | 补写回逻辑与解析失败分支 |
汇总表按“分组、任务数、典型日志片段、拟改动位置”四列记录,每跑一轮更新一次。前四类属于适配与执行链路问题,改动成本低;只有第四类“动作可执行但状态不符”才是 Agent 本身的能力问题。把这两类混在一起看,很容易把适配层的漏字段误判成 Agent 效果差,也容易为了修个别任务反复调提示词而收益有限。字段含义与实际取值仍需结合你用的任务文件和运行日志确认,本文给出的键名与结构只是可替换的骨架。