Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/core/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,6 @@ def refusal_from_answer(answer):
return None
first = answer.strip().splitlines()[0].strip() if answer.strip() else ""
first = first.strip("*").strip()
if first.startswith("我无法回答"): # B49:结论写成"我无法回答:…"
first = first[1:]
return first if is_refusal(first) else None
4 changes: 2 additions & 2 deletions app/eval/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
),
EvalCase(
id="agg_013", category="aggregation",
question="各地区销售总额的中位数是多少?",
question="先算出各地区的总销售额,这几个总额的中位数是多少?", # B52:原题可读成各地区单笔销售额的中位数
ground_truth="result = (df['units'] * df['price']).groupby(df['region']).sum().median()",
keywords=["groupby", "sum", "median"],
),
Expand All @@ -100,7 +100,7 @@
),
EvalCase(
id="agg_015", category="aggregation",
question="各地区总销售额的平均值是多少?", # B47:原题"平均每个地区的销售额"可读成各地区的单笔平均
question="先算出各地区的总销售额,这几个总额的平均值是多少?", # B47/B52:原题"平均每个地区的销售额"可读成各地区的单笔平均
ground_truth="result = (df['units'] * df['price']).groupby(df['region']).sum().mean()",
keywords=["groupby", "sum", "mean"],
),
Expand Down
8 changes: 4 additions & 4 deletions app/eval/comparator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
阶段 1.5 第二轮(B29,来自 Qwen3.5 实测复核):
13. 期望 1 行表("哪条记录"),实际是这一行的 Series(索引是列名)→ 转成 1 行表再比。
14. 期望索引/列是月份序号(1–12,groupby(dt.month) 的结果),实际是 '2024-01' / Timestamp / Period → 比较月份部分。
15. ISO 周标签('2024-W01')也算时间索引,两边都是时间索引时按位置比值。
15. ISO 周标签('2024-W01',阶段 3d 起也可带括号说明 '2024-W09 (02-26)')也算时间索引,两边都是时间索引时按位置比值。
16. 模型自己起键名的汇总 dict:期望数值时 ≤4 项也可(规则 10 的 Series 仍 ≤3);
期望标签元组(最高/最低月份)时,取 dict 里的标签值按顺序比较。
17. 实际多了"总计/合计/All/Total"汇总行或列(crosstab margins=True)而期望没有 → 去掉再比。
Expand All @@ -43,7 +43,7 @@
阶段 3c(B46,来自 eval-3b 的 ReAct 复核;只在上面的规则都不匹配时才用,且要求"唯一"以免奖励罗列多种口径):
20. a) 期望是标签、实际是按值排好序的数值 Series(榜首不并列)→ 榜首标签即答案;
b) 实际是带附加信息的 dict(≤8 项、键是文字而不是月份):
期望数值时,第一层数值项里恰好一个等于期望;期望标签时,第一层恰好一个标签值且等于期望;
期望数值时,第一层数值项里有一项等于期望(阶段 3d B50:原为"恰好一个",几种口径算出同一个值时也算);期望标签时,第一层恰好一个标签值且等于期望;
期望 Series/表时,dict 里恰好一个子对象(Series/表/dict)等于期望;
期望 Series 且索引是文字标签时,每个标签恰好对应一个"含该标签的键"且数值相等('工作日平均销量' ↔ '工作日');
嵌套 dict 里的标量不参与({'口径A': {'复购率': …}, '口径B': {…}} 这种罗列口径的不算对);
Expand All @@ -58,7 +58,7 @@

_MONTH_RE = re.compile(r"^\d{4}-\d{2}$")
_QUARTER_RE = re.compile(r"^(?:\d{4})?\s*Q([1-4])$", re.IGNORECASE)
_WEEK_RE = re.compile(r"^\d{4}-?W\d{1,2}$", re.IGNORECASE)
_WEEK_RE = re.compile(r"^\d{4}-?W\d{1,2}(?:\s*[((][^()()]*[))])?$", re.IGNORECASE) # B51:可带 '(02-26)' 这类说明
_CN_QUARTER_RE = re.compile(r"^(?:\d{4}\s*年?\s*)?第?\s*([一二三四1-4])\s*季度?$")
_CN_NUM = {"一": 1, "二": 2, "三": 3, "四": 4}
_MARGIN_LABELS = {"总计", "合计", "总和", "汇总", "all", "total", "sum"}
Expand Down Expand Up @@ -140,7 +140,7 @@ def _extras_eq(a, e, tol, ordered) -> bool:
leaves = [v for v in vals.values() if _is_num(v) or _pct(v) is not None]
if len(leaves) == len(vals) and max(len(k.strip()) for k in vals) <= 4:
return False # {'华东': 1, '华北': 2, …}:是分组汇总表,碰巧含期望值不算(同规则 10)
return sum(1 for v in leaves if _scalar_eq(v, e, tol)) == 1
return any(_scalar_eq(v, e, tol) for v in leaves) # B50:几个口径算出同一个值(整行去重 / 按 order_id 去重)也算
if _is_label_value(e):
labels = [v for v in vals.values() if _is_label_value(v)]
return len(labels) == 1 and _scalar_eq(labels[0], e, tol)
Expand Down
11 changes: 7 additions & 4 deletions app/eval/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ def _git_exe() -> str:

def _git(*args):
global _git_problem
exe = _git_exe()
try:
r = subprocess.run([_git_exe(), "-C", _REPO, *args], capture_output=True, text=True, timeout=5)
except FileNotFoundError:
_git_problem = "找不到 git(不在 PATH 上;可设环境变量 DATACHAT_GIT 指向 git.exe)"
r = subprocess.run([exe, "-C", _REPO, *args], capture_output=True, text=True, timeout=5)
except FileNotFoundError as e:
# B53:记下实际用的路径和系统报错,方便区分"真找不到"和"找到了但启动失败"
where = "不在 PATH 上" if exe == "git" else ("路径存在但启动失败" if os.path.exists(exe) else "路径不存在")
_git_problem = f"找不到 git({where};可设环境变量 DATACHAT_GIT 指向 git.exe):exe={exe!r},{e}"
raise
except Exception as e:
_git_problem = f"调用 git 失败:{type(e).__name__}: {e}"
_git_problem = f"调用 git 失败:exe={exe!r},{type(e).__name__}: {e}"
raise
if r.returncode != 0:
_git_problem = f"git {args[0]} 退出码 {r.returncode}:{(r.stderr or '').strip()[:200]}"
Expand Down
37 changes: 36 additions & 1 deletion docs/DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ xfail 用 strict 模式:问题一旦修好,对应测试会"意外通过"并
- S 删除 eval-3 上的 eval_compare_oracle_20261010_092954.json 后合并(或由本次 PR 一并处理)。
- 合并后建议 S 再跑一次 `--agent react`,验证 B39/B40/B44 的实际效果;workflow 不必重跑。

## [8] 2026-10-10 · 阶段 3c ReAct 复跑复核与修复 · commit <待提交>
## [8] 2026-10-10 · 阶段 3c ReAct 复跑复核与修复 · commit 16d51c3(PR #8)

### 复跑(S 运行,分支 eval-3b 4a435e0,docs/eval/phase3b/)
- 代码 69a63e0,模型 Qwen3.5-397B-A17B(同一网关),temperature 0,Python 3.11.17,pandas 3.0.6,130 题,n=1;只跑 react。
Expand Down Expand Up @@ -458,3 +458,38 @@ xfail 用 strict 模式:问题一旦修好,对应测试会"意外通过"并
- S 再跑一次 `--agent react`(有字典 / 无字典),验证 B46 提示词收紧后 dict 形态是否减少、B47 改题后的效果;顺便看报告里有没有 git_sha_note。
- 阶段 3 收尾后进入阶段 4(见 roadmap/plan.md)。

## [9] 2026-10-10 · 阶段 3d ReAct 复跑收尾 · commit <待提交>

### 复跑(S 运行,分支 eval-3c fc5d567,docs/eval/phase3c/)
- 代码 16d51c3,模型 Qwen3.5-397B-A17B(同一网关),temperature 0,130 题,n=1;只跑 react。
- 报告 git_sha 为 "16d51c3?",git_sha_note 写的是"找不到 git(不在 PATH 上)"。但 S 的环境里 `where git` 和 `shutil.which('git')` 都返回 D:\Git\cmd\git.exe,说明是"找到了但启动失败",原因待查(B53)。

| 配置 | 3b 实跑 | [8] 重评预计 | **3c 实跑** | 本阶段重评 | 应拒答 |
|---|---|---|---|---|---|
| react 有字典 | 92.3% | 96.9% | **97.7%**(127/130) | **98.5%**(128/130) | 8/9 → 9/9 |
| react 无字典 | 91.5% | 96.9% | **95.4%**(124/130) | **97.7%**(127/130) | 8/9 |
| workflow 有字典 / 无字典(阶段 3 报告重评) | — | 98.5% / 97.7% | — | 98.5% / 97.7%(不变) | — |

- 两次都没有网关错误(基础设施失败 0);B47 改过的 agg_015、corr_007、ord_030 都答对了。
- result 是 dict 的题:有字典从 17 题降到 13 题,无字典从 19 题降到 14 题。B46 收紧提示词有效果,但没有消除。
- 本阶段重评后剩下的错题:
- 有字典:agg_013(题意歧义,B52 已改题,重评仍按旧题计)、inv_018(问"是多少"却返回 8 行表,结论 43.14 是对的,属于违反 result 契约)。
- 无字典:ts_010(result 是两个 Series 的 to_string 文本,结论正确,违反契约)、ord_024(应拒答却编了"美妆",真错)、ord_027(没按 order_id 去重,真错)。

### 本阶段修复
| ID | 级别 | 位置 | 问题与修复 |
|---|---|---|---|
| B49 | P2 | app/core/result.py | 结论写成"我无法回答:…"(inv_021)时没有识别为拒答。refusal_from_answer 允许开头多一个"我",其余仍要求"无法回答:<原因>" |
| B50 | P2 | app/eval/comparator.py | 规则 20 b 要求第一层"恰好一个"数值等于期望,几种口径算出同一个值时被拒(ord_011:整行去重、按 order_id 去重都是 40;ord_017 同理)。"恰好一个"其实拦不住罗列不同口径(一对一错照样唯一),所以改成"至少一项等于期望";≤8 项、分组表排除、嵌套标量不参与这几条限制不变 |
| B51 | P3 | app/eval/comparator.py | 周标签写成 '2024-W09 (02-26)'(inv_016)没有识别为时间索引。规则 15 的周标签允许带括号说明 |
| B52 | P2 | app/eval/cases.py | agg_013 "各地区销售总额的中位数"和 agg_015 是同一种歧义,模型理解成了各地区单笔销售额的中位数 → 改为"先算出各地区的总销售额,这几个总额的中位数是多少?",agg_015 统一成同样的写法。注意:题面变了,与之前的基线不完全可比 |
| B53 | P3 | app/eval/report.py | git_sha_note 只写"找不到 git",分不清是真的找不到还是启动失败。现在写出实际使用的 exe 路径、它是否存在,以及系统原始报错 |

### 指标快照
- 单测:Linux Python 3.12 **386 passed**(新增断言,用例数不变)。

### 下一步
- 下次正式跑评测时看 git_sha_note 的原始报错,定位 B53。
- 剩下两类问题是 agent 能力问题:不去重(每次约 1 题)、result 形态违反契约(每次约 1 题)。留到阶段 4 之后用 n≥3 的重复运行来观察。
- 进入阶段 4(见 roadmap/plan.md)。

10 changes: 8 additions & 2 deletions tests/test_comparator.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def test_rule15_iso_week_labels():
e = pd.Series([120, 380], index=pd.to_datetime(["2024-01-07", "2024-01-14"]))
assert results_equal(pd.Series([120, 380], index=["2024-W01", "2024-W02"]), e)
assert not results_equal(pd.Series([380, 120], index=["2024-W01", "2024-W02"]), e)
assert results_equal(pd.Series([120, 380], index=["2024-W01 (01-01)", "2024-W02(01-08)"]), e) # B51
assert not results_equal(pd.Series([120, 380], index=["2024-W01 x", "2024-W02 y"]), e)


def test_rule16_named_summary_dict():
Expand Down Expand Up @@ -203,6 +205,8 @@ def test_refusal_from_answer():
assert refusal_from_answer("**无法回答:没有供应商字段**") == "无法回答:没有供应商字段"
assert refusal_from_answer("无法直接回答,因为没有成本字段") is None
assert refusal_from_answer("无法回答") is None
assert refusal_from_answer("我无法回答:数据中没有价格字段。") == "无法回答:数据中没有价格字段。" # B49
assert refusal_from_answer("我认为无法回答:没有价格") is None
assert refusal_from_answer(None) is None


Expand All @@ -228,8 +232,10 @@ def test_rule20_scalar_inside_dict():
a = {"2023年内离职人数": 14, "2023-01-01在职人数": 183, "流失率(%)": 7.65}
assert results_equal(a, 7.65)
assert not results_equal(a, 7.5)
# 不唯一(≤4 项的 dict 归规则 10 管,这里用 5 项)
assert not results_equal({"甲项指标值": 7.65, "乙项指标值": 7.65, "丙项指标值": 1, "丁项指标值": 2, "戊项指标值": 3}, 7.65)
# B50:两种口径算出同一个值(≤4 项的 dict 归规则 10 管,这里用 5 项)
assert results_equal({"甲项指标值": 7.65, "乙项指标值": 7.65, "丙项指标值": 1, "丁项指标值": 2, "戊项指标值": 3}, 7.65)
assert results_equal({'原始行数': 1218, 'order_id唯一数': 1200, '整行去重后行数': 1200, 'order_id去重后行数': 1200,
'整行去重_符合条件': 40, 'order_id去重_符合条件': 40}, 40)
assert results_equal({"甲项指标值": 7.65, "乙项指标值": 7.7, "丙项指标值": 1, "丁项指标值": 2, "戊项指标值": 3}, 7.65)
assert not results_equal({"2024-01": 1.0, "2024-02": 7.65, "2024-03": 3.0}, 7.65) # 时间序列
assert not results_equal({f"k{i}": i for i in range(9)} | {"x": 7.65}, 7.65) # 太多项
Expand Down
Loading