
企业微信

飞书
选择您喜欢的方式加入群聊

扫码添加咨询专家
在 Text-to-SQL 系统中,元数据质量直接决定了 SQL 生成的准确性。但传统的元数据管理往往只关注技术层面(表名、字段名、数据类型),忽略了业务语义。AskTable 通过 MetaAdmin 语义层,将技术元数据和业务语义有机结合,让 AI 真正理解企业数据。
技术元数据示例:
CREATE TABLE ord_dtl (
ord_id VARCHAR(32),
cust_id VARCHAR(32),
prod_id VARCHAR(32),
qty INT,
amt DECIMAL(10,2),
sts TINYINT,
crt_tm DATETIME
);
问题:
ord_dtl 是什么意思?订单详情?qty 是数量,但是什么的数量?amt 是金额,但是含税还是不含税?sts 的值 1、2、3 分别代表什么状态?crt_tm 是创建时间还是下单时间?用户提问:"上个月的订单金额是多少?"
AI 的困惑:
ord_dtl 表还是 order_detail 表?amt 字段还是 amount 字段?sts 字段吗?什么值才算有效订单?AskTable 的 MetaAdmin 语义层提供了完整的业务语义:
Table(
name="ord_dtl",
origin_desc="ord_dtl", # 原始表名
curr_desc="订单详情表,记录每笔订单的商品明细信息", # 业务描述
fields={
"ord_id": Field(
name="ord_id",
data_type="VARCHAR(32)",
curr_desc="订单ID,关联订单主表",
),
"amt": Field(
name="amt",
data_type="DECIMAL(10,2)",
curr_desc="订单金额,单位:元,不含税,不含运费",
),
"sts": Field(
name="sts",
data_type="TINYINT",
curr_desc="订单状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消",
),
}
)
现在 AI 可以:
ord_dtl 是订单详情表amt 是不含税不含运费的金额sts 为 2、3、4 的订单才是有效订单上图展示了 MetaAdmin 的三层架构:Schema(模式)→ Table(表)→ Field(字段)。每一层都包含原始描述和业务描述,通过这种分层结构,AI 能够准确理解数据的业务含义和层次关系。
AskTable 采用三层结构:Schema → Table → Field
@dataclasses.dataclass(kw_only=True)
class StructureBase:
name: str
origin_desc: str | None = None # 原始描述(通常是表名/字段名)
curr_desc: str | None = None # 当前描述(业务语义)
curr_desc_stat: str = "origin" # 描述状态:origin/manual/ai_generated
@dataclasses.dataclass()
class Field(StructureBase):
schema_name: str
table_name: str
sample_data: str | None = None # 示例数据
data_type: str | None = None # 数据类型
is_index: bool = False # 是否是索引字段
index_count: int = 0 # 索引基数
visibility: bool = True # 是否可见
identifiable_type: IdentifiableType = IdentifiableType.plain # 敏感信息类型
is_vector_synced: bool = True # 是否已同步到向量库
@property
def full_name(self) -> str:
return f"{self.schema_name}.{self.table_name}.{self.name}"
@dataclasses.dataclass()
class Table(StructureBase):
schema_name: str
fields: dict[str, Field] = dataclasses.field(default_factory=dict)
@property
def full_name(self) -> str:
return f"{self.schema_name}.{self.name}"
@dataclasses.dataclass()
class Schema(StructureBase):
custom_configs: dict | None = None
tables: dict[str, Table] = dataclasses.field(default_factory=dict)
@property
def full_name(self) -> str:
return self.name
@dataclasses.dataclass(kw_only=True)
class MetaAdmin:
datasource_id: str | None = None
schemas: dict[str, Schema] = dataclasses.field(default_factory=dict)
@property
def schema_count(self):
return len(self.schemas.values())
@property
def table_count(self):
return sum([len(schema.tables.values()) for schema in self.schemas.values()])
@property
def field_count(self):
return sum([
len(table.fields.values())
for schema in self.schemas.values()
for table in schema.tables.values()
])
def __repr__(self):
return f"[{self.datasource_id}] {self.schema_count} S, {self.table_count} T, {self.field_count} F"
设计亮点:
origin_desc: str | None = None # 原始描述
curr_desc: str | None = None # 当前描述
curr_desc_stat: str = "origin" # 描述来源
为什么需要两个描述?
应用场景:
# 场景 1:手动优化描述
field.origin_desc = "amt"
field.curr_desc = "订单金额,单位:元,不含税"
field.curr_desc_stat = "manual"
# 场景 2:AI 生成描述
field.origin_desc = "amt"
field.curr_desc = "订单金额(根据历史数据分析得出)"
field.curr_desc_stat = "ai_generated"
# 场景 3:回退到原始描述
field.curr_desc = field.origin_desc
field.curr_desc_stat = "origin"
sample_data: str | None = None # 示例数据
is_index: bool = False # 是否是索引
index_count: int = 0 # 索引基数
visibility: bool = True # 是否可见
identifiable_type: IdentifiableType = IdentifiableType.plain # 敏感类型
用途:
示例数据:帮助 AI 理解字段内容
field.sample_data = "['北京', '上海', '广州', '深圳']"
# AI 可以推断这是地区字段
索引信息:优化查询性能
field.is_index = True
field.index_count = 1000000 # 高基数,适合作为过滤条件
# AI 生成 SQL 时优先使用索引字段
可见性控制:隐藏敏感字段
field.visibility = False # 用户无法查询此字段
敏感信息标记:数据脱敏
field.identifiable_type = IdentifiableType.phone # 手机号
# 查询结果自动脱敏:138****5678
MetaAdmin 提供了强大的过滤功能,支持按名称、正则表达式过滤:
def filter_fields_by_names(
self, field_full_names: list[tuple[str, str, str]]
) -> "MetaAdmin":
"""
按字段全名过滤
field_full_names: [(schema_name, table_name, field_name), ...]
"""
filtered_meta = MetaAdmin(datasource_id=self.datasource_id)
for schema_name, table_name, field_name in field_full_names:
if schema := self.schemas.get(schema_name):
if table := schema.tables.get(table_name):
if field := table.fields.get(field_name):
# 创建 schema(如果不存在)
if schema_name not in filtered_meta.schemas:
filtered_schema = dataclasses.replace(schema, tables={})
filtered_meta.schemas[schema_name] = filtered_schema
else:
filtered_schema = filtered_meta.schemas[schema_name]
# 创建 table(如果不存在)
if table_name not in filtered_schema.tables:
filtered_table = dataclasses.replace(table, fields={})
filtered_schema.tables[table_name] = filtered_table
# 添加 field
filtered_schema.tables[table_name].fields[field_name] = field
return filtered_meta
应用场景:
# 向量检索返回相关字段
relevant_fields = [
("public", "orders", "order_id"),
("public", "orders", "amount"),
("public", "customers", "customer_name"),
]
# 只保留相关字段
filtered_meta = meta.filter_fields_by_names(relevant_fields)
# 传给 LLM 的元数据大幅减少
print(filtered_meta) # [ds_001] 1 S, 2 T, 3 F
def filter_tables_by_names(
self, table_full_names: list[tuple[str, str]]
) -> "MetaAdmin":
"""
按表全名过滤
table_full_names: [(schema_name, table_name), ...]
"""
filtered_meta = MetaAdmin(datasource_id=self.datasource_id)
for schema_name, table_name in table_full_names:
if schema := self.schemas.get(schema_name):
if table := schema.tables.get(table_name):
if schema_name not in filtered_meta.schemas:
filtered_schema = dataclasses.replace(schema, tables={})
filtered_meta.schemas[schema_name] = filtered_schema
else:
filtered_schema = filtered_meta.schemas[schema_name]
filtered_schema.tables[table_name] = table
return filtered_meta
应用场景:
# Agent 需要查看特定表的详细信息
tables_to_show = [
("public", "orders"),
("public", "order_items"),
]
filtered_meta = meta.filter_tables_by_names(tables_to_show)
# 返回完整的表结构(包含所有字段)
return filtered_meta.to_markdown()
def filter_by_regex(
self, schema_pattern=None, table_pattern=None, field_pattern=None
) -> "MetaAdmin":
"""
按正则表达式过滤
"""
filtered_meta = MetaAdmin(datasource_id=self.datasource_id)
schema_regex = re.compile(schema_pattern) if schema_pattern else None
table_regex = re.compile(table_pattern) if table_pattern else None
field_regex = re.compile(field_pattern) if field_pattern else None
for schema in self.schemas.values():
if schema_regex and not schema_regex.match(schema.name):
continue
filtered_schema = dataclasses.replace(schema, tables={})
schema_added = False
for table in schema.tables.values():
if table_regex and not table_regex.match(table.name):
continue
filtered_table = dataclasses.replace(table, fields={})
table_added = False
for field in table.fields.values():
if field_regex and not field_regex.match(field.name):
continue
filtered_table.fields[field.name] = field
table_added = True
if table_added:
filtered_schema.tables[table.name] = filtered_table
schema_added = True
if schema_added:
filtered_meta.schemas[schema.name] = filtered_schema
return filtered_meta
应用场景:
# 场景 1:只查询订单相关的表
order_meta = meta.filter_by_regex(table_pattern=r"^ord.*")
# 场景 2:只查询金额相关的字段
amount_meta = meta.filter_by_regex(field_pattern=r".*amt.*|.*amount.*|.*price.*")
# 场景 3:排除测试表
prod_meta = meta.filter_by_regex(table_pattern=r"^(?!test_).*")
支持多个 MetaAdmin 的合并和差集操作:
def merge_metas(allow_metas: list[MetaAdmin], deny_metas: list[MetaAdmin]) -> MetaAdmin:
"""
合并多个 allow_metas,并从结果中移除 deny_metas 中的字段
"""
result_meta = MetaAdmin()
# 合并所有 allow_metas
for allow_meta in allow_metas:
for schema_name, schema in allow_meta.schemas.items():
for table_name, table in schema.tables.items():
for field_name, field in table.fields.items():
add_field(result_meta, schema_name, table_name, field_name,
schema, table, field)
# 移除 deny_metas 中的字段
for deny_meta in deny_metas:
for schema_name, schema in deny_meta.schemas.items():
for table_name, table in schema.tables.items():
for field_name, field in table.fields.items():
remove_field_if_exists(result_meta, schema_name, table_name, field_name)
return result_meta
应用场景:
# 场景 1:权限控制
user_allowed_meta = get_user_allowed_meta(user_id) # 用户有权限的字段
user_denied_meta = get_user_denied_meta(user_id) # 用户被禁止的字段
final_meta = merge_metas([user_allowed_meta], [user_denied_meta])
# 场景 2:多角色合并
role1_meta = get_role_meta("sales") # 销售角色可见的字段
role2_meta = get_role_meta("marketing") # 市场角色可见的字段
combined_meta = merge_metas([role1_meta, role2_meta], [])
MetaAdmin 可以导出为 Markdown 格式,便于传给 LLM:
def to_markdown(
self,
level: Literal["table", "field"] = "field",
include: set[str] = set(),
exclude: set[str] = set(),
) -> str:
"""
转换为 Markdown 格式
level: "table" 只显示表名,"field" 显示字段详情
include: 额外包含的字段属性
exclude: 排除的字段属性
"""
# 构建数据结构
result = {
"schemas": [schema_to_dict(schema) for schema in self.schemas.values()],
}
# 转换为 Markdown
return dict_to_markdown(result, table_format_keys=("fields",))
输出示例:
## Schemas
### public
**Description:** 公共模式
#### Tables
##### orders
**Full Name:** public.orders
**Description:** 订单表,记录所有客户订单信息
**Fields:**
| name | desc | data_type |
|------|------|-----------|
| order_id | 订单ID,主键 | VARCHAR(32) |
| customer_id | 客户ID,关联客户表 | VARCHAR(32) |
| amount | 订单金额,单位:元,不含税 | DECIMAL(10,2) |
| status | 订单状态:1-待支付,2-已支付,3-已完成 | TINYINT |
| created_at | 订单创建时间 | DATETIME |
##### order_items
**Full Name:** public.order_items
**Description:** 订单明细表,记录订单中的商品信息
**Fields:**
| name | desc | data_type |
|------|------|-----------|
| item_id | 明细ID,主键 | VARCHAR(32) |
| order_id | 订单ID,关联订单表 | VARCHAR(32) |
| product_id | 商品ID,关联商品表 | VARCHAR(32) |
| quantity | 商品数量 | INT |
| price | 商品单价,单位:元 | DECIMAL(10,2) |
优势:
需求:销售部门只能查询自己负责地区的订单数据
实现:
# 1. 获取用户角色的元数据
role = get_user_role(user_id)
accessible_meta = role.get_accessible_meta(datasource)
# 2. 过滤可见字段
for schema in accessible_meta.schemas.values():
for table in schema.tables.values():
# 移除不可见字段
fields_to_remove = [
field_name
for field_name, field in table.fields.items()
if not field.visibility
]
for field_name in fields_to_remove:
table.fields.pop(field_name)
# 3. 传给 Agent
agent = DBAgent(datasource=datasource, meta=accessible_meta, assumed_role=role)
效果:
需求:客服人员可以查询用户信息,但手机号和身份证号需要脱敏
实现:
# 1. 标记敏感字段
user_table.fields["phone"].identifiable_type = IdentifiableType.phone
user_table.fields["id_card"].identifiable_type = IdentifiableType.id_card
# 2. 查询时自动脱敏
result = datasource.accessor.query("SELECT * FROM users LIMIT 10")
for row in result:
for field_name, field in user_table.fields.items():
if field.identifiable_type == IdentifiableType.phone:
row[field_name] = mask_phone(row[field_name]) # 138****5678
elif field.identifiable_type == IdentifiableType.id_card:
row[field_name] = mask_id_card(row[field_name]) # 110***********1234
需求:自动生成字段描述,提升元数据质量
实现:
async def enhance_metadata(meta: MetaAdmin) -> MetaAdmin:
"""使用 AI 增强元数据描述"""
for schema in meta.schemas.values():
for table in schema.tables.values():
# 生成表描述
if not table.curr_desc or table.curr_desc == table.name:
table.curr_desc = await generate_table_description(table)
table.curr_desc_stat = "ai_generated"
for field in table.fields.values():
# 生成字段描述
if not field.curr_desc or field.curr_desc == field.name:
field.curr_desc = await generate_field_description(
table, field, field.sample_data
)
field.curr_desc_stat = "ai_generated"
return meta
async def generate_field_description(table: Table, field: Field, sample_data: str) -> str:
"""使用 LLM 生成字段描述"""
prompt = f"""
表名:{table.name}
表描述:{table.curr_desc}
字段名:{field.name}
数据类型:{field.data_type}
示例数据:{sample_data}
请生成一个简洁的字段描述(不超过 50 字),说明:
1. 字段的业务含义
2. 数据的单位(如果有)
3. 特殊说明(如枚举值含义)
"""
response = await llm_client.create_completion(
messages=[{"role": "user", "content": prompt}],
model="gpt-4o-mini"
)
return response.choices[0].message.content.strip()
效果:
class MetaAdmin:
_lazy_loaded: bool = False
def ensure_loaded(self):
"""延迟加载元数据"""
if not self._lazy_loaded:
self._load_from_database()
self._lazy_loaded = True
def _load_from_database(self):
"""从数据库加载元数据"""
# 只加载必要的信息
# 详细信息按需加载
pass
def update_field_description(
self,
schema_name: str,
table_name: str,
field_name: str,
new_desc: str
):
"""增量更新字段描述"""
field = self.schemas[schema_name].tables[table_name].fields[field_name]
field.curr_desc = new_desc
field.curr_desc_stat = "manual"
field.is_vector_synced = False # 标记需要重新同步到向量库
# 只更新这一个字段,不重建整个元数据
await update_field_vector(field)
class MetaCache:
_cache: dict[str, MetaAdmin] = {}
_ttl: int = 3600 # 1 小时
@classmethod
def get(cls, datasource_id: str) -> MetaAdmin | None:
cache_key = f"meta:{datasource_id}"
if cache_key in cls._cache:
meta, timestamp = cls._cache[cache_key]
if time.time() - timestamp < cls._ttl:
return meta
return None
@classmethod
def set(cls, datasource_id: str, meta: MetaAdmin):
cache_key = f"meta:{datasource_id}"
cls._cache[cache_key] = (meta, time.time())
表描述:
# ✅ 好的描述
table.curr_desc = "订单表,记录所有客户订单信息,包括订单金额、状态、创建时间等"
# ❌ 差的描述
table.curr_desc = "orders" # 没有业务含义
字段描述:
# ✅ 好的描述
field.curr_desc = "订单金额,单位:元,不含税,不含运费"
# ❌ 差的描述
field.curr_desc = "金额" # 信息不足
# 手机号
field.identifiable_type = IdentifiableType.phone
# 身份证号
field.identifiable_type = IdentifiableType.id_card
# 邮箱
field.identifiable_type = IdentifiableType.email
# 银行卡号
field.identifiable_type = IdentifiableType.bank_card
# 隐藏内部字段
field.visibility = False
# 隐藏测试表
table.visibility = False
# 隐藏废弃字段
field.visibility = False
field.curr_desc = f"{field.curr_desc}(已废弃)"
AskTable 的 MetaAdmin 语义层通过分层元数据管理、动态过滤和权限控制,让 AI 真正理解企业数据:
语义层是 Text-to-SQL 系统的基础,高质量的元数据直接决定了 SQL 生成的准确性。通过 MetaAdmin,AskTable 实现了技术元数据和业务语义的完美结合。