zhatrix's picture
update space
d409cb8 verified
Raw History Blame Contribute Delete
5.53 kB
"""评测:对 eval/cases.jsonl 逐条跑 Agent,按规则打分(关键词命中 / 工具调用正确 / JSON 字段正确)。
用法:uv run python scripts/eval.py [--no-tools] [--no-rag] [--limit N] [--cases eval/stress.jsonl] [--out result.jsonl]
"""
from __future__ import annotations
import argparse
import asyncio
import json
import re
import time
from pathlib import Path
from logicllm import config
from logicllm.agent.core import LogisticsAgent
CASES = config.ROOT / "eval" / "cases.jsonl"
def score(case: dict, answer: str, steps: list) -> tuple[float, str]:
tools_called = [s.content["name"] for s in steps if s.kind == "tool_call"]
notes = []
ok = 1.0
if "expect_tool" in case:
if case["expect_tool"] not in tools_called:
ok = 0.0; notes.append(f"未调用 {case['expect_tool']} (实际 {tools_called})")
if "expect_tools" in case: # 多个工具都必须被调用
miss_t = [t for t in case["expect_tools"] if t not in tools_called]
if miss_t:
ok = 0.0; notes.append(f"未调用 {miss_t} (实际 {tools_called})")
if "expect_no_tool" in case and tools_called:
ok = 0.0; notes.append(f"不应调用工具,实际 {tools_called}")
norm = answer.replace(" ", "")
if "keywords" in case:
miss = [k for k in case["keywords"] if k.replace(" ", "") not in norm]
if miss:
ok = min(ok, 1 - len(miss) / len(case["keywords"])); notes.append(f"缺关键词 {miss}")
if "lead" in case: # 结论必须出现在回答开头(前 20 字),防止"4 天……不是 3 天"这类对照句被 forbid 误判、也防止先错后对
head = norm[:20]
if not any(k.replace(" ", "") in head for k in case["lead"]):
ok = 0.0; notes.append(f"开头未给出结论 {case['lead']}(实际开头「{head}」)")
if "bold" in case: # 结论必须出现在加粗段(**…**)里:数值套档题让模型先写比较过程、再加粗给结论,评分只看结论
bolds = "".join(re.findall(r"\*\*(.+?)\*\*", norm))
if not any(k.replace(" ", "") in bolds for k in case["bold"]):
ok = 0.0; notes.append(f"加粗结论未命中 {case['bold']}(实际加粗「{bolds[:30]}」)")
if "keywords_any" in case: # 命中其一即可
if not any(k.replace(" ", "") in norm for k in case["keywords_any"]):
ok = 0.0; notes.append(f"任一关键词均未命中 {case['keywords_any']}")
if "forbid" in case: # 出现即零分(用于臆测/编造检查)
hit = [k for k in case["forbid"] if k.replace(" ", "") in norm]
if hit:
ok = 0.0; notes.append(f"出现禁用词 {hit}")
if "expect_json" in case:
m = re.search(r"\{.*\}", answer, re.S)
try:
got = json.loads(m.group(0)) if m else {}
except json.JSONDecodeError:
got = {}
exp = case["expect_json"]
hit = sum(1 for k, v in exp.items() if str(got.get(k, "")).strip() == str(v))
ok = min(ok, hit / len(exp)); notes.append(f"JSON 字段 {hit}/{len(exp)}")
return ok, "; ".join(notes)
async def main():
ap = argparse.ArgumentParser()
ap.add_argument("--no-tools", action="store_true")
ap.add_argument("--no-rag", action="store_true")
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--cases", default=str(CASES), help="题库文件,默认 eval/cases.jsonl")
ap.add_argument("--out", default="", help="把每题的回答/工具调用/得分写入 jsonl,便于分析失败题")
a = ap.parse_args()
cases = [json.loads(l) for l in Path(a.cases).read_text(encoding="utf-8").splitlines()
if l.strip() and not l.strip().startswith("//")]
if a.limit:
cases = cases[:a.limit]
agent = LogisticsAgent(use_tools=not a.no_tools, use_rag=not a.no_rag)
print(f"后端 {config.LLM_BACKEND}:{config.LLM_MODEL} 工具={not a.no_tools} RAG={not a.no_rag} 用例 {len(cases)}\n")
total, by_cat, records = 0.0, {}, []
t0 = time.time()
for i, c in enumerate(cases, 1):
try:
turns = c.get("turns") or [c["q"]]
history, steps_all = [], []
for t in turns:
history.append({"role": "user", "content": t})
res = await agent.run(history)
history.append({"role": "assistant", "content": res.answer})
steps_all.extend(res.steps)
s, note = score(c, res.answer, steps_all)
except Exception as e: # noqa: BLE001
s, note, res = 0.0, f"异常 {e}", None
total += s
by_cat.setdefault(c["cat"], []).append(s)
flag = "✅" if s >= 0.99 else ("🟡" if s > 0 else "❌")
qshow = c.get("q") or " ▸ ".join(c.get("turns", []))
print(f"{flag} [{c['cat']}] {qshow[:44]} {s:.2f} {note}")
records.append({"case": c, "score": s, "note": note, "answer": res.answer if res else "",
"tool_calls": [st.content for st in steps_all if st.kind == "tool_call"] if res else []})
print(f"\n总分 {total / len(cases):.3f} 用时 {time.time() - t0:.0f}s")
for k, v in by_cat.items():
print(f" {k}: {sum(v) / len(v):.3f} ({len(v)})")
if a.out:
Path(a.out).write_text("\n".join(json.dumps(r, ensure_ascii=False) for r in records) + "\n", encoding="utf-8")
print(f"明细已写入 {a.out}")
await agent.client.aclose()
if __name__ == "__main__":
asyncio.run(main())