Day 19 --- Query 改写 & 意图识别
主题 :让模糊查询变精准,让检索策略变智能
端口 :8091
技术栈:Spring Boot 3.4.3 + LangChain4j 1.13.1 + Java 17
1. Day 19 比 Day 18 多了什么
| 维度 | Day 18 | Day 19 |
|---|---|---|
| Query 处理 | 无(直接用原始 query) | LLM 改写(指代消解 + 省略补全) |
| 意图理解 | 无(固定三路) | LLM 四分类 + 意图感知路由 |
| 检索策略 | 固定三路全开 | 动态策略(按意图选择路径) |
| API 端点 | 3 个 | 7 个(新增 4 个) |
| Pipeline | 召回 → 融合 | 改写 → 分类 → 策略 → 召回 → 融合 |
| 降级处理 | 无 | LLM 失败降级(返回原始 query) |
2. 核心概念速览
Query 改写
- 指代消解:"昨天那个产品怎么样" → "码哥AI中台产品功能和评价"
- 省略补全:"SDK初始化" → "SDK初始化步骤方法教程"
- 降级:LLM 失败时返回原始 query
意图识别
- 四分类:knowledge(知识库)/ order(订单)/ chat(闲聊)/ complaint(投诉)
- 策略路由 :
- knowledge → 三路全开
- order → 关键词加重(权重×2)+ ES
- chat → 仅向量检索
- complaint → 仅 ES BM25
全链路 Pipeline
原始Query → LLM改写 → 意图识别 → 策略选择 → 并行召回 → RRF融合 → Top-K结果
3. 快速启动
bash
# 1. 进入项目目录
cd day19-query-rewrite
# 2. 确保 PostgreSQL + Elasticsearch 已启动
# 3. 编译运行(首次启动自动初始化知识库)
mvn clean spring-boot:run
# 4. 访问教学前端
open http://localhost:8091
前置依赖:
-
PostgreSQL 14+(已安装 PGVector 扩展)
-
Elasticsearch 8.x
-
Docker 快速启动 ES:
bashdocker run -d --name es -p 9200:9200 \ -e "discovery.type=single-node" \ -e "xpack.security.enabled=false" \ elasticsearch:8.15.0
4. 项目结构
day19-query-rewrite/
├── pom.xml
├── docs/
│ ├── day19-ai-concepts-teaching.md # 教学文档(5个核心概念)
│ └── day19-reference.md # 代码参考速查
├── README.md
└── src/main/
├── java/com/day19/demo/
│ ├── Day19Application.java # 启动入口
│ ├── config/
│ │ ├── ChatModelConfig.java # LLM + Embedding Bean
│ │ ├── ElasticsearchConfig.java # ES 客户端配置
│ │ └── DataInitializer.java # 知识库自动初始化
│ ├── core/
│ │ ├── HybridSearchResult.java # 统一检索结果 DTO
│ │ └── RrfExperimentResult.java # RRF 实验 DTO
│ ├── dto/
│ │ └── ApiResult.java # 统一 API 响应
│ ├── rag/
│ │ ├── VectorSearchService.java # PGVector 向量检索
│ │ ├── KeywordSearchService.java # N-gram 关键词检索
│ │ ├── ElasticsearchService.java # ES BM25 检索
│ │ ├── QueryRewriteService.java # ⭐ Query 改写
│ │ ├── IntentClassifierService.java # ⭐ 意图分类
│ │ └── MultiRecallService.java # ⭐ 意图感知多路召回
│ └── controller/
│ └── SearchController.java # REST API(7个端点)
└── resources/
├── application.yml # 配置(端口 8091)
├── schema.sql # PGVector 建表
├── data.sql # 种子数据(空)
├── static/
│ └── index.html # 教学前端(3个Tab)
└── docs/
├── 码哥科技.txt
├── SDK集成指南.txt
├── API文档.txt
└── 部署指南.txt
5. 核心实现
5.1 Query 改写服务
java
@Service
public class QueryRewriteService {
public String rewrite(String originalQuery) {
// 1. 构建 System Prompt(指代消解 + 省略补全规则)
// 2. 调用 LLM 改写
// 3. 降级:LLM 失败返回原始 query
}
public String rewriteWithHistory(String query, List<String> recentMessages) {
// 带对话历史的上下文感知改写
}
}
System Prompt 核心:
- 指代消解:替换"那个"、"它"、"这个"
- 省略补全:补充缺失的动作和场景
- 关键词扩展:适度添加同义词
- 只输出改写文本,不要解释
5.2 意图识别服务
java
@Service
public class IntentClassifierService {
public IntentResult classify(String query) {
// 1. 构建 System Prompt(四分类规则)
// 2. 调用 LLM 返回 JSON
// 3. 解析 JSON → IntentResult
}
public RetrievalStrategy getRetrievalStrategy(IntentResult intent) {
// knowledge → 三路全开
// order → 关键词×2 + ES
// chat → 仅向量
// complaint → 仅 ES
}
}
5.3 意图感知多路召回
java
@Service
public class MultiRecallService {
public Map<String, Object> searchIntentAware(String query, String tableName) {
// 步骤1:意图分类
// 步骤2:Query 改写(knowledge 意图)
// 步骤3:策略选择(按意图选路径)
// 步骤4:并行召回(只执行选中的路径)
// 步骤5:RRF 融合
}
}
6. 配置说明
yaml
server:
port: 8091 # Day 19 端口
# Query 改写 & 意图识别配置(Day 19 新增)
query:
rewrite:
enabled: true # 是否启用改写
use-history: true # 是否使用对话历史
intent:
enabled: true # 是否启用意图识别
default-strategy: knowledge # 降级时的默认意图
# 多路召回配置(沿用 Day 18)
multi-recall:
rrf:
default-k: 60 # RRF 默认 k 值
experiment-ks: 10,30,60,100 # k 值实验列表
recall-top: 20 # 每条路径取 Top-N
final-top: 5 # 最终返回条数
7. API 端点详解
| # | 端点 | 用途 | 测试命令 |
|---|---|---|---|
| 1 | GET /search |
三路召回+RRF(标准) | curl "localhost:8091/search?query=产品" |
| 2 | GET /search/intent-aware |
意图感知检索 | curl "localhost:8091/search/intent-aware?query=SDK" |
| 3 | GET /query/rewrite |
Query改写 | curl "localhost:8091/query/rewrite?query=那个产品" |
| 4 | GET /query/classify |
意图分类 | curl "localhost:8091/query/classify?query=订单" |
| 5 | GET /experiment/k-values |
k值实验 | curl "localhost:8091/experiment/k-values?query=部署" |
| 6 | GET /search/raw |
三路原始数据 | curl "localhost:8091/search/raw?query=API" |
| 7 | GET /demo/intent-pipeline |
全链路演示 | curl "localhost:8091/demo/intent-pipeline?query=产品" |
8. 测试指南
测试 Query 改写
bash
# 模糊查询 → 改写
curl "http://localhost:8091/query/rewrite?query=昨天那个产品怎么样"
# 预期:rewritten = "码哥AI中台产品功能特点和评价"
curl "http://localhost:8091/query/rewrite?query=怎么部署"
# 预期:rewritten = "生产环境部署步骤和方法"
测试意图识别
bash
curl "http://localhost:8091/query/classify?query=SDK初始化失败"
# 预期:intent=knowledge, confidence>0.8
curl "http://localhost:8091/query/classify?query=我的订单到哪了"
# 预期:intent=order, keywords包含"订单"
curl "http://localhost:8091/query/classify?query=为什么这么慢"
# 预期:intent=complaint, keywords包含"投诉"或"反馈"
测试意图感知检索
bash
# knowledge 意图 → 三路全开
curl "http://localhost:8091/search/intent-aware?query=SDK集成方法"
# chat 意图 → 仅向量路
curl "http://localhost:8091/search/intent-aware?query=你好"
# 对比两种方式的结果差异
测试全链路 Pipeline
bash
curl "http://localhost:8091/demo/intent-pipeline?query=码哥科技的产品"
# 查看 6 个步骤的详细结果和每步耗时
9. 踩坑记录
9.1 LLM 改写输出格式不稳定
现象 :LLM 有时输出 "改写后:XXX" 而不是纯文本
解决:在代码中 strip 前缀/引号,并设置长度上限(超过原始 3 倍 → 降级)
9.2 意图分类 JSON 解析失败
现象 :LLM 返回的 JSON 被 Markdown 代码块包裹
解决:用正则去掉 ```json ... ``` 包装再解析
9.3 改写+分类串行太慢
现象 :两次 LLM 调用总耗时 2-3 秒
原因 :改写和分类都依赖 deepseek-v3,API 延迟约 1-1.5s/次
优化:如果改写结果对分类影响不大,可以并行调用
9.4 chat 意图只开向量路可能不够
现象 :闲聊时向量检索返回的结果可能不相关
原因 :闲聊 query(如"今天怎么样")与知识库内容语义距离远
策略:chat 意图可以完全不检索,直接由 LLM 生成回复
9.5 order 意图模拟数据不足
现象 :学习项目的知识库中没有订单数据
说明 :Day 19 的 order/complaint 意图主要用于展示意图路由机制,
实际效果依赖于业务数据的质量
10. 面试怎么说
"用户问题很模糊怎么办?"
"我们在检索前加了 Query 改写管道。用 LLM 做指代消解和省略补全,
比如 '那个产品怎么样' 会改写为 '码哥AI中台产品功能和评价'。
改写是异步的、带降级的------LLM 挂了不影响检索,直接用原 query。"
"怎么提高检索命中率?"
"我们引入了意图感知路由:先分类用户意图(知识库/订单/闲聊/投诉),
不同意图走不同的检索策略。比如订单意图加重关键词路、闲聊意图只走向量路。
从固定三路升级到意图驱动的动态路径,减少了噪音召回。"
"意图识别怎么做的?"
"基于 Prompt 的分类方法。用 System Prompt 告诉 LLM 四种意图的定义和判断规则,
让 LLM 输出 JSON。优点是零样本,不需要训练数据;缺点是依赖 LLM 调用延迟。
生产环境可以用 BERT 分类器替代,延迟降低到 10ms。"
"Pipeline 的瓶颈在哪?"
"瓶颈在 LLM 调用------改写+分类两次调用占 90% 的耗时。优化方向:
- 缓存常见 query 的改写结果;2) 改用更快的本地模型;3) 改写和分类可以并行。"