Skip to content

dataset: 跟随 d172b62 重生成,统一工具错误协议落到数据里 - #14

Merged
chenweihang0160-alt merged 6 commits into
LoveLearnLearning:mainfrom
chenweihang0160-alt:dataset-20260902
Sep 5, 2026
Merged

dataset: 跟随 d172b62 重生成,统一工具错误协议落到数据里#14
chenweihang0160-alt merged 6 commits into
LoveLearnLearning:mainfrom
chenweihang0160-alt:dataset-20260902

Conversation

@chenweihang0160-alt

Copy link
Copy Markdown
Contributor

这次带来什么

跟随上游 d172b62 重生成,并把 d29d3e4 的统一工具错误协议全面落到数据里。

schema_version 仍是 711faf60(工具面没变,学习空间 22 个),
样本 1407 → 1414(+7,正是你们新加的 structured_runtime),tool_error 49 → 56。

收下了你们两笔的全部改动

commit 我们做了什么
7b5b7eb system prompt 的 alias / teaching trigger → 重抓提示词缓存
d29d3e4 structured_runtime 7 条种子 + gen_structured_runtime + validatemessage 键 + capture_system_prompts --keys 可重复,四处全收validate.py / seeds/tool_errors.yaml 做了三方合并

structured_runtime 那 7 条我们逐条对着 backend/agent/tools/execution_errors.py 核过,
messageretryable 全部逐字一致,并且把这个对账做成了常设闸门(见下)。

🔴 三处我们改了你们的产出(都带证据)

capture_* 抓的层:ToolExecutionResult 有三个投影

d29d3e4 之后工具返回从 dict 变成 ToolExecutionResult,我们两个 capture 脚本
当场 TypeError: Object of type ToolExecutionResult is not JSON serializable

链路我们逐层追过才动手:

ToolExecutionResult.model_content
  → loop_runtime.tool_result_channels()      拆三投影
  → loop_runtime.serialize_tool_result()     json.dumps(..., default=str)
  → ToolObservation.model_text
  → agent.py:361  {"role": "tool", "content": observation.model_text}

模型看见的是 model_contentdisplay_content 走界面、audit_metadata 是独立字段。
两个 capture 已改成取 model_contentesa/fixtures.py 的三个错误构造函数
也改成统一协议 {ok, error_code, error, retryable, tool, attempt, message} ——
test_fixture_contract 当时红了 5 条(「线上有我们没有:['attempt','error_code','message','retryable']」),
现在 72 通过 / 0 失败

data/ir/tool_errors.jsonl 里两种格式并存

你们那份里:

① [Error]: 字符串      7        ④ 旧的 ok/error/detail   6      ← 和 ⑥ 并存
② 带 error 键          12       ⑥ 统一结构化协议         7
③ 阻断 / 空结果        7

gen_structured_runtime 产出的是新协议,但 gen_denied / gen_exec_error
走的还是 fixtures 里旧的 {ok, error, detail} —— 同一个文件里两种格式,
而线上只有一种。本 PR 里那 6 条已经归到 ⑥(13 = 7 + 6)。

顺带:gen_external / gen_tool_errors 有三处 result["detail"]
在新协议下是 KeyError。已改成调 validate._failure_text() ——
模型看见的那句、和 validate 认不认得那句,必须由同一个函数说了算
两处各写一份迟早分叉。

seeds/tool_errors.yamlwrapped 段少了 5 条

你们那份 7 条,我们这份 12 条 —— 少的是 unified_public_unconfigured_00..04
[Error]: public knowledge service is not configured)。三方合并已经把两边的新增都保住了,
本 PR 是并集。

新增一道闸门

tests/test_runtime_error_contract.py —— 拿 execution_errors.py
ERROR_MESSAGES / RETRYABLE_ERROR_CODES 逐条核种子,改一个字就红(反向验证过)。
它还报覆盖率:后端 16 个 error_codestructured_runtime 只覆盖 7 个
未覆盖的 9 个是 attachment_not_authorized / invalid_tool_arguments /
memory_policy_denied / non_idempotent_action_rejected / resource_capability_required /
temporary_tool_error / tool_not_available / upstream_unavailable / wall_time_exhausted
要不要补齐,我们听你们的意见。

同时把这 16 句文案连真实行号登记进了 seeds/tool_errors.yamlregistry
—— 否则 validateerror_text 闸门会拦下整批数据(这次就拦了 11 条)。

📌 一个你们已经修掉的阻塞

docs/后端问题反馈.mdP0-4(删除不存在的记忆会让整轮 run 崩掉)——
d29d3e4 之后 delete_core_memory 返回
{"ok": false, "error_code": "tool_internal_error", ...},不再抛异常。
那批「删不存在的记忆之后怎么恢复」的样本可以补了,下一轮会做。

怎么验(都是跑出来的)

本 PR 分支的树上、后端仓库布局下实测:

PYTHONPATH=backend/scripts/dataset python3 -m esa.validate \
    backend/scripts/dataset/data/ir/*.jsonl      → 0 错误 / 1414 条 / 711faf60

七个自测全过:test_validator 58 · test_fixture_contract 72 ·
test_eval_scoring 111 · test_esa_tool_format · test_llamafactory_ingest ·
test_review_ledger 17 · test_runtime_error_contract
在该树上重跑 esa.evalset,考卷指纹 2f4db3af27963d1e 与我们本机一字不差

评测集 440 道 / 补充集 55 道 / 训练池 1087 条 → split 985 / 55 / 47。
零泄露:考卷 94 个模板与训练池 387 个模板交集为 0。

收下上游 7b5b7eb + d29d3e4 的全部改动,并把统一错误协议落到数据里。
1407 → 1414 条,schema_version 仍 711faf60。

三处修正见 PR 描述:capture 抓 model_content 投影、tool_errors.jsonl
两种格式并存、wrapped 段少的 5 条种子。新增 test_runtime_error_contract
对着 execution_errors.py 逐条核对账。
@chenweihang0160-alt

Copy link
Copy Markdown
Contributor Author

补充两条 09-02 下午查出来的,都影响这个 PR 的数据,说明一下当前状态。

① 已修:11 条恢复话术在教模型编造技术原因

d29d3e4 之后每个工具返回都过 normalize_tool_error_resultcapability_runtime.py:101,在 try 之外的正常返回路径上),错误原文进 audit_metadata.legacy_detail —— 模型看不见。而我们的话术逐字引用了它:

我们教的回答:「arXiv 那边没响应([Error]: arXiv API 请求超时(已重试 5 次)),这次检索没拿到结果。」
模型实际看到:{"ok":false,"error_code":"temporary_tool_error","message":"工具暂时无法完成请求","retryable":true,"retry_after_ms":1000}

这是在教模型:看到一句笼统的失败文案,就编出一个具体原因。已全部改掉,话术只保留模型真看得见的东西(自己发的 tool_call 参数、message/retryable、schema 先验)。

顺带修正了 error_code 的取值:Agent 走 BoundToolExecutor,异常经 classify_tool_exception 分类,所以 arxiv 的 503/429/timeout(源码三处都是 raise RuntimeError)在模型侧是 temporary_tool_error(可重试),不是 tool_internal_error。载荷现在与线上 structured_tool_error 的输出逐字节相同。

新增闸门 tests/test_error_payload_visible.py:话术不许引用被协议改写时丢掉的内容。

② 未修,等你们定口径:业务错误文案被当成 error_code

normalize_tool_error_resultsource.get("error_code") or source.get("error") 会把三个计算器的业务错误文案当成 error_code:

calculator 返回 {"expression":"1024/(10-10)", "result":null, "error":"除零错误"}
模型看到     {"ok":false, "error_code":"除零错误", "message":"工具暂时无法完成请求,请稍后重试"}

扫下来 13 种枚举外的 error_code,最长的是整句「函数 'popcount' 调用错误: popcount 仅支持非负整数…」。而且 expression/result 全丢、没进 audit(不在 legacy_* 白名单)—— 模型既不知道算的是哪个式子,也不知道是除零。

这一族我们没改,保持存工具的真实返回 —— 照抄现状等于把 bug 固化成训练目标。等你们定了口径我们跟。详细说明和建议修法另发。

chenweihang0160-alt and others added 3 commits September 3, 2026 15:54
数据
  · 11 条工具失败话术在逐字引用 audit.legacy_detail —— 那一层模型看不见。
    normalize_tool_error_result 在 capability_runtime.py:101 的正常返回路径上,
    每个工具返回都过它,模型实际看到的是 {ok,error_code,message,retryable,...}。
    error_code 按异常类型经 classify_tool_exception 定,不再从字符串猜。
  · 补 7 条拒绝样本(trainonly__ 前缀,考卷逐字节不变):模型判的是
    「用户动机听起来坏不坏」而不是「这个操作该不该做」,同一个删记录操作,
    说了作假动机的拒了、说「看着难受」的放行了。
  · 训练池 1094 条 / 考卷 01e0f55b#440,零泄露。

闸门
  · tests/test_error_payload_visible.py:载荷必须是 normalize 的不动点,
    话术不许引用被改写时丢掉的内容,不许出现 [Error]:。
  · tests/test_runtime_error_contract.py:种子声明的 message/retryable/error_code
    与后端 execution_errors.py 逐字对账。
  · esa/eval.py 拒绝题判据的播报条件从 `if human` 改成 `human or fallback` ——
    一条人工裁定都没命中时原本整段不打印,而那正是最危险的一种。

评测
  · refusal_adjudication.json 40 → 159 条人工裁定,每条带理由。
  · 三张图重画为 base(93523) → nothink(93522) @ 01e0f55b#440,n_dropped=0。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 给后端的可选件:steer/

从残差流第 48 层读一个方向,判「这一刻该不该调工具」,用来压误触发。
不需要训练,线上代价是一次 3072 维点积。**默认不启用**——
接不接、接在哪由后端决定,这里只提供方向、阈值和参考实现。

实测(440 道考卷,方向在训练侧探针集上取、AUROC 在考卷上报,题目零重叠):

  AUROC 0.959       同样算法在 base 模型上是 0.881
  保住 97.6% 正常调用 → 18 道误触发挡下 13 道 → 误触发率 30.5% → 8.5%
  保住 89.6%         → 挡下 14 道            → 6.8%

⚠️「保住 97.6%」= 164 道该调的题里有 4 道会被误压。这是真代价。

🔴 方向绑定某一个 adapter,换模型必须重导;拿旧方向配新模型不会有任何自然的
报错,所以 load() 强制要求写明 expect_adapter,对不上直接抛 AdapterMismatch。
🔴 它判的是「该不该调」不是「模型会不会错」——后者只有 AUROC 0.640,别那么用。
🔴 需要拿到隐状态,OpenAI 兼容 API 那一层拿不到。这是接入的主要成本。

## 其余改动

· esa/eval.py:请求超时 600 → 1800 秒并对**超时**重试一次(别的异常照抛);
  call_endpoint 加 temperature 参数,默认 0.0,评测那一路一个字没改。
  拒绝题判据的播报条件从 `if human` 改成 `human or fallback`——
  一条人工裁定都没命中时原本整段不打印,而那正是最危险的一种。
· tests/test_tool_coverage.py:考卷考到的工具训练池必须有;被 L3 明确留出的
  工具训练池必须为零(后者更重要:否则 L3 会悄悄失效而报告照绿)。
· tools/check_adapter_fresh.py:状态非 COMPLETED 时开一个窄口子,
  必须同时给 --completed-proof 且 trainer_state.json 自证跑满才放行。自测 6 → 10 条。
· refusal_adjudication.json:人工裁定 140 → 199 条,每条带理由。
· 三张图重画为 base(93523) → ep10(94275),n_dropped=0。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
真挂上闸门重跑被压制的 18 道再整份判分(440 道考卷,拒绝项人工裁定):

  误触发率 FPR   30.5% → 8.5%(5/59)
  工具选择准确率 81.1% → 79.9%(−1.2)
  漏调率 FNR      5.9% →  8.3%(+2.4,仍达标)
  忠实度 / 响应率 / 格式 / 恢复率 / 参数匹配:一个点都没掉
  达标数 8/12 → 8/12

⚠️ FPR 8.5% 仍未跨过 ≤5 的目标,达标数也没变——这个闸门是把大问题变成
小问题,不是把它解决掉。README 里照实写了。

结论对「怎么模拟压制」不敏感:用了两种偏差方向相反的模拟(把 tools 置空 /
保留 tools 另加一句不要调用),18 道回答逐字全不一样,十二项完全相同。
(tool_choice="none" 不能用:API 收下这个字段但不执行。)

唯一的接入成本是拿到隐状态——OpenAI 兼容 API 那一层拿不到,需要后端配合。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chenweihang0160-alt

Copy link
Copy Markdown
Contributor Author

再补一条,和数据无关,但它挡着 CI —— 而 CI 红会挡住这个 PR。

POST /student/classes/join 建路由时就炸,一行能修

2026-09-05 对着 8f25ce8 复核过,仍然存在。

# backend/core/web/routers/student_teaching.py:15   ← 少了 JoinClassRequest
from backend.core.web.teaching_schemas import InvitationResponseRequest, SubmissionCreateRequest

# backend/core/web/routers/student_teaching.py:93-94
@router.post("/classes/join")
def join_class(body: JoinClassRequest, request: Request, session: CurrentSession) -> dict:

JoinClassRequest 本身是好的,定义在 backend/core/web/teaching_schemas.py:31

为什么 import 这个模块不报错:文件第 5 行有 from __future__ import annotations
注解全成了字符串,模块导入阶段不解析。FastAPI 建路由时才去解析,那一刻才炸。
所以它不是「某个请求失败」,是应用起不来

复现(不用装 fastapi,get_type_hints 就是 FastAPI 建路由时调的那个):

from __future__ import annotations
import typing

def join_class(body: JoinClassRequest) -> dict:   # 复刻 :94 的签名
    return {}

typing.get_type_hints(join_class)
# NameError: name 'JoinClassRequest' is not defined

⚠️ 说清楚界限:上面验的是机制,我们这边没装 fastapi,没跑起真实的 FastAPI 应用
你们那边起一次服务就能坐实。

修法:15 的 import 补上 JoinClassRequest。一行。


顺带同步一下这个 PR 的状态:dataset/steer/ 那个推理期工具调用闸门已经做完端到端实测了
(不是估算,是真挂上去重跑再整份判分):误触发率 30.5% → 8.5%,代价是工具选择 −1.2、
漏调 +2.4,忠实度/响应率/格式/恢复率/参数匹配一个点没掉。⚠️ 8.5% 仍然没跨过 ≤5 那条线,
它是把大问题变成小问题,不是解决它。

唯一的接入成本是要拿到隐状态(第 48 层最后一个 token),走 OpenAI 兼容 API 那层拿不到,
得在能访问 output_hidden_states 的地方接。这件事需要你们配合才能落地,
细节和参考实现都在 dataset/steer/README.md,用不用你们定。

@chenweihang0160-alt

Copy link
Copy Markdown
Contributor Author

更正上一条:/classes/join 那个 bug 你们已经修了,是我搞错了。

8696eeestudent_teaching.py:15 的 import 已经补上 JoinClassRequest
时间线是我的评论(08:28:57Z)比你们的修复(08:27:13Z)晚了一分四十四秒。

原因不是巧合,是我核实的方式不可靠:我用 gh api .../contents/... 拉的文件,
那个接口有缓存,返回的是旧内容——连它告诉我的「最新 commit 是 8f25ce8」也是旧的。
所以我写的「对着 8f25ce8 复核过,仍然存在」,复核的其实是一份过期快照。
以后我改用 raw.githubusercontent.com/<repo>/<commit>/... 指定 commit 拉,不再信那个接口的 HEAD。

抱歉浪费你们的注意力。上一条里闸门那部分(FPR 30.5% → 8.5%、需要拿隐状态才能接入)不受影响,仍然有效。


顺带两条关于 8696eee 的反馈,都和数据集这个 PR 直接相关:

① 谢谢把数据集的闸门接进了 CI。 不过 test_llamafactory_ingest.py 那个跳过分支在 CI 上会永远触发——它判的是 $HOME/Downloads/llamafactory-0.9.4 存不存在,CI runner 上不会有这个目录,所以这道闸门在 CI 里一次都不会真跑。而且 0.9.4 也不是我们集群实际用的版本(0.9.5.dev0)。它守的是「LLaMA-Factory 静默吃掉训练样本」,是条要紧的闸门,我在这个 PR 里改成从环境变量取路径、并且找不到就明确失败而不是跳过。

② 另外五个测试也建议一起接进去test_error_payload_visible.pytest_esa_tool_format.pytest_runtime_error_contract.pytest_tool_coverage.pytest_verbosity_tools.py。第一个和你们这次做的事是同一个方向——它查的是「我们教给模型的话术,不许引用模型实际看不见的内容」。

③ 关于 problem_solving_expert v1 → v2:我们原本打算保留 v1 的 description,查证之后放弃了。v1 正文第 242 行写着「批改 → `homework_review` + `error_diagnosis`」,而运行时一轮只能加载一个主 Skill——那是个兑现不了的承诺。v2 删得对,我们跟随 v2 重新生成了数据。

chenweihang0160-alt and others added 2 commits September 5, 2026 18:08
## 冲突
19 个文件,数据全部取我们的——核对过 main 那份在每个实质维度上更旧:
缺 has_records(58 题)、缺措辞无害 7 条、缺考卷/训练池重划。schema_version
两边同为 711faf60,22 个工具逐项相同,429 道共有题 gold 一字未变。

## 跟随 v2 重生成
原本打算保留 v1 的 skill description,查证后放弃:v1 正文第 242 行写着
「批改 → `homework_review` + `error_diagnosis`」,而运行时一轮只能加载一个
主 Skill——那是兑现不了的承诺,正是 8696eee 新增的
test_skill_bodies_match_runtime_capabilities 要拦的东西(v1 三条断言全败)。

重生成的影响已全量量化:440 道考题每题只有 1 个差异块,全局被改掉的行
只有 1 种(problem_solving_expert 的摘要);gold/题面/工具/对话历史逐字不变;
skills_rag 27 条不涉及任何被改过的 skill;split 仍是 1003/43/48。
考卷指纹 01e0f55bfd4e12bd → d441611fb5556b53。

## 生成器:吸收防护,拒绝闸门降级
- valid_lures():吸收,但判据从「悄悄过滤失效工具」改成「一个都不许失效」
- gen_new_tools 的跳过:同样改成硬报错(上游版会静默少生成一整组)
- gen_supplement 把 assert 降成 print+break:拒绝。ASK_USER 是「追问命中率」
  的来源,静默少生成会让指标在更小的分母上算而报表全绿
改完重跑,两个生成器产物逐字节不变。

## CI
- 去掉 test_llamafactory_ingest 的跳过分支:它判 $HOME/Downloads/llamafactory-0.9.4
  存不存在,CI runner 上永远没有,所以那道闸门在 CI 里一次都不会真跑,
  日志里只有一行 Skipping——而它守的正是「LLaMA-Factory 静默吃掉训练样本」。
  判断收进测试自己:找不到源码时退出码 0 但打印整块「这道闸门没有运行」,
  带 --require 则判失败(开训前用)。新增 $ESA_LF 指定路径。
- 新增 Render dataset artifacts 步骤:test_llamafactory_ingest 需要 data/out/
  (按设计不入库),没有它必然 FileNotFoundError。这步顺带验证生成管线能在
  干净 checkout 上从 IR 跑到 data/out/。
- 补上另外五个测试(error_payload_visible / esa_tool_format /
  runtime_error_contract / tool_coverage / verbosity_tools)。
11 个测试已在本仓库布局下逐个实测通过,evalset 产物与项目内逐字节一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
backend/agent/rag/try/experiment.py:21 的 json_compact 全文件只出现在 import
那一行。ruff 的 F401 让 Lint 步骤失败,而 Lint 在 Dataset contract tests
之前——backend job 一红就停,本 PR 新接进 CI 的 11 个数据集测试**一个都跑不到**。

这个失败不是本 PR 引入的:main 最近四次 CI 全红,8696eee 自己也红,失败项
(这条 F401 + frontend 的 home_shell.dart deprecated_member_use)与本 PR
逐字相同。动别人目录下的文件是因为它挡着所有人的 backend CI,改动限于删掉
那一个名字,同 import 的 text_compact / token_count 都在用、保留。
`python -m ruff check .`(与 CI 同一条命令)全量通过。

frontend 那个 Flutter 废弃 API 没动——onReorder → onReorderItem 涉及 newIndex
语义变化,该由前端的人改。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chenweihang0160-alt
chenweihang0160-alt merged commit 469904f into LoveLearnLearning:main Sep 5, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant