一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Spring AI Graph:零基础构建Supervisor首部曲_RAG子图与Supervisor路由实践避坑指南

时间:2026-05-30 14:15:01 编辑:袖梨 来源:一聚教程网

智能路由系统Supervisor通过统一入口自动分发请求至RAG/代码审查/通用对话三大子模块,实现精准任务处理。本文将深入解析其核心架构与实现细节。

一个入口,自动分发到 RAG / 代码审查 / 通用对话三个子图——这就是 Supervisor 做的事。

整体架构

系统根据用户输入内容自动判断任务类型,将请求智能路由至对应处理模块。

一、RAG 子图:intent→route→search→rerank→generate

1.1 状态定义

系统采用状态机模式进行流程控制,核心状态定义如下:

java

public class DreamSaaSOverAllState extends BaseOverAllState {
    public static final String userInput = "userInput";
    public static final String queryIntent = "queryIntent";        // keyword / semantic / hybrid
    public static final String queryIntentSource = "queryIntentSource"; // llm / heuristic / default
    public static final String searchResult = "searchResult";
    public static final String rerankedResult = "rerankedResult";
    public static final String finalAnswer = "finalAnswer";
    public static final String errorMessage = "errorMessage";
    public static final String route = "route";                   // Supervisor 路由:ragRoute / reviewCodeRoute / ch@tRoute    // ReAct 专用
    public static final String reasoningTrace = "reasoningTrace";
    public static final String reactRound = "reactRound";
    public static final String reactAction = "reactAction";       // FINAL_ANSWER / CONTINUE_THINKING
    public static final String reactDraftAnswer = "reactDraftAnswer";
}

状态键采用统一管理机制,确保子图间数据传递准确无误。其中route字段专用于路由控制,reactDraftAnswer则用于暂存ReAct模式的中间结果。

1.2 意图分析:keyword 还是 hybrid?

系统采用两级策略进行意图判断:

java

private ClassifyOutcome classify(String userInput) {
    if (!StringUtils.hasText(userInput)) {
        return new ClassifyOutcome(INTENT_HYBRID, "default");
    }
    try {
        ChatClient client = ch@tClientSelector.selectClient(null);
        String raw = client.prompt()
                .system(SYSTEM_PROMPT)
                .user("用户 query:n" + userInput)
                .call()
                .content();
        String intent = normalizeIntent(raw);
        return new ClassifyOutcome(intent, "llm");
    } catch (RuntimeException ex) {
        // LLM 挂了走规则兜底
        String intent = heuristicClassify(userInput);
        return new ClassifyOutcome(intent, "heuristic");
    }
}static String heuristicClassify(String userInput) {
    String q = userInput.trim();
    // 短词 + 非问句 → keyword
    if (q.length() <= 24 && !looksLikeQuestion(q)) {
        return INTENT_KEYWORD;
    }
    // 含疑问词/问号 → hybrid
    if (containsAny(q, "什么", "如何", "怎么", "为什么", "为何", "哪些", "是否", "吗", "?", "?")) {
        return INTENT_HYBRID;
    }
    // 含中文 → hybrid
    if (q.matches("(?s).*[u4e00-u9fff].*")) {
        return INTENT_HYBRID;
    }
    // 纯英文数字 → keyword
    if (q.matches("[A-Za-z0-9_.-/]+")) {
        return INTENT_KEYWORD;
    }
    return INTENT_HYBRID;
}private record ClassifyOutcome(String intent, String source) {}

提示词设计简洁高效,确保输出标准化:

plaintext

你是检索路由分类器。根据用户 query 判断检索策略,只输出一个词:
- keyword:专有名词、产品名、类名、API、错误码、缩写、短术语 lookup
- hybrid:需要理解语义的一般问题、操作步骤、原因解释、对比分析
禁止输出其它文字。

1.3 条件边路由:addConditionalEdges

系统核心亮点在于动态路由机制:

java

stateGraph.addConditionalEdges(
    RagQaGraphNames.INTENT_ANALYZE,
    AsyncEdgeAction.edge_async(retrievalRouteNode),  // 条件边决策器
    Map.of(
        IntentAnalyzeNode.INTENT_KEYWORD, RagQaGraphNames.KEYWORD_SEARCH,
        IntentAnalyzeNode.INTENT_HYBRID, RagQaGraphNames.HYBRID_SEARCH));

addConditionalEdges方法接收三个关键参数:

  1. 当前节点标识:指定决策点
  2. 边动作实现:返回目标节点标识
  3. 映射关系:将决策结果映射到实际处理节点

java

public String apply(OverAllState state) {
    String intent =
            Objects.toString(
                    state.value(DreamSaaSOverAllState.queryIntent, IntentAnalyzeNode.INTENT_HYBRID), "")
                    .trim();
    // 返回值匹配 addConditionalEdges 的 Map key,决定下一步去哪
    return IntentAnalyzeNode.INTENT_KEYWORD.equals(intent)
            ? IntentAnalyzeNode.INTENT_KEYWORD
            : IntentAnalyzeNode.INTENT_HYBRID;
}

该机制实现了节点执行后的动态路径选择,是图编排能力的核心体现。

1.4 完整 RAG 子图配置

java

@Bean
public CompiledGraph ragQaSubGraph() throws GraphStateException {
    // 1. 定义状态策略(Replace 覆盖,不是累加)
    KeyStrategyFactory keyStrategyFactory = () -> {
        HashMap strategies = new HashMap<>();
        strategies.put(userInput, new ReplaceStrategy());
        strategies.put(queryIntent, new ReplaceStrategy());
        strategies.put(searchResult, new ReplaceStrategy());
        // ... 其他字段
        return strategies;
    };    StateGraph stateGraph = new StateGraph(keyStrategyFactory);
    
    // 2. 注册节点
    stateGraph.addNode(INTENT_ANALYZE, AsyncNodeAction.node_async(intentAnalyzeNode));
    stateGraph.addNode(KEYWORD_SEARCH, AsyncNodeAction.node_async(keywordSearchNode));
    stateGraph.addNode(HYBRID_SEARCH, AsyncNodeAction.node_async(hybridSearchNode));
    stateGraph.addNode(RERANK, AsyncNodeAction.node_async(rerankNode));
    stateGraph.addNode(GENERATE, AsyncNodeAction.node_async(generateNode));    // 3. 定义边
    stateGraph.addEdge(START, INTENT_ANALYZE);
    
    // 4. 条件边:intent 分析结果决定走哪条检索路径
    stateGraph.addConditionalEdges(
        INTENT_ANALYZE,
        AsyncEdgeAction.edge_async(retrievalRouteNode),
        Map.of(INTENT_KEYWORD, KEYWORD_SEARCH, INTENT_HYBRID, HYBRID_SEARCH));
    
    // 5. 后续固定路径
    stateGraph.addEdge(KEYWORD_SEARCH, RERANK);
    stateGraph.addEdge(HYBRID_SEARCH, RERANK);
    stateGraph.addEdge(RERANK, GENERATE);
    stateGraph.addEdge(GENERATE, END);    return stateGraph.compile();
}

实际数据处理流程如下:

plaintext

START → [intentAnalyze] → 
        ├─ keyword → [keywordSearch] → [rerank][generate] → END
        └─ hybrid → [hybridSearch] → [rerank][generate] → END

二、Supervisor 主图:意图分发到子图

2.1 IntentRouterNode:比 RAG 更粗粒度的分类

主图与子图的意图识别存在粒度差异:

  1. RAG子图:区分keyword/hybrid检索策略
  2. Supervisor:区分ragRoute/reviewCodeRoute/ch@tRoute任务类型

java

private static final String system_prompt = """
    你是一个专业的意图分析助手,根据用户输入的问题分析用户的意图
    输出下面枚举值的其中一个,不能包含其他内容
    ragRoute、reviewCodeRoute、ch@tRoute,分别对应:文档检索、代码审查、大模型对话
    必须严格遵守要求,只能输出 ragRoute、reviewCodeRoute、ch@tRoute 这三个值的其中一个
    """;@Override
public Map<String, Object> apply(OverAllState state) {
    // 规则优先,避免 LLM 波动
    String ruleRoute = ruleBasedRoute(userInput);
    if (ruleRoute != null) {
        return Map.of(DreamSaaSOverAllState.route, ruleRoute);
    }
    // LLM 兜底
    String content = client.prompt().system(system_prompt).user(userInput).call().content();
    return Map.of(DreamSaaSOverAllState.route, normalizeIntent(content));
}static String ruleBasedRoute(String userInput) {
    if (!StringUtils.hasText(userInput)) {
        return null;
    }
    String q = userInput.trim();
    if (containsAny(q, "查文档", "检索", "知识库", "RAG", "rag", "文档搜索", "搜文档", "查资料")) {
        return SupervisorGraphNames.RAG_ROUTE;
    }
    if (containsAny(q, "代码审查", "code review", "review code", "审查代码", "CR ", " cr")) {
        return SupervisorGraphNames.REVIEW_CODE_ROUTE;
    }
    return null; // 走 LLM
}

2.2 IntentRouteDispatcher:条件边决定去哪个子图

java

@Override
public String apply(OverAllState state) {
    String route = state.value(DreamSaaSOverAllState.route, "");
    if (route.contains(SupervisorGraphNames.RAG_ROUTE)) return SupervisorGraphNames.RAG_ROUTE;
    if (route.contains(SupervisorGraphNames.REVIEW_CODE_ROUTE)) return SupervisorGraphNames.REVIEW_CODE_ROUTE;
    return SupervisorGraphNames.CHAT_ROUTE; // 默认走通用对话
}

2.3 Supervisor 图配置

java

@Bean
public CompiledGraph supervisorGraph(
        IntentRouterNode intentRouterNode,
        IntentRouteDispatcher intentRouteDispatcher,
        CodeReviewSubGraphStubNode codeReviewSubGraphStubNode,
        @Qualifier("ragQaSubGraph") CompiledGraph ragQaSubGraph,
        @Qualifier("generalChatSubGraph") CompiledGraph generalChatSubGraph) {    StateGraph stateGraph = new StateGraph(keyStrategyFactory);
    
    // 注册节点(子图作为节点嵌入)
    stateGraph.addNode(DreamSaaSOverAllState.route, AsyncNodeAction.node_async(intentRouterNode));
    stateGraph.addNode(SupervisorGraphNames.RAG_ROUTE, ragQaSubGraph);        // RAG子图
    stateGraph.addNode(SupervisorGraphNames.REVIEW_CODE_ROUTE, AsyncNodeAction.node_async(codeReviewSubGraphStubNode)); // 暂用占位
    stateGraph.addNode(SupervisorGraphNames.CHAT_ROUTE, generalChatSubGraph); // Chat子图    // 定义边
    stateGraph.addEdge(START, DreamSaaSOverAllState.route);
    
    // 条件边:路由结果决定去哪个子图
    stateGraph.addConditionalEdges(
        DreamSaaSOverAllState.route,
        AsyncEdgeAction.edge_async(intentRouteDispatcher),
        Map.of(
            SupervisorGraphNames.RAG_ROUTE, SupervisorGraphNames.RAG_ROUTE,
            SupervisorGraphNames.REVIEW_CODE_ROUTE, SupervisorGraphNames.REVIEW_CODE_ROUTE,
            SupervisorGraphNames.CHAT_ROUTE, SupervisorGraphNames.CHAT_ROUTE));    // 每个子图结束后统一到 END
    stateGraph.addEdge(SupervisorGraphNames.RAG_ROUTE, END);
    stateGraph.addEdge(SupervisorGraphNames.REVIEW_CODE_ROUTE, END);
    stateGraph.addEdge(SupervisorGraphNames.CHAT_ROUTE, END);    return stateGraph.compile();
}

设计亮点在于子图可作为节点嵌入主图,Supervisor只需关注任务分发,无需了解子图内部实现细节。

三、Chat 子图:ReAct 模式

Chat模块采用ReAct交互模式:

plaintext

START → [think] → [act][route]
                        ├─ CONTINUE → [think] (循环,最多2轮)
                        └─ FINAL_ANSWER → [format] → END

java

// Chat 子图配置
stateGraph.addNode(ChatSubGraphNames.THINK, AsyncNodeAction.node_async(ch@tReActThinkNode));
stateGraph.addNode(ChatSubGraphNames.ACT, AsyncNodeAction.node_async(ch@tReActActNode));
stateGraph.addNode(ChatSubGraphNames.FORMAT, AsyncNode