Files
ThearchyHelios 271bc6e782 口径标签逐格判:引用了好几列的查询,每个数按它自己那一格标
真实模型下问「上周的销售额是多少」,助手写 SUM(amount) AS 销售额, COUNT(*) AS 订单数, MIN / MAX(paid_date) AS 区间首日 /
末日,报告四列都引用了,口径「周销售」只取 SUM(amount)。按「全部引用列」投影后还是 4 列,对不上口径的 1 列,整条比、
四个数都标相关口径,说明整段兜底。

- ledger/match._cells / _cell(algo 4):整条(含按全部引用列投影)没证明、又引用了这条查询两列以上的,被引用的每一列单独
  投影到那一列,和每份口径的每个指标读的那一列(card_reads;口径版本没有口径卡的按取数的每一列)逐个比,仍只走
  match_template / explain_sql_diff,上次的投影规则照用:投影不了(分组列、DISTINCT……)、过了时限、不可比的格照用整条
  查询的那一个候选。结果记在 queries[].cells {列名: {status, candidates}},另记 one_row(sql_semantics.one_row:不分组的
  聚合,一定只出一行)。整条这一层的结论、候选一字不改;老工件没有 cells,照旧按整条读。
- ledger/labels:单元格按它那一格的结论标(_view);指标用了同一条查询的好几格时,只有那条查询一定只出一行才把逐格的
  结论合起来用(多行的结果里每一列各自对得上,不等于每一行的几列一起对得上)。整题照拍板 L1 不变:每个数都证明来自
  同一份口径才等价;一格证明了、别的格只是相关的整题不等价,原因写「有的数和相关口径不一致,不来自口径」(以前一律说
  「这次的推理」)。出具入口照 ledger/issue 现有判法只在整题等价时给,没有放宽。
- 逐格证明了的口径各处都算上(labels.candidates_of):工件 meta 的 calibers(召回按旧版本反查)、ledger/uses 的使用记录、
  召回时找原答案对应的那一格、E4;E1 另数各种结论各几格(cells),E2 的兜底比例按标签上给人看的那一份说明数。
- GET /runs/{id}/ledger 的 queries 带上逐格的 cells(遮罩同候选);证据片段的 ledger 键、轮次摘要的计数本来就按每个数的
  标签,数据变了、形状不变。前端只补类型;check-ledger 加一段:同一条查询一格来自口径(正文挂小标记)、一格相关(差在哪
  先说多查了几列),整题不等价不给出具。
- 评测逐题比 SQL(eval/score.sql_match)取和期望配上的那一格(score.matched_cells:评分时配上的格),没给时取逐格证明了
  的那一格;结果带 column。runner、rescore 传它。

测试:tests/test_ledger_cells.py(同一条查询一列等价、一列不等价各自标对;两份口径只证明了一份标那一份;口径多个指标按列
配对;指标跨两格只在一行的查询上用逐格结论;分组列、DISTINCT 逐格也不投影;照真实例子造的虚构库真跑一遍:销售额标等价
已证明,订单数、首末日标相关,整题不等价、不给出具、不自动出具,证据片段的 ledger 键一致;评测取配上的那一格)。去掉逐格
再跑,10 条失败;去掉「一行才合起来用」、去掉评测取格,各 1 条失败。
2026-10-17 13:07:19 +08:00

153 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""口径账本的记法(docs/design/next-round/02-口径账本.md 做法 2,拍板 L8):比对结果存成内容寻址工件,不建新表。
一次比对 = 一份工件 kind=caliber_match(格式 agentlab.caliber_match/1)+ 一条封存之后追加的事件 ledger.matched
{artifact, manifest_hash, algo, counts, trigger}(engine/run_append 的 sealed 方式,同按需裁判的 evidence.judged):
- 工件内容只放比对本身:运行 id、对着哪一次封存(manifest_hash / manifest_seq)、算法版本、考虑过的口径版本(工作流、版本、
级别、graph_hash)、每条查询的状态和绑定、差异条目、列映射、每个数的标签、整题判断。不放时间、耗时、工作流名(名字会改,
界面读的时候再取):同一份封存、同一个算法版本、同一批口径,再比一次得到的工件一字不差、哈希一样。
- 工件引用的 meta 记 {calibers: [等价已证明的「工作流@版本」], related: [相关的], counts}:召回按旧版本反查、统计按种类数,
都只扫 artifacts(kind 有索引),不逐份读内容。
- 事件说的是「这一次封存的这份比对」:读的时候只认 manifest_hash 等于运行现在封存的那几条,取最后一条(之后口径有变、补比
过的,以最新的为准)。工件读不出、被改过、不是这次运行这次封存的,照实说读不出,不拿它标数。
trigger 是谁触发的:sealed(运行封存后马上比的)、backfill(启动时补比)、on_demand(打开账本时按需比的)。后两种是事后按
那时的口径回看的,界面据此写「按现在的口径回看」。它只在事件里,不进工件内容(不然同一份比对因为触发方式不同哈希就不同)。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from sqlalchemy import select
from app.core.events import EventType
from app.db.base import SessionLocal
from app.db.models import Run, RunEvent
#: 工件的种类和内容格式
KIND = "caliber_match"
FORMAT = "agentlab.caliber_match/1"
#: 比对算法的版本:绑定、证明、标签的规则改了就加一,界面和统计据此分得清是哪一版算法比的。
#: 2(第 3 波):认出助手选用的口径(查询 composed、标签 how=composed),指标的标签记它取自哪几条查询(queries,认证队列
#: 认「被报告引用的查询」用)
#: 3:整条比不等价时按报告引用的列投影再比(ledger/match 的 _projected),候选记 projection;没投影的照旧
#: 4:按全部引用列投影仍没证明的查询,被引用的每一列逐格各比一次(queries[].cells、one_row),每个数按它那一格标;老工件
#: 没有 cells,照旧按整条查询读
ALGO = "4"
#: 触发方式(见模块说明)。certified(第 3 波):从问数据固化出的口径发布之后,对来源运行补比的(ledger/certify)
TRIGGERS = ("sealed", "backfill", "on_demand", "certified")
@dataclass(frozen=True)
class Recorded:
"""一次运行现在这次封存的最新一份比对。content 读不出时为 None,problem 写为什么。"""
artifact: str
seq: int
ts: Any
trigger: str
algo: str | None
content: dict[str, Any] | None
problem: str | None = None
@property
def retro(self) -> bool:
"""事后按那时的口径回看的(启动时补比、打开时按需比的),不是运行一封存就比的。"""
return self.trigger != "sealed"
#: 读不出比对工件时的说法
UNREADABLE = "比对记录读不出来(文件缺失)"
TAMPERED = "比对记录与哈希不一致,疑似被修改,不能用来标口径"
FOREIGN = "比对记录不属于这次运行的这次封存,不能用来标口径"
def counts_of(queries: list[dict[str, Any]]) -> dict[str, int]:
"""每种查询状态各几条(proven / composed / related / none / skipped),0 也写。"""
out = {status: 0 for status in ("proven", "composed", "related", "none", "skipped")}
for query in queries:
status = str(query.get("status") or "")
if status in out:
out[status] += 1
return out
def meta_of(content: dict[str, Any]) -> dict[str, Any]:
"""工件引用的 meta:{calibers: [等价已证明的「工作流@版本」], related: [相关的], counts}。"""
from app.ledger.labels import candidates_of
proven: list[str] = []
related: list[str] = []
for query in content.get("queries") or []:
# 逐格证明了的口径也算(召回按旧版本反查、统计按种类数都只读这一份 meta)
for cand in candidates_of(query):
key = str(cand.get("caliber") or "")
if not key:
continue
bucket = proven if cand.get("status") == "proven" else related
if key not in bucket:
bucket.append(key)
related = [key for key in related if key not in proven]
return {"calibers": sorted(proven), "related": sorted(related), "counts": counts_of(content.get("queries") or [])}
async def save(run_id: str, content: dict[str, Any], *, seal: tuple[int | None, str | None],
trigger: str) -> str | None:
"""存比对工件、追加 ledger.matched。返回工件 id;运行在比对期间换了封存(没抢到)时只存了工件、没追加事件,返回 None
(不算这次运行的比对:读的时候只认事件)。"""
from app.core.artifact_store import put_json
from app.engine.run_append import append_events
if trigger not in TRIGGERS:
raise ValueError(f"不认识的比对触发方式:{trigger}")
meta = meta_of(content)
artifact = await put_json(content, kind=KIND, run_id=run_id, meta=meta)
written = await append_events(run_id, [(EventType.LEDGER_MATCHED, None, {
"artifact": artifact, "manifest_hash": content.get("manifest_hash"), "algo": content.get("algo"),
"counts": meta["counts"], "trigger": trigger,
})], require="sealed", seal=seal)
return artifact if written else None
async def latest(run_id: str) -> Recorded | None:
"""这次运行现在这次封存的最新一份比对;没比对过(或者比对的是以前的封存)为 None。"""
from app.core.artifact_store import load
async with SessionLocal() as session:
manifest = await session.scalar(select(Run.manifest_hash).where(Run.id == run_id))
if not manifest:
return None
rows = (await session.execute(
select(RunEvent.seq, RunEvent.ts, RunEvent.data).where(
RunEvent.run_id == run_id, RunEvent.type == EventType.LEDGER_MATCHED)
.order_by(RunEvent.seq.desc()))).all()
for seq, ts, data in rows:
if not isinstance(data, dict) or data.get("manifest_hash") != manifest or not data.get("artifact"):
continue
artifact = str(data["artifact"])
trigger = str(data.get("trigger") or "sealed")
try:
content = load(artifact)
except ValueError:
return Recorded(artifact, seq, ts, trigger, data.get("algo"), None, TAMPERED)
if not isinstance(content, dict):
return Recorded(artifact, seq, ts, trigger, data.get("algo"), None, UNREADABLE)
if content.get("format") != FORMAT or content.get("run_id") != run_id \
or content.get("manifest_hash") != manifest:
return Recorded(artifact, seq, ts, trigger, data.get("algo"), None, FOREIGN)
return Recorded(artifact, seq, ts, trigger, data.get("algo"), content)
return None
async def matched_run_ids(session: Any, run_ids: list[str]) -> set[str]:
"""这几次运行里有过比对事件的(不管是哪一次封存的):启动补比据此跳过比过的。"""
if not run_ids:
return set()
rows = await session.execute(select(RunEvent.run_id).where(
RunEvent.run_id.in_(run_ids), RunEvent.type == EventType.LEDGER_MATCHED).distinct())
return {r for (r,) in rows}
__all__ = ["ALGO", "FORMAT", "KIND", "TRIGGERS", "Recorded", "counts_of", "latest", "matched_run_ids", "meta_of", "save"]