意图驱动:AI 如何理解你要找什么

意图驱动:AI 如何理解你要找什么

前两期聊了记忆系统的"为什么"、"存什么"和"怎么存"。这一期聊最精巧的部分——怎么找

用户问了一个问题,系统怎么判断该去 CCB 找还是 MAGMA 找?怎么决定检索的深度和范围?怎么从两个引擎的结果中挑出最相关的?

答案藏在意图驱动路由里。

一、六种意图分类

系统将用户查询分为六类意图。这六种类型不是随意拍的,而是基于对实际使用场景的观察总结出来的。

意图 说明 示例
simple-fact 简单事实查询 "什么是 X?"、"怎么使用 Y?"
preference-recall 用户偏好回忆 "我之前说过..."、"我的偏好是..."
causal-reasoning 因果推理 "为什么...?"、"X 会导致什么?"
entity-relationship 实体关系 "X 和 Y 的关系?"、"A 与 B 的区别?"
temporal-analysis 时间线分析 "X 的发展历史?"、"时间线是怎样的?"
hybrid 混合问题 包含多种意图的复杂问题

这六种意图对应不同的检索策略。比如"用户偏好用 pnpm"——这是简单事实,CCB 里一查就有,不需要开 MAGMA。"为什么这个 bug 反复出现?"——这是因果推理,需要 MAGMA 的图谱数据来追溯因果关系。

二、双层分类机制

意图分类不是靠单一方法,而是LLM + 规则的双层机制:

用户查询
    │
    ▼
┌───────────────────┐
│ 1. LLM 意图分类   │  ← 主要方式
│   置信度 ≥ 0.7     │
└────────┬──────────┘
         │ 如果 LLM 失败或置信度不足
         ▼
┌───────────────────┐
│ 2. 规则匹配       │  ← Fallback
│   置信度 ≥ 0.5     │
└────────┬──────────┘
         │
         ▼
    缓存结果(TTL 24h)

第一层:LLM 分类

使用大模型来分析用户意图:

// LLM 意图分类
async classifyWithLLM(query: string): Promise<IntentResult> {
  const response = await this.llm.chat({
    messages: [{
      role: 'user',
      content: `分析以下查询的意图,返回 JSON:
        { category, confidence, reasoning, keywords }

        查询:${query}

        可选类别:simple-fact, preference-recall,
        causal-reasoning, entity-relationship,
        temporal-analysis, hybrid`
    }]
  })

  return JSON.parse(response.content)
  // { category: 'causal-reasoning', confidence: 0.92, ... }
}

LLM 分类的优势是准确——它能理解语义,不会被表面措辞迷惑。"这个东西怎么回事"和"为什么会出现这种情况",字面不同,但 LLM 都能识别为因果推理。

劣势是有可能失败——网络超时、模型降级、输出格式异常,都有可能。

第二层:规则匹配

基于正则表达式的模式匹配,不需要外部 API,永不失败:

// 规则分类
const rules = {
  'causal-reasoning': [
    /为什么/, /原因/, /导致/, /怎么会出现/, /为何/
  ],
  'preference-recall': [
    /我(?:之前|说过|偏好|喜欢)/, /我的偏好/, /记住/
  ],
  'temporal-analysis': [
    /历史/, /时间线/, /发展/, /演变/, /什么时候/
  ],
  'entity-relationship': [
    /(?:和|与).*(?:关系|区别|不同)/, /(?:比较|对比)/
  ],
  'simple-fact': [
    /什么是/, /怎么用/, /如何使用/, /是什么/
  ]
}

function classifyWithRules(query: string): IntentResult {
  let bestCategory = 'simple-fact'
  let bestScore = 0

  for (const [category, patterns] of Object.entries(rules)) {
    for (const pattern of patterns) {
      if (pattern.test(query)) {
        bestScore += 0.3  // 每个匹配的正则加 0.3
      }
    }
  }

  return { category: bestCategory, confidence: Math.min(bestScore, 1.0) }
}

规则分类的优势是永不失败——不依赖外部服务,不消耗 Token,毫秒级响应。劣势是覆盖面有限——没覆盖到的模式就识别不了。

双层机制的价值:LLM 处理语义理解,规则处理兜底。LLM 正常时走 LLM(准确),LLM 异常时走规则(可靠)。两层都失败?还有 24 小时缓存——相同查询不会重复分析。

三、路由决策矩阵

有了意图分类,下一步是路由决策——不同意图走不同的检索路径:

意图 策略 记忆层 图谱 预估 Token
simple-fact ccb-only L4, L5 500
preference-recall both-priority-ccb L3 800
causal-reasoning both-priority-magma L2, L3, L4 concept, relationship 1200
entity-relationship both-priority-magma L2, L3 entity, relationship 1000
temporal-analysis both-priority-ccb L2, L4 temporal 1500
hybrid both-priority-ccb L2-L5 concept, entity, relationship 2000

这个矩阵是整个意图路由系统的核心。每一行都回答三个问题:

  1. 去哪找——ccb-only 只查 CCB,both-priority-magma 查两者但以 MAGMA 为主
  2. 找哪层——不同意图关注不同记忆层,因果推理需要项目上下文(L2),偏好回忆只需要用户层(L3)
  3. 找多深——预估 Token 数从 500 到 2000,简单问题浅尝辄止,复杂问题全面深入

设计思路

  • 简单事实→ 只查 CCB,精确匹配,500 Token 够了
  • 偏好回忆→ 双引擎但 CCB 优先,因为用户记忆主要在 CCB 里
  • 因果推理→ 双引擎但 MAGMA 优先,需要图谱数据追溯因果链
  • 实体关系→ 双引擎但 MAGMA 优先,需要图谱数据查找实体联系
  • 时间线→ 双引擎但 CCB 优先,项目记忆里时间信息更丰富
  • 混合问题→ 全引擎全层,2000 Token 全面覆盖

我注意到一个有意思的细节:Token 预算差异很大。简单问题 500 Token,混合问题 2000 Token,差了四倍。这说明系统对检索成本有精细的控制——不是每次都全力以赴,而是根据意图的复杂度分配资源。

四、渐进式检索

检索不是单轮的,而是三个阶段逐步深入

┌──────────────────────────────────────┐
│ 阶段 1:ccb-fast                      │
│ 系统:CCB                             │
│ 限制:5 条                            │
│ 超时:100ms                           │
│ 目的:先用 CCB 快速匹配               │
└──────────────┬───────────────────────┘
               │ 结果不够?
               ▼
┌──────────────────────────────────────┐
│ 阶段 2:magma-extend                  │
│ 系统:CCB + MAGMA                     │
│ 限制:10 条                           │
│ 超时:300ms                           │
│ 目的:扩展到 MAGMA 语义搜索           │
└──────────────┬───────────────────────┘
               │ 结果还不够?
               ▼
┌──────────────────────────────────────┐
│ 阶段 3:full-fusion                   │
│ 系统:CCB + MAGMA + FusionVerifier    │
│ 限制:全部                            │
│ 超时:800ms                           │
│ 目的:启用融合验证器,全面检索         │
└──────────────────────────────────────┘

为什么要渐进式? 因为检索成本差异很大:

  • 阶段 1 只查 CCB 文件,100ms 就够,成本极低
  • 阶段 2 加上向量搜索,300ms,成本中等
  • 阶段 3 启用 FusionVerifier(用另一个 LLM 验证结果),800ms,成本最高

如果每次都走阶段 3,简单问题也要 800ms 和大量 Token。渐进式检索让简单问题快速返回,只有真正复杂的查询才走完全流程。

这个设计很像搜索引擎的策略:先用倒排索引快速召回,再用语义模型精排,最后用验证模型确认。层层递进,成本和效果平衡。

五、语义去重

两个引擎并行检索,很可能返回重复结果。CCB 通过关键词找到的,MAGMA 通过语义找到的,可能是同一条记忆。

去重分两层:

5.1 精确去重

基于归一化键名 + 内容哈希:

function exactDuplicate(a, b) {
  // 1. 键名归一化:转小写,去标点
  const keyA = a.name.toLowerCase().replace(/[^\w]/g, '')
  const keyB = b.name.toLowerCase().replace(/[^\w]/g, '')
  if (keyA === keyB) return true

  // 2. 内容哈希比较
  const hashA = simpleHash(a.content)
  const hashB = simpleHash(b.content)
  if (hashA === hashB) return true

  return false
}

精确去重很快,O(1) 的哈希比较。但它只能识别"几乎一样"的内容——表述不同但意思相同的,它识别不了。

5.2 语义去重

基于文本相似度,用 Jaccard + LCS 混合:

function semanticDuplicate(a, b) {
  const wordsA = new Set(a.content.split(/\s+/))
  const wordsB = new Set(b.content.split(/\s+/))

  // Jaccard 相似度(词集合交集/并集)
  const intersection = new Set([...wordsA].filter(w => wordsB.has(w)))
  const union = new Set([...wordsA, ...wordsB])
  const jaccard = intersection.size / union.size

  // 最长公共子序列相似度
  const lcsLength = lcs(a.content, b.content).length
  const lcs = lcsLength / Math.max(a.content.length, b.content.length)

  // 混合权重:Jaccard 0.4 + LCS 0.6
  const score = jaccard * 0.4 + lcs * 0.6

  return score >= 0.85  // 阈值 0.85 以上视为重复
}

为什么 Jaccard 和 LCS 混合?

  • Jaccard 只看词集合,速度快但忽略语序。"猫追老鼠"和"老鼠追猫"的 Jaccard 是 1.0,但意思完全不同
  • LCS 考虑语序,能区分"猫追老鼠"和"老鼠追猫",但计算慢

混合使用:Jaccard 负责快速初筛,LCS 负责精确判断。权重上 LCS 占 0.6,因为语序信息更重要。

六、排序算法

去重之后,结果需要排序。不是简单的按相关性排序,而是综合多个因素:

finalScore = baseScore × priorityMultiplier + layerBonus + recencyBonus

四个因子

  • baseScore:原始相关性分数(0-1),来自 CCB 或 MAGMA
  • priorityMultiplier:来源优先级乘数。CCB 优先时 ×1.1,MAGMA 优先时 ×0.9,相等 ×1.0。这个乘数体现了路由策略——CCB 优先的查询,CCB 结果的权重提高 10%
  • layerBonus:记忆层加分。匹配的层越相关,加分越多,最多 0.1。比如因果推理需要项目记忆(L2),L2 的结果加分
  • recencyBonus:时效性加分。90 天内线性衰减,最多 0.05。越新的记忆越有价值

我特别欣赏这个公式的设计——它不是单一维度的排序,而是把相关性、来源偏好、记忆层、时效性四个正交维度综合起来。每个维度的影响都是有限的(乘数最多 1.1,加分最多 0.15),不会让某一个因素完全主导排序。

七、Token 预算控制

排序完之后,最后一步是Token 预算控制——不是所有结果都能塞进上下文窗口:

function applyTokenBudget(results, maxTokens = 4000) {
  let accumulated = 0
  const selected = []

  for (const result of results) {
    // 每个记忆的 token 不超过总预算/记忆数
    const perMemoryCap = Math.floor(maxTokens / results.length)
    const tokens = Math.min(estimateTokens(result), perMemoryCap)

    if (accumulated + tokens <= maxTokens) {
      selected.push(result)
      accumulated += tokens
    } else {
      // 尝试截断最后一个记忆来塞进去
      const remaining = maxTokens - accumulated
      if (remaining > 0 && result.content.length > 100) {
        selected.push(truncateMemory(result, remaining))
      }
      break
    }
  }
  return selected
}

这个算法有几个有意思的地方:

1. 动态分配——不是每个记忆固定 500 Token,而是 总预算 / 记忆数。10 条记忆,每条 400 Token;5 条记忆,每条 800 Token。

2. 截断而非丢弃——预算不够时,最后一个记忆不是直接丢弃,而是截断。保留开头部分,标注"(内容已截断)"。

3. 按排序顺序保留——排在前面的优先保留,排在后面的先被截断或丢弃。这保证了高相关性的记忆不会因为排在后面而被误杀。

Token 预算控制是记忆系统的最后一道闸门。不管前面检索到多少结果,最终注入上下文的 Token 数是有限的。这个限制迫使系统在检索阶段就必须做好排序和去重——因为最终只有前几条能被保留。

八、完整流程

把上面所有环节串起来,一个完整的检索流程是这样的:

用户提问
    │
    ▼
┌─────────────────────┐
│ 1. 意图分类          │  LLM + 规则双层
│   → causal-reasoning │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ 2. 路由决策          │  查矩阵表
│   → both-priority-   │
│     magma, L2-L4,    │
│     concept+relation │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ 3. 渐进式检索        │  三阶段逐步深入
│   ccb-fast → magma-  │
│   extend → full-     │
│   fusion             │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ 4. 结果融合          │  统一格式 + 去重
│   精确 + 语义两层    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ 5. 综合排序          │  相关性 + 优先级 + 层 + 时效
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ 6. Token 预算控制    │  动态分配 + 截断
└──────────┬──────────┘
           │
           ▼
    注入到 Claude 上下文

六个步骤,每一步解决一个问题:意图分类解决"找什么",路由决策解决"去哪找",渐进式检索解决"怎么找",结果融合解决"合在一起",综合排序解决"哪个优先",Token 预算控制解决"能塞多少"。

九、设计洞察

回顾整个意图驱动检索系统,有几个设计洞察值得提炼:

1. 意图决定一切。 同一个记忆库,不同意图走不同路径。这不是过度设计,而是对"没有银弹"的承认——简单问题不需要复杂检索,复杂问题不值得快速返回。

2. 双层降级。 LLM → 规则 → 缓存,三层兜底。好的系统设计要假设每个环节都可能失败,并为失败做好准备。

3. 渐进式投入。 先花 100ms 试 CCB,不够再加 MAGMA,还不够再上 FusionVerifier。检索成本与查询复杂度匹配,不浪费。

4. 多维度排序。 不是单一按相关性排,而是综合相关性、来源、层、时效。每个维度的影响有限,避免单一因素主导。

5. 硬约束兜底。 Token 预算是硬约束,不管前面检索到多少结果,最终注入的 Token 数有限。这个约束倒逼前面的环节必须做好质量。


这一期拆解了意图驱动检索的完整流程。下一期聊记忆系统的"自我进化"——Dream 自动整理机制、配置体系,以及 Shared Memory System 的代码实战。

系列