查询规划
Query Planning 是 RAG 检索前处理层。它只回答一个问题:
原始用户问题进入
Retriever之前,要不要先变成一条或多条更适合检索的 query?
这一层不负责多路召回、不负责 rerank、不负责回答生成。它在 AI4J 里的位置是:
RagQuery(original + optional history)
-> RagQueryPlanner(optional)
-> Retriever
-> Reranker
-> RagContextAssembler
默认不启用 planner;如果你不配置,rag.search(query) 会保持原行为。
1. 为什么不是 QueryTransformer
这一版故意放在 RAG 里,并命名为 RagQueryPlanner:
- rewrite、multi-query、HyDE、step-back 都是检索前策略,不是普通文本润色。
- 有些策略会产出多条 query,不是
String -> String的单次转换。 - 最终仍要保留原 query,给 rerank、上下文组装和回答生成使用。
所以它的产物不是一个新字符串,而是一个检索计划:
public interface RagQueryPlanner {
RagQueryPlan plan(RagQuery query) throws Exception;
}
RagQueryPlan 里可以有一条或多条 RagQueryVariant。
2. 默认执行语义
当 DefaultRagService 配了 planner 后:
- 先把原始
RagQuery交给RagQueryPlanner;如果RagQuery.history存在,内置 planner 会把它作为对话上下文。 - planner 返回一个
RagQueryPlan。 - 对每个
RagQueryVariant,SDK 复制原RagQuery,只替换query字段,然后执行底层Retriever。 - 多个 variant 的命中用 RRF 风格的 rank fusion 去重融合。
Reranker仍然使用原始 query。RagResult.query、RagContextAssembler仍然使用原始 query。- 如果 planner 抛异常或没有返回可用 query,SDK 回退到原始 query,并在
RagTrace.queryPlan里标记 fallback。
也就是说,调用方仍然只写:
RagResult result = rag.search(RagQuery.builder()
.query("原始用户问题")
.dataset("knowledge-base")
.embeddingModel("text-embedding-3-small")
.topK(8)
.finalTopK(4)
.build());
不会要求业务侧手动套 PlanningRetriever。
3. 最小使用方式
如果你想开箱即用地用模型做检索前改写,可以先创建 ModelRagQueryPlanner:
RagQueryPlanner planner = aiService.getModelRagQueryPlanner(
PlatformType.OPENAI,
"gpt-4o-mini"
);
RagService rag = aiService.getRagService(
PlatformType.OPENAI,
vectorStore,
planner
);
这个默认只做 REWRITE。它会多一次模型调用,把追问、省略、口语化 query 改成一条独立检索 query。
多轮对话场景不要再造 RagMemory,直接复用 core 的 ChatMemory:
ChatMemory memory = new InMemoryChatMemory(new MessageWindowChatMemoryPolicy(6));
memory.addUser("我想接入 ChatFire 视频生成");
memory.addAssistant("可以先接 OpenAI-compatible videos");
RagResult result = rag.search(RagQuery.builder()
.query("那 Suno 呢?")
.history(memory.getItems())
.dataset("ai4j-docs")
.build());
history 应该是不包含当前 query 的最近对话或摘要;如果历史很长,用 MessageWindowChatMemoryPolicy 或 SummaryChatMemoryPolicy 先收敛。
如果你想完全控制策略,也可以自己实现 RagQueryPlanner,然后配进 DefaultRagService:
Retriever retriever = new DenseRetriever(embeddingService, vectorStore);
RagQueryPlanner planner = new RagQueryPlanner() {
@Override
public RagQueryPlan plan(RagQuery query) {
return RagQueryPlan.of(query.getQuery(), Arrays.asList(
RagQueryVariant.rewrite("员工福利政策有哪些"),
RagQueryVariant.stepBack("员工福利制度")
));
}
};
RagService rag = new DefaultRagService(
retriever,
new NoopReranker(),
new DefaultRagContextAssembler(),
planner
);
如果你走 AiService 默认工厂,也可以只用重载入口:
RagService rag = aiService.getRagService(
PlatformType.OPENAI,
vectorStore,
planner
);
如果确实需要 multi-query / HyDE / step-back,可以显式指定策略:
RagQueryPlanner planner = aiService.getModelRagQueryPlanner(
PlatformType.OPENAI,
"gpt-4o-mini",
Arrays.asList(RagQueryVariantType.REWRITE, RagQueryVariantType.STEP_BACK),
3,
true
);
ModelRagQueryPlanner 不会用一个大 prompt 一次生成所有策略。显式传多种策略时,它会按策略分别调用模型:
REWRITE -> 一条独立检索 query
MULTI_QUERY -> 多条替代表达 query
HYDE -> 一段假想答案/文档文本
STEP_BACK -> 一条更高层背景 query
这样 prompt 目标更清楚;代价是多策略会带来多次模型调用。
4. 四种常见策略怎么表达
4.1 Rewrite
用于把省略、追问、口语化问题改成独立检索 query。
return RagQueryPlan.single(
query.getQuery(),
RagQueryVariant.rewrite("AI4J RAG 如何配置向量检索")
);
适合:
- 多轮对话里的“那怎么接?”
- 用户输入很口语化
- query 中有代词、省略和上下文依赖
4.2 Multi-query expansion
用于从不同表达角度召回同一问题的相关文档。
return RagQueryPlan.of(query.getQuery(), Arrays.asList(
RagQueryVariant.multiQuery("AI4J RAG 向量检索配置"),
RagQueryVariant.multiQuery("AI4J VectorStore search topK filter"),
RagQueryVariant.multiQuery("AI4J 知识库问答 检索参数")
));
适合:
- 文档里同一概念有多种叫法
- 只靠一条 query 召回不稳定
- 需要覆盖中英文、缩写、旧术语
4.3 HyDE
HyDE 通常让模型先生成一段“假想答案/假想文档”,再拿这段文本去检索。
在 AI4J 里,它就是一个 HYDE 类型的 query variant:
return RagQueryPlan.single(
query.getQuery(),
RagQueryVariant.hyde("AI4J 的 RAG 配置通常包括 embedding model、VectorStore、topK、filter 和 context assembler。")
);
适合:
- 原 query 太短,语义信号不足
- 文档更像答案段落,而不是问题标题
- dense retrieval 更吃语义上下文
4.4 Step-back query
Step-back 先抽一个更高层的问题,再检索背景知识。
return RagQueryPlan.of(query.getQuery(), Arrays.asList(
RagQueryVariant.rewrite("AI4J RAG 如何配置 Pinecone 检索"),
RagQueryVariant.stepBack("AI4J RAG 检索链路包含哪些组件")
));
适合:
- 用户问题太具体,直接检索容易漏掉背景文档
- 需要先找原则、架构、总览类知识
- 细节文档和概念文档分散在不同页面
5. 和 Hybrid Retrieval 的区别
这两个能力经常被混在一起,但它们不是同一层:
| 能力 | 输入 | 做什么 | 位置 |
|---|---|---|---|
RagQueryPlanner | 一条原始 query + 可选 history | 产出一条或多条检索 query | Retriever 之前 |
HybridRetriever | 一条 query | 调多个 retriever 并融合结果 | Retriever 层 |
Reranker | 原 query + 候选 hits | 对候选结果重新排序 | 检索之后 |
如果同时使用,顺序是:
原 query
-> planner 生成多个 query variants
-> 每个 variant 调一次 base retriever
-> planner 层融合 variant 结果
-> reranker 用原 query 重排
-> assembler 用原 query 组装上下文
如果 base retriever 本身是 HybridRetriever,那就是:
多个 query variants × 多个 retrievers
这会增加成本和延迟,应该只在召回质量确实需要时启用。
6. Trace 怎么看
开启 includeTrace 后,RagTrace 会包含:
queryPlanplanningDurationMsretrievedHitsrerankedHits
planner 异常时不会中断检索,会回退原 query:
RagTrace trace = result.getTrace();
if (trace.getQueryPlan() != null && trace.getQueryPlan().isFallback()) {
System.out.println(trace.getQueryPlan().getFallbackReason());
}
7. 什么时候不要用
不要默认给每个 RAG 都加 planner。它会增加额外 LLM 调用、延迟和不确定性。
优先在这些场景启用:
- 召回明显受 query 表达影响
- 用户输入大量是追问、省略、短句,且调用方能提供
RagQuery.history - 文档术语和用户术语经常不一致
- 需要 multi-query / HyDE / step-back 改善召回
如果只是格式清理,例如去多余空格、全角半角归一,通常没必要上 planner;在业务入参处做轻量 normalize 就够了。
8. 最该记住的结论
RagQueryPlanner 是 RAG 检索前处理,不是 agent planner,也不是通用文本改写器。
它的正确使用方式是:
- 原 query 保留
- 对话历史复用
ChatMemoryItem,不新增 RAG 专用 memory - planner 只产出 retrieval variants
- SDK 内部执行并融合 variants
- rerank 和最终上下文仍回到原 query
这样既能支持 rewrite、multi-query、HyDE、step-back,又不会把 RAG 主链路变成一个过重的 agent runtime。