mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
结论句裁判看得到表名、字段名出现过的 SQL;讲算法的句子挂到那次查询上
- 以前裁判给表名、字段名的证据只有一行名字。「这个数由某表按某字段去重计数得出」这种讲算法的句子, 证据在 SQL 里(COUNT(DISTINCT …)),裁判看不到,只能判「证据不支持」——面板上明明写着「出现在查询 Q1」 - 现在摘录里附上名字出现过的查询的 SQL(每条截 300 字,最多 3 条,只读封存范围内的工件): 表用它自己记的查询;只在 SQL 里出现、不是结果列的字段,从它所在的表用到的查询里找, 文档目录里没有这张表时看全部查询,SQL 里要写了字段名(允许 t.字段 这种前缀)和表名才算 - 写作提示:有表结构时多一句——说明一个数是怎么算出来的(按哪个字段去重、怎么汇总、筛了什么条件), 句末挂 [[see:Qn]] 指向算出它的那次查询;只挂表名、字段名指不出是哪一次查询算出的
This commit is contained in:
@@ -2656,7 +2656,9 @@ CELL_RULES = (
|
||||
ENTITY_RULES = (
|
||||
"表和字段(系统会核对):提到表名、字段名时写 [[t:表名]]、[[c:表名.字段名]],或者放进反引号(`表名`);"
|
||||
"只写下面列出的、或者这次查过的数据源里真实存在的名字——本次运行的表结构快照、查询用到的表、"
|
||||
"查询结果列里都没有的名字,会被标成「可能是编造的名字」。"
|
||||
"查询结果列里都没有的名字,会被标成「可能是编造的名字」。\n"
|
||||
"说明一个数是怎么算出来的(按哪个字段去重、怎么汇总、筛了什么条件)时,句末挂 [[see:Q1]] 指向算出它的那次"
|
||||
"查询(下面每张表后面写了哪几次查询用到它):只挂 [[t:]]、[[c:]] 指不出是哪一次查询算出的这个数。"
|
||||
)
|
||||
#: 引原话的写法。只跟着目录里的知识库检索出现
|
||||
QUOTE_RULES = (
|
||||
|
||||
@@ -73,6 +73,8 @@ SQL_CHARS = 300
|
||||
EXCERPT_ROWS = 20
|
||||
EXCERPT_COLS = 12
|
||||
CHUNK_CHARS = 600
|
||||
#: 表名、字段名的摘录里附上它出现过的查询的 SQL,最多几次(「按会员号去重计数」这种句子,证据在 SQL 里)
|
||||
ENTITY_QUERIES = 3
|
||||
EXCERPT_HITS = 3
|
||||
#: 短句:看得见的字(不算标点、空白)少于这么多,又没挂依据、没有方向词的,不送裁判
|
||||
SHORT_CHARS = 6
|
||||
@@ -359,7 +361,7 @@ def _retrieval_excerpt(alias: str, entry: dict[str, Any], picked: list[int], who
|
||||
|
||||
|
||||
def _excerpt(alias: str, entry: dict[str, Any], cands: list[Candidate], fetch: _Fetch,
|
||||
masked: dict[str, Any] | None) -> str | None:
|
||||
masked: dict[str, Any] | None, catalog: dict[str, Any]) -> str | None:
|
||||
kind = entry.get("kind")
|
||||
if kind == "metric":
|
||||
return _metric_excerpt(alias, entry, fetch)
|
||||
@@ -374,13 +376,46 @@ def _excerpt(alias: str, entry: dict[str, Any], cands: list[Candidate], fetch: _
|
||||
picked = [h for c in cands for a, h in c.hits if a == alias]
|
||||
return _retrieval_excerpt(alias, entry, picked, any(alias in c.whole for c in cands), fetch)
|
||||
if kind == "table":
|
||||
return f"【{alias}】表 {entry.get('name')}" + (f"(数据源 {entry['source']})" if entry.get("source") else "")
|
||||
head = f"【{alias}】表 {entry.get('name')}" + (f"(数据源 {entry['source']})" if entry.get("source") else "")
|
||||
return "\n".join([head, *_entity_sql(entry, catalog, fetch)])
|
||||
if kind == "column":
|
||||
table = (entry.get("locator") or {}).get("table")
|
||||
return f"【{alias}】字段 {f'{table}.' if table else ''}{(entry.get('locator') or {}).get('column') or entry.get('name')}"
|
||||
head = f"【{alias}】字段 {f'{table}.' if table else ''}{(entry.get('locator') or {}).get('column') or entry.get('name')}"
|
||||
return "\n".join([head, *_entity_sql(entry, catalog, fetch)])
|
||||
return None
|
||||
|
||||
|
||||
def _entity_sql(entry: dict[str, Any], catalog: dict[str, Any], fetch: _Fetch) -> list[str]:
|
||||
"""表名、字段名出现过的查询的 SQL(每条截到 SQL_CHARS,最多 ENTITY_QUERIES 条)。
|
||||
|
||||
讲算法的句子(「按会员号去重计数得出」)证据在 SQL 里;只给名字,裁判只能判不支持。
|
||||
查询用条目自己记的(表:SQL 里用到它;字段:它是结果列)。只在 SQL 里出现、不是结果列的字段自己没记:
|
||||
有表条目就从它记的查询里找,没有(文档目录只留正文用到的实体)就看全部查询,都只留 SQL 里写了这个
|
||||
字段名(有表名的还要写了表名)的。快照经 fetch 取:取不到(不在封存范围里)的不给。
|
||||
"""
|
||||
locator = entry.get("locator") or {}
|
||||
column = locator.get("column") if entry.get("kind") == "column" else None
|
||||
table = locator.get("table")
|
||||
aliases = [q for q in entry.get("queries") or [] if isinstance(catalog.get(q), dict)]
|
||||
need = [str(column)] if column else []
|
||||
if not aliases and column:
|
||||
owner = catalog.get(f"t:{table}") if table else None
|
||||
aliases = [q for q in (owner.get("queries") or []) if isinstance(catalog.get(q), dict)] \
|
||||
if isinstance(owner, dict) else \
|
||||
[a for a, e in catalog.items() if isinstance(e, dict) and e.get("kind") == "query"]
|
||||
need += [str(table)] if table and not isinstance(owner, dict) else []
|
||||
lines: list[str] = []
|
||||
for q in aliases:
|
||||
snapshot = fetch(catalog[q].get("artifact"))
|
||||
sql = _clean(str(snapshot.get("sql") or "")) if isinstance(snapshot, dict) else ""
|
||||
if not sql or any(not re.search(rf"(?<![\w]){re.escape(n)}(?![\w])", sql, re.I) for n in need):
|
||||
continue
|
||||
lines.append(f"出现在查询 {q}:{sql[:SQL_CHARS]}{'…' if len(sql) > SQL_CHARS else ''}")
|
||||
if len(lines) == ENTITY_QUERIES:
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def _tokens(text: str) -> int:
|
||||
"""估算令牌数:中日韩的字一个字算一个,其余大约 3.5 个字符一个。宁可估多,不估少。"""
|
||||
wide = sum(1 for ch in text if ord(ch) > 0x2E7F)
|
||||
@@ -441,7 +476,7 @@ def prepare(doc: dict[str, Any], catalog: dict[str, Any] | None = None, *, units
|
||||
excerpts: dict[str, str] = {}
|
||||
for alias in dict.fromkeys(a for c in cands for a in c.aliases):
|
||||
entry = catalog.get(alias)
|
||||
if isinstance(entry, dict) and (text := _excerpt(alias, entry, cands, fetch, masked)):
|
||||
if isinstance(entry, dict) and (text := _excerpt(alias, entry, cands, fetch, masked, catalog)):
|
||||
excerpts[alias] = text
|
||||
picked = {c.unit for c in cands}
|
||||
skipped: dict[str, str] = {}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""裁判看引用表名、字段名的句子时,要看得到这些名字出现过的那几次查询的 SQL。
|
||||
|
||||
「这个数由 orders 按 orders.member_no 去重计数得出」这种讲算法的句子,证据在 SQL 里
|
||||
(COUNT(DISTINCT member_no))。以前表名、字段名的摘录只有一行名字,裁判只能判「证据不支持」——
|
||||
面板上明明写着「出现在查询 Q1」,这层关联却没交给裁判。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.engine.evidence import build_catalog, compose_doc
|
||||
from app.engine.judge import ENTITY_QUERIES, SQL_CHARS, prepare
|
||||
|
||||
SOURCE = "warehouse"
|
||||
SCHEMA_ART, Q1_ART, Q2_ART, Q3_ART = "5" * 64, "a" * 64, "b" * 64, "c" * 64
|
||||
SQL1 = "SELECT COUNT(DISTINCT member_no) AS active_users FROM orders WHERE created_at >= '2026-09-01'"
|
||||
SQL2 = "SELECT region, SUM(amount) AS gmv FROM orders GROUP BY region"
|
||||
SQL3 = "SELECT COUNT(*) AS refund_cnt FROM refunds"
|
||||
SNAPS = {
|
||||
Q1_ART: {"columns": ["active_users"], "rows": [[321]], "sql": SQL1, "source": SOURCE},
|
||||
Q2_ART: {"columns": ["region", "gmv"], "rows": [["华东", 100.0]], "sql": SQL2, "source": SOURCE},
|
||||
Q3_ART: {"columns": ["refund_cnt"], "rows": [[4]], "sql": SQL3, "source": SOURCE},
|
||||
}
|
||||
|
||||
|
||||
def query(art: str, columns: list[str], tables: list[str]) -> dict:
|
||||
return {"kind": "query", "node_id": "fetch", "exec": 1, "artifact": art, "tool": f"db_query__{SOURCE}",
|
||||
"source": SOURCE, "columns": columns, "rows": 1, "truncated": False, "schema_artifact": SCHEMA_ART,
|
||||
"tables": tables}
|
||||
|
||||
|
||||
SCHEMA = {"kind": "schema", "node_id": "fetch", "exec": 1, "artifact": SCHEMA_ART, "source": SOURCE,
|
||||
"tables": {"orders": ["id", "member_no", "region", "amount", "created_at"], "refunds": ["id", "amount"]}}
|
||||
|
||||
|
||||
def catalog():
|
||||
return build_catalog(nodes={}, ledger=[query(Q1_ART, ["active_users"], ["orders"]),
|
||||
query(Q2_ART, ["region", "gmv"], ["orders"]),
|
||||
query(Q3_ART, ["refund_cnt"], ["refunds"]), SCHEMA])
|
||||
|
||||
|
||||
def excerpts_for(text: str) -> dict[str, str]:
|
||||
cat = catalog()
|
||||
doc = compose_doc(text, cat)
|
||||
return prepare(doc, cat, loader=SNAPS.get).excerpts
|
||||
|
||||
|
||||
def test_a_table_excerpt_carries_the_sql_of_the_queries_it_appears_in():
|
||||
ex = excerpts_for("活跃用户数由 [[t:orders]] 按 [[c:orders.member_no]] 去重计数得出。")
|
||||
table = ex["t:orders"]
|
||||
assert table.startswith("【t:orders】表 orders")
|
||||
assert "COUNT(DISTINCT member_no)" in table and "Q1" in table
|
||||
assert "SUM(amount)" in table and "Q2" in table # 用到这张表的查询都给
|
||||
assert "refunds" not in table # 没用到这张表的不给
|
||||
|
||||
|
||||
def test_a_column_only_in_the_sql_falls_back_to_queries_that_mention_it():
|
||||
# member_no 不是任何查询的结果列,条目自己没记查询:按所在的表找、只留 SQL 里写了它的
|
||||
ex = excerpts_for("活跃用户数按 [[c:orders.member_no]] 去重计数得出。")
|
||||
column = ex["c:orders.member_no"]
|
||||
assert "COUNT(DISTINCT member_no)" in column and "Q1" in column
|
||||
assert "SUM(amount)" not in column # Q2 用了 orders 但没写 member_no
|
||||
|
||||
|
||||
def test_a_result_column_uses_the_queries_it_came_from():
|
||||
ex = excerpts_for("各区的销售额按 [[c:orders.region]] 汇总。")
|
||||
assert "GROUP BY region" in ex["c:orders.region"] and "Q2" in ex["c:orders.region"]
|
||||
|
||||
|
||||
def test_entity_sql_is_capped():
|
||||
long_sql = "SELECT COUNT(DISTINCT member_no) AS n FROM orders WHERE " + " AND ".join(f"f{i} = 1" for i in range(200))
|
||||
snaps = {art: {**SNAPS[Q1_ART], "sql": long_sql} for art in "defgh"}
|
||||
ledger = [query(art * 64, ["n"], ["orders"]) for art in "defgh"] + [SCHEMA]
|
||||
cat = build_catalog(nodes={}, ledger=ledger)
|
||||
doc = compose_doc("活跃用户数由 [[t:orders]] 去重计数得出。", cat)
|
||||
table = prepare(doc, cat, loader=lambda a: snaps.get(a[0])).excerpts["t:orders"]
|
||||
assert table.count("SELECT") == ENTITY_QUERIES
|
||||
assert all(len(line) < SQL_CHARS + 40 for line in table.splitlines())
|
||||
|
||||
|
||||
def test_an_unsealed_snapshot_is_simply_left_out():
|
||||
# loader 只认封存范围内的工件:取不到的查询不给 SQL,名字那一行照样在
|
||||
cat = catalog()
|
||||
doc = compose_doc("活跃用户数由 [[t:orders]] 去重计数得出。", cat)
|
||||
table = prepare(doc, cat, loader=lambda a: None).excerpts["t:orders"]
|
||||
assert table.startswith("【t:orders】表 orders") and "SELECT" not in table
|
||||
|
||||
|
||||
def test_a_qualified_column_in_the_sql_still_counts():
|
||||
# SQL 里写成 o.member_no(表别名前缀)也算写了这个字段;只沾了边的 member_no_hash 不算
|
||||
snaps = {Q1_ART: {**SNAPS[Q1_ART], "sql": "SELECT COUNT(DISTINCT o.member_no) AS n FROM orders o"},
|
||||
Q2_ART: {**SNAPS[Q2_ART], "sql": "SELECT COUNT(DISTINCT member_no_hash) AS n FROM orders"}}
|
||||
cat = build_catalog(nodes={}, ledger=[query(Q1_ART, ["n"], ["orders"]), query(Q2_ART, ["n"], ["orders"]), SCHEMA])
|
||||
doc = compose_doc("活跃用户数按 [[c:orders.member_no]] 去重计数得出。", cat)
|
||||
column = prepare(doc, cat, loader=snaps.get).excerpts["c:orders.member_no"]
|
||||
assert "o.member_no" in column and "member_no_hash" not in column
|
||||
|
||||
|
||||
def test_the_same_column_name_in_another_tables_query_is_not_evidence():
|
||||
# 只引了字段、没引表(文档目录里没有 t:orders):看全部查询时,别的表上写了同名字段的不算
|
||||
snaps = {**SNAPS, Q3_ART: {**SNAPS[Q3_ART], "sql": "SELECT COUNT(DISTINCT member_no) AS n FROM refunds"}}
|
||||
cat = catalog()
|
||||
doc = compose_doc("活跃用户数按 [[c:orders.member_no]] 去重计数得出。", cat)
|
||||
assert "t:orders" not in doc["catalog"]
|
||||
column = prepare(doc, doc["catalog"], loader=snaps.get).excerpts["c:orders.member_no"]
|
||||
assert "FROM orders" in column and "FROM refunds" not in column
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 写作提示:讲算法的句子挂 [[see:Qn]],别只写表名、字段名
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_writer_is_told_to_point_method_sentences_at_the_query():
|
||||
from app.engine.evidence import catalog_prompt
|
||||
|
||||
prompt = catalog_prompt(catalog())
|
||||
[rule] = [line for line in prompt.splitlines() if "算出来" in line]
|
||||
assert "[[see:Q" in rule and "去重" in rule
|
||||
assert "[[t:" in rule and "[[c:" in rule # 点明只挂表名、字段名说明不了算法
|
||||
|
||||
|
||||
def test_a_catalog_without_tables_does_not_carry_the_method_rule():
|
||||
from app.engine.evidence import catalog_prompt
|
||||
|
||||
ledger = [{"kind": "query", "node_id": "fetch", "exec": 1, "artifact": Q1_ART, "tool": f"db_query__{SOURCE}",
|
||||
"source": SOURCE, "columns": ["active_users"], "rows": 1, "truncated": False}]
|
||||
assert "算出来" not in catalog_prompt(build_catalog(nodes={}, ledger=ledger))
|
||||
Reference in New Issue
Block a user