Commit Graph

306 Commits

Author SHA1 Message Date
ThearchyHelios 87f0979a90 SQL 差异说明第 3 轮补充:NOT 按德摩根律往里推;结果看行出来先后的一段不拆 BETWEEN、不去 MIN / MAX 里的 DISTINCT
- NOT (p AND q) 写成 NOT p OR NOT q、NOT (p OR q) 写成 NOT p AND NOT q(p、q 是条件时,三值逻辑下照样成立),在
  外连接改写、唯一键判断之前也做一遍。复验者的「意思一样却说了具体的不同」压测脚本在本轮的按主键去掉 LEFT JOIN
  上找到了:ON 写成 NOT (NOT (s.id = o.store_id) OR NOT (s.region = 'east')) 时认不出按主键关联,说成「B 用 LEFT
  JOIN 关联 stores 表,A 没有」;往里推之后两边一样。
- 结果看行出来先后的一段不拆 BETWEEN、不去掉 MIN / MAX 里的 DISTINCT、不按德摩根律改写(一个 BETWEEN 和两个比较
  估算出的行数可能不同,换一种扫描的先后,GROUP_CONCAT 拼出来的先后就跟着变)。
- 测试:德摩根律的等价改写进 _SAME_R3,NOT (a AND b) 对 NOT a AND NOT b 进 _DIFF_R3;「排除」那条说法跟着往里推
  之后的写法改。
2026-10-05 15:06:48 +08:00
ThearchyHelios f91615624d SQL 差异说明第 3 轮补充:靠主键认出 COUNT(主键) 和带主键的 DISTINCT;结果看行出来先后的一段不再去重条件、去重排序键、换 COUNT
- 给了表结构时:这一段里一定不为空的列(主键、NOT NULL,不在 LEFT JOIN 可空的一侧)上的 COUNT(x) 就是 COUNT(*);
  只有一张表、选出来的列包含它的主键(或者每一列都不为空的唯一约束)时 DISTINCT 去掉(_constraint_rewrites)。
- 在真实的 MySQL 8.0 上压测时看到:GROUP_CONCAT 那一段同一个条件写两遍,MySQL 按条件估算的行数变了、换了关联的
  先后,拼出来的先后就不同。结果看行出来先后的一段(见 _Sensitivity)不再去掉重复的条件、重复的排序键,不把 COUNT(x)
  换成 COUNT(*),外层条件不挪进这样的分组子查询的 HAVING;有 GROUP_CONCAT、窗口函数、既不分组也不聚合的列时不去掉
  负数 LIMIT。差异说明里只差在重复写的分组键、排序键上的不说成不同。
- 测试:COUNT(主键)、带主键的 DISTINCT 进 _SAME_SCHEMA_R3;可空一侧的主键、不带主键的 DISTINCT 进 _DIFF_R3。
2026-10-05 14:45:39 +08:00
ThearchyHelios 4557e72c8e SQL 差异说明第 3 轮补充:按唯一键关联又没用到的 LEFT JOIN 去掉,解析时换了写法又没留位置的函数名不在语法树上判等价,说法照原文写列
- 按主键 / 唯一约束关联、关联进来的表在 ON 之外没用到的 LEFT JOIN(左边每一行正好关联上一行或补空)去掉,结果
  怕不怕重复都一样;给了表结构时「按主键关联 users」和「user_id IN (SELECT id FROM users)」不再说成「A 关联了
  users 表,B 没有关联它」(没给表结构时关联可能把行复制,这句话是对的,照说)。schema_cache 的 unique 也读进来。
- 原文写的函数名在重新生成时不见了、又没有哪个节点记下它(PostgreSQL 的 DATE_PART 解析成 EXTRACT、不留位置;
  date_part 返回浮点数、EXTRACT 返回 numeric),这条语句不在语法树上判等价;CAST 一类(x::INT、MySQL 的
  CONVERT(x, CHAR))不算。
- 规范化按关联条件换成同一列之后,差异说明给人看原文写的那一列(「A 按 users.id 分组」不写成 orders.user_id)。
- 留着的 HAVING TRUE 不说成「分组后只保留 TRUE 的组」(读不懂、也没说到真正的不同),只给兜底。
- _finish 的说明按现在的先后重写。

测试:按主键关联的 LEFT JOIN 收进 _SAME_SCHEMA_R3(不是唯一键、后面还接着别的表的进 _DIFF_R3),DATE_PART 进
REGRESSIONS_R3,补「按主键关联对 IN 子查询」的说法测试。
2026-10-05 14:32:07 +08:00
ThearchyHelios 08f75d4002 SQL 差异说明重做(第 3 轮):堵上第 2 轮复验报的不保持语义的规范化,认出复验指出的等价改写,具体说法只说真会影响结果的不同
为什么:第 2 轮复验报了 14 个 blocker(判了等价、结果却不同)和 11 个 major(意思一样,却说了具体的不同;说法
自相矛盾)。等价判定的框架(两边各自规范化、语法树一字不差才算等价)不变,这一轮逐个查清是哪一步不保持语义、
哪一处说法没先确认「真的不同」,按类堵上,并在执行比对语料里收进全部反例。

等价判定(backend/app/data/sql_semantics.py):
- 外连接改内连接(_rejects)分「一定是空值」和「一定不成立」两种:NOT 只能套前者,NOT IN 子查询、NOT IN ()、
  x op ALL (…)、NOT (x = ANY …)(含解析成函数的 ALL (ARRAY[…]))、PostgreSQL 的 || 都留得下没关联上的行。
- 改列别名、EXISTS 写成 SELECT 1、去掉列名、CTE 的列改名之前,先看本段的 WHERE、ON、相关子查询还按名字引用不引用
  这些别名(_alias_refs,SQLite 的 WHERE、ON、相关子查询都认列别名,真实的 SQLite 上试过);qualify 遇到子查询引用
  外层列别名时不走认列那一级。
- USING 的列和列别名同名不改写(ORDER BY 先认别名);派生来源的列同名不走认列那一级(SQLite 认第一个);没有
  GROUP BY、别处也没有聚合的 HAVING TRUE 留着(PostgreSQL 里只出一行)。
- RANDOM() 这类不确定的函数轻量一级也不走;SQLite 双引号里的名字只看这条语句里看得见的表(_column_in_scope);
  子查询的别名遮住外层、又认不准是哪一个的不走规范化(SQLite 里层找不到列就往外层找)。
- MySQL:/*! … */ 会执行、! 比比较结合得紧,不在语法树上判等价;-- 后面不跟空白是两个减号,先拆开再解析;
  BETWEEN 上下界类型不同或不是常量时不改写。
- 函数节点记下原文写的名字(_mark_surfaces):SQLite 的 MOD() 和 %、PostgreSQL 的 LOG10(x) 和 LOG(10, x) 解析成
  同一个节点,记下名字就不会判等价;同义写法按方言文档列在 _SAFE_RESPELLINGS。
- 自己压测找到的老问题:结果看行出来先后的一段(GROUP_CONCAT 等)按 WHERE 把 LEFT JOIN 改成内连接,SQLite 的
  扫描先后真的会变(EXPLAIN QUERY PLAN 一个先扫 refunds、一个先扫 orders),这样的一段不再改写。

认出复验指出的等价改写(每一步都写了为什么保持语义,执行比对守门):
- EXISTS 里的去重、分组、UNION / UNION ALL,IN 里不影响取值集合的 GROUP BY、DISTINCT、UNION;分组后 COUNT(*) >= 1。
- 不分组、只取可空一侧跳过空值的聚合时 LEFT JOIN 写成内连接(COALESCE(SUM(r.amount), 0) 等);ON 之外没用到、
  结果不怕重复的行的 LEFT JOIN 去掉;同一组 AND 里已经保证、或表结构里确定不为空(nullable、主键)的 IS NOT NULL
  去掉;给了表结构、类型一样时按关联条件相等的两列换成同一列(分组、排序、选出来的列)。
- 一模一样的分组键、排序键;SQLite 的负数 LIMIT、PostgreSQL 的 LIMIT NULL、LIMIT 的整数算式。

具体说法:被关联条件定成相等的列、IN / EXISTS 里的去重分组、只取可空一侧的聚合、没用到又不怕重复的 LEFT JOIN、
各支自己已经去重的 UNION ALL、只出一行时的排序、自己关联自己的关联条件,都不再说成具体的不同(只给兜底);
<>、NOT IN、NOT LIKE 照实说「和 X 为空的行」也筛掉了,NOT 套几个条件的说「只保留 NOT (…) 的行」。

normalize_sql:解析时会丢掉写法的(会执行的注释、!、MOD() 这类重新生成换了名字的函数)改按原文逐词规范化,
会执行的注释里的代码算结构;只改字面量的照旧同一个模板(补了测试)。

测试:fixtures/sql_pairs.py 收进第 3 轮的反例(执行的 _DIFF_R3 / _SAME_R3 / _SAME_TYPED_R3 / _SAME_SCHEMA_R3 /
_SAME_HARD_R3,只查结论的 REGRESSIONS_R3 / DIALECT_SAME_R3),schema_cache 带上 nullable 和 primary_key(和探查
出来的一样);专挑写法差异的那组数据加了 'State'。
2026-10-05 14:19:55 +08:00
ThearchyHelios 02b08f9ad4 SQL 差异说明重做(第 2 轮补充):在真实的 PostgreSQL / MySQL 上复查,浮点数求和的一段不调换先后、外层条件只把聚合结果和「相等就是一样」的分组键挪进 HAVING;排列数先算再展开
为什么:第 2 轮的主体提交只在 SQLite 上执行比对。这次私起了 PostgreSQL 18 和 MySQL 8.0(复验者语料的那份数据),
拿复验者的全部语料和自己针对新改写造的 22 对重跑,抓到两处在真实的库上结果不同、却判了等价的:
- 套在子查询里的 UNION ALL 调换各支,外面 SUM:浮点数加的先后变了,1902.2 对 1902.2000000000003(PG、MySQL)。
- 外层 WHERE BINARY t.city = 'SH' 挪进分组子查询的 HAVING:MySQL 自己会把外层只涉及分组键的条件推进子查询的 WHERE
  先筛行,不分大小写时和 HAVING 结果不同。

做了什么(backend/app/data/sql_semantics.py):
- _Sensitivity:PostgreSQL、MySQL 里对不知道是不是精确数(整数、定点小数)的值 SUM / AVG / STDDEV / VARIANCE,这一段
  算「结果看行出来的先后」,连同给它供行的子查询、CTE 不调换来源、不排分组键、不并子查询。SQLite 不算(它用
  Kahan-Babuska-Neumaier 求和,关联先后也不跟着 FROM 变;语料 120 对在 7 组数据上按精确值比没有差别)。
- 外连接按 WHERE 变成内连接、内连接的条件挪进 WHERE:三种库的优化器自己也做,执行先后不变,看先后的一段也照做;
  只有 SQLite 写了 CROSS JOIN 的不挪。
- 外层 WHERE 挪进分组子查询的 HAVING 只挪聚合结果和「相等就是一样」的分组键上的条件;引用了好几次的简单 CTE 有
  一处在看先后的一段里就不拆开(PostgreSQL 会先把它算出来存着)。只筛行选列的子查询照样并进求浮点数和的外层:三种库
  自己也会并(PostgreSQL 的 subquery pull-up、MySQL 的 derived_merge、SQLite 的 flattening),执行先后一样(在两个
  库上核对过结果一字不差)。
- 同名来源的排列先算个数再展开:十几个同名表时以前先把十几的阶乘个排列都列出来(一条 12 张表自连接的语句跑了两分钟
  以上),现在超了上限直接按排好的先后。
- 模块说明照实写清上面几条和 SQLite 求和的假设。

测试:REGRESSIONS_R2 补这两处(PG、MySQL 的 UNION ALL 求和、MySQL 的分组键条件);DIALECT_SAME 里两对需要在求和的
一段调换关联先后的 PG / MySQL 改写移到 KNOWN_UNRECOGNIZED(只给兜底说明)。

验证(最终代码):tests/test_sql_semantics.py + tests/test_verify_agent.py 601 passed,tests/test_sql_equivalence.py
375 passed,全量 -n 8:7856 passed、11 skipped。等价改写 124 对认出 120 对(97%),不给表结构 117 对(94%)。复验者
vsadv 全部语料(928 次判定,SQLite / PG / MySQL 执行)无 blocker、判等价的没有一对执行结果不同,自己针对新改写的 32 对
同样;advfp 180 对等价对上具体说法 0 条;vsprobe 变异压测(种子 9、10)约 1.6 万对无误判等价、无异常;自己的压测
约 1.65 万条可执行,规范形式相同的组执行结果全一致。
2026-10-05 12:16:20 +08:00
ThearchyHelios f16860f704 SQL 差异说明重做(第 2 轮):规范化只留能讲清为什么保持语义的几步,结果看行出来先后的一段不动来源;具体说法先确认两边真的不同、另一边真的没有,其余给兜底
为什么:第 2 轮复验抓到第 1 版规范化里一批不保持语义的改写(15 个 blocker),说明里一批假话、矛盾话(10 多个
major)。根子是借用了 sqlglot simplify 等现成改写、又把「相等」当成「一样」、没管结果看行出来先后的查询;说明这边
是先改名再比、只看这一段有没有那个条件。这一轮把改写收窄到每一步都讲得清为什么保持语义,说明这边每句话都先验证。

规范化(backend/app/data/sql_semantics.py):
- 去掉 sqlglot 的 simplify(Decimal 折叠 0.1 + 0.2、-(-x) 化成 x、三项加乘拍平合常量、BETWEEN SYMMETRIC 丢了
  SYMMETRIC)、pushdown_projections、HAVING 挪进 WHERE(MySQL 不分大小写时同组的值各不一样)、只有一列的 IN 列表
  写成 =(SQLite 里 a IN (b) 是 a = +b)。只留自己写的几步:AND / OR、比较两边、一个节点的加乘两边排序,NOT 套
  比较翻过来,BETWEEN 拆开,IN 常量列表去重排序,COUNT(1)、MIN/MAX(DISTINCT)、IIF / IF 写成 CASE、CASE ELSE NULL、
  恒真的 1 = 1 去掉,IS NOT NULL / NOT LIKE 各方言统一成 NOT (…)。
- 新增 _Sensitivity:取行(含有 ORDER BY 并列)、DISTINCT ON、窗口函数、GROUP_CONCAT 这类聚合、聚合里的散列、当成标量
  的子查询,以及去重、分组、MIN/MAX 留下的值「相等却不一样」(MySQL 字符串、PG numeric、算出来的表达式)的一段,
  连同给它供行的子查询、CTE,都不调换来源、不排 GROUP BY 键、不改 DISTINCT、不并子查询。SQLite 的逗号连接和 CROSS
  JOIN 按原文分开(SQLite 不调换 CROSS JOIN 两边)。
- 列名表(CTE、子查询)写进里面那段的别名,实表别名带列名、递归 CTE(含 SQLite 不写 RECURSIVE、CTE 引用自己或后面的)、
  BETWEEN SYMMETRIC、FETCH WITH TIES 不走规范化;FETCH FIRST 写成 LIMIT;USING 只在简单写法下写成 ON。
- EXISTS 改写前先查聚合(含 ORDER BY 里的)和 OFFSET;合并只调换最外层不取行的 UNION ALL(PG/MySQL 另加按位置排序、
  各支列名一样的嵌套);后面有 RIGHT / FULL JOIN 时内连接不挪进 WHERE、开头的来源不调换。
- 子查询并进外层自己写(_merge_derived:只并内连接一侧、只筛行选列的),引用多次的简单 CTE 每处写回一份后整条语句
  的别名重新改成互不相同的(压测撞到的:同一个 CTE 并进外层和相关子查询两处,里层别名遮住外层);外层 WHERE 只挪进
  分组子查询的 HAVING(同一个值)。几个来源的 SELECT * 先写成各来源的 t.*(sqlglot 展开 * 时把实表排在子查询前面)。
- 解析丢了写法的不在语法树上判等价:0x10 和 x'10' 这类字面量、换了名字的类型名(SQLite 的 STRING 是数值亲和);
  SQLite 不带表名、带双引号、表结构里没有的列名保留大小写和引号(找不到这一列时被当成字符串)。

差异说明:
- 换了表的一边照它自己的表说(以前把 B 改名成 A 的表,中文名、detail 归错表)。
- 新增 _Range:同一个对象只和常量比的条件按取值逐段算,留下的行一样的不说(IN 和 OR、冗余 IS NOT NULL、拆成两段的
  时间、COUNT(*) > 1 和 >= 2);关联条件比相等关系的分组;聚合只在参数是列时说。
- 新增 _Usage:说「另一边没有这个条件 / 没有按时间筛选」之前,看另一边整条语句(含相关子查询、CTE、CASE、判空时连
  聚合参数)是否用这一列筛选;条件子查询里不带表名的列没给表结构时不拿来说具体的。
- 子查询要外面写法一模一样(含 NOT、IN 左边的列)才配对,位置照实说「id NOT IN 的子查询」;EXCEPT 按先后逐支比;
  order 只在最外层、没取行、没 DISTINCT ON / 窗口函数时说;LEFT 对 INNER 只在最外层、可空一侧没进 WHERE、结果不只是
  可空一侧的聚合时说;DISTINCT 对同列 GROUP BY、MySQL 表名只差大小写不说成不同;互相矛盾的一对不说。

测试:tests/fixtures/sql_pairs.py 加 nums 表和一组专挑写法差异的数据(0.1 + 0.2、像数字的文本、'SH' / 'sh'),
第 2 轮反例收进语料(_SAME_HARD 20 对、_SAME_R2 11 对、_DIFF_R2 36 对进执行比对;REGRESSIONS_R2 21 对、
DIALECT_SAME_R2 6 对、KNOWN_UNRECOGNIZED 4 对各方言只查结论);执行比对共 7 组数据、372 对,标 same 而没认出的
不许有具体说法;test_sql_semantics.py 补第 2 轮的说法用例和 normalize_sql「只改参数」的用例。这些新用例在上一版
上有 109 处失败。

验证:test_sql_semantics + test_verify_agent + test_sql_equivalence 973 passed;全量 -n 8:7853 passed、11 skipped。
等价改写 124 对认出 120 对(97%),不给表结构 117 对(94%)。复验者的脚本:vsprobe 变异压测 6 个种子约 4.3 万对无
误判等价;在私起的 PostgreSQL 18、MySQL 8.0 上重跑 vsadv 全部语料无 blocker;advfp 的 180 对等价对上具体说法 0 条;
自己的压测 3 个种子各约 1.5 万条可执行,规范形式相同的组执行结果全一致。
2026-10-05 12:01:08 +08:00
ThearchyHelios c55b1983f6 SQL 差异说明重做(第 1 轮):先判等价、再说差在哪——规范形式一字不差才算等价,具体说法只说有把握的,其余给兜底并附规范化后不同的行;等价结论拿 SQLite 执行比对守门
为什么重做:过去五轮按方面归纳任意 SQL,每轮修好一批又冒出一批(第 5 轮还回退过「只在 IN / EXISTS /
标量子查询里用到的 CTE 里的不同」),收不拢。改成先判两条 SQL 是不是一回事,不是的话只说有把握的不同。

做了什么(backend/app/data/sql_semantics.py):
- 新增 explain_sql_diff(a, b, dialect=None, *, schema=None, names=None, labels=("A", "B")) -> SqlDiff
  (equivalent、items、fallback、text_diff、level)。两边各自规范化,每一步只做保持语义的变换;同一级的两个
  规范形式语法树逐节点一样才算等价(不比生成的 SQL:sqlglot 生成 SQLite 时把 CAST(x AS DATE) 写成 DATE(x)),
  否则一律不等价、条目一定不为空。三级:optimizer(sqlglot 的 qualify、eliminate_subqueries、merge_subqueries、
  eliminate_ctes、pushdown_projections、simplify,给了数据源的 schema_cache 就按它认列、展开 SELECT *)、
  light(不认列)、parsed(只统一大小写、空白、引号)。
- 不用的 sqlglot 规则:unnest_subqueries(OR 里的标量子查询改成 CROSS JOIN,子查询没有行时整条都没了)、
  canonicalize(日期比较加 CAST,SQLite 里 CAST(… AS DATE) 取的是数字)、normalize(合取范式人读不懂);
  simplify 去掉同一列上比较的合并、字符串常量比较、等式移项、DATE_TRUNC 改区间(取决于列类型、排序规则、方言);
  merge_subqueries 不并外连接可空一侧算出来的列。
- 本模块自己的几步:去括号、ASC、OFFSET 0,COUNT(1)→COUNT(*),IN 列表去重排序,EXISTS 写成 SELECT 1,
  DISTINCT↔GROUP BY,原样透列的子查询换掉,RIGHT JOIN→LEFT JOIN,WHERE 筛掉了没关联上的行的外连接写成内连接,
  内连接写成逗号连接,起规范的名字(CTE 叫 cte1…、CTE 的列叫 c1…、别名按表名和层数,同名来源排列着试),
  交换律成立的地方排序。
- 具体说法的门槛:两段读的来源对得上(一样,或只差一处说得清的)才逐项比时间列、筛选条件、聚合与去重计数、
  选出来的列、分组键、HAVING、取行和排序、UNION 还是 UNION ALL;条件里的子查询两边各一个、外面一样的往里比
  (第 5 轮回退的 IN-CTE 反例在这里说出来);原文是 LEFT / RIGHT JOIN、规范化成内连接或调了方向的照原文说;
  牵涉 CTE 算出来的列、自己关联自己、说不准的子查询的不说具体的;两边给人看一样的不说。
- 兜底:一条具体说法都没有时给 aspect="other"「两条 SQL 的写法不同,未能归纳成具体差异,请对照原文」,只覆盖
  一部分时追加「另有未能归纳的差异,请对照原文」,fallback 为真;text_diff 是 difflib 比两边 sqlglot pretty
  的输出,最多 40 行。
- diff_facets 签名不变,交给 explain_sql_diff 比两份 facets 记下的原文,返回它的 items。normalize_sql 判
  「只有参数不同」不变,补了测试。

守门(tests/test_sql_equivalence.py、tests/fixtures/sql_pairs.py):虚构库(users、stores、orders、
orders_archive、order_items、refunds、payments、shipments)上 5 组固定种子的数据加 1 组极端数据(空值、重复、
边界值、悬空外键、空表)。305 对 SQL(113 对已知等价、192 对不许判等价,含复验者以前造的、在这几组数据上结果
不同的 91 对)在不给和给表结构两种情况下各判一次:结果不同的绝不判等价,标 diff 的不判等价,换个先后问结论
一样。旧测试五轮复核里报过不同的 234 对、意思一样的 56 对(SQLite / PostgreSQL / MySQL)原样收进来只查结论。

压测(变异 + 等价改写,每个种子约 1.87 万条)时找到、已经堵上的不保持语义的改写:
- merge_subqueries 把外连接可空一侧的常量列并进外层;merge 时同一个 CTE 并进外层和相关子查询两处互相遮住;
- 没给表结构时 qualify 把 WHERE / GROUP BY / HAVING 里和列别名同名的名字当成别名(SQLite 先认列),把 SELECT
  列表里和前面别名同名的名字展开(库里不认 SELECT 列表里的别名):这时不出 optimizer 这一级;
- UNION(去重)、INTERSECT 调换各支:去重留先出现的值(SQLite 里 2 和 2.0、MySQL 不分大小写时 'SH' 和 'sh');
  UNION ALL 带 LIMIT、按名字 ORDER BY 的也要看哪支在前,SQLite 合并结果的排序规则取最左一支:现在只在调换
  不改变结果时排;SQLite 从子查询读 UNION 的类型亲和;
- 一元加号在 SQLite 里去掉类型亲和、解析时却丢了;ORDER BY 别名换成表达式后撞上另一个别名;
- 没有 ORDER BY 的 LIMIT、聚合里既不分组也不聚合的列:调换来源、DISTINCT 写成 GROUP BY、RIGHT 写成 LEFT
  都会改变实际取到的行,这样的一段不做;两边都写了 COLLATE 的比较不对调。

验证:tests/test_sql_semantics.py + tests/test_verify_agent.py 519 passed,tests/test_sql_equivalence.py
307 passed,全量 -n 8:7706 passed、11 skipped。已知等价的 113 对认出 111 对(98%),不给表结构时 109 对
(96%)。压测 4 个种子各约 1.5 万条可执行:规范形式相同的组执行结果全一致,能执行的规范形式结果和原文一致
(带 CAST 的不比:生成 SQLite 时写法变了),生成的等价改写认出 2113/2115;复验者留下的 1201 对里可执行的 440 对没有误判等价,结果相同的 302 对认出 276 对。

仍做不到的(只给兜底或认不出等价)见模块说明:IN 子查询和 EXISTS 互换、窗口函数前后按分区键筛、三项以上的
加乘换结合、和方言有关的同义写法、SQLite 里套一层子查询的 UNION、去重 UNION 调换各支、LATERAL、递归 CTE、
没给表结构时别名和列同名的。假设列没有声明非默认的排序规则(SQLite 比较两列取左边那列的,表结构里看不到)。
2026-10-05 09:28:08 +08:00
ThearchyHelios 007a13d72b SQL 语义复核修正(第 5 轮):读同一张表的派生来源不再透成一张表、相关子查询分内外层、条件分组连接聚合按层计数,去重、UNION、最后出几行进比较
上一轮(3e0ab05)之后复验清单里还有一批用例返回空列表(A24 只报了一半)。最初的问题 1–3(paid / refunded 两个 CTE 换用哪一个的漏报、多一个 vip CTE 时配错、内连接换先后报出自相矛盾的两句)在第 3、4 轮已经修好,这次仍失败的是另外几类,根因各不相同:

读同一张表的几个派生来源:两个原样透出的 CTE(已付、未付,SELECT * FROM orders WHERE …)透过去,p.amount、o.amount 都成了 orders.amount,SUM、WHERE、时间列、ORDER BY、GROUP BY、关联条件、派生列的定义里用的是哪一份都分不出来(CTE 链、子查询套子查询同样);CTE 和它自己读的 orders 关联(父单、子单)也一样;同一个 CTE 关联两次时 _roles 设的 role 透过去就丢了。现在算完一段的来源后看各来源一路透过「只读一个来源」的派生来源最后读的是谁(_base),同一段里有几个读到同一个来源的,其中的派生来源标 opaque(_seal):through、时间列都不透过去,照写两边按结构配好的共同编号(已付配已付、未付配未付),给人看写名字(paid.amount),单看一条语句「只用到一张实表」时也照样写出是哪一份。时间条件给调用方对照数据目录的 detail 另一直换到实表(TimePredicate.table)。只有一个来源读那张表时照旧透过去。

相关子查询读外层那张表:EXISTS、标量子查询、LATERAL 里 c.parent_id = o.id 和 c.id = o.parent_id 都写成 orders.id = orders.parent_id(有子单还是有父单、之前还是之后有过订单分不出)。现在比较用的写法里外层的列在列名后面带「^」(_depth,往外几层带几个;带在列名上,排比较两边的先后只在两边表名一样时才受影响),给人看时内外读同一个来源的照写别名、留着表别名;两边各剩一个 EXISTS(NOT EXISTS)时配成一对说。

按整条语句去重:各段的条件、分组键、连接、聚合原来去重成一个集合,外层的 status = 'paid' 和 IN / EXISTS 子查询、关联进来的汇总 CTE、UNION 另一支里的同一个条件合成了一个,外层少了它报不出来(分组键、连接、聚合同理;聚合去重后只剩「多算了 SUM」一句,算出来的列又被压掉)。现在每一项记下在哪一层(Part / JoinFacet / Aggregate 的 scope,见 _layers:最外层、UNION 第几支、派生来源的共同编号、条件子查询「?」;只筛行的 CTE 和用到它的那一段同层,读同一张表的几个同段 CTE 各算各的层),按(层, key)去重;比的时候同一项在一边出现在几层算几个(_Compare.leftovers):同一层的先配,剩下的不管层也配(条件挪一层常是同一个口径),配不完的才报,另一边也有、只是层少了的说清是哪一层(「原流程在外层也只保留 status = 'paid' 的行,盲算在外层没有这个条件」)。条件子查询那一层多出来的不单说:外层那个条件已经报了,原来还会多出一句读起来像外层也这么筛的「只保留 state = 'done' 的行」。时间条件在列、范围都一样时也按层数比。

去重、合并、最后出几行:DISTINCT、UNION / UNION ALL 原来不进任何一面,分组键挪一层也看不出来。SELECT DISTINCT(这一段没有分组、聚合时)当这一段的分组键收:和按同几列 GROUP BY 一样的不报,一边在子查询里先去重报「去重」;各段的 UNION / INTERSECT / EXCEPT 记下合并方式和支数(SetOpFacet,同层的先配,挪了层的同样写法也配上);最后出来的行按什么分(Facets.grain:分组键 / 只出一行 / 逐行)在分组键都配得上、两边都汇总了时比(先在子查询里按用户分组、外层数用户数,对外层按用户分组)。

说清楚:自己关联自己、关联的方向反了,两边都写 a.amount 时说清各自的 a 是关联条件里出哪一列的那个(Part.roles,实表自连接同样)。

模块说明、Part / JoinFacet / TimePredicate / Aggregate / Facets / FacetDiff、unmatched / leftovers / joins / render 的文档按现在的做法改写,做不到的照实列出:读同一张表的几个派生来源同段时只认配好的编号(一边两个 CTE、另一边把其中一个换成关联实表再在 WHERE 里筛,算不同;同一个 CTE 关联两次对定义成两个一样的 CTE 也算不同);挪了层又多一层时挑哪一个说成多出来的有顺序约定;条件子查询那一层多出来的不单说;UNION 各支按位置配;不加表前缀引用外层的列认不出;LATERAL 里的相关条件两边各说一句;最后出几行只在两边都汇总时比;COUNT(DISTINCT x) 对数 DISTINCT 子查询行数照报不同(有空值时确实不一样)。

测试:复验清单里的用例逐个补成测试(三个方言,CTE 和子查询写法),另加意思一样、不该报的反例(关联换先后、改名、单个透出的 CTE、自连接换别名、相关子查询换别名、两边都两层、条件挪进汇总 CTE、分组写在 CTE 里、GROUP BY 对 DISTINCT、一样的 UNION、最外层 UNION 对包一层)。新加 132 项,其中 119 项在 3e0ab05 上失败,其余 13 项防误报的两边都通过;原有 192 项和 test_verify_agent 照旧通过,全量 7245 项通过。另拿测试里出现过的每条 SQL 在三个方言下和自己比(1051 次),都没有不同。复验清单最后一条只到「b_s」就截断了,看不到全文,按开头补了 DISTINCT 子查询对不去重、对 COUNT(DISTINCT) 的对照。
2026-10-05 06:11:40 +08:00
ThearchyHelios 3e0ab059b1 SQL 语义复核修正(第 4 轮):外连接按主表比、算出来的列按定义比、时间列透过 CTE 认、筛的先后照报、没用到的 CTE 不算,同名却不是同一个的派生来源说清各是什么
上一轮(db91c9e)给派生来源加了结构标识、按结构配对之后,复验还有一批用例返回空列表或者报得自相矛盾。按根因分几类改:

外连接:joins 第二遍按涉及的来源配对、不分方向,orders LEFT JOIN refunds 和 refunds LEFT JOIN orders 配成一对后类型、条件都一样,一句不说(db91c9e 引入的回归),RIGHT JOIN 对调、两个 CTE 对调、子查询写法同样漏。现在 JoinFacet 记下主表(keep)和可空那一侧(nullable),LEFT / RIGHT 按主表比:对调了报「原流程以 orders 为主表外连接 refunds,盲算以 refunds 为主表外连接 orders」,LEFT JOIN 和两边对调的 RIGHT JOIN 是同一回事、不再报成类型不同(原来列为做不到)。同一段 WHERE 里拿可空那一侧的列比较、IN、LIKE、BETWEEN、IS NOT NULL 的(JoinFacet.rejects),这次外连接按内连接比:盲算把可空一侧的条件写进 WHERE、LEFT JOIN 实际成了内连接的照报;IS NULL、OR、套在 COALESCE / CASE 里的不认。

算出来的列(Computed,新的 expression 一面):聚合只按去重后的 (函数, 参数) 集合比,SUM(p.total) - SUM(r.total) 对反过来、退款率分子分母对调报不出;派生来源里不是聚合的列不进任何方面,net.v 谁减谁、x.total 取的是付款还是退款合计、amount * 0.9 对 0.8、CASE 的口径、窗口函数的分区和排序、RANK 对 ROW_NUMBER 都是空列表。现在最外层选出来的列(先剥掉 ROUND、CAST、COALESCE 给默认值这类外壳)按算法比,用到的聚合一样、位置换了照报;派生来源里被外面用到、又不是原样透出一列的列按定义比。只差在聚合上、聚合那一面已经报了的,按透过去的那一列筛时间、时间那一面已经报了的,不再报第二遍。比较时加法、乘法的两项排好先后,DATE()、CAST(… AS DATE)、::date 统一成一种写法,免得写法不同被报成算法不同。

时间:时间条件的列透过「同一列套着日期函数、CAST、MIN / MAX」算出来的派生列,一直换到实表(TimePredicate.via 记下路过的列):日期表达式写在 CTE 里,按支付时间还是下单时间筛照样报出来,带上表名、列名给调用方对照数据目录(三个方言的 DATE()、CAST … BETWEEN、两个同名 CTE 对调后的 MAX(paid_at) 对 MAX(refunded_at))。

结构标识和配对:_Derived 里往里套着的派生来源原来一律写「@」,net 是 paid.total - refunded.total 还是反过来写出来一样。现在按结构写的 shape 写法里引用派生来源的地方写它自己的结构标识(里面那段按 shape 写出来再取摘要),配对之前比较的两边也按结构标识排先后;配对之后仍按共同编号排(配成一对的两个派生来源里面稍有不同时结构标识就不同,按它排会把同一个比较排成两个方向)。_align 同一级先看条件重合、再看名字:两边都叫 paid、一边筛已付一边筛未付时不再按名字配反。配成一对的派生来源里两边起名不同的列按定义配成同一列(total 对 sum_amt,原来列为做不到;_renamed),两边各剩一列时也配上,定义的不同照报。

筛的先后:派生来源取前几行(LIMIT、OFFSET)或算了窗口函数(这一列又不是分区键,含 DISTINCT ON)时,外面的条件不再透成里面那张表的条件;Part 另记一路透到底的写法(pierce)和挡住它的是什么(stage),两边只差在这里时报「原流程先算 t 的窗口函数、再按 status = 'paid' 筛选,盲算先按 status = 'paid' 筛选、再算 t 的窗口函数」「先取 t 的前 10 行、再筛」,时间条件同样。

没用到的 CTE:原来逐段收集连定义了没用的 CTE 里的条件也收,盲算定义了只筛已付的 CTE 却直接查 orders 报不出来,没用的 CTE 里多一个条件又会被报成多筛了一遍。现在只收从最外层一路引用到的段(_Statement._enliven);例外是两边都定义了同一个 CTE、一边把它关联进来一边没用到,没用到的那边照样收它,免得它里面的聚合、分组被说成一边独有(连接那一面已经说了关联了它)。

说清楚:两边同名的派生来源其实不是同一个时(t1 / t2、a / b 定义对调),「按 t1.total > 100 筛选」两边写法一模一样,读起来自相矛盾。Part 记下用到的派生来源各列的来历(origins,一直换到实表的列),给人看的写法依次退到一律写表名、带上来历(t1.total 即 SUM(orders.amount))、来历也一样(两个 CTE 只差里面的条件)时说「盲算的 paid 对应原流程的 open」。

自己关联自己:同一个 CTE、同一张表在一段里出现两次,两个别名按各自在关联条件里出的列区分(_Source.role),b1.total > b2.total 写反了报得出来,换了别名、换了关联先后不报(原来列为做不到)。CTE、子查询自己写了列名的(WITH t(n) AS …,递归 CTE 常这么写)按写的列名认列。

模块说明、Part / JoinFacet / TimePredicate / Computed / Facets / FacetDiff、unmatched、joins 的文档按现在的做法改写,做不到的照实列出:两边起名不同、各多出不止一列且定义都不同的列仍算两列;全换了的派生来源不配对;条件改得一个都不重合又各起了名字的同形派生来源只能按先后配;自连接两个别名在关联条件里出的列一样时分不出;三项以上的加法乘法换序、括号展开算不同算法;QUALIFY、GROUP BY 之后才筛的先后不认;外连接链里连带变成内连接的、套在函数里或写在外层 WHERE 的不认;不是 facets 算出来的 Facets 只能按位置编号比。

测试:复验清单里的用例逐个补成测试(各方言、CTE 和子查询写法),另加意思一样、不该报的反例(LEFT / RIGHT 对调、WHERE 筛主表、外连接链换序、0.9 * amount、DATE 对 CAST、按分区键在窗口前后筛、两边都先取前 10 行、列起名不同、自连接换别名换先后、没用到的 CTE)。新加 64 项,其中 48 项在 db91c9e 上失败,其余是防误报的,两边都通过;原有 128 项和 test_verify_agent 照旧通过,全量 7113 项通过。复验清单最后一条只到「WITH base AS (SE」就截断了,看不到全文,按开头和已知缺口一并处理了 CTE 自连接。
2026-10-05 04:58:17 +08:00
ThearchyHelios db91c9eaf6 SQL 语义复核修正(第 3 轮):两边的 CTE、子查询先按结构配好对再比,不再拿「不写编号」的写法补配
上一轮给派生来源(CTE、FROM 里的子查询)只编了位置编号,编号错开时再按不写编号的写法(派生来源一律写「@」)补配一遍。复验发现三处问题,根因相同:派生来源没有结构上的标识。一是漏报:两个 CTE 都算出 total(付款合计、退款合计),编号一致时 SUM(p.total) 对 SUM(r.total)、WHERE p.total > 100 对 r.total > 100、ORDER BY、HAVING、差值写反,都被补配吞掉、返回空列表。二是误报:盲算多一个排在前面的 CTE(vip),原流程的 t(@1)按编号配给了盲算的 vip(@1),报出「关联 t 表的条件是 orders.user_id = users.id,盲算是 users.id = users.id」「盲算关联了 t 表」。三是误报:内连接换了先后(FROM paid JOIN refunded 换成 FROM refunded JOIN paid),编号跟着对调,比较的两边按编号排成两个方向,报出两条自相矛盾的话。

现在 diff_facets 先给两边每个派生来源算一份结构标识(_Derived):里面读到的实表(FROM / JOIN,往里套着的也算)、选出来的各列规范化后的定义式(total 是 SUM(orders.amount))、分组键、里面的条件、整段规范化后的写法和起的名字;按从严到宽几级一对一地配:整段一样,表和各列定义、分组一样,表和列名一样,表和名字一样,最后三级只在两边各剩一个时才配(只是表一样、只是名字一样、只是列名一样);同一级几对都配得上时先配同名的、再配条件重合多的、最后看先后,哪一级都配不上的不配,没用到的 CTE 也参加配对。配好之后第二条 SQL 的派生来源改用第一条里对应的编号(配不上的另起编号),facets 记下原文(Facets._origin,不进比较和 as_dict),按共同的编号重新分析一遍再比:比较两边谁在前、关联条件的先后都按共同编号排,不再受位置编号左右。_Compare 去掉了不写编号的补配(删掉 Part.anon),loose 里派生来源的编号照写,筛选按同一列配对时列也带编号。

连接:内连接、CROSS JOIN 按涉及的来源和条件比(JoinFacet.sides),不按哪一侧是关联进来的,实表的内连接换了先后也不再报;配成一对的两边名字不同时两个名字都说;一边独有的内连接、关联进来的那一侧另一边也有时,报它接上的另一头(FROM stores JOIN orders、另一边只查 orders:多出来的是 stores,不是 orders;Facets.sources 记各段 FROM / JOIN 的来源拿来对照,条件里子查询的来源不算),已经说了「关联了」的表不再另说一遍「用到了」。取行:CTE、子查询里的取行按所属派生来源配好的编号(LimitFacet.scope_key)配对,一边多一个限了行数的 CTE 不再和另一边的 CTE 凑对;LATERAL 子查询照写它的别名。筛选、HAVING:两边各自独有、读起来却一样的条件(p.total > 100 和 r.total > 100 都只读得出 total > 100)配成一对、改用带来源名字的写法说,不再各说一句。

模块说明、Part / JoinFacet / LimitFacet / Facets、unmatched、joins 的文档按现在的做法改写,做不到的照实列出(外层按名字引用的列两边起名不同算两列;读同一张表、各列定义一样的两个派生来源两边都改了条件又改了名时可能配反;全换了的两个派生来源不配对;自连接分不出两个别名;LEFT JOIN 对调成 RIGHT JOIN、内连接的单表条件写在 ON 还是 WHERE、三张以上不带条件的表换先后仍算不同)。测试补上三处问题的反例(CTE、FROM 子查询、改名又改先后的写法)和上面各项,原有 99 项照旧通过。
2026-10-05 03:36:12 +08:00
ThearchyHelios 35bb23d372 check-ui-kit 跟着改源码路径:缺工具那段报错随发起运行的规则搬到了 engine/launch.py,「新样本的片段在后端源码里都还在」那一项按新位置找 2026-10-05 02:52:28 +08:00
ThearchyHelios 19d7fd7162 合入期 0 核查工具箱:SQL 语义(规范化、方面、差异说明)、只读核查 agent、核查每日花费账、核查设置组、数据源查询工具的强制只读 2026-10-05 02:52:16 +08:00
ThearchyHelios a867bf6852 合入期 0 运行时底座:发起运行的规则抽成 launch_run,运行记下选项快照和定时来源,往停下或已封存的运行里追加事件统一走 append_events,质询用量计进运行用量 2026-10-05 02:52:16 +08:00
ThearchyHelios d5e9ef39ae SQL 语义复核修正(第 2 轮):CTE、子查询起的名字不再进比较,条件里的子查询选出来的列名也不算;派生来源按位置编号,编号错开时再按不写编号的写法配一遍
上一轮只去掉了条件里子查询的表别名,子查询整段写进外层条件的 key 时还带着各自起的名字:标量子查询、IN 子查询给选出来的列起的名字(AVG(amount) AS avg_amount),子查询里 FROM 的子查询别名(s 换成 agg),以及外层引用 CTE、FROM 里的子查询算出来的列时的来源名(派生来源的 key 就是别名)。CTE 改个名,外层的 total > 100 就被报成两边各有一个、互相矛盾的条件;双盲复算里盲算的名字本来就是自己起的,会被说成换了口径。

现在派生来源(引用 CTE 的表、FROM / JOIN 里的子查询和 LATERAL 子查询)按位置编号作比较用的 key:从外往里一层一层地编,CTE 在第一次用到的地方编号,写成 CTE 还是 FROM 里的子查询、CTE 定义的先后都不影响;给人看的写法照写名字。不拿里面那段查询的写法作 key:里面差一个条件,外层按它的列写的条件就全跟着变,同一处不同会报好几遍。条件里的子查询写进外层 key 时,引用 CTE 的表、子查询(含 LATERAL)、子查询自带的 CTE 定义都换成编号,只看取值的子查询(IN、EXISTS、标量子查询)去掉选出来的列名;FROM 里的子查询、CTE 定义选出来的列名留着,外层按名字引用它们。LATERAL 的别名原来没登记成来源,按「别名写错了」照写,一并改成派生来源。

编号只看位置:一边多出一个排在前面的 CTE(比如盲算先关联一个筛 VIP 的 CTE),后面的编号就错开了,同名的 CTE 反倒比原来按名字比时多报出两条自相矛盾的话。Part 加 anon(派生来源一律写「@」、不写编号),diff_facets 每个方面先按编号比,比不上的再按 anon 一对一地配,配上的不算不同;比较两边的先后先按 anon 排、分不出再按编号;连接先配完全一样的再按关联的表配,免得错开的两个 CTE 被配成一对、说成关联条件不同。编号对得上时照样分得清是哪个 CTE 的同名列(p.total > r.total 写反成 r.total > p.total 照报)。筛选条件配对改按列的比较写法(Part.column_keys,CTE、子查询的列写「@.列」),CTE 名不同、阈值不同时说成「按 total > 100 筛选,盲算按 total > 200 筛选」,不再拆成两条各说各的。

仍按名字比的只有 CTE、子查询选出来、外层按名字引用的列(一边叫 total、一边叫 sum_amt),写进了模块说明。
2026-10-05 02:37:33 +08:00
ThearchyHelios 132d126c6f 核查 agent 复核修正:数值字段的各种写法都换成格子引用、认不出的结构下的数不采信;退回普通调用时把结构写进消息,没交的字段不再算「查不到」;收尾的钱按最贵的样子留、留过的不再被上限拦;永远不抛
数只认格子:原来只认 type: number / integer,可为 null 的数(anyOf,Pydantic 的 Optional[float] 就这么写)、$ref 引用进来的、allOf 包着的数值字段都没换成格子引用,模型写的数原样收下。现在先展开本地 $ref、并掉 allOf,anyOf / oneOf 里有一支能放数就整个算数值字段;取值时按值的形状挑分支。认不出的结构(没写类型、文本字段、没写 items 的数组、没写属性的对象)下模型写的数一律丢掉、照记下来,码值里的数照收。嵌套的引用、数组写成一段 JSON 文本的照样认,还不是数组的照实记下格式不对。数值字段引用到日期、名称这类不是数的格子,记成 unresolved,不拿去和原流程的数比。

退回普通调用:原来消息里只有「按给定结构填写」,结构却不在请求里,模型猜不到字段名,交回来对不上的字段全成了 missing,双盲复算会说盲算什么也没查到。现在把结构(JSON Schema)写进消息;没交的字段记 unresolved(模型没有交这个字段),交了 null 才是 missing;一个要求的字段都没交就是没按结构交结果,照错误交回。模型建结构化输出就报错的,直接走普通调用,不白算一次调用。

上限与金额:一条查询自己的时限到了(ToolTimeout 也是 TimeoutError)原来被当成整个核查的时长到了,现在先认 ToolTimeout,原因交回模型、核查照常往下走。收尾要把查到的结果交给模型,原来留钱按眼下的消息估,最后一条查询的大段结果一进来就不够,查完了付不起收尾、结果白查;现在按收尾最贵的样子留(查询结果按 _digest 的上限、最后的说明截到 2000 字、结构也算),留过钱的收尾不再被上限拦下,实际用量超出估算或并行的核查先用掉当天余额时,最多超出一次收尾调用的钱。每日上限缺省按设置里的「核查每日金额上限」,明确传 None 才是不限,质询接口不必各自记得去读。

永远不抛:每日账写不进去、建工具出错这类意外原来会直接抛出,节点失败、质询接口回 500;现在整段包住,按已经攒下的用量、查询照实交回。
2026-10-05 01:46:55 +08:00
ThearchyHelios a427b53874 SQL 语义复核修正:同一个口径不再因为子查询里的别名、HAVING 里的别名被报成不同;取哪几行逐段比,连同 OFFSET 和取前几行时的排序
条件里套着的子查询(EXISTS、IN (SELECT …))原来按外层的来源整个照抄进外层那个条件,别名原样留着:带别名和不带别名的同一条 NOT EXISTS 被报成两处筛选不同,双盲复算就会说盲算换了口径。现在每段 SELECT 在语法树上记段号(复制出来的子查询靠它找回原树上那一段算好的来源,相关子查询照样到外层找),改写时每一列按它所在那一段认,子查询里的表去掉别名;比较两边排序时也按那一段的来源排。HAVING 引用 SELECT 别名(HAVING s > 100)换成别名指的表达式,和 GROUP BY 别名一个读法,聚合函数里的同名列不换。

取哪几行原来只看最外层 LIMIT 的行数:CTE 里 LIMIT 10 和 LIMIT 100、OFFSET 0 和 OFFSET 10、取前 10 行时降序和升序都报成没有不同,取到的却是不同的行。Facets 加 limits,逐段收集(最外层、CTE、FROM 里的子查询;条件里的子查询已经整个写在条件里,不单算,免得报两遍)行数、跳过的行数,以及这一段的排序(ORDER BY 1、别名换成 SELECT 里的表达式);OFFSET 0 算没写,排序只在限了行的段里比。原来的 limit(最外层行数)照旧保留。
2026-10-05 01:46:55 +08:00
ThearchyHelios c8363206bd 发起时就定下的事件和运行记录同一个事务写进事件流:受管级别那条日志、目录变化、口径升版处置以前在 run_manager.start 返回之后另外写,这时运行已经建好、执行任务已经起了。其中任何一次写失败(库被锁超过等待时长之类),launch_run 抛出普通异常,运行却在执行;定时任务按「LaunchRefused 以外的异常记未能发起」会把一次正在执行的正式运行记成未能发起,封存范围里也少了受管级别那条日志(governed_formal 找不到它就退回看工作流现在的 status,中途改过画布的会按已发布级别出具)。另外这几条和执行抢先后,运行结束得快时可能落到封存之后。
- run_manager.start 加 notes:(类型, node_id, 数据) 的列表,先 flush 拿到运行的 id,和运行记录同一个事务提交,提交之后广播,最后才建执行任务。写不进去就不建运行,launch_run 抛出的任何异常都说明运行没有开始执行
- launch_run 在发起前把这几条事件备好交给 start,不再事后用 run_manager.note 写。顺序和以前实际落库的一样(日志、目录变化、口径升版处置,然后 run.started),现在是确定的
- 没有 notes 的发起(探索运行、问数据)和以前完全相同

没有照复核建议的「写失败就记日志、照常返回运行」:那样正式运行会在缺了受管级别记录的情况下照常出具,可能悄悄降成已发布级别。

测试:用触发器让口径升版处置那条写不进去,launch_run 抛出 IntegrityError、库里没有多出运行(旧代码留下一条正在执行的运行,这条变红);服务发起的正式运行里,受管级别日志和口径升版处置排在 run.started 前面,都在封存范围里
2026-10-05 01:18:43 +08:00
ThearchyHelios fc57ed0e32 追加事件收紧几处,封存后追加的质询不计进运行用量。复核提出:停在审批时追加只看运行还停着,关于第一张审批卡的质询问完时运行可能已经批过它、停到了第二张上,问答照样写进去;没有终态事件的老封存运行追加之后 verify 由一致变成不一致;返回给调用方的事件 id 和库里的不一样;测试证明不了先落库再广播;封存后追加的质询平时不计用量,继续运行之后又被计进去。
- append_events 加 approval_id(只用于 interrupted):问的是哪张审批卡,那张卡仍待审批才写。核对放在和其余条件同一次条件更新里(EXISTS 待审批的那条),批过了、不是这次运行的卡都不写、返回 None。条件更新写明不同步会话里的运行:条件里带了 EXISTS 时 SQLAlchemy 会悄悄换一种同步办法、给语句加上 RETURNING
- manifest_seq 为空的老封存运行,封到哪一条是 verify_manifest 按事件类型推出来的:有终态事件的封到最后一条终态事件,追加在后面不碍事,照旧追加;没有终态事件的封到最后一行,追加一条范围就挪到它身上,我们自己写的批注被当成篡改,现在不追加
- _stage 落库时用事件自己的 id:广播出去的、交回给调用方的和库里的是同一个 id
- 运行用量不计数据带 post_seal 的质询:运行用量是这次运行自己花的,结束时就定下了、写进了终态事件;封存后的批注另有自己的账,同按需裁判的判定。继续运行之后它落进新的封存范围,照样不计

测试:两道审批的运行批过第一道、停在第二道,钉住第一道的质询不写、钉住第二道的写,读完运行之后那张卡被批了也不写,返回的 id 和序号与落库的一致;广播时查库,每条都已落库,落空的一条都不广播(把广播挪到条件更新之前这条就变红,以前的测试照样通过);老封存三种(无终态、run.finished、run.failed)追加前后 verify 都一致,无终态的不写;失败封存的运行追加 post_seal 质询后继续运行,运行用量里没有它。去掉审批卡的核对、改回老封存一律追加、改回计 post_seal、去掉落库 id,对应的测试各自变红
2026-10-05 01:18:03 +08:00
ThearchyHelios f8b2857630 进程重启后接号不再撞上已有的序号:以前恢复、继续运行、放弃、启动时补日志都只按 run.last_seq 接号,而 last_seq 会被写小。按需裁判记判定(api/evidence.py 的 _append_judged)的条件更新不带 last_seq 条件:它取了号还没写,封存后追加(run_append)抢先提交了,它再提交时把 last_seq 写回自己那个更小的号。同一进程里序号计数只增不减,不碍事;重启后计数从 0 起,继续运行写出的 run.resumed 就和封存后追加的事件同号,重新封存时两条同号的事件一起进了清单,而清单按序号排序算哈希。
- runner 加 _seq_floor:接号起点取 last_seq 和库里实际最大序号中大的那个。恢复、继续运行、放弃都改用它,run_append 原来手写的同一段兜底也换成它
- 启动时(setup)先把落后的 last_seq 按库里实际的最后一条改正,再处理上次留下的运行中记录。_append_judged 规格不许改,它重启后也只按 last_seq 接号,不改正的话它第一次追加就撞号;有 (run_id, seq) 索引,五千次运行、一百五十万条事件实测约 7 毫秒。updated_at 不动,这是补记录,不是运行有了变化
- _stage 不带运行对象时的更新只往大里改:另开会话记的事件和执行任务同时在写,号大的先提交、号小的后提交,以前会把 last_seq 写小

测试:last_seq 写小、清掉本进程计数之后继续运行、恢复、放弃都接在实际的最后一条后面,全流无重复序号、verify 通过;_stage 先提交大号再提交小号,last_seq 不变;按真实的交错(裁判停在条件更新之前,封存后追加先提交)把 last_seq 写小,重启(shutdown、清计数、setup)后 last_seq 改正、别的运行原样不动,再裁判、继续运行都不撞号。去掉启动时的改正,只有裁判那条变红;去掉 _seq_floor,继续运行、恢复、放弃三条变红;_stage 改回无条件写,对应那条变红
2026-10-05 01:17:18 +08:00
ThearchyHelios f1c068d6a9 核查工具箱收尾:连接的方面补上另一侧(关联条件里出现的别的表,CROSS JOIN 写它之前的来源),双盲复算对照数据目录里的关系时用得上;只读 agent 收尾前先拿到收尾用的模型再预留金额,模型拿不到时不在每日账上白记一次调用;收尾消息的参数改名 limit_words,说清是哪个上限的说法 2026-10-05 00:42:02 +08:00
ThearchyHelios 1efa5f7b20 核查用的只读 agent:engine/verify_agent.py 的 run_verify_agent 给双盲复算、反方审稿、审批质询共用。工具只建给定数据源的 db_schema / db_query,一律 force_readonly;自己管工具调用循环(不依赖 NodeContext / @task),查询次数、时长、金额(这一次、全局每日)任何一项到顶就停止查询、按已有结果收尾,收尾那次时长到顶另给宽限,金额每一轮都给它留出钱;模型不在价格目录里时金额上限无效,照裁判的说法记一条说明;每次调用记进核查的每日账。成功的查询按本 agent 自己的顺序编 Q1、Q2…,给了 emit 才发 llm / tool 事件(role: verify,查询的 tool.end 带 query_artifact,随运行封存、能下钻),绝不往证据台账写 kind: query,免得进了报告的写作目录。最后按调用方的结构交结果:数值字段换成格子引用 {alias, row, column},由代码从快照取值,模型自己写的数字不采信、取不到是 None;理由里的数字写成 [[v:Qn.rX.列]],用本 agent 自己的台账 build_catalog + compose_doc 组装,裸数字会被标出;结构化输出失败退回普通调用抠 JSON。上传表格按调用方给的数据版本查。永远不抛,模型没配好、调用失败、触顶都落在返回值里 2026-10-05 00:34:17 +08:00
ThearchyHelios e46ff55fd4 数据源查询工具加 force_readonly:核查的 agent 另起一路、没有审批关卡兜底,要查的源却可能可写。开了就不管源可不可写,一律按只读守卫检查:先过一遍和 run_query 相同的检查(多条语句、结构变更照原来的说法拒),再按只读判,写语句交回「这次查询只允许读取数据」、不执行,数据原样;查询照常落查询快照。工具描述跟着写「只读」,审批判定也不再把写语句当危险调用。只靠守卫这一道:连接池按源缓存、不分只读与否,不能为此另起一个只读池(缓存键相同,会把这个源平时用的可写连接换成只读)。build_datasource_tools 透传这个参数,不传时一切照旧 2026-10-05 00:26:04 +08:00
ThearchyHelios 031aae9343 SQL 语义:data/sql_semantics.py 给双盲复算、运行对比、反方审稿共用。normalize_sql 按方言解析、重新生成(去注释、统一关键字和空白、按方言转换不加引号的名字),再把字符串和数字换成占位符:只改了字面量的两条 SQL 规范文本相同(定时运行每期日期不同,不算模板变了),字面量按规范文本里的先后列出;解析不了退回逐词规范化,照样认得出只改了字面量。facets 把查询拆成表、连接(类型 / 关联进来的表 / ON 条件)、WHERE 合取式、时间条件(列 / 运算符 / 取值)、分组、聚合(函数 / 参数 / 去重)、HAVING、LIMIT:别名换成真实表名,比较两边排好顺序,GROUP BY 1 和别名换成表达式,CTE、子查询逐段收集,只透出一张表的 CTE 换回那张表的列,逗号连接写在 WHERE 里的等式算关联条件,相关子查询到外层找别名;解析不了带 parse_error,从不抛。diff_facets 每处不同给一句中文(「A 按支付时间 paid_at 筛时间,B 按下单时间 created_at」「A 排除了 is_deleted = 1 的行,B 没有」「A 对 order_id 去重计数,B 没去重」),两边叫法可以换成「原流程」「盲算」,列的中文名可以用 catalog_labels 从数据目录取;只换了一张表时说一次「查的是哪张表」,其余按不写表名的写法比。复用 sqlcheck 的 dialect_of、_conjuncts、_bare_column、_unparen 2026-10-05 00:22:51 +08:00
ThearchyHelios 6b272de6f9 审批质询的用量计进运行用量:质询不在图里执行、不发 llm.end,花的 token 和金额写在 approval.inquiry 事件数据的 usage 里(input_tokens、output_tokens、cost_usd、calls,可带 unpriced_calls),以前运行用量只认图的状态和 llm.end,质询花的钱在运行页上看不见。_usage_of_events 把质询的用量单独加总放在 inquiry 下:llm.end 这本账要和状态那本比大小,质询花的两本里都没有,混进去比会被状态那本盖掉。_finalize 照旧算出图的用量(成功取状态,其余两本取大)后再加上质询的,明细另记在运行用量的 inquiry 下;停在审批时被放弃的运行,放弃时补上这次停下期间的质询。停过两次、两次都问过的,按上次记下的 inquiry 先减后加,每次质询只计一遍。没有质询的运行用量和以前逐字相同 2026-10-05 00:18:22 +08:00
ThearchyHelios 3dbd69b561 往停下的运行里追加事件统一走 engine/run_append.py 的 append_events(run_id, items, *, require, seal=None):审批质询要在运行停在审批时把问答写进运行、随运行结束一起封存,探索运行封存之后还要能追加批注。事件流是封存的依据,清单按 seq 排序、run_events 的 (run_id, seq) 又不唯一,撞号会让封存核对时对时不对,所以照 _append_judged 的做法:同一运行在进程内排队,读 last_seq 后从 bus 接号(另取库里实际的最大序号兜底,last_seq 被别处写小了也不撞号),写事件和条件更新 last_seq 在同一事务里——interrupted 要求运行仍停在审批,sealed 要求仍是终态且封存没变,任一条落空就回滚、返回 None,调用方据此说明未写入运行记录;提交之后才广播。sealed 的事件带 post_seal: true;运行记录上的封存若早于最后一条结束事件(继续运行后又结束、还没重新封存的那一下),不算现在的封存,不追加,免得事件被接下来的封存收进去;调用方可传 seal 固定开始时看到的封存。事件类型加 approval.inquiry。测试用人工审批停住的运行:追加两条后批准、封存,verify 通过且改动追加的事件 verify 就对不上;恢复、继续运行、重新封存、别处写入抢在中间时都不写、last_seq 不动;连续和并发追加序号严格递增、全流无重复;恢复后前面的节点不重复执行,Agent 里批过的工具不重复执行(含升级前写下的断点) 2026-10-05 00:14:40 +08:00
ThearchyHelios 0eaaf71346 运行记下选项快照和来源:runs 加 options(JSON,默认 {})和 schedule_id(可空,建索引)两列。后面的核查设置要在发起时定下、整次运行沿用,审批停下期间改了设置,恢复和重放仍按发起时的(同 approval_default 的道理);定时任务要能找回自己发起的运行。RunManager.start 收 options、schedule_id 并落库,选项先深拷一份,调用方之后改自己的 dict 不影响快照;选项非空时写进每一段的 run.started(进封存范围),为空时 run.started 和以前逐字相同;恢复、继续运行照运行记录沿用,节点从 RunContext.extra 的 run_options 读同一份拷贝。RunOut 带 schedule_id,GET /api/runs 可按 schedule_id 筛,launch_run 原样往下交。旧库走 _COLUMN_MIGRATIONS 补列,存量运行回填 {} 和 NULL;create_all 不碰已有的表,新列的索引另由 _INDEX_MIGRATIONS 补建,索引名和新库 create_all 建出来的相同 2026-10-05 00:07:07 +08:00
ThearchyHelios aa828021d7 核查设置:设置里加 verify 一组,双盲复算、反方审稿、审批质询共用。核查模型(接入、模型)空着沿用证据裁判模型的取值链;双盲复算 {启用, 允许的相对误差默认 1e-6, 查询次数, 时长}、反方审稿 {启用, 每份报告最多审几句, 每句查询次数, 时长}、审批质询 {启用, 每次查询次数, 时长},另有核查每日金额上限,上限存 null 表示不限。写法照裁判一组:PUT 先查后写,有一项不对整个请求回 422、说明写设置页上的叫法;库里存坏的项读出来按默认,三项各自那一组逐项补。verify_settings(session) 给节点和质询接口读,verify_model_spec(session, node_config) 按节点 → 设置 → judge_model_spec 取模型,整组取一级、不跨级拼。核查的每日计数不从设置接口进出,另加只读的 GET /api/settings/verify/spend 给设置页看今天花了多少 2026-10-05 00:06:03 +08:00
ThearchyHelios b459c3bb58 核查花费账:engine/spend.py 照结论句裁判的每日计数做一本核查用的每日账(设置表 verify_spend 一行,按本地日期滚动)。调用前按估算预留,加上这笔会超过每日上限就不记、返回 None,上限为 None 表示不限;调用后按实际用量找补,跨了天不找补;价格目录里没有的模型只计次数。和裁判分开记:核查一次要查好几轮库、花得多,记在同一本账上会把当天的裁判挤到判不了、让报告出具无故降档。读改写按事件循环各持一把锁,并发预留不会一起越过上限 2026-10-05 00:06:03 +08:00
ThearchyHelios 7820556ab6 发起运行的规则抽成 engine/launch.py 的 launch_run:以前全写在 api/runs.py 的 start_run 里、只认 HTTP 请求,后面的定时任务要用同一套规则发起正式运行,另抄一份迟早走样。正式运行只能从当前发布版本发起、口径升版未声明策略就拦、绑定的工具不存在就拦、发起后记受管级别那条日志和目录变化、口径升版处置,全部搬进服务;拒绝时抛 LaunchRefused(status_code, message, code),start_run 只剩一层包装,把它换回和以前逐字相同的响应(带机器码的仍回 {detail, code})。正式运行的快照图不成形,以前由 main.py 的 ValueError 兜底回 400,现在在服务里换成同样的 400 拒绝,调度器也拿得到明确的拒绝。node_setting、tool_bindings、missing_tools 跟着搬过去,runs.py 再导出,旧的导入路径照旧可用;runner 固定数据版本取绑定时改从 engine/launch 导入,不再依赖接口层。新测试先把 HTTP 的 17 种拒绝(含工具缺失的数据、工具、混合、超过 8 处四种说法)按原话写死、在旧代码上通过,再逐条比对直接调 launch_run 的结果;另测服务发起的正式运行照样写治理事件 2026-10-05 00:03:24 +08:00
ThearchyHelios 6041073ef2 检查脚本骨架:check-all 另起的后端带 AGENTLAB_SCHEDULER=0,拷来的数据目录不会替原库到点发起运行、发出 webhook。新加 check-schedules、compare、verify、inquiry、bundle、whatif 六个占位检查(只打一行通过,输出格式和别的检查一样),登记进 ORDER 和 ESTIMATE,各工作包换成真检查 2026-10-04 23:40:20 +08:00
ThearchyHelios 90dac0992b 复核套件骨架(前端):节点库加双盲复算、反方审稿(把关类,图标、配色、空配置,说明和卡片写明尚未生效,检查器照常打开)。路由加 /runs/compare(排在 /runs/:runId 前,标签页标题写「运行对比」)和 /schedules、/schedules/:id,都是页头加「功能开发中」的占位页;导航和命令面板在设置前加「定时任务」,快捷键 ⌥8,设置仍是 ⌥7;八个导航项在 480px 高的窗口里放不下,矮窗口的导航项从 40px 收到 36px。请求层从 api/client.ts 拆到 api/http.ts,client.ts 原样转出原有导出并加转请求函数;schedules、compare、bundle、inquiry、whatif 各一个空的命名空间文件,从 http.ts 取请求函数、挂到 api 上,不和 client.ts 互相导入。terms.ts 给各包占好文案块(RUN_COMPARE_TEXT 和配方对照的 COMPARE_TEXT 分开)。审批卡留两个插槽:payload.kind 为 dual_check 时的 DualCheckApproval,报告核对概况后面的 ChallengeSummary,都先返回 null。预览页自动收 dev/fixtures/*.tsx 导出的 cases,?syn= 先查这里再走原有用例。check-shell 跟着改导航项和快捷键的断言,check-copy 的 for_model 检查把 http.ts 和 client.ts 合起来看 2026-10-04 23:40:20 +08:00
ThearchyHelios a3058e7df8 复核套件骨架(后端):先把后续几个工作包要用的位置占好,各包只改自己的文件。节点类型加双盲复算 dual_check、反方审稿 challenge,执行器是占位(记一条 info 日志后跳过,产出 skipped 和原因),校验按普通节点走,叫法进 labels,暂不写进助手的节点说明。出具契约留一个通用口子:成果节点上游任何节点产出里的 issuance_gaps(字符串列表)收进缺口,前面加上「节点类型「节点名」:」,出具随之降档、不会升档;只认这个成果的上游,写坏的条目跳过。路由先登记 schedules、compare、bundle、inquiry、whatif 五个空前缀,挂在 /api/runs 下的四个排在 runs.router 之前,以后加固定路径不会被 /{run_id} 吃掉。定时任务的调度先放空的 start / stop,AGENTLAB_SCHEDULER 默认开、0 / false / off 关,写进设置;启动时开着才 start,测试环境默认关 2026-10-04 23:40:20 +08:00
ThearchyHelios ec7f6a573d 合入运行质量修复:结构化抽取截断重试、写作目录按模型窗口定篇幅并按来源均衡、缺失断言交回重写、审批卡标明运行类别并附报告核对概况、交给审批的报告探索运行也在节点里裁判、裁判小批分波地问、流式用量回传、金额估不出时写「金额无法估算」、证据目录省略警告照实说、日期时间按可读格式显示、助手写 SQL 用中文别名、大库没有数据目录时提示先起草 2026-10-04 19:09:21 +08:00
ThearchyHelios 536c9de116 证据目录的省略警告照实说省略了什么:放不下不一定省略了查询。真实运行里查询全部列出、只是有的只列前几行,警告却写「0 次只列编号和摘要()。写作模型看不到这些查询的明细」。现在分三种写:有查询只列编号和摘要的照旧;查询都列出、有的只列前几行的写「N 次查询都列出了数据,其中 M 次只列前 3 行」并按来源列次数;只省略了表和字段说明的照实说。事件多带 compact、compact_rows,各来源带 compact;时间线按同样三种写标题。另外把测试夹具、注释和 check-decode 里沿用的那次运行的真实查询次数换成虚构的次数或概括说法 2026-10-04 12:28:07 +08:00
ThearchyHelios 46330dfd96 模型不在价格目录里时运行金额写「金额无法估算」,不写 $0:真实运行里 token 记到了上百万,金额却是 $0,看着像没花钱。llm.end 发出时,用了 token、模型却不在价格目录里的标 unpriced(目录里标 0 价的本地模型不算);运行用量按 llm.end 数出 unpriced_calls,成功的运行也补上(状态里的用量分不出 0 是免费还是没价)。运行详情、画布运行面板、航迹的金额:一分都没估出来写「金额无法估算」,估出一部分写「≥ $x」,悬停说几次调用不在价格目录中。check-trace 补断言;流式用量测试和注释里的真实调用次数换成概括说法 2026-10-04 12:27:38 +08:00
ThearchyHelios 720f842b82 结论句裁判小批分波地问:一次调用最多 8 句(原来 40 句),第一批单独问、之后每波最多 4 批同时问,几批共用剩下的时长;超时只丢没回来的那几批,回来的判定照样留下。真实运行里推理模型拿着 40 句的摘录在 30 秒的时长上限内没回来,交给审批的报告一句都没判成,审批卡只能写全部未裁判;同一个模型 5 句一批 3 秒多就回来了。金额上限按已花的加这一波已定下的估算算;第一批调不通(鉴权、网关挂了)时只撞这一次,后面的统一记未裁判 2026-10-04 12:27:38 +08:00
ThearchyHelios fa842a7bec 交给人工审批的报告,探索运行也在节点里裁判结论句:审批人批准前就能看到判了几句、几句证据相矛盾。按需裁判要等运行封存之后才能判,封存前放开会动证据清单的完整性约束,所以这种报告改在节点里当场判,和正式运行同一条路;没有审批的探索运行照旧按需判,不在节点里花钱。审批节点审哪几份报告的认法抽成 reviewed_report_ids,审批卡和报告节点共用,两边说的「要人审」对得上。审批卡上没开裁判的探索报告改写「这份报告没有开启结论句裁判……开启后,交给审批的报告会在批准前判完」;开了裁判却没在节点里判的老运行另给一句。助手指引、出口契约注释、前端类型注释同步;审批暂停时按需裁判仍拒绝,测试改成只看点击不多一次调用 2026-10-04 12:03:14 +08:00
ThearchyHelios ee0700eca6 合入 FA:结构化抽取截断重试、写作清单按模型窗口定预算并按来源均衡、缺失断言交回重写、日期时间按可读格式显示 2026-10-04 11:43:21 +08:00
ThearchyHelios 470f8b6641 报告里单元格的 ISO 日期时间显示成「2026-06-30 08:21:39」,只改显示
报告表格里的时间以前照快照原样显示成 2026-06-30T08:21:39,读起来费劲。[[v:]] 和 [[table:]] 渲染出来的 ISO 日期时间改成用空格隔开(小数秒、时区照留),写作目录和流式输出同一套写法。快照里的值、引用记下的 value、eid、证据下钻都不变。

复核按文档生成时的规则:老文档存的是带 T 的字,拿新规则重新渲染会对不上、被当成文档被改过。所以显示规则也记版本:新文档里真有日期时间被改写时在顶层记 cell_display: 2(参与内容哈希),没有被改写的文档和以前一字不差;没有这个字段的按 1 版(原样)复核,记了认不出的值报 bad_schema。出口复核、期 4 的金样(test_provenance_unchanged)都照旧通过;裁判摘录是给模型看的,不改
2026-10-04 11:33:09 +08:00
ThearchyHelios 4e1adec205 审批卡标明运行类别、附上审的报告的核对概况:真实运行里审批说明写着「批准后才会正式出具」,那却是一次探索运行;报告里一句结论和证据相矛盾,审批人批准时完全不知道。人工审批的卡上加一行运行类别和批了会怎样(探索运行写「批准后输出,这是探索运行,不是正式出具」,正式运行写从已发布版本发起);human 节点挂起时,上游直接连着的、或标题和说明里引用了正文(nodes.<id> 或报告的 assign_to)的报告撰写节点各带一份核对概况(payload.reports:同 report.checked 的 stats、结论句策略、裁判摘要、报告文档工件 id),卡上写「N 个数字都有出处」和结论句的核对状态(节点里判过的写判了几句、几句证据相矛盾;没开裁判或探索运行写「结论句尚未核对」),并给查看逐段证据的入口,点数字打开证据面板。check-stream 补断言 2026-10-04 11:29:04 +08:00
ThearchyHelios 546d0e9326 没有数据目录的大库:助手「参考了 N 张表」那一行提示先起草目录。真实运行里的大库一张目录都没有,挑表和 SQL 检查都用不上目录,用户也不知道该先去起草。服务端 context 操作的每个数据源多带 source_id 和 catalog_tables(去掉驳回项后还有内容的表数);前端 copilotContext 在要挑表的大库 catalog_tables 为 0 时给出下一步「这个数据源还没有数据目录,起草后挑表和 SQL 检查会更准」,入口直达 /data/catalog/:sourceId(几个库时点名,老服务端不给目录表数时不提示)。check-decode、check-stream、check-chat 补断言 2026-10-04 11:28:49 +08:00
ThearchyHelios 799c80b97e 清单有省略时不许断言「没有数据」:写作规则加一条,核对认出缺失断言交回重写一次,仍有的只标注
一次真实运行里写作者只看到上百次查询里的前二十来次,却写出「某类数据缺失、本轮没有查到任何结果」这样的断言,可那个 Agent 实际查到了上百条记录。

- 写作规则:清单有省略、或有来源的结构化结论为空时,不得断言没有查到、不存在、缺失、没有数据,只能写「本报告未覆盖」并说明依据不足
- 组装文档:清单有省略时按一组明确的说法认出缺失断言(没有查到、未查询到、查不到、没有任何记录、数据为空、数据缺失、不存在相关记录、暂无数据等;「不存在明显差异」「不代表没有数据」这类不算,代码块不查),记新违规 absence_claim,带 span、所在片段和原句
- 和句中的 [[see:]] 一样归入 REWRITE_ONLY:交回重写一次,重写后仍有的只标注、不判失败;只剩这几类时不再按最多重写次数反复重写。仍有时报告节点发 report_absence_claim(带 count、texts),运行复核算作缺口
- 证据面板的违规清单标「缺失断言」并写明为什么依据不足,时间线给 report_absence_claim 专门的一行,文案在 terms.ts;check-decode、check-evidence 补断言
- 没展开的查询的摘要在没有表结构快照时从查询快照里的 SQL 认表名
- 回归测试:合成的门店、会员、卡券、活动库真跑「4 个 Agent 并行查库 → 报告」(查询次数虚构、分布悬殊),覆盖抽取截断重试、两次截断记空值、清单均衡和缺失断言的重写
2026-10-04 11:28:23 +08:00
ThearchyHelios 4e876ff9d8 写作者的出处清单按写作模型的窗口定篇幅,放不下时分优先级、按来源均衡,省略了什么照实说
一次真实运行里证据台账有上百次查询(4 个 Agent 查询次数悬殊),清单写死 12000 字,十万字以上的目录被截到一万两千字以内,只有前二十来次查询进了写作者的视野;查询按编号排,前面的 Agent 占满了预算,有的 Agent 一次都没露面。

- 预算:写作模型上下文窗口的 40%(字数按 1 字约 1 token 保守算),窗口先看模型接入里给这个模型登记的 context,再看模型目录;拿不到按 128K(和 Agent 护栏同一个保守值),即 51200 字;下限 12000、上限 160000
- 第一优先:各 Agent 经按出处核对交来的结构化字段(换成报告目录里的全局编号,值按单元格引用的渲染规则写,给出可照抄的引用标记)、口径卡指标和运行输入,一律列出;结构化结论为空的来源写明不代表没有数据
- 查询按产出它的节点分组、轮流展开:字段引用到的查询先展开,再给每次查询列前 3 行,有余量再补到 20 行;表和字段、知识库检索最多占预算的四分之一
- 没展开的查询列出编号和一行摘要(表、行数);约定只能在句末写 [[see:Qn]] 作依据,不猜行列、不凭摘要写数,数字照样逐字核对
- 清单开头明说目录有省略、各来源各省略了几次;报告节点发 report_catalog_trimmed 警告(带 queries、omitted、nodes),时间线给它专门的标题和下一步,运行复核算作缺口
- 放得下时清单和以前一字不差(多出的只有结构化字段那一节)
2026-10-04 11:17:13 +08:00
ThearchyHelios bf98ce16b8 运行复核把 agent_field_truncated 算作缺口(degraded),和 agent_field_unverified 同一档:字段都记空值、答案能用,缺口是输出额度在推理阶段用完了 2026-10-04 11:14:41 +08:00
ThearchyHelios f4a9955cc3 助手写查库 SQL 时给结果列起中文别名:真实运行里报告表格的表头全是英文缩写的原始列名([[table:]] 原样用查询结果的列名)。节点手册里 Agent 查库的说明和写 SQL 的硬性要求都加上一条:结果列用中文别名,按方言加标识符引号(MySQL / MariaDB 用反引号,PostgreSQL、Oracle、SQLite 用双引号,不用单引号),只给最外层 SELECT 起别名;自查的列名核对把带引号的中文标识符抹掉再判,不再因为有带引号的中文别名就整条放弃核对、把猜错的英文列名一起放过去 2026-10-04 11:13:12 +08:00
ThearchyHelios a46bad0592 助手搭多来源汇总的分析报告时默认开结论句裁判:节点手册的 report 一条写明几个 Agent 分头查库、或者几次查询合在一起写成的分析报告默认写 claims: judge,不写 judge 的预算(取设置里的默认),正式运行在节点里自动判、探索运行由读的人按需判,用户明确不要的不加。和受管级别「judge 必须写预算、拿不准就问」的取舍:那条只管要按受管级别发布的模板,普通草稿里默认开的 judge 不为预算问人;发布前修复缺预算要问的规矩不变。问数据路径原本定了不替人打开结论句策略,保留,并写明这条默认在问数里不适用 2026-10-04 11:10:28 +08:00
ThearchyHelios fa5cb1d47b 助手写人工审批的说明不再许诺「正式出具」:真实运行里一次探索运行的审批写着「批准后才会正式出具」,成果节点也没有出具契约。节点手册的 human 一条写明只有成果节点配了出具契约、从已发布版本发起的正式运行才会出具,审批说明写「批准后输出」,运行类别由审批卡标明;自查新增 human_formal_issuance:这一轮写的审批标题、说明里有「正式出具」就交回模型改 2026-10-04 11:07:40 +08:00
ThearchyHelios b45a4966c5 OpenAI 兼容接入开启流式用量回传:ChatOpenAI 只在没填 base_url 时默认请求流尾的用量,兼容接口都填了 base_url,一次运行 上百次模型调用记成 0 token、0 美元。构造时传 stream_usage=True;服务方不认 stream_options(点名它,或只回 400 / 422)时同一次调用去掉它重发、照常流式输出,并记下这个接口之后不再带它;重发也失败就报重发的错、不记。测试用假的 SSE 服务核对请求体、合并后的用量、退回和运行用量里的 token 与金额 2026-10-04 11:05:33 +08:00
ThearchyHelios a43c530fb8 结构化抽取被截断时加大额度重试,仍截断就照实说是额度在推理阶段用完了
一次真实运行里 4 个开了按出处核对字段的 Agent 抽取全部失败:抽取关掉思考、额度 4096,可经 OpenAI 兼容接口接入的推理模型关不掉思考,额度在推理阶段就用完了(openai 抛 LengthFinishReasonError);日志却叫人「稍后重试、去设置测试该模型」,两样都不解决问题。

- 额度默认至少 16384,节点配了更大的用节点的
- 认出输出被截断:openai 的 length limit 类错误、finish_reason=length、Anthropic 的 stop_reason=max_tokens;被截断时额度翻倍(最多 32768)重试一次,每次调用各记一行,被截断那次的用量照样记账
- 服务方嫌额度太大(400,报错里有 max_tokens 一类字样)时退回 8192、4096,不能因为额度提高反而抽不成
- 两次都截断:新日志 code agent_field_truncated(带 fields、max_tokens),原话说清额度在推理阶段用完、结构化结论没能写出来,下一步是换非推理模型或调大最大输出 token;字段照旧记空值、不以 0 代替。非截断类的失败照旧是 agent_field_unverified
- 时间线给 agent_field_truncated 专门的标题、副标题和下一步,入口落到该 Agent 的最大输出 token;check-decode 补断言
2026-10-04 11:02:06 +08:00
ThearchyHelios b76758e371 合入业务数据目录:数据目录与表间关系、助手按任务挑表、数据剖析、基于目录的 SQL 检查、合并查询节点、截断结果不当全量、对话纠正与目录版本提醒,以及完整性检查中发现问题的修复
- 数据目录:每张表的粒度、业务主键、业务日期、有效记录条件,每个字段的含义、单位、度量类型、码值,以及表间关联;来源与状态分开记,推断的只作提示;随表结构快照冻结进证据
- 助手建图:表多的库先按需求挑表、沿关联补上中间表,只给相关表的全部字段和目录;挑不出时回退,只探查了部分表时如实说明
- 数据剖析:默认关闭、按数据源开启,只读并经过守卫,有次数和时长上限;核实关联的覆盖率和基数(外键关系也核实基数),给出码值候选
- SQL 检查:七类问题按依据的状态定级,接入助手自查、受管发布门禁和运行时出具
- 合并查询节点:跨库先聚合再按键合并,截断输入拒收,证据追到两边原始查询
- 截断的查询结果整组参与计算时指标标为不完整、出具降档
- 对话里纠正数据事实生成目录修改提案,人确认后保存;发布时记下目录版本,正式运行时有变化就提醒
2026-10-04 09:51:15 +08:00