系列《VeapAI 自然语言操作实战》本篇为系列总览。 文中涉及的表名、字段名、枚举值与接口路径均已对照仓库 veap-cloud/DB/veap.sql 与源码核对代码路径可直接在仓库中定位。## 一、起因一次「查个数据要提单」上个月同事在群里问我「查一下状态为启用的用户为什么还要提单排期」我打开那套系统看了看。左边是固定菜单右边是固定查询表单想加一个「按状态查用户」的入口要走需求评审、排期、开发、测试、发版。查一次数据的时间成本比数据本身贵得多。这种情况在存量信息化系统里很普遍。系统建的时候是按功能点设计的菜单是给「知道自己要干什么」的人用的。可业务同事的真实需求往往是一句大白话「帮我看看张三的账号现在是什么状态。」我们想让这类系统多一种操作方式用自然语言说需求系统自己去找表、找接口、找字段执行完把结果说清楚。真正动手才发现卡点不在大模型而在三处断链1. **表语义断了**模型不知道 veap_user 表的 status 字段里1 代表启用还是停用2. **接口语义断了**模型不知道哪个接口是查列表、哪个是改数据3. **知识语义断了**模型不知道用户口中的「启用」在系统里对应哪个存储值。三处任何一处缺了命令都落不到实处。下面这套设计本质上就是把这三处断链接上再用一条可视化编排的链路把它们串起来。平台上这个流程叫「全局语义命令体系」是项目内部术语。为了对外好理解本文统称它为**自然语言操作链路**。先看一眼这条链路的样子——左边是命令文本这一个唯一入口中间是五段判断右边三层语义各管一段事实![自然语言操作链路全景三层语义 一个编排]## 二、一条命令要走完的五个阶段链路里定义了一组稳定的阶段标识stageCode命令每往前走一步都会把阶段和摘要写进执行日志与运行上下文。取值集合是九个textintent | locate | plan | execute | present | guide | none | confirm | canceled前五个是主链阶段后四个是引导、不可识别、待确认、已取消这几种分支状态。| 阶段 | stageCode | 谁完成 | 产出 || --- | --- | --- | --- || 意图识别 | intent | 意图识别节点 意图校验脚本 | intentJson意图、复杂度、操作类型、匹配状态、路由键 || 表定位 | locate | 表定位节点 定位校验脚本 | targetTableCode、targetTableTitle、pkField、titleField || 计划 | plan | 执行计划节点 计划校验与组装脚本 | 过滤条件、排序、条数上限、聚合、分组、接口选择、载荷 || 执行 | execute | 列表查询节点 或 结构化查询节点 | resultJson || 呈现 | present | 结果呈现节点 | finalMessageMarkdown 正文 |意图识别的取值集合是固定的。意图五选一query | stat | analyze | operate | guide复杂度二选一simple | complex。操作类型四选一add | edit | delete | grant匹配状态三选一certain | ambiguous | none。这些值直接决定后面走哪条分支所以脚本会逐项强校验不在集合内就报错。整条链路的规模是实打实的**49 个节点、58 条连线**。用到的节点类型取自一个 19 项的固定集合- 流程骨架start、end、condition、loop、var-merge- 模型与接口llm、http、private-http- 逻辑与输出script、reply- 文件能力file-read、file-write、file-directory-read、file-download、pdf-write- 数据与交互mysql、oracle、form-dialog、list-dialog这些类型注册在一处用了未注册的类型会直接报错。流程定义存在 ai_agent_process 表里关键字段如下注释原文来自建表语句| 字段 | 类型 | 注释原文 | 我们的用法 || --- | --- | --- | --- || process_type | varchar(32) | 流程类型唯一取值 agent_flow | 默认值就是 agent_flow || definition_json | text | 图编排定义 JSONnodes/edges/definition_info 合并 | 49 个节点与 58 条边都在这里 || start_button_label | varchar(32) | 启动按钮名称宽度8ASCII1/非ASCII2 | 保存时校验宽度超了不放行 || allow_edit / max_usage / used_count | tinyint/int | 0 时 max_usage 必须 0 | 控制使用次数创建实例时事务内递增 |表上两个索引 idx_tenant_service、idx_tenant_del 也说明了设计取向这个平台是按租户隔离的所有查询都从租户维度切进去。## 三、三层语义不猜只查这套链路里有一条贯穿始终的原则**禁止兜底推断只做显式声明**。落到实现上就是三类数据各有一个唯一事实来源。### 3.1 数据语义数据表字典veap_dict_table 与 veap_dict_field 是表语义的唯一事实来源。前者的关键字段| 字段 | 注释原文 | 说明 || --- | --- | --- || table_code | 表名称默认自动生成 | 表编码AI 侧一切引用的起点 || title | 表标题数据表标题 | 用户口语里的「用户」「角色」靠它对齐 || pk_field | 主键字段 | 写操作定位记录用 || pid_field | 父级字段 | 树形结构识别 || state_field | 状态字段名称 | 状态类命令的落点 || del_field | 删除字段名称 | 逻辑删除口径 || sort_field | 排序字段名称 | 「最近创建的」这类命令的落点 || module_code | 表所属模块默认 sys 系统模块通过数据字典配置 | 功能引导时按模块归类 || status / del_state / sort | 有效状态 / 删除状态 / 显示次序 | 目录资源只取 status1 且 del_state0按 sort 排序 |字段表 veap_dict_field 里AI 侧真正关心的是这些| 字段 | 注释原文 | 为什么重要 || --- | --- | --- || field_code | 字段名称 | 计划里的字段引用必须命中它 || db_type | 数据库类型 | 判断能不能承载非数字值 || is_indexed | 是否索引 | 排查慢查询时的第一手线索 || parent_id | 上级字段主键支持多层嵌套 | 结构化字段的层级 || json_path | JSON/ARRAY字段路径如address.city、items[].name或items[0].name | 非平铺字段的取值路径 || schema_json | JSON/ARRAY字段结构描述JSON Schema | 驱动前端动态表单 |这里有个容易踩的坑值得先讲清楚**「这个字段能不能用来查」不在字典表里**。是否显示为查询项、支持哪种查询方式等值、模糊、范围来自在线开发的生成器快照 veap_generate_field。AI 侧拿到的字段清单由端点 POST /system/veapDictTable/listFieldMetas 返回。它会左关联最新一次生成记录的字段快照返回八个键- fieldCode、title、desp、fieldType字段基本信息- dbType数据库类型- isQuery、queryType是否作为查询项、查询方式- javaField列表接口请求体里的属性名这个划分是刻意的字典表管「这字段是什么」生成器快照管「这字段能怎么用」。两者混在一起后面就没法回答「为什么这个条件查不出来」。### 3.2 接口语义生成的接口清单模型不能靠接口标题猜用途。我们用的是积木库的全量 Java 扫描结果。AI 侧拿清单的端点按表编码返回最小集清单POST /blockgen/veapBlockFunc/listGeneratedApis。每个元素五个键id、title、reqUrl、reqType、apiType。其中 apiType 是四个值之一**新增、编辑、删除、自定义**。「自定义」就是列表查询接口。计划校验脚本按 apiType 精确匹配挑接口比如新增操作必须命中 apiType新增 的那条禁止按标题文本猜。匹配口径也能查到表编码先经驼峰转换得到类名称再关联 veap_block_class 找到对应控制器。这段查询落在 veap-cloud/veap-modules/veap-blockgen/ 的 VeapBlockFuncMapper.xml 里。不是散在代码注释里的口头约定。### 3.3 知识语义知识主题与上下文资源用户说的「启用」「停用」得翻译成存储值 1 和 0。这类翻译不能靠模型自由发挥我们把它做成了**离线预构建的静态资源**一共三类| 资源 | 知识主题编码 | 一行正文长什么样 || --- | --- | --- || 表目录 | agent.globalCmd.catalog | 表编码veap_user / 表标题用户 || 表关系 | agent.globalCmd.relations | 在表目录基础上多出 主键字段id / 标题字段nick_name || 字典翻译 | agent.globalCmd.dictMap | 字典用户状态sys_user_status / 选项启用 → 1 |它们经过一条固定的三步链产出语义模板任务取数渲染成知识条目知识主题把条目按 sort 合并成上下文资源最后挂到具体节点上注入提示词。涉及的表是 ai_knowledge_topic 与 ai_app_context_resource。后者有个唯一索引值得单独说sqlUNIQUE INDEX uk_owner_key(owner_user_id, res_key)资源键 res_key 就等于知识主题的 code配合归属用户构成唯一约束。这意味着同一份资源不会因为多跑一次同步任务而出现两份重复执行是幂等的。到这里三层语义各就各位**字典管字段语义生成器管可用能力知识主题管业务术语**。## 四、编排本身也是配置出来的链路不是写死在代码里的它是一张画布上的图。打开流程定义页49 个节点分成几类- 负责理解意图识别、表定位、执行计划、授权对象解析- 负责校验意图校验、定位校验、字段归一化、接口归一化、计划校验与组装- 负责执行列表查询、结构化查询、写接口- 负责交互新增确认、修改确认、删除确认、授权确认- 负责收尾结果呈现、分析报告、结果消息节点是有类型的类型从一个固定集合里取注册点在 NodeRegistry。用了未注册的类型执行时直接抛技术性错误不会静默跳过。节点上的「入参说明 / 出参说明 / 补充说明」也不是随手写的文案。这套说明有统一契约标题右侧一个信息提示符内容固定三节布局文案集中在十个模块里渲染落点逐个对应到面板组件上。改一处文案改的是模块不是复制粘贴。写操作这条线最能体现「配置出来」的意思。新增、修改、删除、授权四条子链都按同一个节奏走。先按定位条件查目标行把结果整理成统一的三键行行主键、行标题、原始 JSON弹出确认列表让用户看一眼。确认之后才调写接口。确认弹窗不是摆设。它对应的是一条明确的约束**写操作必须经人工确认后才执行取消就是取消**。## 五、几个较真的取舍### 5.1 为什么表目录要离线做最早的做法是运行期拉取命令进来先调接口把表目录查出来喂给模型。跑通之后我们把它拆了。原因很简单表字典和字典项都是平台级静态事实一天变不了几次而每次命令都要为它付一次接口耗时。更重要的是静态化的内容可以离线评审——哪些表该进目录、字段描述写没写清楚摊开就能看。换来的是刷新要跑两步执行语义模板任务、同步知识主题。这个代价我们认也如实写进了使用说明表字典或字典项变更后需要重跑运行期只读资源。不是所有数据都适合静态化。字段清单和接口清单就留在运行期因为它们的输入是命令里动态解析出来的表编码。这条判断标准后来成了团队内部的一句话只有「平台级静态事实 无脚本校验依赖」的数据才静态化。### 5.2 一次真实的覆盖事故有一版链路两个并行节点各自去拉字段清单和接口清单。拉取本身没问题问题是两个节点的输出都写进了引擎的固定键 data。引擎的规则是后完成的覆盖先完成的。字段清单先回接口清单后回于是字段归一化脚本读到的是接口清单。下一段计划校验报错「执行计划引用未知字段status」。那一轮所有查询命令全断报错信息指向字段根因却在键名。修法是把并行段两个节点的输出声明改成专属键fieldMetasData 和 apiMetasData脚本按专属键读取禁止读 data。规则也跟着写进契约同源并行段的节点不允许共用固定键。这件事之后我们的判断更明确了**并行省时间但并行段的输出契约必须显式声明**。### 5.3 宁可空结果也不猜系统里有一条不那么讨喜的规矩匹配不到就如实说匹配不到。命令落不到任何表上直接给可读提示后中断不去猜最像的那张表。字段清单为空判定为「目标表不在数据表目录或未登记字段」提示去补字典不进下一步。过滤字段没配查询方式明确报「生成列表接口不会生成该字段的过滤条件」而不是静默降级成全表查询。这条规矩的代价是前期配置要做扎实。好处也很直接出问题时错误信息指向要修的那一步而不是让模型编一个看起来合理的答案。### 5.4 性能这件事我们只写机制对外讲性能容易滑向数字游戏。这套链路里能拿出来的都是机制和限额不是倍数- **并发**引擎每轮调度「所有依赖已满足的节点」没有依赖关系的链自然并行。字段清单和接口清单就是一段同源并行段。- **批量**节点实例的初始化是一次请求批量写入Mapper 固定 14 列多行 VALUES主键与返回顺序严格对齐数量不符直接抛错。- **缓存**知识主题检索有三层缓存主题映射、集合映射、缓存水位检索结果缓存 10 分钟查询向量缓存 24 小时。- **硬顶**结果行数硬顶 200 行注入模型用于呈现的数据不超过 50 行超出就截断并在正文里注明。- **幂等**向量对象映射用内容 hash 加嵌入模型 ID 判断要不要重算内容没变就不重算。不写「提升多少倍」是因为我们没做过拿得出手的压测。宁可少写一句也不要编一个数字。## 六、想跑起来要几步代码在 Giteehttps://gitee.com/mindock/veap。环境需要 JDK 17、Maven 3.6、MySQL 8.x、Redis以及可选的 Milvus。后端按单体模式打包bashgit clone https://gitee.com/mindock/veapcd veap-cloudmvn -B -pl veap-package/veap-monolith -am -DskipTests package前端bashcd veap-uinpm installnpm run dev起来之后大致三步在数据表字典里登记表与字段在流程定义页打开画布看这张图然后构建三类语义资源表目录、表关系、字典翻译并挂到节点上。跑通之后一条命令的结果和结论弹窗长这样![一条命令的运行结果与结论弹窗]源代码里可以直接找到这几处- veap-ui/src/views/ai/agentDesign/画布编排器节点面板、连线与启动参数- veap-ui/src/components/AiAgentEngine/执行器节点处理器、调度与状态集合- veap-cloud/veap-modules/veap-blockgen/接口扫描与接口清单查询- veap-cloud/veap-modules/veap-ai/语义模板、上下文资源与运行数据落库- veap-cloud/DB/veap.sql全部建表语句ai_agent_ 开头的表族## 写在最后这套东西里没有魔法。它做的事很朴素把表语义、接口语义、业务术语整理成机器可查的清单。然后用一张图把「理解 → 定位 → 计划 → 执行 → 呈现」串起来每一步都强校验写操作都让用户确认一次。它也有明确的边界当前一个命令执行一次不做多轮追问结构化查询只开放读能力写操作一律走在线生成的接口靠既有权限体系兜住语义资源的刷新是离线的两步不是实时的。如果你手上的系统也在被「改个查询要排期」困扰这套链路值得看一眼画布上那 49 个节点是怎么连的。它的价值不在节点数量在于每个节点都只干一件事并且干完就校验一次。觉得哪里绕、哪里可以更省欢迎到仓库提 issue我们更想知道它在你的业务里卡在哪一步。