AI 辅助组件分类与标签:自动构建组件目录与搜推体系

AI 辅助组件分类与标签:自动构建组件目录与搜推体系

一、组件库 300+ 个组件,找"带搜索功能的下拉框"用了 15 分钟

新同事接需求:在表单中添加一个"支持远程搜索的下拉选择框"。他在组件库文档里翻了 10 分钟,关键词试了"Search Select"、"Autocomplete"、"Remote Dropdown"——最终在第 13 分钟发现叫Select组件,showSearch属性负责搜索,onSearch负责远程数据源。

问题不在组件文档,在于组件的可发现性差。开发者知道"我需要什么",但不知道"这套组件库里叫什么"。这是信息检索的经典问题:用户的查询意图与实际文档索引之间的语义鸿沟。

AI 在这里的价值:自动为每个组件生成多维度的标签和用例描述,构建可搜索的组件语义索引。

二、组件分类与标签生成管道

flowchart LR A[组件源码<br/>TSX + Props 定义] --> B[静态分析] B --> C1[Props 签名提取] B --> C2[组件文本内容提取] B --> C3[JSDoc 注释提取] A --> D[使用示例分析] D --> E1[高频 Props 组合] D --> E2[典型使用场景] C1 --> F[AI 标签生成] C2 --> F C3 --> F E1 --> F E2 --> F F --> G1[功能标签<br/>'search' 'filter' 'input'] F --> G2[场景标签<br/>'表单' '数据录入' '列表选择'] F --> G3[联动标签<br/>'配合Table使用' '替代原生select'] G1 --> H[组件搜索索引] G2 --> H G3 --> H H --> I[搜索 API] I --> J[文档站内搜索] I --> K[IDE 插件] I --> L[Slack bot]

三、自动标签系统实现

// component-discovery/tag-generator.ts // 组件自动标签生成器 // 从组件源码中提取语义特征,生成多维标签 import ts from 'typescript'; import fs from 'fs'; import path from 'path'; interface ComponentDescriptor { /** 组件名称 */ name: string; /** 组件文件路径 */ filePath: string; /** Props 列表 */ props: Array<{ name: string; type: string; required: boolean; defaultValue?: string; description?: string; }>; /** JSDoc 描述 */ description: string; /** 组件分类 */ category: string; } interface ComponentTags { component: string; /** 功能标签:描述组件"能做什么" */ functionalTags: string[]; /** 场景标签:描述组件"在哪用" */ scenarioTags: string[]; /** 关联标签:与其他组件的搭配关系 */ relatedTags: string[]; /** 别名标签:用户可能用到的其他叫法 */ aliasTags: string[]; /** 标签置信度 0~1 */ confidence: Record<string, number>; } /** * 从组件源文件中提取描述符 * * 使用 TypeScript Compiler API 解析 TSX 文件, * 提取接口定义、Props 类型、JSDoc 注释 */ function extractComponentDescriptor(filePath: string): ComponentDescriptor | null { const source = fs.readFileSync(filePath, 'utf-8'); const sourceFile = ts.createSourceFile( filePath, source, ts.ScriptTarget.Latest, true ); let componentName = ''; let description = ''; let category = ''; const props: ComponentDescriptor['props'] = []; // 遍历 AST,寻找组件定义和 Props 接口 function visit(node: ts.Node) { // 1. 提取 JSDoc 注释 const jsDoc = (node as any).jsDoc; if (jsDoc && jsDoc.length > 0) { const comment = jsDoc[0].comment || ''; if (comment.includes('@component')) { componentName = comment.match(/@component\s+(\w+)/)?.[1] || ''; description = comment.replace(/@\w+\s*\w*/g, '').trim(); category = comment.match(/@category\s+(\w+)/)?.[1] || ''; } } // 2. 提取 Props 接口定义 if (ts.isInterfaceDeclaration(node) && node.name.text.endsWith('Props')) { node.members.forEach((member) => { if (ts.isPropertySignature(member)) { const propName = member.name.getText(source); const propType = member.type?.getText(source) || 'any'; const required = !member.questionToken; const propDesc = (member as any).jsDoc?.[0]?.comment || ''; const defaultValue = extractDefaultValue(member, source); props.push({ name: propName, type: propType, required, defaultValue, description: propDesc }); } }); } ts.forEachChild(node, visit); } visit(sourceFile); if (!componentName) return null; return { name: componentName, filePath, props, description, category }; } /** * AI 标签生成 * * 基于组件描述符,自动生成多维度标签 * * 设计意图:用规则引擎(快速、确定)覆盖 70% 的常见标签, * 用 AI(灵活、语义理解)覆盖剩余的 30% 不确定标签 */ async function generateTags( descriptor: ComponentDescriptor ): Promise<ComponentTags> { // ---- 第一层:规则引擎(快速、无成本) ---- const ruleTags = generateRuleBasedTags(descriptor); // ---- 第二层:AI 补充(语义理解、慢但有深度) ---- const aiTags = await generateAITags(descriptor); // ---- 合并 + 去重 ---- const merged = mergeTags(ruleTags, aiTags, descriptor.name); return merged; } /** * 基于规则的标签生成 * * 规则来源: * 1. Props 名称模式匹配(showSearch → 'search' 标签) * 2. 组件名模式匹配(DatePicker → 'date' 'picker') * 3. 已知组件类型的静态映射表 */ function generateRuleBasedTags(descriptor: ComponentDescriptor): Partial<ComponentTags> { const functionalTags = new Set<string>(); const aliasTags = new Set<string>(); // ---- Props 驱动的功能推断 ---- const propNameMap: Record<string, string> = { 'showSearch': 'search', 'filterOption': 'filter', 'onSearch': 'search, remote', 'loading': 'async, loading-state', 'pagination': 'pagination', 'sortable': 'sort', 'draggable': 'drag-and-drop', 'maxLength': 'validation', 'required': 'validation', 'disabled': 'disable', 'readOnly': 'read-only', 'placeholder': 'placeholder', 'allowClear': 'clearable', 'multiple': 'multi-select', 'treeData': 'tree', 'virtual': 'virtual-scroll', 'lazyLoad': 'lazy-load', }; for (const prop of descriptor.props) { const tags = propNameMap[prop.name]; if (tags) { tags.split(',').forEach(t => functionalTags.add(t.trim())); } } // ---- 组件名驱动的功能推断 ---- const namePatterns: Array<{ pattern: RegExp; tags: string[] }> = [ { pattern: /select$/i, tags: ['select', 'dropdown'] }, { pattern: /picker$/i, tags: ['picker', 'date'] }, { pattern: /upload/i, tags: ['upload', 'file'] }, { pattern: /table/i, tags: ['table', 'data-display'] }, { pattern: /modal|dialog/i, tags: ['modal', 'dialog', 'popup'] }, { pattern: /form/i, tags: ['form', 'data-entry'] }, { pattern: /cascader/i, tags: ['cascader', 'hierarchy', 'tree-select'] }, { pattern: /autocomplete/i, tags: ['autocomplete', 'search', 'suggestion'] }, { pattern: /tree/i, tags: ['tree', 'hierarchy', 'nested'] }, ]; for (const { pattern, tags } of namePatterns) { if (pattern.test(descriptor.name)) { tags.forEach(t => functionalTags.add(t)); } } // ---- 生成别名标签 ---- const aliasMap: Record<string, string[]> = { 'Select': ['下拉框', '下拉选择', 'Dropdown Select', 'Combo Box'], 'DatePicker': ['日期选择器', 'Calendar', '日历'], 'Modal': ['对话框', '弹窗', 'Dialog', 'Popup'], 'Table': ['表格', '数据表格', 'Grid', 'Data Grid'], 'Upload': ['上传', '文件上传', 'File Picker'], 'Cascader': ['级联选择', '多级菜单', 'Cascade Selector'], }; aliasMap[descriptor.name]?.forEach(t => aliasTags.add(t)); return { functionalTags: Array.from(functionalTags), aliasTags: Array.from(aliasTags) }; } /** * 基于 AI 的标签生成 * * 将组件描述符(名称、Props、描述)组装成 Prompt, * 让 LLM 补充规则引擎覆盖不到的语义标签 */ async function generateAITags(descriptor: ComponentDescriptor): Promise<Partial<ComponentTags>> { const prompt = ` 你是一个 UI 组件库的标签专家。请为以下组件生成多维度标签。 ## 组件信息 - 名称: ${descriptor.name} - 分类: ${descriptor.category} - 描述: ${descriptor.description} - Props: ${descriptor.props.map(p => `${p.name}: ${p.type}${p.required ? ' (必填)' : ''}`).join(', ')} ## 输出格式 JSON 对象: { "functionalTags": ["标签1", "标签2"], // 组件功能相关:search, filter, upload... "scenarioTags": ["场景1", "场景2"], // 使用场景:表单构建、数据展示、导航... "relatedTags": ["关联组件1"], // 经常搭配使用的组件 "aliasTags": ["同义词1", "同义词2"] // 用户可能搜索的其他叫法 } ## 要求 - functionalTags 不要超过 10 个 - scenarioTags 不要超过 5 个 - 标签尽量用英文小写,除别名外 `.trim(); // 调用 LLM API // const response = await llm.chat({ prompt, temperature: 0.2 }); // return JSON.parse(response); return { functionalTags: [], scenarioTags: [], relatedTags: [], aliasTags: [] }; } /** * 合并规则标签和 AI 标签 * 去重 + 保留置信度 */ function mergeTags( ruleTags: Partial<ComponentTags>, aiTags: Partial<ComponentTags>, componentName: string ): ComponentTags { const mergeArr = (...arrays: (string[] | undefined)[]) => { return [...new Set(arrays.flat().filter(Boolean))]; }; return { component: componentName, functionalTags: mergeArr(ruleTags.functionalTags, aiTags.functionalTags), scenarioTags: mergeArr(ruleTags.scenarioTags, aiTags.scenarioTags), relatedTags: mergeArr(ruleTags.relatedTags, aiTags.relatedTags), aliasTags: mergeArr(ruleTags.aliasTags, aiTags.aliasTags), confidence: { // 规则标签置信度高于 AI 标签 'functionalTags': 0.9, 'scenarioTags': 0.7, 'relatedTags': 0.6, 'aliasTags': 0.8 } }; } function extractDefaultValue(member: ts.PropertySignature, source: string): string | undefined { const initializer = member.initializer; if (initializer) return initializer.getText(source); return undefined; }
// component-discovery/search-engine.ts // 组件搜索引擎——基于标签的多维检索 interface SearchResult { component: string; score: number; // 综合匹配分数 matchedTags: string[]; // 匹配到的标签 reason: string; // 为什么推荐(可解释性) } /** * 组件搜索引擎 * * 设计意图:不是简单的关键词匹配, * 而是语义标签 + 别名 + 场景的多维度加权检索 */ class ComponentSearchEngine { private index: Map<string, ComponentTags> = new Map(); // 倒排索引:tag → component[] private invertedIndex: Map<string, Set<string>> = new Map(); /** * 导入组件标签数据,构建搜索索引 */ importTags(tagsList: ComponentTags[]): void { for (const tags of tagsList) { this.index.set(tags.component, tags); // 构建倒排索引 const allTags = [ ...tags.functionalTags, ...tags.scenarioTags, ...tags.aliasTags ]; for (const tag of allTags) { const normalized = tag.toLowerCase().trim(); if (!this.invertedIndex.has(normalized)) { this.invertedIndex.set(normalized, new Set()); } this.invertedIndex.get(normalized)!.add(tags.component); } } } /** * 搜索组件 * * @param query - 用户查询(自然语言) * @returns 匹配的组件列表,按分数降序 * * 检索策略: * 1. 查询分词(中文用结巴分词,英文按空格+驼峰拆分) * 2. 每个词在倒排索引中查找匹配的标签 * 3. 加权评分:精确标签匹配 > 别名匹配 > 场景匹配 */ search(query: string): SearchResult[] { const tokens = this.tokenize(query); const scores = new Map<string, number>(); for (const token of tokens) { const normalized = token.toLowerCase(); // 遍历倒排索引,查找包含该 token 的标签 for (const [tag, components] of this.invertedIndex) { if (tag.includes(normalized) || normalized.includes(tag)) { for (const component of components) { const tags = this.index.get(component)!; let weight = 1; // 加权:功能标签 > 场景标签 > 别名标签 if (tags.functionalTags.includes(tag)) weight = 3; else if (tags.scenarioTags.includes(tag)) weight = 2; else if (tags.aliasTags.includes(tag)) weight = 1.5; scores.set( component, (scores.get(component) || 0) + weight ); } } } // 直接匹配组件名(最高权重) for (const [component] of this.index) { if (component.toLowerCase().includes(normalized)) { scores.set( component, (scores.get(component) || 0) + 5 ); } } } // 按分数排序 return Array.from(scores.entries()) .sort(([, a], [, b]) => b - a) .map(([component, score]) => ({ component, score, matchedTags: [], // 可补充实现 reason: `匹配标签: ${component}` })); } /** * 查询分词 * * 支持英文(空格 + 驼峰)和中文(调用中文分词库) */ private tokenize(query: string): string[] { // 检测是否包含中文 const hasChinese = /[\u4e00-\u9fff]/.test(query); if (hasChinese) { // 调用中文分词库(如 jieba-wasm) // return jieba.cut(query); return [query]; // 简化实现 } // 英文分词:按空格分割 + 驼峰拆分 return query .split(/\s+/) .flatMap(word => word.split(/(?=[A-Z])/).map(w => w.toLowerCase()) ); } }

四、标签系统的维护成本

标签漂移。组件升级后(如Select新增了mode="tags"支持多选标签),其标签需要同步更新。解决方案:每次组件发版时自动重新生成标签,作为 CI 流程的一部分。

语言壁垒。国际化团队的开发者可能用 "calendrier"(法语)或 "カレンダー"(日语)搜索日期选择器。支持多语言标签需要为每种语言维护独立的别名映射表。成本较高——建议先覆盖中英文,其他语言按需扩展。

过度标签化。如果一个组件有 30 个标签,"搜索框"、"选择器"、"输入组件"、"表单控件"……标签变得和文档一样长,搜索精度反而下降。限制每个组件标签总数 ≤ 15 个。

五、总结

AI 辅助组件分类与标签的核心思路是"降低组件的发现成本"

  1. 规则引擎覆盖 70%——基于 Props 名称和组件名称的确定性模式匹配
  2. AI 补充 30%——语义理解、场景推断、别名生成等规则无法覆盖的部分
  3. 倒排索引——标签 → 组件的反向映射,支持 O(1) 搜索
  4. 多维加权检索——功能标签 > 场景标签 > 别名标签 > 组件名

最终交付:文档站搜索框变成"我要一个能搜索的下拉框"就能匹配到Select组件的语义搜索引擎。