知识主题 + Milvus:把业务术语喂给模型的三步离线构建

发布时间:2026/10/14 3:22:31
知识主题 + Milvus:把业务术语喂给模型的三步离线构建 系列《VeapAI 自然语言操作实战》第 3 篇核心篇 相关源码veap-cloud/veap-modules/veap-ai/src/main/java/com/veap/ai/semanticTemplate/语义模板执行与渲染、veap-cloud/veap-modules/veap-job/定时任务、veap-cloud/DB/veap.sql知识主题、知识条目、上下文资源等表## 一、模型的第三个盲区业务黑话前面讲过表语义和接口语义。这两层补齐之后模型知道该查哪张表、该调哪个接口、该用哪些字段。但它还是会在一个地方翻车**值**。用户说「查一下状态为启用的用户」模型很自然地会去查 status 启用 或者 status enabled。而系统里存的是 1。这不是模型笨。它没有别的信息渠道只能按照训练时见过的模式猜。按理说这个也能靠提示词解决——在提示词里把这个字段的字典项列出来就行。问题是字典项有多少用户状态、角色类型、流程状态、单据类型、审核结果……一张表几个几十张表就是几百条。全塞进提示词token 成本先不说模型对中间部分的注意力也会衰减。我们最后选的做法是**把这些术语关系提前固化成一份可挂载的资源离线产出运行期只读。**## 二、三类资源三个知识主题需要提前固定的东西有三类对应三个知识主题| 资源 | 知识主题编码 | 每行长什么样 || --- | --- | --- || 表目录 | agent.globalCmd.catalog | 表编码veap_user 、表标题用户 || 表关系 | agent.globalCmd.relations | 表编码veap_user、表标题用户、主键字段id、标题字段nick_name || 字典翻译 | agent.globalCmd.dictMap | 字典用户状态sys_user_status、选项启用 → 1 |三类资源各挂到不同节点上- 表目录与表关系挂给表定位节点。前者告诉模型「平台里有哪些表」后者告诉它「这张表用什么做主键、用什么做标题」。有了这两条像「删除编码为 temp01 的角色」这种命令模型才能回传正确的编码字段与标题字段。- 字典翻译挂给执行计划节点。它的唯一用途就是值翻译——把「启用」翻译成 1把「停用」翻译成 0。提示词里写得很死状态类字段的条件值必须按这份清单翻译成存储值。三份资源的内容都是纯文本一行一条没有嵌套结构。这是刻意的模型对扁平行文本的引用准确率明显好过对深层 JSON 的引用。而且扁平行便于人工评审——打开资源内容从上到下扫一遍谁的标题没写清楚一眼能看出来。三个知识主题在平台上长这样![知识主题列表三个平台级主题]## 三、三步构建链全部用既有能力不写新代码这三份资源不是手工维护的它们由一条固定的三步链产出。整条链复用的是平台已有的语义模板能力没有为它新增任何后端代码。![语义资源三步构建链模板任务 → 知识主题 → 上下文资源 → 节点挂载]**第一步生成模板。**平台有「语义模板」这个能力一条取数语句加一个渲染模板逐行渲染出知识条目。我们为三类资源各配了一份模板取数口径如下| 资源 | 取数口径 | 渲染模板 || --- | --- | --- || 表目录 | veap_dict_tablestatus1、del_state0按 sort 排序 | 表编码{{tableCode}} 换行 表标题{{title}} || 表关系 | 同上外加主键字段非空 | 表目录基础上再加 主键字段{{pkField}}、标题字段{{titleField}} || 字典翻译 | veap_dict_option 左连接 veap_dict_type选项有效且存储值非空 | 字典{{dictTitle}}{{dictCode}} 换行 选项{{optionLabel}} → {{optionValue}} |三个语义模板的编码分别是 globalCmdCatalog、globalCmdRelations、globalCmdDictMap与知识主题的编码配成一对。三份语义模板在平台上的配置是这样的![语义模板任务取数语句与渲染模板]顺便说一句取数语句的四个稳定字段。语义模板执行时会自动带上四个字段业务主键、来源类型、来源信息、标题。前三个用来做条目的幂等定位——**同一张表反复执行不会产生重复条目**因为业务主键是唯一锚点。这个设计比「先清空再插入」稳得多中途失败也不会留下半份残数据。**第二步执行模板。**每个模板执行一次流程是按取数语句拿数据 → 逐行渲染模板 → 写入知识条目。执行过程全程留痕落在 ai_semantic_transform_log 表里。入口有两个人工和定时都覆盖textPOST /ai/semanticTemplate/execute 请求体 {templateId:模板主键,maxRows:0}定时任务 aiSemanticTemplateExecuteByIdTask invokeTarget execute(模板主键)maxRows 传 0 表示不限制行数——表目录这类资源必须全量少一行就意味着那张表的命令会被判定为「未定位到表」。**第三步同步资源。**知识条目写在 ai_knowledge_entry 里但那不是链路能读到的位置。链路读到的是上下文资源 ai_app_context_resource。中间这一步由主题同步完成text同步动作知识主题 → 上下文资源唯一写入点AiAppContextResourceKnowledgeSyncAppService入口内网 POST /ai/inner/appContextResource/syncAllKnowledgeTopics或定时任务 aiAppContextResourceKnowledgeSyncTask映射规则resource.res_key topic.coderesource.content_text 该主题下条目正文按 sort 合并resource.source_session_id String(topicId)同步结果落在这张表上字段不长但有两个细节值得看| 字段 | 注释原文 | 细节 || --- | --- | --- || res_key | 资源key与 AiConsole 上下文 key 语义一致 | 取值就是知识主题编码两处编码体系合一 || content_text | 资源内容文本/JSON 字符串 | 条目按 sort 合并成的整段文本 || source_session_id | 来源 sessionId可选便于追溯 | 这里存的是主题主键的字符串形式用于反查来源 || owner_user_id res_key | 唯一索引 uk_owner_key | 同一份资源不会重复 |知识主题那张表 ai_knowledge_topic 上与这条链相关的字段有五个- code主题编码资源键的来源- title主题标题注入提示词后成为资源小节的标题- semantic_template_id语义模版主键指向产出内容的模板- semantic_status语义转换状态标识转换状态- parent_id上级主题主键主题可多层本次三类资源都是平铺的。**第四步挂到节点上。**最后一步在画布上完成把资源挂到对应的大模型节点上。表定位节点挂两份表目录、表关系执行计划节点挂一份字典翻译。节点配置里的资源引用与提示词里的占位符必须成对出现——挂了资源但提示词里没有占位符后端会在调用前直接报可读错误不会静默忽略。注入后的提示词长这样资源标题自动成为小节标题text【全局命令-表目录】表编码veap_user表标题用户表编码veap_role表标题角色【全局命令-表关系】表编码veap_user表标题用户主键字段id标题字段nick_name## 四、和 Milvus 那条线怎么接到这里可能会有人问这套东西和向量库有什么关系关系在知识主题这张表上。同一个 ai_knowledge_topic 既能产出上下文资源供给大模型节点也是向量检索的检索对象。平台里那套「文档 → 知识库 → 向量化 → 问答溯源」的知识库链路入口就是知识主题。平台对这两条路做了统一的映射层让节点在挂载知识主题时不必关心背后是静态文本还是向量检索- 主题映射缓存资源键到主题 id 的映射- 集合映射缓存主题到 Milvus 集合与契约字段的映射- 水位缓存缓存构建的时间戳只增不减用来判断缓存是否可用- 检索结果缓存相同主题、相同问题、相同参数的结果缓存 10 分钟- 查询向量缓存相同嵌入模型与相同内容的查询向量缓存 24 小时。缓存构建的唯一写入点在服务端应用启动时会预热也提供了刷新入口前端工具栏上有一个刷新缓存的按钮。静态与动态这两条路的切换关系如下![向量与知识主题衔接静态保底、动态增强、错误边界]真正有意思的是**回退策略**。节点挂载知识主题时如果这个主题没有对应的向量资源就按静态资源注入如果向量检索过程中 Milvus 不可用就退回静态内容记录一条警告日志但命令继续执行。只有非可用性问题比如契约就绪检查失败、嵌入模型报错、缓存损坏才会中断。判断哪些错误属于「可容错」依据是错误码区间Milvus 相关的 500xx 归入可容错400xx 与其他一律抛出。这条边界写得很窄——**能降级的只有向量库不可用不能降级的是配置错了**。## 五、两个诚实的取舍### 5.1 哪些数据可以静态化哪些不行我们一开始想把字段清单和接口清单也一起静态化省掉运行期的两次接口调用。想了一天放弃了。原因有两个。第一字段清单和接口清单的输入是动态的——取决于命令解析出来的表编码而表编码要到运行时才知道。第二更要紧的这两个清单要被脚本读取并逐字段断言。静态资源注入的是提示词文本脚本读不到它等于把强校验削弱了。最后定下的判断标准是一句话**只有「平台级静态事实 无脚本校验依赖」的数据才静态化其余一律运行期拉取。**按这条标准过一遍结论很清楚表目录、表关系、字典翻译三个都可以字段清单、接口清单、业务数据三个都不行。这条判定后来成了团队约定避免以后有人「顺手也静态化一下」把校验削弱了还不知道。### 5.2 离线化换来的代价静态化的好处前面说了运行期不再请求、内容可离线评审、结果可预期。代价也要如实讲**资源有刷新延迟**。表字典或字典项改了之后需要重跑两步——执行语义模板、同步知识主题。两步都可以挂定时任务但它们是定时的不是实时的。也就是说新加了一张表可能要等下一轮任务跑完AI 才能识别到它。我们没做「运行期自动同步」。理由是运行期同步会把一次命令的耗时和一个不确定的批处理绑在一起还可能因为并发执行导致资源内容互相覆盖。宁可让使用者知道「改了字典要等刷新」也不要造一个看起来实时、实际不可控的通道。顺带一句知识主题这张表上还有审核与开放两个状态位。这两位的存在说明知识条目是可以先审后开放的——对静态资源链来说它们一般保持开放但如果某个环境要用审核流程这套结构是现成的。## 六、这条链上的一个隐蔽坑讲一个排查了很久的问题关于缓存水位。水位的作用是标记「这份缓存是哪一轮构建出来的」。判断逻辑很朴素读出来的水位如果没有值就认为缓存无效需要重建。上线后出现了奇怪的现象缓存明明构建成功下次读取却总说水位缺失然后重建一次。反复重建性能和日志都不好看。根因在序列化上。全局的 JSON 序列化配置对长整型注册了一个转字符串的规则写入时是数字实际存进缓存的是带引号的字符串。读取时用「是不是数字类型」判断判断失败于是报「水位缺少构建标记」。修法是在写入端显式转成字符串读取端按纯数字文本解析不依赖类型判断。同时把这条教训写进契约**跨进程缓存的读写统一走文本口径不要依赖运行时类型。**这类问题的价值在于它提醒我们缓存这种东西写入和读取两端必须共享同一份显式的格式约定靠「类型推断」迟早出问题。## 七、想自己搭一遍顺序上建议这样先把表字典和字典项维护好因为三步链的输入就是它们再建三个知识主题与三个语义模板然后依次执行模板、同步主题、把资源挂到节点上。判断成功的标准很直接打开上下文资源列表三份资源的 res_key 分别是那三个主题编码内容分别是表目录、表关系、字典翻译三类文本。表目录那行里能看到 表编码veap_dict_table字典翻译那行里能看到 选项启用 → 1。![上下文资源内容表目录 / 表关系 / 字典翻译三份文本]然后跑一条带字典值的命令试试比如「查询状态为启用的用户」。在参数面板里展开执行计划如果过滤条件的值是 1 而不是「启用」就说明第三层语义真的接上了。换一篇聊聊另一件事这套编排在性能上做了哪些取舍以及运行出错之后该从哪里看起。