助手按需查看:点名的一个都没认出来时(常见的是拿数据源说明里的中文词当表名),同一次查看按需求在那几个来源里挑,不让模型为拿真名再看一次,没认出来的照样写「没找到」;候选库一张表都没探查到时不再说「按需求没挑出来」,照实说尚未探查结构、不交空目录,查看算没完成;点名不在这一轮的数据源不补空目录(真实模型验收发现)

This commit is contained in:
ThearchyHelios
2026-10-14 23:58:12 +08:00
parent c6a29bfd7c
commit e2841cb027
3 changed files with 77 additions and 8 deletions
+25 -8
View File
@@ -4,7 +4,9 @@
1. need 没写:回「look 要写 need」,不算次数;次数用完:回「按已查看的结构完成」,不再查看。
2. targets 按类型分组(旧写法 tables:[{source, table}] 读进来换成 kind=table)。写了 name 的点名直取,不调挑选模型;
只写了 kind(和 source)的、或者整个没写 targets 的,按 need 在那一类里挑(没写时挑这个入口的第一类)。
3. 挑选失败或挑出来是空的:交回那一类的目录(status=index),写明原因,请模型点名再看。
3. 挑选失败或挑出来是空的:交回那一类的目录(status=index),写明原因,请模型点名再看。点名的一个都没认出来时,
按 need 在那几个来源里挑(同只写类别),没认出来的照样写「没找到」;候选里一个能查看的都没有(库还没探查表
结构)时没有目录可交,status=failed,照实说原因。
4. 这一批对象这一轮都已经看过:回「已在上面」,不重新排一遍。
5. 各类给详情,合计受 LOOK_RESULT_CHARS 约束;记一条记录(id、by、need、status、objects、用时)。
@@ -36,6 +38,7 @@ INDEX_PROMPT = ("没能按需求挑出{what}:{reason}。下面是候选的{wha
EMPTY_PROMPT = ("按这个需求没有挑出相关的{what}。这一步确实要用到{what}的话,从下面的目录里点名再 look;"
"不需要就接着写:\n{index}")
FAILED_PROMPT = "这一次查看出错了:{reason}。按已有的信息接着写"
UNAVAILABLE_PROMPT = "没有可查看的{what}:{reason}。按已有的信息接着写,不要猜{what}的名字和字段"
#: 按需求挑出来是空的:写进 look 事件的 reason,界面上照原话显示
EMPTY_REASON = "按这个需求没有挑出相关的{what}"
UNKNOWN_KIND_PROMPT = "这一轮不能查看这一类对象:{kinds}(能查看的:{allowed})"
@@ -184,21 +187,33 @@ class LookService:
keys: set[str] = set()
indexed: list[str] = []
empty: list[str] = []
unavailable: list[str] = []
budget = LOOK_RESULT_CHARS
for kind_name in dict.fromkeys([*named, *picks]):
kind = self.kinds[kind_name]
found, missing = await kind.resolve(named.get(kind_name, [])) if kind_name in named else ([], [])
fallback = bool(missing) and not found and kind_name not in picks
if fallback:
# 点名的一个都没认出来(常见的是拿说明里的中文词当表名):按 need 在那几个来源里挑,和只写类别时一样,
# 不让模型为了拿到真名再 look 一次。没认出来的照样写进「没找到」,不拿挑出来的冒充点名的
picks[kind_name] = list(dict.fromkeys(str(t["source"]) for t in missing if t.get("source"))) or None
if kind_name in picks:
within = picks[kind_name] or sources
chosen, reason = await kind.pick(need, within)
if reason is not None or not chosen:
index = await kind.index(within)
indexed.append(INDEX_PROMPT.format(what=kind.label, reason=reason, index=index) if reason
else EMPTY_PROMPT.format(what=kind.label, index=index))
if reason is not None:
if not index and reason is not None:
# 候选里一个能查看的都没有(库还没探查表结构):没有目录可交,照实说原因,算查看没完成
unavailable.append(UNAVAILABLE_PROMPT.format(what=kind.label, reason=reason))
record["reason"] = reason
else:
empty.append(kind.label)
elif index or not fallback:
# 点名的来源本身不在这一轮里(目录是空的):「没找到数据源」已经说清楚了,不再补一段空目录
indexed.append(INDEX_PROMPT.format(what=kind.label, reason=reason, index=index) if reason
else EMPTY_PROMPT.format(what=kind.label, index=index))
if reason is not None:
record["reason"] = reason
else:
empty.append(kind.label)
found += [o for o in chosen if kind.key(o) not in {kind.key(f) for f in found}]
if not found and not missing:
continue
@@ -210,8 +225,10 @@ class LookService:
if unknown:
parts.append(UNKNOWN_KIND_PROMPT.format(kinds="、".join(dict.fromkeys(unknown)),
allowed="、".join(self.kinds)))
parts += indexed
if indexed and not keys:
parts += indexed + unavailable
if unavailable and not indexed and not keys:
record["status"] = "failed"
elif indexed and not keys:
# 一个对象都没给出详情、交回的是目录:挑选失败(有原因)或按需求没挑出来(返回空)
record["status"] = "index"
record.setdefault("reason", EMPTY_REASON.format(what="、".join(empty)))
+6
View File
@@ -236,6 +236,12 @@ class TableKind:
async def pick(self, need: str, sources: list[str] | None) -> tuple[list[TableRef], str | None]:
candidates, _ = self._candidates(sources)
if not any(self.plans[i].tables for i in candidates):
# 候选库一张表都没探查到:不是「按需求没挑出来」,照实说为什么是空的(和概览行同一句)
bare = [self.plans[i] for i in candidates] if sources else self.plans
if bare:
return [], ";".join(f"数据源「{p.name}」{introspect.why_empty(p.source.schema_cache or {})}"
for p in bare)
pool = [TableRef(i, key, "picked") for i in candidates for key in self.plans[i].tables]
if len(pool) <= PICK_LIMIT:
return pool, None
+46
View File
@@ -458,3 +458,49 @@ async def test_the_old_tables_field_still_looks(scenic_source):
result = await looks.look({"op": "look", "need": "门店",
"tables": [{"source": scenic_source.name, "table": "stores"}]}, LookBudget())
assert result.record["objects"][0]["named"] == ["stores"] and "store_code TEXT" in result.text
async def test_names_that_match_nothing_fall_back_to_picking_by_need(monkeypatch, small_source):
"""模型拿说明里的中文词点名(「订单」「客户」)一个都没认出来:同一次查看里按需求挑,不让它为了拿到真名再看一次。
小库不调挑选模型、全部给出;没认出来的照样写进「没找到」,不拿挑出来的冒充点名的。"""
fake = FakePicker(error=AssertionError("小库不该调挑选模型"))
_use_picker(monkeypatch, fake)
looks = await _service([small_source])
result = await looks.look({"op": "look", "need": "每位客户的订单金额", "targets": [
{"kind": "table", "source": small_source.name, "name": "订单"},
{"kind": "table", "source": small_source.name, "name": "客户"}]}, LookBudget())
record = result.record
assert record["status"] == "ok" and not fake.calls
(block,) = record["objects"]
assert block["named"] == [] and sorted(block["picked"]) == ["customers", "orders"]
assert block["missing"] == ["订单", "客户"]
assert "customer_id" in result.text and f"没找到:{small_source.name} 的 订单" in result.text
async def test_names_in_an_unknown_source_do_not_hand_back_an_empty_index(small_source):
looks = await _service([small_source])
result = await looks.look({"op": "look", "need": "退款", "targets": [
{"kind": "table", "source": "没有这个库", "name": "refunds"}]}, LookBudget())
assert "没找到数据源「没有这个库」" in result.text
assert "目录" not in result.text and result.record["status"] == "ok"
async def test_a_source_never_introspected_says_so_instead_of_an_empty_pick():
"""库接上了但还没探查表结构:不是「按需求没挑出来」,照实说为什么是空的,查看算没完成(不交空目录)。"""
async with SessionLocal() as session:
row = DataSource(name=f"look_bare_{uuid.uuid4().hex[:8]}", kind="sqlite", database="/nonexistent.db",
description="门店、订单、客户")
session.add(row)
await session.commit()
await session.refresh(row)
session.expunge(row)
try:
looks = await _service([row])
for line in ({"op": "look", "need": "各地区销售额"},
{"op": "look", "need": "各地区销售额", "targets": [{"kind": "table", "name": "订单"}]}):
result = await looks.look(line, LookBudget())
assert result.record["status"] == "failed", line
assert result.record["reason"] == f"数据源「{row.name}」尚未探查结构"
assert "尚未探查结构" in result.text and "目录" not in result.text
finally:
await drop_source(row.id)