意图驱动: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 |
这个矩阵是整个意图路由系统的核心。每一行都回答三个问题:
- 去哪找——
ccb-only只查 CCB,both-priority-magma查两者但以 MAGMA 为主 - 找哪层——不同意图关注不同记忆层,因果推理需要项目上下文(L2),偏好回忆只需要用户层(L3)
- 找多深——预估 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 的代码实战。
系列:
- 上一篇:双引擎架构:文件 vs 向量数据库
- 下一篇:自我进化:AI 记忆的自动整理与巩固