From 04cf763be83b5cbd5da19ffb2df6eb976b56a972 Mon Sep 17 00:00:00 2001 From: Smilewithoutfalling <91455578+Smilewithoutfalling@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:52 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E9=98=B6=E6=AE=B5=203d=EF=BC=9Aapp/core/re?= =?UTF-8?q?sult.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/result.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/core/result.py b/app/core/result.py index 87321f6..b23b0f0 100644 --- a/app/core/result.py +++ b/app/core/result.py @@ -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 From 8f7a5f1e30f0fe23114dfab1a6e5922812a6876f Mon Sep 17 00:00:00 2001 From: Smilewithoutfalling <91455578+Smilewithoutfalling@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:54 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E9=98=B6=E6=AE=B5=203d=EF=BC=9Aapp/eval/ca?= =?UTF-8?q?ses.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/eval/cases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/eval/cases.py b/app/eval/cases.py index c37f0c7..349a8d0 100644 --- a/app/eval/cases.py +++ b/app/eval/cases.py @@ -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"], ), @@ -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"], ), From 7ffee6d4b74d61040bded46a884dc8d8621457d7 Mon Sep 17 00:00:00 2001 From: Smilewithoutfalling <91455578+Smilewithoutfalling@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:55 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E9=98=B6=E6=AE=B5=203d=EF=BC=9Aapp/eval/co?= =?UTF-8?q?mparator.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/eval/comparator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/eval/comparator.py b/app/eval/comparator.py index 422a570..393f0da 100644 --- a/app/eval/comparator.py +++ b/app/eval/comparator.py @@ -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)而期望没有 → 去掉再比。 @@ -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': {…}} 这种罗列口径的不算对); @@ -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"} @@ -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) From 1dbb89a3b8fd81486dfd40b3daeea63b4b0045ba Mon Sep 17 00:00:00 2001 From: Smilewithoutfalling <91455578+Smilewithoutfalling@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:56 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E9=98=B6=E6=AE=B5=203d=EF=BC=9Aapp/eval/re?= =?UTF-8?q?port.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/eval/report.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/eval/report.py b/app/eval/report.py index 474d24a..5825da0 100644 --- a/app/eval/report.py +++ b/app/eval/report.py @@ -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]}" From 54efa643bdc2abc7046a0d308907fb7fef654c2b Mon Sep 17 00:00:00 2001 From: Smilewithoutfalling <91455578+Smilewithoutfalling@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:57 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E9=98=B6=E6=AE=B5=203d=EF=BC=9Adocs/DEVLOG?= =?UTF-8?q?.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DEVLOG.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index ea7ef3d..14e2c5b 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -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。 @@ -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)。 + From 81d2da7fd0ed3e2987b9a1655a50e05ae8b89dcd Mon Sep 17 00:00:00 2001 From: Smilewithoutfalling <91455578+Smilewithoutfalling@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:42:59 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E9=98=B6=E6=AE=B5=203d=EF=BC=9Atests/test?= =?UTF-8?q?=5Fcomparator.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_comparator.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_comparator.py b/tests/test_comparator.py index e6176b3..3a740f4 100644 --- a/tests/test_comparator.py +++ b/tests/test_comparator.py @@ -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(): @@ -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 @@ -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) # 太多项