> 系列:VeapAI 自然语言操作实战(全体系架构专题)
> 本文要点:表语义 · 操作语义 · 字段结构 · 术语与范围;每个构件一个唯一事实来源
## 一句"查启用的角色",模型要猜三件事
"查一下状态为启用的角色",这句话人听了毫不费力,模型拿到却要先猜三件事。
第一,角色是哪张表,`veap_role` 还是 `view_user_role`?第二,状态是哪个字段,`status` 还是 `del_state`?第三,"启用"翻译成什么值,1 还是"0"?任何一次猜错,结果都不可信。
语义层的全部工作,就是把这些猜测提前变成事实。它由四个构件组成,每个构件占用平台里一个早已存在的"唯一事实来源"。
![语义层总览:表语义、操作语义、字段结构、术语与范围四个构件,各自的事实来源与消费方]
## 表语义与操作语义:两份"清单"
先看前两个构件,它们回答的是"系统里有什么"。
表语义来自数据字典。`veap_dict_table` 与 `veap_dict_field` 登记了每张表的编码、标题、字段与查询配置。对外只需要一个端点:
`POST /system/veapDictTable/listFieldMetas?tableCode=<表码>`
它返回 8 个键的最小集:
- 字段信息:`fieldCode`、`title`、`desp`、`fieldType`、`dbType`;
- 查询配置快照:`isQuery`、`queryType`、`javaField`。
键的数量被刻意控制——多出来的字段会让下游脚本的断言失去意义。
操作语义来自在线开发的生成结果。`veap_block_func` 与 `veap_block_class` 关联出每张表的接口清单:
`POST /blockgen/veapBlockFunc/listGeneratedApis?tableCode=<表码>`
返回 6 个键:`id`、`title`、`reqUrl`、`reqType`、`apiType`、`apiKind`。这里有两个精确取值集合值得记住。
`apiType` 由生成期声明映射而来:新增、编辑、删除、导入、导出、授权、自定义。`apiKind` 更细一层:list、info、dropdown、add、edit、delete 等。选接口时按这两个字段精确匹配用途,"删一个用户"永远命中 delete,而不是靠标题里哪个词看起来像。
## 字段结构:schema_json 是给机器看的表单
第三个构件是元数据标准,回答"知识条目长什么样"。
`ai_metadata_standard` 管层级与编码,`ai_metadata_field` 管字段本身。每个字段带一段 `schema_json`,表注释写得很直白:
> 字段JSON,Schema定义,支持复杂校验、前端动态表单,schema_json字段存储完整JSON Schema片段,支持:
> 类型、枚举、正则、范围、依赖、嵌套对象/数组等;
> 支持 required、default、pattern、min/max、format 等属性。
也就是说,字段结构不是写死的表单配置,而是一份三方共读的契约。前端据此渲染动态表单,加工链路据此校验模型输出,评测据此生成用例。
这里有一个取舍值得说。早期版本有“标准模板”机制,先建模板再套标准。后来我们删掉了模板表,换成“标准复制”。入口是 `POST /metadataStandard/copyAiMetadataStandard`。传入 `{sourceId, code, title}`,事务内把源标准的字段整份深拷贝出来。
理由很简单:模板与标准是两层定义,字段到底以谁为准总在扯皮。删掉一层之后,字段定义的唯一来源只剩 `ai_metadata_field`;复制出来的标准是独立快照,改哪个都不牵连另一个。
![元数据标准与字段:标准层级、字段列表与 schema_json 在这里维护]
## 术语与范围:主题绑定标准,应用绑定主题
第四个构件解决"术语归哪里、召回查哪里"。
知识主题表 `ai_knowledge_topic` 有一个 `standard_code` 字段,注释就一句话:元数据标准编码。它把"术语容器"和"字段结构"扣在一起——某个主题下的知识条目,字段必须符合对应标准。
![知识主题:standard_code 把主题与字段结构绑定在一起]
应用侧则通过 `ai_app_topic_knowledge_config` 声明召回范围。这张表的表头注释值得逐句读:
> 本表是 AiQuestion/通用在线知识问答场景的知识检索范围与召回参数唯一事实来源。
> query_limit 与 min_score 同属检索参数,必须由本表统一管理;禁止在 ai_app_service.service_inst_conf 中重复存储。
> enabled=0 的记录不得参与问答召回;sort 只表示追加顺序,不表示启停状态。
字段注释同样把话说死:`query_limit` 必须大于 0 且不超过 10;`min_score` 为空表示不启用;`prepend_title` 必须非空;另有 `weight_factor` 与 `permission_factor` 两个系数。
一句话总结这张表的设计意图:召回不是"大概取几条",而是每个参数都有出处、有边界、有唯一归属。
![应用知识配置:知识主题的召回条数、阈值、权重与权限系数都在这里声明]
## 四条链一眼看全
把四个构件连起来看,语义层其实是四条链:
![四条语义链:表语义、操作语义、知识语义、召回范围四条链的事实来源、链路与消费方,引用一律精确匹配]
- 表语义链:系统表结构 → 数据表字典 → 字段清单(8 键) → 字段校验消费;
- 操作语义链:生成代码 → 接口关联 → 接口清单(6 键) → 执行计划选接口;
- 知识语义链:业务字典 → 语义模板 → 知识条目 → 上下文资源(LLM 条件翻译);
- 召回范围链:应用服务(topic_id)→ 应用绑定 → 知识主题(standard_code)→ 向量化任务(standard_id)。
四条链共享同一个规矩:每条链只有一个事实来源,引用一律精确匹配。
## 源码走读:每条链的落点
- 字典与字段清单:`veap-cloud/veap-modules/veap-system` 的 `VeapDictTableController#listFieldMetas`,表码解析实现是 `selectActiveVeapDictTableByCode`。
- 接口清单:`veap-cloud/veap-modules/veap-blockgen` 的 `VeapBlockFuncController#listGeneratedApis`,返回对象 `VeapBlockFuncApiMetaVO` 定义了 6 键契约。
- 元数据标准:`veap-cloud/veap-modules/veap-ai` 的 `AiMetadataStandardController`(含 `copyAiMetadataStandard`)与 `AiMetadataFieldController`。
- 主题与绑定:`AiKnowledgeTopicController`、`AiAppTopicKnowledgeConfigController`。
- 上下文资源:`AiAppContextResourceController` 与知识同步服务(主题 code → 资源,正文按序合并)。
## 较真:一次"近似表"引发的字段清单错位
语义层的稳定性,靠的是一次事故换来的。
有一轮测试,某些命令的字段校验会偶发地把合法字段判为“未知字段”。排查到最后,问题出在表码解析。列表页的查询实现用的是模糊匹配,`table_code like '%...%'`。而系统里恰好有 `veap_role` 和 `veap_role_user` 这样的前缀近似表,模糊匹配先命中了错误的那张。字段清单自然整份错位。
修法不复杂:为表码解析新增一个精确匹配查询,条件为 `table_code` 精确匹配且 `del_state=0`。同时把"禁止复用模糊匹配的列表查询做表码解析"写进了实现契约。现在这个约束就写在 `VeapDictTableController#listFieldMetas` 的注释里,谁改这段代码之前都会先看到它。
教训也很通用:当"查一下"和"猜一下"共用一个实现时,出错的一定是语义。列表页要模糊,语义层要精确,这是两种需求,不能凑合。
## 复现与验证
三个动作即可验证这一层:
1. 字段清单:`POST /system/veapDictTable/listFieldMetas?tableCode=veap_role`,确认返回 8 键集合;
2. 接口清单:`POST /blockgen/veapBlockFunc/listGeneratedApis?tableCode=veap_role`,确认每个接口都带 `apiType` 与 `apiKind`;
3. 召回范围:`SELECT enabled, query_limit, min_score FROM ai_app_topic_knowledge_config WHERE app_topic_id = <应用主题id>;`,核对参数是否落在边界内。
顺带可以验证"精确"这件事:把表码改成 `veap_role_u`(不存在的近似值),接口返回空数组,而不是给你一张别的表的数据。
语义层的边界也说清楚:它只描述系统,不修改系统。字典、接口、标准、绑定的写入都有各自的管理页面与权限,语义层在运行期全程只读。
事实摆好了,下一个问题自然出现:这些"事实"从哪里来,又怎么持续保鲜。