# 模板元数据向量索引(统一服务:Agent 查表 + AI 模板匹配) > 版本:v2.0(2026-10-19,纠偏后) > 模块:`ai-server/src/main/java/com/zsjz/ai/module/aiclean/index/` > 关键约束:**只读模板库元数据,不扫描 DuckDB**。 --- ## 1. 要解决的问题 平台有两套模板元数据,代表两类「可被 Agent 使用的表」: | 来源 | 元数据表 | 产生时机 | |---|---|---| | 既有模板库 | `table_info` + `table_field` | 上传清洗识别出的新表 / 模板管理页手工新建 | | AI 模板库 | `ai_clean_template` + `ai_clean_field` | AI 智能清洗出口 B 自建动态表 `ai_clean_{id}` | 业务约束:**DuckDB 里的表必须先登记进这两套元数据,才算「可被 Agent 使用的表」**; 不扫描 DuckDB 系统表(既避免把 `govern_tree`/`raw_*` 等内部表暴露给模型,也让表清单与模板库、治理树、 AI 模板匹配保持同一口径)。 向量化结果有两个用途,**共用同一份索引**: 1. **Agent 问数 / 生成 SQL**:`search_table_schema` 语义检索可选表(返回物理表名 + 物理列名 + 中文注释), `list_tables` 直接列元数据目录; 2. **AI 模板匹配**:`AiTemplateMatcher` 腿 3 的向量召回(候选补充/排序,不单独判档)。 ## 2. 设计结论 ``` table_info + table_field ──┐ ├─> AiCleanTemplateVectorIndex ──> pgvector 表 rag_tpl_d{dims} ai_clean_template + field ─┘ ↑ 增量事件 / 全量重建 / 启动自举 │ ├─> searchTables(query) → Agent 查表(生成 SQL) └─> recall(query) → AI 模板匹配召回 ``` ### 2.1 存储 - 表名 `rag_tpl_d{dims}`(按 embedding 维度隔离;换模型维度变了自然落新表)。 - 列:`doc_id` 主键 / `content` 原文 / `payload jsonb` / `embedding vector(dims)`。 - `doc_id`:既有模板 `t:{table_info.id}`,AI 模板 `a:{ai_clean_template.id}`。 - `payload`:`{kind, templateId|aiTemplateId, tableNameEn, tableNameCn, mainId, headers[], category, fields:[{name(物理列名), en(语义英文名), cn(中文名), type, required}]}`。 - 写:`ON CONFLICT (doc_id) DO UPDATE`;删:按 `doc_id` 精确删除。 ### 2.2 文档与查询文本 - 文档(建索引时嵌入):`模板类型` + `表名(中文/英文)` + `表头` + `字段清单(英文名/类型/中文注释/必填)`。 - 查询:Agent 用自然语言(如「包含手机号和身份证号的表」);模板匹配用主块表头。 - 由于文档同时包含中英文表名、表头、字段语义,两种查询都能命中。 ### 2.3 什么时候建 | 场景 | 入口 | |---|---| | 模板新增/更新/删除 | `TemplateMetaChangedEvent`(`TABLE` / `AI_TEMPLATE` × `UPSERT` / `DELETE`)→ 异步增量 | | 启动自举 | `ApplicationReadyEvent`:`auto-rebuild-on-startup=true` 且可用、向量表不存在/为空 → 后台全量重建 | | 存量补建 / 换嵌入模型 | `POST /js/a/aiClean/templateVector/rebuild` | | 状态观测 | `GET /js/a/aiClean/templateVector/status` | 事件发布点:`TableInfoService#create/update/delete`(afterCommit)、`AiCleanStore#register/deleteTemplate`。 上传清洗、AI 清洗链路无需单独触发 —— 新建模板最终都走这两个入口。 ### 2.4 两个用途的接入方式 | 用途 | 入口 | 行为 | |---|---|---| | Agent 查表 | `RagSchemaService.search()` / `SqlAnalysisTool.listTables()` | `searchTables` 返回 `tableName`(物理表名)+ `columns[].name`(物理列名);`list_tables` 走 `WorkspaceSchemaCatalog.allTables()`(同一套元数据、实时) | | AI 模板匹配 | `AiTemplateMatcher.mergeRecall` | `recall` 只补候选/调排序,**不参与档位判定**(向量分不得触发 STRONG/出口 A) | ### 2.5 失败语义(fail-open) - 没配 `agent_model` 中 `type='embedding'` 活跃模型 / 开关关闭:`isAvailable()=false`,`recall`/`searchTables` 返回空。 - 全量重建连续嵌入失败 ≥5 次自动中止(通常是模型/Key 不可用),避免刷屏;返回已写入数。 - `search_table_schema` 在索引为空时返回 409 且提示先 rebuild;`list_tables` 不依赖向量索引,仍可用。 - 事件发布/索引更新失败只 `log.warn`,绝不影响模板落库与清洗主流程。 ## 3. 配置 ```yaml zsjz: ai-clean: vector: enabled: true auto-index-on-template-change: true auto-rebuild-on-startup: true # 存量模板首次部署/升级后自动补建 top-k: 12 # 模板匹配召回条数 score-threshold: 0.30 max-documents-per-rebuild: 5000 etl: rag: enabled: true # 是否注册 search_table_schema 工具 top-k: 5 score-threshold: 0.3 ``` ## 4. 接口 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/js/a/aiClean/templateVector/status` | `{available, vectorTable, embeddingModel, dimensions, docCount}` | | POST | `/js/a/aiClean/templateVector/rebuild` | 全量重建,返回写入文档数 | ## 5. 与问数链路的关系 - `RagSchemaService` 已重写为**统一索引的查询门面**:`search` 调 `AiCleanTemplateVectorIndex.searchTables`, `reindexWorkspace` 调 `rebuildNow`,不再维护按案件/按 DuckDB 的表结构索引。 - 因此「Agent 能查的表」= 两套模板元数据里登记的表(既有模板 + AI 动态表), 不再包含「库里存在但未登记元数据」的表。 ## 6. 验证 - 单测(离线):`AiCleanTemplateVectorIndexTest`(文档/payload 物理列名、searchTables 映射、AI 模板不过滤 ONLINE、 连续失败熔断)、`WorkspaceSchemaCatalogTest`(两套元数据合并、物理列名、无字段跳过、异常上抛)、 `RagSchemaServiceTest`(search/reindex 门面映射、索引空 409)、`SqlAnalysisToolTest`(list_tables 含 AI 表、兜底)、 `AiTemplateMatcherTest`(召回只影响候选、异常降级)。 - 真库(需 dev PG + pgvector + 有效 embedding 模型):调一次 `/aiClean/templateVector/rebuild`, `select count(*) from rag_tpl_d1024` 应 = 「有字段的 table_info + 有字段的 ai_clean_template」数量; 再新建/更新一份模板,行数应增量 +1/覆盖。 ## 7. 已知边界 / 后续 - 索引最终一致:异步构建,模板刚落库的极短窗口内可能检索不到(不影响指纹/打分腿)。 - 换嵌入模型且维度变化后旧 `rag_tpl_d{旧dims}` 表需人工 drop。 - 向量召回只解决“召回”不解决“判对”;AI 模板匹配的出口硬门保持不变。 - 若后续做「AI 模板相似合并去重 / 别名词典」,可直接复用这套 `rag_tpl_d*` 表,无需再建索引。