用 Spring AI Alibaba 打造智能查询增强引擎

用 Spring AI Alibaba 打造智能查询增强引擎

系列导读 :在上一篇文章《基于 Spring AI Alibaba 构建混合 RAG Agent》中,我们描绘了一套融合"侦探的灵活"与"会计的严谨"的架构蓝图。其中,查询增强(Query Enhancement)被置于六大核心步骤之首。很多开发者会有疑问:"大模型上下文窗口那么大,直接把历史对话丢进去不就行了?为什么还要多此一举去改写问题?"

本文将深入代码一线,通过真实的 QueryEnhancementHook 实现,为你揭示:没有查询增强的 RAG,就像让一个记忆力超群但耳朵背的专家去工作------他记得住所有书,却听不清你在问什么

一、为什么"丰富的上下文"救不了检索?

在 RAG(检索增强生成)的流程中,存在一个致命的时序错位

  1. 用户提问:"它多少钱?"(依赖上文)
  2. 检索阶段 (Retrieval):系统拿着"它多少钱?"去向量数据库搜索。👉 失败!数据库里没有"它",只有"iPhone 15"。
  3. 生成阶段 (Generation):系统把检索到的(可能是错误的)文档 + 完整的历史对话丢给 LLM。
  4. LLM 回答:LLM 看着历史对话说:"哦,你刚才问的是 iPhone,所以它是 8999 元。"

发现问题了吗

虽然 LLM 在最后一步通过上下文猜对了答案,但检索阶段已经失败了。LLM 是基于"错误检索到的文档"在强行解释,或者完全依靠它训练时的内部知识在回答(这会导致幻觉,且无法利用企业私有知识库)。

查询增强的核心价值 ,就是在第 2 步检索发生之前 ,把"它多少钱?"改写成"iPhone 15 Pro Max 最新价格是多少?"。
只有问题清晰了,检索才能精准;只有检索精准了,RAG 才有意义

二、代码实战:Spring AI Alibaba 中的 QueryEnhancementHook

基于 Spring AI Alibaba 强大的 AgentHook 机制,我们可以轻松在 Agent 执行前拦截请求,完成"翻译"工作。以下是我们生产环境中的核心实现。

1. Hook 的定义与入口

我们定义了一个 QueryEnhancementHook,它继承自 AgentHook,并标注 @HookPositions({HookPosition.BEFORE_AGENT}),确保它在任何工具调用或检索发生之前执行。

java 复制代码
@Slf4j
@RequiredArgsConstructor
@HookPositions({HookPosition.BEFORE_AGENT})
public class QueryEnhancementHook extends AgentHook {

    private final ChatClient chatClient; // 用于执行重写的小模型

    @Override
    public String getName() {
        return "query_enhancement";
    }

    @Override
    public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
        // ... 核心逻辑见下文拆解
    }
}

2. 核心三步走策略

beforeAgent 方法中,我们并没有盲目地调用大模型,而是执行了严密的三步走策略

第一步:精准提取"当前意图"

用户的消息列表里可能包含多轮对话,我们需要锁定最后一条用户消息。

java 复制代码
// 提取历史消息
List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());

// 【关键】获取最后一个有效的用户查询
String originalQuery = messages.stream()
        .filter(msg -> msg instanceof UserMessage)
        .map(msg -> ((UserMessage) msg).getText())
        .reduce((first, second) -> second) // 取最后一条
        .orElse("");

if (originalQuery.trim().isEmpty()) {
    return CompletableFuture.completedFuture(Map.of()); // 无内容则跳过
}
第二步:注入"长期记忆" (User Profile)

这是本架构的亮点之一。传统的查询重写只依赖短期对话历史,而忽略了用户是谁

我们通过 config.store() 加载用户的长期画像(如职位、偏好、过往关注点)。用户画像更新以及持久化应该在每次与LLM交互后进行,后面会有详细介绍。

java 复制代码
// 加载长期记忆:用户档案 & 偏好
String userContext = loadUserContext(config);

// loadUserContext 内部逻辑:
// 1. 从 Store 获取 user_profiles/{userId}_profile
// 2. 获取 user_profiles/{userId}_preferences
// 3. 格式化为 prompt 的一部分,例如:
// 【用户档案】
// - 职位: 高级Java架构师
// - 偏好: 喜欢简洁的代码示例,不喜欢长篇理论

场景举例

  • 用户问:"推荐个方案。"
  • 无画像:重写为"推荐一个技术方案。"(太泛)
  • 有画像 :重写为"为高级Java架构师 推荐一个高并发微服务架构方案 ,要求简洁的代码示例。"(精准命中知识库)
第三步:LLM 智能重写 (The Magic)

构造一个精心设计的 Prompt,调用 chatClient 进行重写。这里我们采用了降级策略:如果 LLM 挂了,直接返回原查询,保证系统可用性。

java 复制代码
private String enhanceQueryLogic(String currentQuery, List<Message> history, String userContext) {
    
    // 构建对话历史上下文
    String conversationHistory = history.stream()
            .map(m -> m.getMessageType() + ": " + m.getText())
            .collect(Collectors.joining("\n"));

    // 动态指令:长文本压缩 vs 短文本补全
    String instruction = currentQuery.length() > 500 
            ? "用户输入过长,请压缩至 500 字以内,保留关键实体,消除歧义。"
            : "结合【对话历史】和【用户背景】,消除指代歧义(如'它'),补充隐含主语,使查询成为独立完整的陈述句。";

    String promptText = String.format("""
            ### 角色
            你是一个专业的查询重写专家。
            
            ### 用户背景(来自长期记忆)
            %s
            
            ### 对话历史(短期记忆)
            %s
            
            ### 当前用户输入
            %s
            
            ### 任务指令
            %s
            
            ### 输出要求
            1. 仅输出重写后的查询文本,不要任何解释。
            2. 必须利用【用户背景】补全缺失的主语或偏好。
            3. 必须保留所有具体参数(日期、ID、版本)。
            4. 如果原句已清晰,则原样输出。
            
            ### 重写后的查询
            """, userContext, conversationHistory, currentQuery, instruction);

    try {
        // 调用 LLM 执行重写
        String result = chatClient.prompt().user(promptText).call().content();
        return result != null ? result.trim() : currentQuery;
    } catch (Exception e) {
        log.warn("LLM 查询增强失败,降级使用原始查询", e);
        return currentQuery; // 故障安全
    }
}

这里会从短期记忆中加载对话历史并注入上下文,长对话可能超过 LLM 的上下文窗口。常见的解决方案包括:

  • 修剪消息。在调用 LLM 之前移除前 N 条或后 N 条消息
  • 删除消息。从 Graph 状态中永久删除消息
  • 总结消息。总结历史中较早的消息并用摘要替换它们【推荐】
  • 自定义策略。自定义策略(例如消息过滤等)

这允许 Agent 在 reasoning-acting 循环中持续跟踪对话而不超过 LLM 的上下文窗口。后面会介绍我的实现。

3. 状态更新:无缝替换

一旦获得增强后的查询,我们不是把它放在一边,而是直接替换 state 中的消息列表。这样,后续的 RAG 节点、Tool Router 拿到的就是已经"翻译"好的完美问题,无需任何额外适配。

java 复制代码
if (!enhancedQuery.equals(originalQuery)) {
    log.info("查询已增强:[{}] -> [{}]", originalQuery, enhancedQuery);
    
    // 构建新消息列表,仅替换最后一条 UserMessage
    List<Message> enhancedMessages = new ArrayList<>(messages);
    int lastUserIndex = findLastUserMessageIndex(messages); // 辅助方法:倒序查找
    
    if (lastUserIndex != -1) {
        enhancedMessages.set(lastUserIndex, new UserMessage(enhancedQuery));
    }

    // 返回修改后的 state,Spring AI 会自动应用
    return CompletableFuture.completedFuture(Map.of("messages", enhancedMessages));
}

三、效果对比:有无增强的天壤之别

让我们看两个真实场景的对比,感受查询增强的威力。

场景 A:多轮对话中的指代消解

步骤 用户行为 无增强 (传统 RAG) 有增强(混合 RAG)
Round 1 用户:"2026年的病假流程是什么?" 检索:"2026年病假流程" ✅ 成功 检索:"2026年病假流程" ✅ 成功
Round 2 用户:"那事假呢?" 检索:"那事假呢" ❌ 失败 (向量库无此句) 结果:胡乱匹配或返回空 重写:"2026年的事假申请流程是什么?" ✅ 成功 (精准命中)
Round 3 用户:"需要审批吗?" 检索:"需要审批吗" ❌ 失败 (不知道指哪个流程) 重写:"2026年事假申请流程需要审批吗?" ✅ 成功 (上下文完整)

场景 B:个性化偏好注入

维度 无增强 有增强
用户画像 (系统未知) 职位:运维工程师 ;偏好:只要 Shell 脚本,不要 Python
用户提问 "怎么重启服务?" "怎么重启服务?"
重写结果 "怎么重启服务?" "Linux 环境下 重启服务的Shell 脚本命令是什么?"
检索结果 混杂了 Java、Python、Windows 的通用教程 精准命中 Linux Shell 相关文档
最终体验 用户需要二次追问"我要 Shell 版的" 一次命中,用户感觉 AI"懂我"

四、避坑指南与最佳实践

在实现过程中,我们也踩过一些坑,总结以下几点供参考:

  1. 不要过度重写
    • 如果用户的问题已经很清晰(如"什么是 Spring Boot?"),LLM 可能会画蛇添足。
    • 对策:在 Prompt 中明确强调"如果当前输入已经非常清晰且无指代,则原样输出"。
  2. 性能与成本的平衡
    • 查询增强需要额外调用一次 LLM。
    • 对策:
      • 使用小模型(如 Qwen-Turbo 或本地部署的 7B 模型)专门做重写,速度快、成本低。
      • 配合语义缓存(本系列下一篇重点),高频问题直接缓存,根本不需要触发重写和检索。
  3. 容错设计
    • LLM 可能会超时或报错。
    • 对策 :务必像代码中那样做 try-catch 降级。增强是"锦上添花",不能因为增强失败导致整个对话不可用。Fail-fast, fallback to original.

五、结语

查询增强(Query Enhancement)绝不是多余的步骤,它是连接人类模糊语言机器精准检索的唯一桥梁。

通过 Spring AI Alibaba 的 AgentHook,我们不仅实现了指代消解,更将长期用户画像融入到了检索的源头。这使得我们的 RAG 系统不再是一个冷冰冰的搜索引擎,而是一个能听懂"弦外之音"、记得住"老客喜好"的智能助手。

下一步预告

问题清楚了,检索也精准了,但如果每次都要重新查一遍,速度够快吗?下一篇我们将深入语义缓存(Semantic Cache)的实现,揭秘如何让高频问题实现"毫秒级"响应,进一步降低 Token 成本!

相关推荐
Arva .1 小时前
Spring 的三级缓存,两级够吗
java·spring·缓存
爱喝一杯白开水2 小时前
Java 定时任务完全指南
java
njsgcs2 小时前
图卷积是如何处理不同输入长度的 消息传递
人工智能
毕设源码-郭学长2 小时前
【开题答辩全过程】以 高校自动排课系统的设计与实现为例,包含答辩的问题和答案
java
哥本哈士奇2 小时前
使用OpenClaw的Skills对接本地系统
人工智能
IT_陈寒2 小时前
SpringBoot实战:3个隐藏技巧让你的应用性能飙升50%
前端·人工智能·后端
. . . . .2 小时前
Claude Code Plugins 目录结构与加载机制
人工智能
GJGCY2 小时前
2026企业级智能体架构:记忆机制、RAG检索与任务规划对比
人工智能·经验分享·ai·智能体
indexsunny2 小时前
互联网大厂Java面试实战:从Spring Boot到微服务架构的深度解析
java·spring boot·spring cloud·kafka·prometheus·security·microservices