上一章把第一个落地点定在了只读问答,选了一张字段稳定、查询量大的表。表选好之后会立刻撞上一个更基础的问题:模型不认识这张表。老库的字段名是 st、flag、ref_id 这类缩写,模型猜不出哪个是订单状态、哪个是删除标记、哪个是外键。你让它「查一下这个客户最近的订单」,它连「客户」对应哪一列都不知道。
判断放开头:这一章解决的不是模型会不会写 SQL,而是模型读不读得懂你的表。而读懂这件事,靠的是一次性把字段含义翻译成模型能消费的文本,不需要动数据、不需要改表结构——这就是「补一层语义」。
语义层补什么
语义层是旧库外面的一层描述,一共三样东西:字段字典、枚举映射、注入方式。字段字典回答「每一列是什么意思」;枚举映射回答「这一列里的 1、2、3 分别代表什么状态」;注入方式回答「这些描述怎么在每次请求时塞进模型的上下文」。
要特别强调它补的是「解释」,不是「数据」。旧库里的数据原样不动,字段名原样不动,触发器和外键也原样不动。你只是在查询入口处多拼一段文本,让模型在生成 SQL 之前先读到这些说明。这一层的边界就在这里:它让模型更懂数据,但改变不了数据本身的质量。脏数据、断外键、NULL 满天飞,这些是数据治理的活,语义层一概不碰。把边界划清楚,后面才不会越做越乱。
字段字典
字段字典是这张表每一列的说明,写成模型一眼能看懂的短句。老系统里最容易坑人的是缩写和 NULL 语义,所以字典里要特别标出这两类。下面是一个最小字段字典,放在 Python 里,跟上一章的只读工具配套:
# semantic_layer.py —— 在旧库外补一层字段语义
FIELD_DICT = {
"orders": {
"desc": "订单主表,一条记录是一次下单",
"columns": {
"id": "订单号,主键,自增",
"st": "订单状态枚举,见 STATUS_MAP",
"cust": "客户 ID,关联 customers.id",
"amt": "订单金额,单位分,可能为 NULL 表示未支付",
"flag": "删除标记,1 表示已软删除,查询时必须过滤 flag=0",
},
}
}
STATUS_MAP = {
"orders.st": {"1": "待支付", "2": "已支付", "3": "退款中", "4": "已取消"},
}
字典里最关键的两行是 amt 的「单位分、可能为 NULL」和 flag 的「软删除过滤」。这两条不写清楚,模型算出来的金额会差 100 倍,还会把已经删掉的订单当成有效数据读出来。第一版字典不用求全,先把会答错的那几列标出来,比列出一百列没用的注释强得多。等问答跑起来、看到具体答错案例,再回头补字典,比一上来就想穷尽所有字段实际得多。
枚举映射
枚举映射单独拿出来说,是因为它是语义层里错误率最高的一块。上一章提过字段漂移:同一个 st 在不同历史时期含义可能不一样,早期 3 表示「取消」,后来改成「退款中」。你在枚举映射里写的是「当前」的含义,模型按这个去理解全部历史数据,就一定会读错一部分。
所以枚举映射要带一句时间说明,把漂移这件事摊给模型看,让它回答时主动标注「按当前枚举理解,历史值可能有歧义」,而不是拍胸脯给结论。映射本身保持现状,不替你去改历史数据——真要修漂移是数据治理的事,不属于这一章的范围。这一步看着多余,其实是在给模型划诚实边界:宁可让它说不确定,也不要让它用一套枚举硬套十年数据。
把语义喂给模型
字段字典和枚举映射都只是数据,真正起作用的是注入这一步。做法是把它们拼进系统提示词,每次请求都带上,让模型生成查询前先读过这些说明:
def build_prompt(user_query: str) -> str:
lines = ["你是一个只读查询助手,下面是 orders 表的字段说明:"]
for name, info in FIELD_DICT["orders"]["columns"].items():
lines.append(f"- {name}: {info}")
lines.append("状态枚举含义:")
for key, val in STATUS_MAP.items():
lines.append(f"- {key}: {val}")
lines.append("注意:flag=1 是软删除,查询必须过滤;amt 单位是分。")
lines.append("历史数据的枚举值可能有歧义,回答时请标注。")
lines.append(f"用户问题:{user_query}")
return "\n".join(lines)
这段注入跟上一章 READONLY_PREFIX 白名单是配套的:白名单管住「只读」,语义层管住「读得懂」。一个防写坏,一个防读错,两件事都要在入口处做,不要指望模型自己守规矩。
数据不搬家的取舍
不搬家、不改表,换来的是上线快、回滚快、不动主链路,代价是能力有上限:语义层只能让模型「读得懂」,改不了数据本身的脏乱差。字段名是拼音缩写,字典能翻;但字段里存的是乱码、外键断了、NULL 满天飞,字典补不了,那得先清洗数据。
所以语义层是第一步,不是最后一步。它解决「模型理解数据」的问题,把数据质量问题单独隔离出来,让你能分阶段处理:先用字典让问答跑起来,再回头去治理那些脏数据。反过来,如果一开始就想着「先把数据搬干净再上 AI」,这个项目大概率死在迁移里——迁移本身就是几个月的大工程,等你搬完,业务的耐心和预算早就耗尽了。
下一章
语义层补好之后,模型能读得懂表了,但「读得懂」不等于「答得对」。同一个问题,模型可能答对,也可能一本正经地胡说。下一章解决它——给这个只读问答接一套能自动判断答案对错的评测,不用人工逐条看。