本文基于「灶台导航」小程序的应急处理功能,分享知识库的设计、关键词匹配算法、上下文传递等实现思路。

一、功能需求

在烹饪过程中,用户可能遇到紧急情况(如烫伤、油锅起火),需要快速获取处理方法。

用户提问:"油锅起火了怎么办?"
    │
    ▼
关键词匹配 / AI 理解
    │
    ▼
返回应急处理步骤

核心需求

  • 快速响应(秒级)
  • 准确匹配用户意图
  • 结构化展示处理步骤
  • 支持追问(上下文)

二、数据结构设计

2.1 emergencies 集合

// database/collections/emergencies.json
{
  "_id": "emerg_001",
  "category": "烫伤",              // 分类
  "keywords": ["烫伤", "烫到", "被烫", "热水烫"],  // 关键词
  "title": "烫伤应急处理",          // 标题
  "severity": "warning",           // 严重程度:warning/danger
  "steps": [                       // 处理步骤
    {
      "order": 1,
      "content": "立即用流动的冷水冲洗烫伤部位",
      "duration": "10-15分钟",
      "important": true
    },
    {
      "order": 2,
      "content": "不要涂抹牙膏、酱油等偏方",
      "important": true
    },
    {
      "order": 3,
      "content": "如有水泡,不要自行刺破",
      "important": false
    },
    {
      "order": 4,
      "content": "严重烫伤请立即就医",
      "important": true
    }
  ],
  "warnings": [                    // 注意事项
    "不要直接用冰块冷敷",
    "不要撕扯粘在伤口的衣物"
  ],
  "relatedQuestions": [            // 相关问题
    "烫伤后可以涂什么药膏?",
    "如何判断烫伤严重程度?"
  ],
  "createTime": "2026-03-01"
}

2.2 分类设计

分类 示例关键词 严重程度
烫伤 烫伤、被烫、热水 warning
烧伤 烧伤、火烫 danger
油锅起火 油锅、起火、着火 danger
刀伤 切到手、刀伤、流血 warning
食物中毒 中毒、呕吐、腹泻 danger
异物卡喉 卡住、噎住、喉咙 danger

三、关键词匹配实现

3.1 简单匹配

/**
 * 简单关键词匹配
 */
async function simpleMatch(message) {
  const db = cloud.database()

  // 获取所有应急知识
  const res = await db.collection('emergencies').get()

  for (const item of res.data) {
    for (const keyword of item.keywords) {
      if (message.includes(keyword)) {
        return item
      }
    }
  }

  return null
}

3.2 加权匹配

/**
 * 加权关键词匹配
 */
async function weightedMatch(message) {
  const db = cloud.database()
  const res = await db.collection('emergencies').get()

  let bestMatch = null
  let bestScore = 0

  for (const item of res.data) {
    let score = 0

    // 关键词匹配
    for (const keyword of item.keywords) {
      if (message.includes(keyword)) {
        // 完全匹配加高分
        if (message === keyword) {
          score += 10
        } else {
          score += keyword.length  // 按关键词长度加分
        }
      }
    }

    // 分类匹配(额外加分)
    if (message.includes(item.category)) {
      score += 5
    }

    if (score > bestScore) {
      bestScore = score
      bestMatch = item
    }
  }

  // 设置阈值,避免误匹配
  if (bestScore < 3) {
    return null
  }

  return bestMatch
}

3.3 正则表达式匹配

/**
 * 正则匹配
 */
async function regexMatch(message) {
  const db = cloud.database()
  const res = await db.collection('emergencies').get()

  for (const item of res.data) {
    // 构建正则模式
    const patterns = item.keywords.map(k => {
      // 处理特殊字符
      const escaped = k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      return escaped
    })

    const regex = new RegExp(patterns.join('|'), 'i')

    if (regex.test(message)) {
      return item
    }
  }

  return null
}

3.4 多条件匹配

/**
 * 多条件综合匹配
 */
async function comprehensiveMatch(message) {
  const db = cloud.database()
  const res = await db.collection('emergencies').get()

  const results = []

  for (const item of res.data) {
    let score = 0
    const matchedKeywords = []

    // 1. 关键词匹配
    for (const keyword of item.keywords) {
      if (message.includes(keyword)) {
        score += keyword.length * 2
        matchedKeywords.push(keyword)
      }
    }

    // 2. 分类匹配
    if (message.includes(item.category)) {
      score += 10
    }

    // 3. 标题匹配
    if (message.includes(item.title)) {
      score += 15
    }

    // 4. 同义词扩展
    const synonyms = getSynonyms(message)
    for (const syn of synonyms) {
      if (item.keywords.includes(syn)) {
        score += syn.length
      }
    }

    if (score > 0) {
      results.push({
        item,
        score,
        matchedKeywords
      })
    }
  }

  // 按分数排序
  results.sort((a, b) => b.score - a.score)

  // 返回最高分的结果
  if (results.length > 0 && results[0].score >= 3) {
    return results[0].item
  }

  return null
}

/**
 * 获取同义词
 */
function getSynonyms(word) {
  const synonymMap = {
    '烫': ['烫伤', '被烫', '热'],
    '火': ['起火', '着火', '燃烧'],
    '刀': ['切', '割', '划'],
    '中毒': ['食物中毒', '拉肚子', '呕吐']
  }

  const result = []
  for (const [key, values] of Object.entries(synonymMap)) {
    if (word.includes(key)) {
      result.push(...values)
    }
  }
  return result
}

四、云函数实现

4.1 emergencyHandler 云函数

由于紧急情况在项目初期用到极少,而且大部分情况都大同小异比如盐放多了类似的,所有我们采用云数据库做小数据量的语义匹配。

// cloudfunctions/emergencyHandler/index.js
const cloud = require('wx-server-sdk')
const { success, serverError } = require('../utils/response')

cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })

const db = cloud.database()

exports.main = async (event, context) => {
  const { action, message, emergencyId } = event

  try {
    switch (action) {
      case 'match':
        return await handleMatch(message)
      case 'detail':
        return await handleDetail(emergencyId)
      case 'search':
        return await handleSearch(message)
      case 'related':
        return await handleRelated(emergencyId)
      default:
        return await handleMatch(message)
    }
  } catch (err) {
    console.error('应急处理失败:', err)
    return serverError(err.message)
  }
}

/**
 * 匹配应急知识
 */
async function handleMatch(message) {
  if (!message) {
    return success({ match: null })
  }

  // 1. 尝试关键词匹配
  const match = await comprehensiveMatch(message)

  if (match) {
    return success({
      match: match,
      confidence: 'high',
      source: 'keyword'
    })
  }

  // 2. 降级:返回常见问题列表
  const hotRes = await db.collection('emergencies')
    .limit(5)
    .get()

  return success({
    match: null,
    suggestions: hotRes.data.map(item => ({
      id: item._id,
      title: item.title,
      category: item.category
    })),
    message: '未找到匹配的应急知识,以下是常见问题'
  })
}

/**
 * 获取详情
 */
async function handleDetail(emergencyId) {
  if (!emergencyId) {
    return success({ detail: null })
  }

  const res = await db.collection('emergencies')
    .doc(emergencyId)
    .get()

  return success({
    detail: res.data
  })
}

/**
 * 搜索
 */
async function handleSearch(keyword) {
  if (!keyword) {
    const res = await db.collection('emergencies')
      .limit(20)
      .get()
    return success({ list: res.data })
  }

  const res = await db.collection('emergencies')
    .where({
      $or: [
        { title: db.RegExp({ regexp: keyword, options: 'i' }) },
        { category: db.RegExp({ regexp: keyword, options: 'i' }) },
        { keywords: keyword }
      ]
    })
    .get()

  return success({
    list: res.data,
    keyword: keyword
  })
}

/**
 * 获取相关应急知识
 */
async function handleRelated(emergencyId) {
  // 获取当前项的分类
  const currentRes = await db.collection('emergencies')
    .doc(emergencyId)
    .get()

  if (!currentRes.data) {
    return success({ related: [] })
  }

  const category = currentRes.data.category

  // 获取同分类的其他项
  const res = await db.collection('emergencies')
    .where({
      category: category,
      _id: db.command.neq(emergencyId)
    })
    .limit(5)
    .get()

  return success({
    related: res.data
  })
}

/**
 * 综合匹配
 */
async function comprehensiveMatch(message) {
  const res = await db.collection('emergencies').get()

  let bestMatch = null
  let bestScore = 0

  for (const item of res.data) {
    let score = 0

    // 关键词匹配
    for (const keyword of item.keywords) {
      if (message.includes(keyword)) {
        score += keyword.length * 2
      }
    }

    // 分类匹配
    if (message.includes(item.category)) {
      score += 10
    }

    if (score > bestScore) {
      bestScore = score
      bestMatch = item
    }
  }

  return bestScore >= 3 ? bestMatch : null
}

五、上下文传递

5.1 对话上下文

当用户追问时,需要保持上下文:

用户:油锅起火怎么办?
AI:油锅起火处理方法:1. 立即关闭火源...
用户:可以用水浇吗?
AI:不可以!用水浇油火会导致油溅射...  ← 需要理解上下文

5.2 上下文实现

// cloudfunctions/emergencyHandler/index.js

// 存储对话上下文
const sessionCache = new Map()

exports.main = async (event, context) => {
  const { action, message, sessionId } = event

  // 获取上下文
  let sessionContext = null
  if (sessionId) {
    sessionContext = sessionCache.get(sessionId)
  }

  try {
    switch (action) {
      case 'match':
        return await handleMatchWithContext(message, sessionContext)
      // ...
    }
  } catch (err) {
    // ...
  }
}

/**
 * 带上下文的匹配
 */
async function handleMatchWithContext(message, context) {
  // 如果有上下文,先判断是否为追问
  if (context && context.lastMatch) {
    const isFollowUp = checkFollowUp(message, context.lastMatch)

    if (isFollowUp) {
      // 追问:基于上次匹配的内容回答
      const followUpAnswer = await handleFollowUp(message, context.lastMatch)
      return success(followUpAnswer)
    }
  }

  // 新问题:进行匹配
  const match = await comprehensiveMatch(message)

  if (match) {
    // 保存上下文
    const newSessionId = `session_${Date.now()}`
    sessionCache.set(newSessionId, {
      lastMatch: match,
      lastMessage: message,
      timestamp: Date.now()
    })

    return success({
      match: match,
      sessionId: newSessionId,
      confidence: 'high'
    })
  }

  // 未匹配
  return success({
    match: null,
    suggestions: await getHotEmergencies()
  })
}

/**
 * 检查是否为追问
 */
function checkFollowUp(message, lastMatch) {
  // 追问关键词
  const followUpIndicators = [
    '可以', '能', '要', '需要', '然后', '之后',
    '呢', '吗', '什么', '怎么', '为什么'
  ]

  for (const indicator of followUpIndicators) {
    if (message.includes(indicator)) {
      return true
    }
  }

  return false
}

/**
 * 处理追问
 */
async function handleFollowUp(message, lastMatch) {
  // 预设追问回复
  const followUpAnswers = {
    '水': {
      type: 'warning',
      content: '不可以用水浇灭油火!水会使油溅射,火势更大。'
    },
    '盖': {
      type: 'success',
      content: '可以用锅盖盖住,隔绝氧气灭火。'
    },
    '灭火器': {
      type: 'success',
      content: '可以使用干粉灭火器,但不能用水基灭火器。'
    }
  }

  // 检查追问内容
  for (const [key, answer] of Object.entries(followUpAnswers)) {
    if (message.includes(key)) {
      return {
        type: 'follow_up',
        relatedTo: lastMatch.title,
        answer: answer
      }
    }
  }

  // 无法回答的追问
  return {
    type: 'unknown',
    relatedTo: lastMatch.title,
    answer: {
      type: 'info',
      content: '这个问题建议您查看详细说明或咨询专业人士。'
    }
  }
}

六、前端实现

6.1 应急页面

<!-- pages/emergency/emergency.wxml -->
<page-header title="应急处理" showBack="{{true}}" />

<view class="emergency-container">
  <!-- 搜索区 -->
  <view class="search-box">
    <input 
      class="search-input"
      placeholder="描述您遇到的问题..."
      value="{{inputValue}}"
      bindinput="onInput"
      bindconfirm="onSearch"
    />
    <button class="search-btn" bindtap="onSearch">搜索</button>
  </view>

  <!-- 匹配结果 -->
  <view class="match-result" wx:if="{{matchResult}}">
    <view class="result-header {{matchResult.severity}}">
      <text class="result-title">{{matchResult.title}}</text>
      <text class="result-category">{{matchResult.category}}</text>
    </view>

    <view class="result-steps">
      <view class="step-item" wx:for="{{matchResult.steps}}" wx:key="order">
        <view class="step-number">{{item.order}}</view>
        <view class="step-content">
          <text class="{{item.important ? 'important' : ''}}">{{item.content}}</text>
          <text wx:if="{{item.duration}}" class="step-duration">约 {{item.duration}}</text>
        </view>
      </view>
    </view>

    <view class="result-warnings" wx:if="{{matchResult.warnings.length > 0}}">
      <text class="warning-title">⚠️ 注意事项</text>
      <view class="warning-item" wx:for="{{matchResult.warnings}}" wx:key="index">
        <text>• {{item}}</text>
      </view>
    </view>

    <!-- 相关问题 -->
    <view class="related-questions" wx:if="{{matchResult.relatedQuestions.length > 0}}">
      <text class="related-title">相关问题</text>
      <view 
        class="related-item" 
        wx:for="{{matchResult.relatedQuestions}}" 
        wx:key="index"
        bindtap="onRelatedTap"
        data-question="{{item}}"
      >
        {{item}}
      </view>
    </view>
  </view>

  <!-- 建议 -->
  <view class="suggestions" wx:if="{{suggestions.length > 0 && !matchResult}}">
    <text class="suggest-title">常见问题</text>
    <view 
      class="suggest-item" 
      wx:for="{{suggestions}}" 
      wx:key="_id"
      bindtap="onSuggestTap"
      data-id="{{item.id}}"
    >
      <text class="suggest-category">{{item.category}}</text>
      <text class="suggest-title">{{item.title}}</text>
    </view>
  </view>
</view>

6.2 页面逻辑

// pages/emergency/emergency.js
const { callFunction } = require('../../utils/cloud')

Page({
  data: {
    inputValue: '',
    matchResult: null,
    suggestions: [],
    sessionId: null
  },

  onLoad() {
    this.loadSuggestions()
  },

  onInput(e) {
    this.setData({ inputValue: e.detail.value })
  },

  async onSearch() {
    const message = this.data.inputValue.trim()
    if (!message) return

    wx.showLoading({ title: '搜索中...' })

    try {
      const data = await callFunction('emergencyHandler', {
        action: 'match',
        message: message,
        sessionId: this.data.sessionId
      })

      this.setData({
        matchResult: data.match,
        suggestions: data.suggestions || [],
        sessionId: data.sessionId
      })

    } catch (err) {
      wx.showToast({ title: '搜索失败', icon: 'none' })
    } finally {
      wx.hideLoading()
    }
  },

  async loadSuggestions() {
    try {
      const data = await callFunction('emergencyHandler', {
        action: 'search'
      })

      this.setData({
        suggestions: data.list.map(item => ({
          id: item._id,
          category: item.category,
          title: item.title
        }))
      })
    } catch (err) {
      console.error('加载建议失败:', err)
    }
  },

  onSuggestTap(e) {
    const id = e.currentTarget.dataset.id
    this.loadDetail(id)
  },

  async loadDetail(id) {
    try {
      wx.showLoading({ title: '加载中...' })

      const data = await callFunction('emergencyHandler', {
        action: 'detail',
        emergencyId: id
      })

      this.setData({
        matchResult: data.detail,
        inputValue: data.detail.title
      })

    } finally {
      wx.hideLoading()
    }
  },

  onRelatedTap(e) {
    const question = e.currentTarget.dataset.question
    this.setData({ inputValue: question })
    this.onSearch()
  }
})

七、结合 AI 增强

当关键词匹配不到时,可以调用 AI 进行理解:

async function handleMatch(message) {
  // 1. 先尝试关键词匹配
  const keywordMatch = await comprehensiveMatch(message)

  if (keywordMatch) {
    return {
      match: keywordMatch,
      source: 'keyword',
      confidence: 'high'
    }
  }

  // 2. 关键词匹配失败,调用 AI 理解
  try {
    const aiResult = await callAI(message)

    if (aiResult.matched) {
      // AI 识别到相关应急知识
      const detail = await db.collection('emergencies')
        .doc(aiResult.emergencyId)
        .get()

      return {
        match: detail.data,
        source: 'ai',
        confidence: 'medium'
      }
    }
  } catch (err) {
    console.error('AI 理解失败:', err)
  }

  // 3. 都失败,返回建议
  return {
    match: null,
    suggestions: await getHotEmergencies()
  }
}

async function callAI(message) {
  // 调用 DeepSeek API
  const response = await got.post('https://api.deepseek.com/chat/completions', {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`
    },
    json: {
      model: 'deepseek-chat',
      messages: [
        {
          role: 'system',
          content: '你是厨房安全助手。根据用户描述,判断是否与厨房安全相关,返回 JSON 格式。'
        },
        { role: 'user', content: message }
      ],
      response_format: { type: 'json_object' }
    },
    responseType: 'json'
  })

  return JSON.parse(response.body.choices[0].message.content)
}

八、总结

应急知识库设计要点:

要点 说明
数据结构 分类 + 关键词 + 步骤 + 注意事项
关键词匹配 加权匹配 + 阈值判断
上下文传递 sessionId + 追问识别
降级方案 热门问题推荐
AI 增强 关键词失败时调用 AI

通过关键词匹配 + AI 理解的组合方案,可以快速响应用户的应急查询需求。


作者:「倒灶了队」

项目:灶台导航 - 微信小程序

更新时间:2026-05-25

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐