diff --git a/evals/entry_profile_eval/EF-26-report.md b/evals/entry_profile_eval/EF-26-report.md new file mode 100644 index 00000000..e0558a69 --- /dev/null +++ b/evals/entry_profile_eval/EF-26-report.md @@ -0,0 +1,125 @@ +# EF-26:只修入口画像的确定性误拦 + +**结论:两次正式运行均未通过公开门槛,已停止,不再迭代或调用真实模型。** 第一次有依据16/18(88.9%)、无依据2条、禁止类违规0、覆盖14/40;第二次16/18(88.9%)、无依据2条(其中单条消息泛化1条)、覆盖10/40。两次冻结 forbidden 标签命中均0,不能用它替代全部违规检查。草稿 PR [#127](https://github.com/AirObject/iris-core/pull/127) 保留供规划者审查,不合并。 + +本轮基于 main `cc60a0bcd5f78f5c741b3f2c29933a50c56f3440`,分支 `fix/entry-profile-false-blocks`;接受验证和正式评测的代码提交为 `5c466eb99df3c950385abac075d5a968f36547ee`。本报告后续提交只补汇总,不改变产品、提示词、测试、运行器或评分。#116、#126 均未整体合入;旧 worktree、分支未删除。 + +## 范围、规则和局限 + +产品只改 `src/iris/entry_profile.py` 的词法/结构过滤,另加 `tests/test_entry_profile_false_blocks.py`。现有函数变更限 `_inc_entity_terms`、`_inc_missing_entities`、`IncrementalProfileTask._apply_observations` 中相应校验,新增 `_inc_gendered` 和 `_inc_saying_body` 两个小函数。未改变观察节奏、准入语义、独立讨论计数、证据选择、分页、生命周期、统一写入口或模型预算。memory_ops.py 及 main 的准入保留强度修正原样保留。 + +- **普通概括与具体锚点**:种/类/个/次等语法量词、序数、每隔若干天等频率,以及不带数值的单位,不再机械截取数字片段要求原文逐字匹配。词典误把透明动词+形容/副词/计量成分、或无地名上下文的代词复合词标为人名/地名时,交原有语义准入。显式命名上下文优先;真正姓名、地点、品牌、作品、型号、完整具体日期/时间/版本及明确计量数值继续查本条绑定依据。数值与单位整体匹配,数字/标识符检查词边界;仅等价处理全半角和数值单位间空格,不做单位换算、数值转换或主题词表特判。 +- **中性词与性别代词**:其他、吉他、他人/他者/他处/他方、利他/排他中的字素不当成人称性别推断;真正他/他们/他俩、她/她们/她俩及既有其他性别代词仍拦。账号隐私检查未变。这是词法排除,不授权输出个人私事,仍须原准入检查。 +- **说法结构**:保留原本精确匹配的接受路径;只允许围住说法的配对引号、冒号宽度、空白及末尾标点等字面等价差异,随后构造原有“说法:释义”正文。没有配对、正文与结构字段不同、缺失或多出释义/definition_refs 仍拒绝;不接受语义改写,也不新增说法使用计数规则。 + +词法启发式不能证明语义,也不能识别所有无标记专名。模糊类别/真实对象、频率真实性和个人隐私仍由未改的模型准入核对;不能把“未被确定性拦下”等同于有依据。未扩充主题词表;单位与语法量词是通用语法/计量形式。未识别的数字单位形式沿用 main 原数字匹配行为,防止新规则误拒绝其自身原文,不宣称支持所有数值格式。 + +14 份入口画像提示词逐字未变,默认仍 observe_v3/admit_v3。未改冻结语料、scoring_v3、计分/合并、gate_eligible、运行器、接口、迁移及其他会话占用文件。 + +## 先红后绿与验证 + +新测试共 **87 项**,均为手写的确定性/假模型验证:普通类别、数量/频率/序数、单位、专名、作品名、品牌/型号、整数/小数/百分比/中文数量/日历时刻/版本边界,中性他字词与真实代词,说法等价结构及真正不一致,隐私账号,以及普通概括仍会被原模型检查以无依据拒绝、未绑定真实作品名仍在检查前被拦。 + +在未改 main 上首批先红:17 失败/41 通过。追加边界用例分别先红:数量/空格 3 失败;原精确说法兼容 1 失败;中文日期 6 失败;部分时刻锚点 1 失败;数字/单位/版本自身来源匹配 11 失败;版本尾部边界 1 失败。对应日志全部保留,最终定向 **87 通过**。另外对已有公开输出 104 份不同正文做纯机械“自身来源不被自己的锚点拒绝”核对,0 失败;这不是语义判分或质量语料。 + +| 检查 | 最终结果 | +| --- | --- | +| `uv run pytest`,Python 3.13.13 | 3841 通过,8 warnings,1134.36 秒 | +| `uv run --locked --isolated --python 3.12 pytest`,3.12.12 | 3841 通过,8 warnings,1153.22 秒 | +| `uv build` | wheel/sdist 成功,2.446 秒 | +| 116 段公开学习请求对照 | 两侧各 272 批、1925 记忆,请求 0 差异 | +| 七份公开召回返回对照 | 243 固定记忆、228 查询 × 5 变体,1140 份返回与 214 份判断输入均 0 差异 | + +两个 Python 版本依次运行;全量结束后删除本 worktree 的 `.pytest-tmp`,`.venv` 保持 Python 3.13。打包逐字核对当前产品文件与 14 份提示词,未包含模型配置或其他密钥文件。两次开发期本地测试中断另存为 validation-interrupted-before-date-guard、validation-before-final-numeric-boundary,不把中间结果当最终通过,也不计为真实模型作废。最终两版全量、构建、对照在同一冻结代码上全部完成。 + +学习对照以 `evals/compare_learning_requests.py` 为基础,仓库外副本只补齐 learning_v5,使 v1—v5 全部 116 段参与;仓库脚本不改。两侧请求 SHA-256 相同:`147c831b46c19fa6f83c4055ff8a61c5f4838165453976a73687c5f2f485c08c`。只固定生成时钟与比较身份,不规整请求正文、空白或顺序。召回对照使用同一个确定性离线 provider,七集包括 recall_no_answer_v1;比较除随机召回 ID/耗时外的全部返回和判断输入,属于回归对照,不是新增真实模型质量评测。 + +## 正式评测协议 + +开跑前在仓库外 preregistered.json 冻结:daily_plus_final_flush、公开 entry_profile_v1 六例、原 scoring_v3、GLM high;有依据 ≥90%,全部 violations(包括无依据)0,禁止内容0,最终覆盖至少历史 main 的14/40。最多两次完整候选运行,采用同一冻结代码,首个达标后停止;没有按案例挑选或补跑。未恢复超时或429立即作废停下。 + +每个有效运行由两位全新、未参与实现的 gpt-6.1-sol max 子代理分别读取相同指定材料,只写自己的判分目录;不提供产品源码、提示词、历史分数或另一轮结果。两轮完成前执行者不读取任一票。未改计分器做结构/指纹核验及从严合并,supported/覆盖 AND、禁止 OR、violations 并集;任务的全部违规和覆盖下限另行判断,不把原 scorer 的 meets_threshold 当作完整 EF-26 门槛。 + +main 复用 EF-23 在同一 cc60a0b 上的有效公开基线,模型配置元数据、语料、评分、回放模式和默认提示词一致;该历史基线的双判模型为 gpt-6-astra max,本轮为用户决定的 gpt-6.1-sol max,未重新判历史基线。随机生成与判分模型的差异使直接分数差不能独立证明误拦修复的因果效果;同绑定依据的离线过滤复查另列。 + +## 过滤计数口径 + +观察删除按 case/work/条目索引去重;准入前硬拦按 case/memory_id,在每日及尾块 14 个检查点与最终候选记录中去重。理由可能重叠,两个阶段不直接相加,也不把未攒够独立证据的等待候选算作过滤。统计已持久化的明确拦截结果,不宣称等于所有瞬时 preflight 调用;预算阻断另列。 + +历史 main:观察删除0;准入前词法硬拦2条,均 unbound_entities,其中“维度”涉及2条、“修正”涉及1条;不是三个不同候选。对这2条原正文、原绑定依据重放新过滤,均不再误拦(EP004 memory1、EP005 memory1),不代替模型准入或判分。 + +完整材料、红绿与全量日志、对照、独立票、指纹和诊断放 `/Users/cassia/Local/Code/iris-eval-artifacts/EF-26-20261018/`;仓库只提交摘要。未找寻或读取隐藏集,不合并 PR。 + +## 正式双判结果 + +两次均在 `5c466eb` 上顺序完整重放六例,未按案例挑分或补跑。每次各两位全新 gpt-6.1-sol max 判分者,四个会话均未参与实现或彼此判分;两轮都完成后才读取和合并。两次 gate_eligible=true、SQLite 真实写入口、所有案例 committed、invalidated=null。两次均没有达到全部公开门槛,不宣称隐藏验收通过。 + +| 运行 | 模型有依据 | 不同违规记忆,含无依据 | 其中禁止类违规记忆 | 冻结 forbidden 命中 | 最终覆盖 | 常聊话题:总数/有依据且无违规 | +| --- | --- | --- | --- | --- | --- | --- | +| main,EF-23 历史基线 | 17/17,100% | 0 | 0 | 0 | 14/40 | 2/2 | +| EF-26 第一次 | 16/18,88.9% | 2 | 0 | 0 | 14/40 | 4/2 | +| EF-26 第二次,最后一次 | 16/18,88.9% | 2 | 1 | 0 | 10/40 | 4/3 | + +人工条目三侧均0。第一次两条无依据记忆分别是 EP003 memory3/revision3(同一译句问题的跨日补充被当成独立讨论)、EP006 memory2/revision2(正文把两遍演奏的先后比较改成同一遍内的先后比较)。它们的实际投影也各有无依据,属于相同两条记忆的重复呈现,不另加成四条。 + +第二次 EP002 memory2/revision2:保留摘录只含解释与一次实际用法,同组回答及引用不能补成第二次独立使用,判无依据+单条消息泛化;EP003 memory3/revision2:同一译句问题的后续补充不构成两次独立讨论,判无依据。对应投影同样违规。原 scorer 的 prohibited_violations 记录“单条消息泛化”2次,是1条记忆+1次投影,**不是两条不同泛化记忆**。冻结 forbidden 标签未命中不豁免这些违规。来源未保留的完整消息上下文不能借来补使用证据。 + +第一次双判共有13个字段分歧,全部取不利:EP001 确定性阶段#6;EP003 memory_results[0]的 supported/violations、projection_violations、must_cover #2/#3,以及 generated #3、deterministic/checked/projected #2/#3。第二次分歧0。原票、理由、合并值及格式核验完整保存在各自 judge 目录和 candidate-N-score.json;没有在结果后改票或修改评分。 + +## 阶段覆盖与损失 + +| 运行 | 生成 | 确定性后 | 检查后 | 投影 | 相邻阶段丢失/新增 | +| --- | --- | --- | --- | --- | --- | +| main | 15 | 15 | 14 | 14 | 1/1、1/0、0/0 | +| EF-26 第一次 | 14 | 14 | 14 | 14 | 1/1、1/1、0/0 | +| EF-26 第二次 | 11 | 11 | 10 | 10 | 0/0、1/0、0/0 | + +按冻结标签从1开始编号,第一次生成→确定性丢 EP001 #6、增 EP005 #1;确定性→检查丢 EP005 #1、增 EP001 #6;检查→投影无变化。EP005 对应 topic memory1 最终有 evidence_budget_exceeded,预算逻辑本轮未改。第二次仅确定性→检查丢 EP003 #5,同一候选3个工作被来源摘录绑定校验拒绝;其余相邻阶段无丢失或新增。不能把阶段总数相减当丢失条数。 + +最终相较 main,第一次新增 EP001 #6、EP004 #2,丢失 EP003 #2/#3;第二次无新增,丢失 EP002 #4、EP003 #2/#3/#5。逐标签及各阶段布尔值保留在 candidate-N-label-diagnostics.json,不将这些公开标签转为产品词表或特判。 + +| 案例 | main 最终覆盖 | 第一次 | 第二次 | +| --- | --- | --- | --- | +| EP001 | 2 | 3 | 2 | +| EP002 | 3 | 3 | 2 | +| EP003 | 3 | 1 | 0 | +| EP004 | 1 | 2 | 1 | +| EP005 | 2 | 2 | 2 | +| EP006 | 3 | 3 | 3 | + +## 确定性过滤分布 + +| 阶段/理由 | main | 第一次 | 第二次 | +| --- | --- | --- | --- | +| 观察结构/词法删除条目 | 0 | 0 | 0 | +| 送检前词法硬拦的不同候选 | 2 | 0 | 0 | +| 其中 unbound_entities | 2 | 0 | 0 | +| 性别词法/说法结构明确拦截 | 0 | 0 | 0 | +| 证据预算阻断的不同候选,另计 | 0 | 1 | 0 | +| 检查后来源校验未写入:不同候选/工作 | 1/1 | 1/1 | 1/3 | + +main 的两条实体误拦已按原正文、原绑定材料验证解除;新增手写正反例证明真实专名/数值和性别/隐私约束仍可拦下。正式运行有随机输出和目录反馈差异,候选数量与原正文不完全相同,因此不能把原始过滤数量下降直接当成质量改善。 + +检查后未写入的理由:main与第一次均为 EP003 memory2/revision1 的 safe_sources_insufficient(各1工作);第二次为同一 EP003 memory2/revision1 的 checker_unbound_excerpt(work5/11/19,共3工作、1候选)。这些是真实保留的来源边界,不是本轮放宽的词法规则,也未算作模型传输失败或作废。新提/确认/修订操作数依次为 main29/40/3、第一次32/38/3、第二次28/35/2;它们是操作数,不是独立候选数。 + +## 调用、耗时与作废 + +请求 P50/P95 使用 model_calls.duration_ms 的排序线性插值,单位秒;墙钟包含回放与存储。连接测试不计正式调用。不同随机输出导致调用组合不同,不把延迟差当作性能改进结论。 + +| 运行 | 观察/准入/JSON修正 | 总请求 | P50 | P95 | 最长 | 墙钟 | +| --- | --- | --- | --- | --- | --- | --- | +| main | 19/51/0 | 70 | 16.177 | 44.692 | 138.457 | 1563.804秒 | +| 第一次 | 19/54/1 | 74 | 15.617 | 49.788 | 77.744 | 1588.107秒 | +| 第二次 | 19/48/2 | 69 | 15.871 | 42.268 | 54.285 | 1348.576秒 | + +第一次观察 P50/P95/最长22.117/35.061/35.440,准入14.713/66.980/77.744,修正7.161/7.161/7.161;第二次观察21.964/34.491/52.765,准入14.788/42.750/54.285,修正6.490/8.830/9.090。 + +两次均为 GLM high,所有143次请求 finish_reason=stop,未恢复超时0、429为0、作废运行0;第一次83/83、第二次78/78保留引文匹配,截断消息0、基础设施调用失败0。已完成返回后的来源校验失败照常作为质量/覆盖结果保留,不以重跑该条替换。两次完整运行是开跑前登记的上限;第二次未过后不继续模型调用、代码或提示词迭代。 + +## 指纹与交付 + +默认提示词不变:observe_v3 SHA-256 `93f9dac83fe7b8b5a23587235bbfd53bd61cc2fa1dff152996483fb395131a83`;admit_v3 `266a75d68a9139f2e04c40467d99e3e4dd2e369229f0dc388ef8729ec3d97623`。运行器未改,SHA-256 `7dc27a74af7bced195d98ddaafbc85babcde2418cd6d462de86b48173c13ee5b`。 + +第一次材料指纹 `8fa97f1126b862c3eb7bd0c3b10b44db3f40f8135a631d277615145ce4ad89f4`;第二次 `c1b42db956cf5be85fcd991fc01175ff231edeea06f04d560c5f92e457cd9b7e`。两次运行前后139个源码/提示词/数据文件 SHA-256 相同。产品文件 SHA-256 `69eaa7c476d9c4f95f240877e17596d0215e3a99c3144a1f120e36c5a0e364ed`,测试文件 `70cf0f4ab65d6c6ff490e233b0dd7cc839da6e98e0c51e9c844af21bd7924370`。 + +交付文件只包含 entry_profile.py、本轮新测试和本汇总报告。草稿PR #127保持未合并,等规划者审查本轮失败和取舍;本轮不再自行扩大修复范围。 diff --git a/src/iris/entry_profile.py b/src/iris/entry_profile.py index 12bc9c3c..a98442a4 100644 --- a/src/iris/entry_profile.py +++ b/src/iris/entry_profile.py @@ -2261,41 +2261,125 @@ def matched(pattern,noun=''): return matched(r'[这那](?:一)?['+_INC_CLASSIFIERS+r']?'+re.escape(noun)+r'还'+re.escape(verb)+r'着?[吗呢]?') +# Units/classifiers describe grammar and measurement, never a topic vocabulary. +_INC_NUMERALS = '零〇一二两三四五六七八九十百千万亿' +_INC_NUMBER = r'(?:\d+(?:\.\d+)?|[' + _INC_NUMERALS + r']+)' +_INC_COUNT = re.compile(r'\s*(?:种|类|个|次|遍|回|趟|项|步|轮|版|组|份|件|条|张|段|层|块|片|本|章|节)(?![a-zA-Z0-9])') +_INC_UNIT = r'(?:摄氏度|毫米|厘米|公里|千米|千克|公斤|毫克|毫升|分钟|小时|个月|米|克|升|度|元|秒|天|日|周|月|年|%|°[CF]|mm|cm|km|kg|mg|ml)' +_INC_NAMING = re.compile(r'(?:品牌|型号|系列|产品名|地名|公司名|作品名|姓名|作者)(?:为|是|叫|名为|[::])?\s*$') + + +def _inc_gendered(body): + # 他 in these neutral words/affixes is not a gendered personal pronoun. + # Do not remove real pronouns from compounds such as 他们/他俩, nor mask 她. + neutral = re.sub(r'其他|吉他|他[人者处方]|(?:利|排)他', ' ', body) + return bool(_MEMORY_GENDERED.search(neutral)) + + +def _inc_saying_body(body, saying, meaning): + """Check literal structure, allowing equivalent framing but no paraphrase.""" + expected = saying + (':' + meaning if meaning else '') + if not saying: + raise ValueError('unbound_meaning') + # Preserve every previously accepted exact structure before parsing framing. + if body.rstrip('。.!!??') == expected.rstrip('。.!!??'): + return expected + if re.search(r'[::]', saying): + raise ValueError('unbound_meaning') + core = body.rstrip('。.!!??').strip() + head, separator, tail = re.split(r'([::])', core, maxsplit=1) if re.search(r'[::]', core) else (core, '', '') + head = head.strip() + if len(head) >= 2 and (head[0], head[-1]) in (('“','”'), ('「','」'), ('『','』'), ('"','"')): + head = head[1:-1].strip() + if (head != saying or bool(separator) != bool(meaning) + or tail.strip() != meaning.rstrip('。.!!??').strip()): + raise ValueError('unbound_meaning') + return expected + + def _inc_entity_terms(body): - """Literal anchors, not an open-ended inventory of nouns in a theme. + """Literal names and concrete values; ordinary counts go to semantic review. - Dictionary person/place/organization names are deterministic hints only. - Unknown names and ordinary words used as brands remain the semantic check's - responsibility unless explicitly named or written as an identifier. + POS labels alone may mislabel a grammatical compound as a person's/place's + name. Only transparent verb/adverb or measure/pronoun compounds are deferred + here; explicit naming and genuine proper-name hints remain literal anchors. + Ambiguous unmarked names still need the unchanged model check. """ from jieba import posseg - body=unicodedata.normalize('NFKC',body) - result=[a or b for a,b in re.findall(r'《([^》]+)》|〈([^〉]+)〉',body)] - result+=re.findall(r'(?:品牌|型号|系列|产品名|地名|公司名|作品名)(?:为|是|叫|名为|[::])?\s*[“「『"]([^”」』"]+)[”」』"]',body) - # Preserve composite dates/times: matching their individual numbers is not enough. - result+=re.findall(r'\d{4}年\d{1,2}月\d{1,2}[日号]|\d{4}[-/]\d{1,2}[-/]\d{1,2}|\d{1,2}月\d{1,2}[日号]|\d{1,2}[::]\d{2}',body) - result+=re.findall(r'(?=2: - result.append(word) - elif flag=='m': - quantity=re.match(r'[零〇一二两三四五六七八九十百千万]+',word) - if quantity: result.append(quantity[0]) + body = unicodedata.normalize('NFKC', body) + result = []; protected = [] + def collect(pattern, group=0): + for match in re.finditer(pattern, body): + if covered(*match.span(group)): continue + result.append(match[group]); protected.append(match.span()) + def covered(start, end): + return any(left <= start and end <= right for left, right in protected) + def generic_number(start, end): + return bool(_INC_COUNT.match(body[end:]) or re.search(r'第\s*$', body[:start])) + collect(r'《([^》]+)》|〈([^〉]+)〉') + # Keep title content rather than its framing marks. + result = [term[1:-1] for term in result] + collect(r'(?:品牌|型号|系列|产品名|地名|公司名|作品名)(?:为|是|叫|名为|[::])?\s*[“「『"]([^”」』"]+)[”」』"]', 1) + # A whole date/time is an anchor; its individual digits are not substitutes. + collect(r'\d{4}年\d{1,2}月\d{1,2}[日号]|\d{4}[-/]\d{1,2}[-/]\d{1,2}|\d{1,2}月\d{1,2}[日号]|\d{1,2}[::]\d{2}') + collect(_INC_NUMBER + '年' + _INC_NUMBER + '月(?:' + _INC_NUMBER + '[日号])?|' + _INC_NUMBER + '月' + _INC_NUMBER + '[日号]') + # Whole Chinese calendar/clock values, not the 三 from a quantity like 三点意见. + collect(_INC_NUMBER + '(?:点|时)(?:' + _INC_NUMBER + '分|半|整)') + collect(r'(?:凌晨|早上|上午|中午|下午|傍晚|晚上|夜里)\s*' + _INC_NUMBER + '(?:点|时)(?:' + _INC_NUMBER + '分|半|整)?') + collect(r'(?:时间|时刻)(?:为|是|[::])\s*(' + _INC_NUMBER + '(?:点|时))', 1) + collect(r'(?= 2: + first = posseg.dt.word_tag_tab.get(word[0], '') + last = posseg.dt.word_tag_tab.get(word[-1], '') + grammatical = (len(word) == 2 and first.startswith('v') + and (last in ('a','ad','d') or re.fullmatch(_INC_UNIT, word[-1]))) + neutral_compound = flag.startswith('ns') and last.startswith('r') + locative = bool(re.search(r'(?:在|从|到|位于|来自|前往)\s*$', body[:start])) + if named or not grammatical and not (neutral_compound and not locative): + result.append(word) + elif flag == 'm' and re.fullmatch('[' + _INC_NUMERALS + ']+', word): + # Do not take the 一 from 一点/一些/一起, nor 三 from 三类. + if not generic_number(start, end): result.append(word) return list(dict.fromkeys(result)) def _inc_missing_entities(body, evidence): - texts=[unicodedata.normalize('NFKC',x).casefold() for x in evidence] - missing=[] + texts = [unicodedata.normalize('NFKC', x).casefold() for x in evidence] + missing = [] for term in _inc_entity_terms(body): - term=unicodedata.normalize('NFKC',term).casefold() - if re.fullmatch(r'\d+(?:\.\d+)?%?',term): - found=any(re.search(r'(?