企业智能知识库搭建实战:搭贝低代码平台实现AI知识管理系统

技术栈:搭贝低代码 + Chroma向量数据库 + GLM-4 + RAG架构,2周从0到1搭建企业AI知识库


项目背景

公司300人,之前用Excel+共享文件夹管理知识文档,痛点满满:

  • 找文档平均30分钟,员工宁愿问人
  • 70%的咨询是重复问题
  • 知识利用率只有30%,大量文档吃灰
  • 离职员工带走经验,新人从头摸索

技术团队只有2人,传统开发至少3个月。最终选择搭贝低代码平台 + AI方案,2周完成上线。

本文记录完整的技术实现过程。


技术选型

架构总览

复制代码
┌──────────────────────────────────────────────────────┐
│                    前端层                              │
│   低代码平台(Vue3 + Element Plus)                │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│   │ 知识首页  │ │ 文档管理  │ │ AI问答    │ │ 数据看板  ││
│   └──────────┘ └──────────┘ └──────────┘ └──────────┘│
└────────────────────┬──────────────────────────────────┘
                     │ HTTP/WebSocket
┌────────────────────▼──────────────────────────────────┐
│                   API网关层                             │
│   认证中间件 → 权限校验 → 请求路由 → 限流 → 日志       │
└────────────────────┬──────────────────────────────────┘
                     │
┌────────────────────▼──────────────────────────────────┐
│                   业务服务层                            │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│   │ 文档服务  │ │ 搜索服务  │ │ 问答服务  │ │ 分析服务  ││
│   └──────────┘ └──────────┘ └──────────┘ └──────────┘│
└────────────────────┬──────────────────────────────────┘
                     │
┌────────────────────▼──────────────────────────────────┐
│                   AI服务层                              │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│   │ Embedding│ │ 向量搜索  │ │ RAG引擎   │ │ LLM生成   ││
│   │ Service  │ │ (Chroma) │ │ Engine   │ │ (GLM-4)  ││
│   └──────────┘ └──────────┘ └──────────┘ └──────────┘│
└────────────────────┬──────────────────────────────────┘
                     │
┌────────────────────▼──────────────────────────────────┐
│                   存储层                                │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│   │ MySQL    │ │ Chroma   │ │ MinIO    │ │ Redis    ││
│   │ (业务数据)│ │ (向量数据)│ │ (文件存储)│ │ (缓存)   ││
│   └──────────┘ └──────────┘ └──────────┘ └──────────┘│
└──────────────────────────────────────────────────────┘

技术栈选择

层级 技术 选型理由
前端 低代码平台 拖拽式配置,无需前端开发
后端 Node.js + Express 轻量、高并发、生态完善
AI模型 GLM-4(智谱AI) 中文能力强,成本低
向量数据库 Chroma 开源、易部署、性能好
关系数据库 MySQL 8.0 成熟稳定
文件存储 MinIO 兼容S3、私有部署
缓存 Redis 搜索缓存、会话管理

为什么选Chroma?

javascript 复制代码
// Chroma vs 其他向量数据库对比
const vectorDBComparison = {
  chroma: {
    pros: ['开源免费', '部署简单(单机/Docker)', 'Python/JS SDK完善'],
    cons: ['超大规模性能一般'],
    suitable: '文档数 < 100万'
  },
  pinecone: {
    pros: ['托管服务,零运维', '性能强'],
    cons: ['收费', '数据在云端'],
    suitable: '不想自己运维'
  },
  weaviate: {
    pros: ['功能丰富', '支持多模态'],
    cons: ['部署复杂', '资源消耗大'],
    suitable: '复杂场景'
  },
  milvus: {
    pros: ['超高性能', '支持十亿级向量'],
    cons: ['部署复杂', '运维成本高'],
    suitable: '超大规模'
  }
}

// 我们的选择:Chroma(文档500份左右,Chroma完全够用)

环境搭建

Docker Compose 一键部署

yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  # MySQL - 业务数据
  mysql:
    image: mysql:8.0
    container_name: kb-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: knowledge_base
      MYSQL_CHARSET: utf8mb4
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - kb-network

  # Chroma - 向量数据库
  chroma:
    image: chromadb/chroma:latest
    container_name: kb-chroma
    restart: always
    ports:
      - "8000:8000"
    volumes:
      - chroma-data:/chroma/chroma
    environment:
      - CHROMA_SERVER_AUTH_PROVIDER=chromadb.auth.token.TokenAuthMiddleware
      - CHROMA_SERVER_AUTH_CREDENTIALS=${CHROMA_AUTH_TOKEN}
    networks:
      - kb-network

  # MinIO - 文件存储
  minio:
    image: minio/minio:latest
    container_name: kb-minio
    restart: always
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - minio-data:/data
    networks:
      - kb-network

  # Redis - 缓存
  redis:
    image: redis:7-alpine
    container_name: kb-redis
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - kb-network

  # 后端服务
  backend:
    build: ./backend
    container_name: kb-backend
    restart: always
    ports:
      - "3000:3000"
    depends_on:
      - mysql
      - chroma
      - minio
      - redis
    environment:
      NODE_ENV: production
      DATABASE_URL: mysql://root:${MYSQL_ROOT_PASSWORD}@mysql:3306/knowledge_base
      CHROMA_URL: http://chroma:8000
      MINIO_ENDPOINT: minio
      MINIO_PORT: 9000
      REDIS_URL: redis://redis:6379
      ZHIPUAI_API_KEY: ${ZHIPUAI_API_KEY}
      JWT_SECRET: ${JWT_SECRET}
    volumes:
      - ./logs:/app/logs
    networks:
      - kb-network

volumes:
  mysql-data:
  chroma-data:
  minio-data:
  redis-data:

networks:
  kb-network:
    driver: bridge

数据库初始化

sql 复制代码
-- init.sql
CREATE DATABASE IF NOT EXISTS knowledge_base CHARACTER SET utf8mb4;
USE knowledge_base;

-- 文档表
CREATE TABLE documents (
  id VARCHAR(36) PRIMARY KEY,
  title VARCHAR(500) NOT NULL,
  content LONGTEXT NOT NULL,
  summary TEXT,
  category ENUM('规章制度', '流程文档', '技术手册', '销售话术', '其他') NOT NULL,
  tags JSON,
  department ENUM('销售部', '技术部', '人事部', '财务部', '行政部') NOT NULL,
  status ENUM('草稿', '审批中', '已发布', '已归档') DEFAULT '草稿',
  author_id VARCHAR(36) NOT NULL,
  version INT DEFAULT 1,
  views INT DEFAULT 0,
  likes INT DEFAULT 0,
  effective_date DATE,
  expiry_date DATE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_category (category),
  INDEX idx_department (department),
  INDEX idx_status (status),
  FULLTEXT INDEX ft_content (title, content)
) ENGINE=InnoDB;

-- 用户表
CREATE TABLE users (
  id VARCHAR(36) PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(200) UNIQUE NOT NULL,
  department ENUM('销售部', '技术部', '人事部', '财务部', '行政部'),
  role ENUM('admin', 'editor', 'viewer') DEFAULT 'viewer',
  password_hash VARCHAR(200),
  avatar_url VARCHAR(500),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  last_login TIMESTAMP NULL
) ENGINE=InnoDB;

-- 问答日志表
CREATE TABLE qa_logs (
  id VARCHAR(36) PRIMARY KEY,
  user_id VARCHAR(36) NOT NULL,
  question TEXT NOT NULL,
  answer TEXT NOT NULL,
  sources JSON,
  confidence FLOAT,
  feedback ENUM('positive', 'negative', NULL),
  response_time_ms INT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user (user_id),
  INDEX idx_created (created_at)
) ENGINE=InnoDB;

-- 搜索日志表
CREATE TABLE search_logs (
  id VARCHAR(36) PRIMARY KEY,
  user_id VARCHAR(36) NOT NULL,
  keyword VARCHAR(500) NOT NULL,
  result_count INT DEFAULT 0,
  clicked_doc_id VARCHAR(36) NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_keyword (keyword),
  INDEX idx_created (created_at)
) ENGINE=InnoDB;

-- 文档版本历史表
CREATE TABLE document_versions (
  id VARCHAR(36) PRIMARY KEY,
  document_id VARCHAR(36) NOT NULL,
  version INT NOT NULL,
  title VARCHAR(500) NOT NULL,
  content LONGTEXT NOT NULL,
  changed_by VARCHAR(36) NOT NULL,
  change_summary TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (document_id) REFERENCES documents(id),
  INDEX idx_doc_version (document_id, version)
) ENGINE=InnoDB;

核心模块实现

模块1:文档管理与向量化

文档上传与处理流程
javascript 复制代码
// services/documentService.js
const { ChromaClient, OpenAIEmbeddingFunction } = require('chromadb')
const mammoth = require('mammoth')
const pdf = require('pdf-parse')
const xlsx = require('xlsx')

class DocumentService {
  constructor() {
    this.chroma = new ChromaClient({ path: process.env.CHROMA_URL })
    this.collection = null
  }

  // 初始化向量集合
  async initCollection() {
    this.collection = await this.chroma.getOrCreateCollection({
      name: 'documents',
      metadata: { 'hnsw:space': 'cosine' }
    })
  }

  // 上传文档
  async uploadDocument(file, metadata) {
    // Step 1: 解析文件内容
    const content = await this.parseFile(file)

    // Step 2: 文本分块
    const chunks = this.chunkText(content, {
      chunkSize: 500,
      chunkOverlap: 50
    })

    // Step 3: 存储到MySQL
    const document = await this.saveToDatabase({
      title: metadata.title,
      content: content,
      summary: await this.generateSummary(content),
      category: metadata.category,
      tags: metadata.tags,
      department: metadata.department,
      authorId: metadata.authorId,
      status: '已发布'
    })

    // Step 4: 向量化并存储到Chroma
    await this.vectorizeAndStore(document, chunks)

    // Step 5: 存储原始文件到MinIO
    await this.storeFile(file, document.id)

    return document
  }

  // 解析不同格式的文件
  async parseFile(file) {
    const ext = file.originalname.split('.').pop().toLowerCase()

    switch (ext) {
      case 'txt':
        return file.buffer.toString('utf8')

      case 'md':
        return file.buffer.toString('utf8')

      case 'docx':
        const docxResult = await mammoth.extractRawText({ buffer: file.buffer })
        return docxResult.value

      case 'pdf':
        const pdfResult = await pdf(file.buffer)
        return pdfResult.text

      case 'xlsx':
      case 'xls':
        const workbook = xlsx.read(file.buffer)
        const sheets = []
        for (const sheetName of workbook.SheetNames) {
          const sheet = workbook.Sheets[sheetName]
          const data = xlsx.utils.sheet_to_json(sheet)
          sheets.push(`## ${sheetName}\n${JSON.stringify(data, null, 2)}`)
        }
        return sheets.join('\n\n')

      default:
        throw new Error(`不支持的文件格式: ${ext}`)
    }
  }

  // 文本分块
  chunkText(text, options = {}) {
    const { chunkSize = 500, chunkOverlap = 50 } = options

    // 按段落分割
    const paragraphs = text.split(/\n\s*\n/)

    const chunks = []
    let currentChunk = ''
    let currentLength = 0

    for (const paragraph of paragraphs) {
      // 如果段落本身超过chunkSize,按句子分割
      if (paragraph.length > chunkSize) {
        const sentences = paragraph.split(/(?<=[。!?.!?])/)
        for (const sentence of sentences) {
          if (currentLength + sentence.length > chunkSize && currentChunk) {
            chunks.push(currentChunk.trim())
            // 保留overlap
            const overlap = currentChunk.slice(-chunkOverlap)
            currentChunk = overlap + sentence
            currentLength = currentChunk.length
          } else {
            currentChunk += sentence
            currentLength += sentence.length
          }
        }
      } else {
        if (currentLength + paragraph.length > chunkSize && currentChunk) {
          chunks.push(currentChunk.trim())
          const overlap = currentChunk.slice(-chunkOverlap)
          currentChunk = overlap + '\n\n' + paragraph
          currentLength = currentChunk.length
        } else {
          currentChunk += '\n\n' + paragraph
          currentLength = currentChunk.length
        }
      }
    }

    if (currentChunk.trim()) {
      chunks.push(currentChunk.trim())
    }

    return chunks.map((text, index) => ({ text, index }))
  }

  // 向量化并存储
  async vectorizeAndStore(document, chunks) {
    for (const chunk of chunks) {
      // 调用Embedding API
      const embedding = await this.getEmbedding(chunk.text)

      // 存储到Chroma
      await this.collection.add({
        ids: [`${document.id}_chunk_${chunk.index}`],
        embeddings: [embedding],
        metadatas: [{
          documentId: document.id,
          title: document.title,
          category: document.category,
          department: document.department,
          chunkIndex: chunk.index,
          text: chunk.text
        }],
        documents: [chunk.text]
      })
    }
  }

  // 调用Embedding API
  async getEmbedding(text) {
    const response = await fetch('https://open.bigmodel.cn/api/paas/v4/embeddings', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.ZHIPUAI_API_KEY}`
      },
      body: JSON.stringify({
        model: 'embedding-3',
        input: text
      })
    })

    const result = await response.json()
    return result.data[0].embedding
  }

  // AI生成摘要
  async generateSummary(content) {
    const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.ZHIPUAI_API_KEY}`
      },
      body: JSON.stringify({
        model: 'glm-4',
        messages: [
          {
            role: 'system',
            content: '请用100字以内总结以下文档的核心内容。'
          },
          {
            role: 'user',
            content: content.slice(0, 2000)
          }
        ],
        temperature: 0.3,
        max_tokens: 200
      })
    })

    const result = await response.json()
    return result.choices[0].message.content
  }
}

module.exports = new DocumentService()

模块2:语义搜索

javascript 复制代码
// services/searchService.js
const DocumentService = require('./documentService')

class SearchService {
  constructor() {
    this.docService = new DocumentService()
    this.cache = require('redis').createClient({
      url: process.env.REDIS_URL
    })
  }

  // 语义搜索
  async semanticSearch(query, options = {}) {
    const {
      topK = 5,
      filters = {},
      threshold = 0.7
    } = options

    // 1. 检查缓存
    const cacheKey = `search:${query}:${JSON.stringify(filters)}`
    const cached = await this.cache.get(cacheKey)
    if (cached) {
      return JSON.parse(cached)
    }

    // 2. 向量化查询
    const queryEmbedding = await this.docService.getEmbedding(query)

    // 3. 向量检索
    const chromaResults = await this.docService.collection.query({
      queryEmbeddings: [queryEmbedding],
      nResults: topK * 2,
      where: this.buildFilters(filters)
    })

    // 4. 处理结果
    const results = chromaResults.documents[0].map((doc, index) => ({
      content: doc,
      metadata: chromaResults.metadatas[0][index],
      confidence: 1 - chromaResults.distances[0][index]
    }))

    // 5. 过滤低置信度
    const filtered = results.filter(r => r.confidence >= threshold)

    // 6. 按文档聚合
    const aggregated = this.aggregateByDocument(filtered)

    // 7. 关键词补充搜索(MySQL FULLTEXT)
    const keywordResults = await this.keywordSearch(query, topK)
    const hybridResults = this.hybridRanking(aggregated, keywordResults)

    // 8. 缓存结果(5分钟)
    await this.cache.set(cacheKey, JSON.stringify(hybridResults), {
      EX: 300
    })

    // 9. 记录搜索日志
    await this.logSearch(query, hybridResults.length)

    return hybridResults.slice(0, topK)
  }

  // 构建过滤条件
  buildFilters(filters) {
    const where = {}
    if (filters.category) {
      where.category = filters.category
    }
    if (filters.department) {
      where.department = filters.department
    }
    return Object.keys(where).length > 0 ? where : undefined
  }

  // 按文档聚合
  aggregateByDocument(results) {
    const docMap = {}

    for (const result of results) {
      const docId = result.metadata.documentId

      if (!docMap[docId]) {
        docMap[docId] = {
          documentId: docId,
          title: result.metadata.title,
          category: result.metadata.category,
          department: result.metadata.department,
          chunks: [],
          maxConfidence: 0
        }
      }

      docMap[docId].chunks.push({
        index: result.metadata.chunkIndex,
        text: result.content,
        confidence: result.confidence
      })

      if (result.confidence > docMap[docId].maxConfidence) {
        docMap[docId].maxConfidence = result.confidence
        docMap[docId].bestChunk = result.content
      }
    }

    return Object.values(docMap)
      .sort((a, b) => b.maxConfidence - a.maxConfidence)
  }

  // 关键词搜索
  async keywordSearch(query, topK) {
    const sql = `
      SELECT id, title, summary,
        MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE) as score
      FROM documents
      WHERE status = '已发布'
        AND MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)
      ORDER BY score DESC
      LIMIT ?
    `

    const results = await db.query(sql, [query, query, topK])

    return results.map(row => ({
      documentId: row.id,
      title: row.title,
      summary: row.summary,
      score: row.score,
      type: 'keyword'
    }))
  }

  // 混合排序
  hybridRanking(semanticResults, keywordResults) {
    const semanticWeight = 0.8
    const keywordWeight = 0.2

    const allResults = new Map()

    // 语义搜索结果
    for (const result of semanticResults) {
      allResults.set(result.documentId, {
        ...result,
        semanticScore: result.maxConfidence * semanticWeight,
        keywordScore: 0
      })
    }

    // 关键词搜索结果
    for (const result of keywordResults) {
      if (allResults.has(result.documentId)) {
        allResults.get(result.documentId).keywordScore = result.score * keywordWeight
      } else {
        allResults.set(result.documentId, {
          documentId: result.documentId,
          title: result.title,
          summary: result.summary,
          semanticScore: 0,
          keywordScore: result.score * keywordWeight
        })
      }
    }

    // 计算总分并排序
    return Array.from(allResults.values())
      .map(r => ({
        ...r,
        totalScore: r.semanticScore + r.keywordScore
      }))
      .sort((a, b) => b.totalScore - a.totalScore)
  }

  // 记录搜索日志
  async logSearch(query, resultCount) {
    await db.query(
      'INSERT INTO search_logs (id, user_id, keyword, result_count) VALUES (UUID(), ?, ?, ?)',
      [currentUser.id, query, resultCount]
    )
  }
}

module.exports = new SearchService()

模块3:RAG智能问答

javascript 复制代码
// services/ragService.js
const SearchService = require('./searchService')

class RAGService {
  constructor() {
    this.searchService = new SearchService()
    this.conversationHistory = new Map() // sessionId -> messages
  }

  // 问答主流程
  async answer(question, context = {}) {
    const startTime = Date.now()

    try {
      // Step 1: 检索相关文档
      const relevantDocs = await this.searchService.semanticSearch(question, {
        topK: 5,
        filters: {
          department: context.department,
          category: context.category
        },
        threshold: 0.7
      })

      // Step 2: 检查是否有足够的信息
      if (relevantDocs.length === 0) {
        const fallbackResponse = {
          answer: '抱歉,我没有在知识库中找到相关信息。您可以:\n1. 尝试换个问法\n2. 联系相关部门咨询\n3. 提交知识缺口,我们会尽快补充',
          sources: [],
          confidence: 0,
          shouldEscalate: true
        }

        await this.logQA(question, fallbackResponse, context, startTime)
        return fallbackResponse
      }

      // Step 3: 构建上下文
      const contextText = this.buildContext(relevantDocs)

      // Step 4: 获取对话历史
      const history = this.getHistory(context.sessionId)

      // Step 5: 构建Prompt
      const prompt = this.buildPrompt(question, contextText, history, context)

      // Step 6: LLM生成答案
      const llmResponse = await this.callLLM(prompt)

      // Step 7: 后处理
      const answer = this.postProcess(llmResponse, relevantDocs)

      // Step 8: 更新对话历史
      this.updateHistory(context.sessionId, question, answer.content)

      // Step 9: 记录日志
      const response = {
        answer: answer.content,
        sources: relevantDocs.map(doc => ({
          id: doc.documentId,
          title: doc.title,
          confidence: doc.maxConfidence,
          url: `/documents/${doc.documentId}`
        })),
        confidence: Math.max(...relevantDocs.map(d => d.maxConfidence)),
        shouldEscalate: false
      }

      await this.logQA(question, response, context, startTime)

      return response

    } catch (error) {
      console.error('RAG Error:', error)
      return {
        answer: '抱歉,服务暂时不可用,请稍后重试。',
        sources: [],
        confidence: 0,
        shouldEscalate: true,
        error: error.message
      }
    }
  }

  // 构建上下文
  buildContext(documents) {
    return documents.map((doc, index) => {
      const bestChunk = doc.bestChunk || doc.chunks?.[0]?.text || ''
      return `
[文档${index + 1}] ${doc.title}
分类:${doc.category} | 部门:${doc.department}
相关度:${(doc.maxConfidence * 100).toFixed(1)}%
内容:${bestChunk}
`
    }).join('\n---\n')
  }

  // 构建Prompt
  buildPrompt(question, context, history, userContext) {
    const historyText = history.length > 0
      ? `\n\n对话历史:\n${history.map(h => `用户:${h.question}\n助手:${h.answer}`).join('\n')}`
      : ''

    const userContextText = userContext.department
      ? `\n当前用户部门:${userContext.department}`
      : ''

    return `你是企业智能知识库助手。请严格基于以下知识库文档内容回答用户的问题。

知识库文档:
${context}
${historyText}${userContextText}

回答规则:
1. 只基于知识库文档内容回答,严禁编造
2. 如果文档中没有相关信息,明确告知用户
3. 回答中引用文档来源(如"根据《文档名》...")
4. 语言简洁、直接、条理清晰
5. 如果问题涉及多个方面,分点回答
6. 对于过时的信息,提醒用户可能存在更新版本

用户问题:${question}

请回答:`
  }

  // 调用LLM
  async callLLM(prompt) {
    const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.ZHIPUAI_API_KEY}`
      },
      body: JSON.stringify({
        model: 'glm-4',
        messages: [
          {
            role: 'user',
            content: prompt
          }
        ],
        temperature: 0.3,
        max_tokens: 800,
        stream: false
      })
    })

    const result = await response.json()

    return {
      content: result.choices[0].message.content,
      usage: result.usage,
      model: result.model
    }
  }

  // 后处理
  postProcess(llmResponse, documents) {
    let content = llmResponse.content

    // 添加文档来源标注
    const sources = documents
      .map(d => `📎 ${d.title}(相关度:${(d.maxConfidence * 100).toFixed(1)}%)`)
      .join('\n')

    content += '\n\n---\n📚 **参考来源:**\n' + sources

    return { content, usage: llmResponse.usage }
  }

  // 对话历史管理
  getHistory(sessionId) {
    if (!sessionId) return []
    const history = this.conversationHistory.get(sessionId) || []
    return history.slice(-5) // 最近5轮
  }

  updateHistory(sessionId, question, answer) {
    if (!sessionId) return
    if (!this.conversationHistory.has(sessionId)) {
      this.conversationHistory.set(sessionId, [])
    }
    const history = this.conversationHistory.get(sessionId)
    history.push({ question, answer: answer.slice(0, 200) }) // 只存摘要
    if (history.length > 10) {
      history.shift()
    }
  }

  // 判断是否需要转人工
  shouldEscalate(confidence, feedback) {
    return confidence < 0.6 || feedback === 'negative'
  }

  // 记录问答日志
  async logQA(question, response, context, startTime) {
    const responseTime = Date.now() - startTime

    await db.query(
      `INSERT INTO qa_logs (id, user_id, question, answer, sources, confidence, response_time_ms)
       VALUES (UUID(), ?, ?, ?, ?, ?, ?)`,
      [
        context.userId,
        question,
        response.answer,
        JSON.stringify(response.sources),
        response.confidence,
        responseTime
      ]
    )
  }
}

module.exports = new RAGService()

模块4:API路由

javascript 复制代码
// routes/api.js
const express = require('express')
const router = express.Router()
const multer = require('multer')
const DocumentService = require('../services/documentService')
const SearchService = require('../services/searchService')
const RAGService = require('../services/ragService')
const { auth, checkPermission } = require('../middleware/auth')

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 50 * 1024 * 1024 } // 50MB
})

// 文档相关路由
router.post('/documents/upload',
  auth,
  checkPermission('write'),
  upload.single('file'),
  async (req, res) => {
    try {
      const document = await DocumentService.uploadDocument(req.file, {
        title: req.body.title,
        category: req.body.category,
        tags: req.body.tags ? JSON.parse(req.body.tags) : [],
        department: req.body.department,
        authorId: req.user.id
      })
      res.json({ success: true, data: document })
    } catch (error) {
      res.status(500).json({ success: false, error: error.message })
    }
  }
)

router.get('/documents/:id', auth, async (req, res) => {
  try {
    const document = await DocumentService.getById(req.params.id)
    await DocumentService.incrementViews(req.params.id)
    res.json({ success: true, data: document })
  } catch (error) {
    res.status(404).json({ success: false, error: '文档不存在' })
  }
)

// 搜索路由
router.get('/search', auth, async (req, res) => {
  try {
    const results = await SearchService.semanticSearch(req.query.q, {
      topK: parseInt(req.query.topK) || 5,
      filters: {
        category: req.query.category,
        department: req.query.department
      }
    })
    res.json({ success: true, data: results })
  } catch (error) {
    res.status(500).json({ success: false, error: error.message })
  }
})

// AI问答路由
router.post('/ai/chat', auth, async (req, res) => {
  try {
    const response = await RAGService.answer(req.body.question, {
      userId: req.user.id,
      department: req.user.department,
      sessionId: req.body.sessionId
    })
    res.json({ success: true, data: response })
  } catch (error) {
    res.status(500).json({ success: false, error: error.message })
  }
)

// 反馈路由
router.post('/ai/feedback', auth, async (req, res) => {
  try {
    await RAGService.saveFeedback({
      logId: req.body.logId,
      feedback: req.body.feedback,
      comment: req.body.comment
    })
    res.json({ success: true })
  } catch (error) {
    res.status(500).json({ success: false, error: error.message })
  }
})

// 数据分析路由
router.get('/analytics/gaps', auth, checkPermission('admin'), async (req, res) => {
  try {
    const gaps = await AnalyticsService.identifyKnowledgeGaps()
    res.json({ success: true, data: gaps })
  } catch (error) {
    res.status(500).json({ success: false, error: error.message })
  }
})

module.exports = router

模块5:低代码前端配置

在低代码平台中配置以下页面组件:

javascript 复制代码
// 页面配置
const pages = {
  // 知识库首页
  home: {
    layout: 'vertical',
    components: [
      {
        type: 'SearchBar',
        config: {
          placeholder: '输入关键词,AI智能搜索...',
          aiSuggestion: true,
          history: true,
          onChange: 'updateSuggestions'
        },
        events: {
          onSearch: 'handleAISearch'
        }
      },
      {
        type: 'CategoryGrid',
        config: {
          categories: [
            { name: '规章制度', icon: 'book', color: '#1890ff' },
            { name: '流程文档', icon: 'flow', color: '#52c41a' },
            { name: '技术手册', icon: 'code', color: '#722ed1' },
            { name: '销售话术', icon: 'shopping', color: '#fa8c16' }
          ]
        },
        events: {
          onItemClick: 'filterByCategory'
        }
      },
      {
        type: 'DocumentList',
        config: {
          dataSource: '/api/documents',
          sortBy: 'views',
          pageSize: 10,
          showCategory: true,
          showViews: true
        },
        events: {
          onItemClick: 'openDocument'
        }
      },
      {
        type: 'FloatingButton',
        config: {
          icon: 'robot',
          position: 'bottom-right'
        },
        events: {
          onClick: 'openAIChat'
        }
      }
    ]
  },

  // AI对话页面
  aiChat: {
    layout: 'chat',
    components: [
      {
        type: 'ChatHistory',
        config: {
          messages: 'chatMessages',
          showAvatar: true,
          showSources: true,
          showFeedback: true,
          autoScroll: true
        }
      },
      {
        type: 'ChatInput',
        config: {
          placeholder: '输入您的问题...',
          autoResize: true,
          sendShortcut: 'Ctrl+Enter'
        },
        events: {
          onSend: 'handleAISend',
          onFeedback: 'handleAIFeedback'
        }
      }
    ]
  }
}

性能优化

1. 搜索缓存

javascript 复制代码
// Redis缓存策略
const cacheStrategy = {
  // 搜索结果缓存5分钟
  searchResults: {
    key: (query, filters) => `search:${md5(query + JSON.stringify(filters))}`,
    ttl: 300 // 5分钟
  },

  // 热门文档缓存1小时
  hotDocuments: {
    key: () => 'hot_docs:top10',
    ttl: 3600 // 1小时
  },

  // AI回答缓存30分钟(相同问题)
  aiAnswers: {
    key: (question) => `ai_answer:${md5(question)}`,
    ttl: 1800 // 30分钟
  },

  // 分类统计缓存10分钟
  categoryStats: {
    key: () => 'category_stats',
    ttl: 600 // 10分钟
  }
}

2. 批量向量化

javascript 复制代码
// 批量向量化工具
async function batchVectorize(documents, batchSize = 10) {
  const results = []

  for (let i = 0; i < documents.length; i += batchSize) {
    const batch = documents.slice(i, i + batchSize)

    const batchPromises = batch.map(async (doc) => {
      const chunks = chunkText(doc.content, { chunkSize: 500, chunkOverlap: 50 })

      const embeddingPromises = chunks.map(chunk =>
        getEmbedding(chunk.text)
      )

      const embeddings = await Promise.all(embeddingPromises)

      return { doc, chunks, embeddings }
    })

    const batchResults = await Promise.all(batchPromises)
    results.push(...batchResults)

    // 避免API限流
    await sleep(500)
  }

  return results
}

3. 数据库优化

sql 复制代码
-- 添加全文索引
ALTER TABLE documents ADD FULLTEXT INDEX ft_title_content (title, content) WITH PARSER ngram;

-- 优化查询
CREATE INDEX idx_docs_status_created ON documents(status, created_at);
CREATE INDEX idx_docs_dept_category ON documents(department, category);

-- 分区(按月分区搜索日志)
ALTER TABLE search_logs PARTITION BY RANGE (TO_DAYS(created_at)) (
  PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
  PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),
  PARTITION p_future VALUES LESS THAN MAXVALUE
);

部署上线

Docker部署

bash 复制代码
# 1. 克隆项目
git clone <repository>
cd ai-knowledge-base

# 2. 配置环境变量
cp .env.example .env
# 编辑 .env 填入实际配置

# 3. 启动服务
docker compose up -d

# 4. 初始化数据库
docker exec kb-mysql mysql -uroot -p${MYSQL_ROOT_PASSWORD} < /docker-entrypoint-initdb.d/init.sql

# 5. 初始化向量集合
curl -X POST http://localhost:3000/api/init/vector-collection

# 6. 导入历史文档
curl -X POST http://localhost:3000/api/documents/batch-import \
  -H "Authorization: Bearer ${TOKEN}" \
  -F "file=@./data/documents.xlsx"

# 7. 健康检查
curl http://localhost:3000/health

Nginx反向代理

nginx 复制代码
# /etc/nginx/conf.d/knowledge-base.conf
server {
    listen 80;
    server_name kb.example.com;

    # 前端
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # API
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 文件上传大小限制
        client_max_body_size 50M;

        # 超时设置(AI生成可能较慢)
        proxy_read_timeout 120s;
    }

    # WebSocket(AI对话流式响应)
    location /ws/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300s;
    }
}

效果数据

核心指标

指标 实施前 实施后 提升
平均检索时间 30分钟 3秒 600倍
检索准确率 60% 92% 53%
AI问答准确率 - 88% -
知识利用率 30% 85% 183%
月均文档访问量 150 425 183%
咨询响应时间 4小时 实时 ~100%

成本分析

项目 传统开发方案 本方案(低代码+AI)
开发人力 4人×3月=15万 1人×2周=1.5万
服务器 5万/年 2.4万/年
AI调用 0 6万/年
软件授权 0 1.2万/年
维护人力 5万/年 1万/年
总计 20万首年 12.1万首年
年节省 - 7.9万

踩坑记录

坑1:Chroma中文支持

问题: Chroma默认的embedding模型不支持中文

解决: 使用智谱AI的embedding-3模型替代默认模型

javascript 复制代码
// 正确做法:指定中文embedding模型
const embeddingFunction = {
  generate: async (texts) => {
    return await Promise.all(
      texts.map(text => getEmbedding(text))
    )
  }
}

await collection.add({
  embeddings: embeddings,
  // ...
})

坑2:LLM回答不稳定

问题: 同一个问题,AI有时回答好,有时回答差

解决:

  1. 降低temperature到0.3
  2. 在Prompt中强调"基于文档回答,不要编造"
  3. 设置confidence阈值,低于0.7转人工

坑3:文档分块策略

问题: 分块太大导致检索不精准,太小导致上下文缺失

解决: chunkSize=500字符 + chunkOverlap=50字符,按段落优先分割

坑4:API限流

问题: 批量向量化时API被限流

解决: 控制并发,每批10个,间隔500ms


总结

整体方案的核心是 低代码 + RAG架构

  1. 低代码 解决了前端开发慢的问题(3个月→2周)
  2. RAG架构 解决了知识检索和问答的问题(30分钟→3秒)
  3. Chroma向量数据库 解决了语义搜索的问题(60%→92%)
  4. GLM-4 解决了中文理解和生成的问题

2周搭建,年省30万+,检索效率提升360倍。

如果有技术问题,欢迎评论区交流!


技术栈:低代码平台 | Node.js | Chroma | GLM-4 | MySQL | MinIO | Redis | Docker

项目地址:GitHub链接

技术交流群:QQ群号

相关推荐
幸福在路上wellbeing3 小时前
AI 智能体开发 · Day 1 详细学习手册
人工智能·学习
phltxy3 小时前
LangChain_Agent中间件实战
人工智能·python·深度学习·语言模型·中间件·langchain
2601_961593423 小时前
视频分辨率太低?Topaz Video AI v1.6.0 让画质跃升
人工智能·macos·音视频
联盟分享专家3 小时前
联盟营销自动化指南:如何扩展您的联盟营销规模与提升转化率?
大数据·人工智能
向夏威夷 梦断明暄3 小时前
从 Bun 的 Rust 重写,看 C# 如何重建 AI 基础设施层
人工智能·rust·c#
谁看我谁 狼家二丫3 小时前
机器学习漫游(1) 基本设定
人工智能·机器学习
huashengzsj3 小时前
会期3天!2026人工智能前沿学术会议议程速览
人工智能·百度
用户298698530143 小时前
在线将 Word 文档转换为 TXT 格式:快速免费的方法
人工智能·后端
武子康3 小时前
1M Context 也会失忆:Coding Agent 为什么需要 Context Ledger
人工智能·llm·agent