资讯动态

Spring AI Graph实战:构建集成RAG与Supervisor路由的智能体工作流

发布时间:2026/8/11 7:35:30 来源:尧图企业网站定制
1. 项目概述当RAG遇上Agent一次关于“图”的探索最近在折腾一个挺有意思的东西叫Spring AI Graph。这玩意儿不是我们平时理解的图表或者数据结构里的图而是一种构建AI应用的新范式。简单来说它把复杂的AI任务拆解成一个个独立的“节点”比如一个RAG查询、一个代码生成器、一个决策器然后用“边”把这些节点连接起来形成一个有向的工作流。你可以把它想象成一个高级版的流程图只不过每个节点背后都是一个实实在在的AI模型或工具整个图能自动执行完成从输入到输出的复杂任务。我这次的目标是从零开始构建一个包含RAG检索增强生成子图和Supervisor监督者路由的智能体。听起来有点绕我换个说法。我想做一个能“思考”的问答系统用户问一个问题系统不是直接去大模型里搜答案而是先派一个“侦察兵”RAG子图去知识库里检索相关资料然后把资料和问题一起交给一个“指挥官”Supervisor。这个指挥官再根据问题的类型和内容决定是让“侦察兵”再深入查查还是调用另一个“专家”比如代码生成节点来处理或者直接给出最终答案。这个“指挥官”就是Supervisor它的决策过程就是路由。这个组合非常实用。RAG负责提供精准、实时的外部知识解决大模型“幻觉”和知识陈旧的问题Supervisor则负责协调和决策让整个系统不再是简单的单次问答而具备了多步推理和任务分解的能力。比如用户问“如何用Spring Boot实现一个文件上传接口并优化其性能”系统可以先通过RAG检索到基础的实现代码和常见的性能瓶颈然后Supervisor判断这个问题涉及“代码实现”和“性能优化”两个子任务从而可能路由到不同的处理节点最终给出一个综合性的、分步骤的解决方案。然而理想很丰满现实很骨感。Spring AI Graph作为一个较新的特性官方文档更像是一个“概念展示”很多深坑需要自己踩。尤其是在将RAG作为一个子图嵌入并让Supervisor根据条件动态路由到它时我遇到了不少官方指南里没写的“惊喜”。这篇文章就是我这次从零搭建到最终跑通Supervisor路由的完整踩坑记录希望能帮你绕过我走过的弯路。2. 核心概念与架构设计拆解在动手写代码之前我们必须把几个核心概念和它们之间的关系理清楚。Spring AI Graph的模型主要借鉴了LangGraph等框架的思想但用Spring的风格进行了封装。2.1 图的构成要素Node, Edge, GraphState首先图由三个基本要素构成节点Node这是图里的工作单元。在Spring AI Graph中一个节点通常是一个实现了SupplierActionRequest、FunctionActionRequest, ActionResponse或ConsumerActionRequest接口的Bean。更常见的是我们使用Bean注解将一个方法声明为节点这个方法接收一个GraphState返回一个更新后的GraphState。边Edge边决定了执行流的方向。在Spring AI Graph中边是通过“条件”来定义的。最常见的是Conditional和Switch。Conditional用于二选一Switch用于多路选择。边的逻辑基于GraphState中的某个值来判断下一步该去哪个节点。图状态GraphState这是一个贯穿整个图执行过程的共享上下文。它是一个类似Map的结构你可以在里面存放任何需要跨节点传递的数据比如用户的原始问题、RAG检索到的文档、大模型的回复、中间决策结果等。每个节点读取并修改GraphState从而推动流程前进。我设计的这个智能体其核心架构是一个两层图结构主图Supervisor Graph负责最高级别的协调和路由。它包含一个Supervisor节点也是一个特殊的子图和几个工具节点如直接回答节点、代码生成节点。Supervisor节点的作用是分析GraphState中的问题决定下一步该调用哪个工具。子图RAG SubGraph这是一个封装好的、功能独立的图专门负责接收一个问题从向量数据库中检索相关文档然后调用大模型生成一个基于这些文档的答案。这个子图会被主图中的Supervisor作为一个“工具”来调用。这种设计的优势在于解耦和复用。RAG子图可以独立开发、测试和优化。主图无需关心RAG内部复杂的检索和生成逻辑只需把它当做一个黑盒工具来调用。同时Supervisor可以根据情况灵活决定是否调用、以及如何调用这个工具。2.2 Supervisor与工具节点的交互模式这里有一个关键点需要理解在Spring AI Graph的语境下Supervisor本身通常是一个Agent节点它内置了一个大语言模型LLM。这个LLM的职责是进行“思考”和“规划”。其工作流程通常是这样的GraphState中包含了用户的input问题。Supervisor节点被激活它内部的LLM会审视GraphState分析问题。LLM根据预定义的“工具列表”Tool List进行判断。每个工具都有一个名称和描述。LLM会想“用户这个问题我该用哪个工具来处理”LLM输出一个决策格式通常是类似{action: tool_name, action_input: ...}。这个决策会被Spring AI Graph框架解析。框架根据action的值将执行流路由到对应的工具节点并将action_input作为参数传递给该工具节点。工具节点执行完毕将结果写回GraphState。执行流通常会再次回到Supervisor节点通过边连接让它根据工具执行的结果决定下一步是继续调用工具还是结束流程并给出最终答案。在这个交互中我们的RAG子图就需要被包装成一个Tool并注册到Supervisor的上下文中这样Supervisor内部的LLM才知道有这么一个工具可用。2.3 技术栈选型与前期准备为了完成这个项目我选择了以下技术栈并说明了理由Spring Boot 3.x Spring AI: 基础框架。Spring AI提供了对主流大模型和向量数据库的统一抽象是构建AI应用的首选。OpenAI GPT-4o / Anthropic Claude 3 Haiku: 作为Supervisor和RAG生成答案的LLM。选择它们是因为API稳定、能力强大且Spring AI原生支持。对于Supervisor需要较强的推理和规划能力GPT-4o是优选对于RAG的答案生成性价比高的Haiku也足够。PGVector Spring AI VectorStore: 作为知识库的存储和检索后端。PGVector是PostgreSQL的扩展部署简单与Spring生态集成好适合中小规模的知识库。Spring AI的VectorStore接口让切换后端变得容易。Spring AI Graph (Experimental): 核心库。需要注意的是截至我实践时Graph模块仍处于experimental实验性阶段API可能会有变动这也是坑多的原因之一。注意实验性功能意味着你可能需要引入特定的快照版本Snapshot仓库并且需要仔细查看对应版本Spring AI的官方文档或源码示例因为主版本文档可能更新不及时。在开始编码前请确保你的pom.xml或build.gradle中正确引入了spring-ai-graph依赖并配置好了AI模型如OpenAI的API密钥。3. 构建RAG子图封装一个独立的检索增强生成单元我们的第一步是先打造一个可靠、可复用的RAG子图。这个子图的功能是输入一个查询字符串输出一个基于知识库的答案。3.1 定义子图的状态State子图也需要自己的GraphState用于在子图内部传递数据。我们定义一个RagState记录类import java.util.List; import java.util.Map; public record RagState( String userQuery, // 用户输入的问题 ListDocument retrievedDocuments, // 检索到的文档列表 String aiResponse, // AI生成的最终答案 MapString, Object metadata // 可扩展的元数据如检索耗时、模型使用情况等 ) { // 提供一个便捷的构造方法用于初始化 public static RagState fromQuery(String query) { return new RagState(query, List.of(), null, Map.of()); } }这里使用record是为了不可变性和简洁性。Document是Spring AI中表示文档的标准类通常包含文本内容和元数据。3.2 实现核心节点检索与生成接下来我们创建两个节点并将它们组装成图。节点1检索节点RetrieveNode这个节点的职责是调用VectorStore进行相似性搜索。import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.graph.api.GraphNode; import org.springframework.ai.graph.api.GraphState; import org.springframework.stereotype.Component; import java.util.List; Component public class RetrieveNode implements GraphNodeRagState { private final VectorStore vectorStore; public RetrieveNode(VectorStore vectorStore) { this.vectorStore vectorStore; } Override public RagState apply(GraphStateRagState graphState) { RagState currentState graphState.getState(); String query currentState.userQuery(); // 执行相似性检索这里假设返回前5个最相关的文档 ListDocument documents vectorStore.similaritySearch(query, 5); // 创建新的状态更新检索到的文档 RagState newState new RagState( currentState.userQuery(), documents, currentState.aiResponse(), currentState.metadata() ); return newState; } }节点2生成节点GenerateNode这个节点利用检索到的文档和原始问题调用大模型生成答案。import org.springframework.ai.chat.ChatClient; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.ai.graph.api.GraphNode; import org.springframework.ai.graph.api.GraphState; import org.springframework.stereotype.Component; import java.util.Map; Component public class GenerateNode implements GraphNodeRagState { private final ChatClient chatClient; private final SystemPromptTemplate systemPromptTemplate; public GenerateNode(ChatClient chatClient) { this.chatClient chatClient; // 定义一个系统提示词模板指导AI基于上下文回答 this.systemPromptTemplate new SystemPromptTemplate( 你是一个专业的助手请严格根据以下提供的上下文信息来回答问题。 如果上下文信息不足以回答问题请如实告知“根据现有资料无法回答该问题”。 上下文信息 {context} 用户问题{question} ); } Override public RagState apply(GraphStateRagState graphState) { RagState currentState graphState.getState(); ListDocument documents currentState.retrievedDocuments(); String question currentState.userQuery(); // 将检索到的文档合并成上下文字符串 String context documents.stream() .map(Document::getContent) .collect(Collectors.joining(\n\n)); // 构造Prompt Prompt prompt systemPromptTemplate.create(Map.of( context, context, question, question )); // 调用大模型 String aiResponse chatClient.call(prompt).getResult().getOutput().getContent(); // 创建新的状态更新AI回复 RagState newState new RagState( currentState.userQuery(), currentState.retrievedDocuments(), aiResponse, currentState.metadata() ); return newState; } }3.3 组装子图并暴露为Tool这是将子图封装成可调用工具的关键步骤。我们需要定义一个Graph并将其包装成一个Spring Bean同时还要实现Tool接口以便Supervisor识别。import org.springframework.ai.graph.api.*; import org.springframework.ai.graph.api.builder.GraphBuilder; import org.springframework.ai.tool.Tool; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class RagGraphConfig { Bean public GraphRagState ragGraph(RetrieveNode retrieveNode, GenerateNode generateNode) { // 使用GraphBuilder构建一个简单的线性图检索 - 生成 return GraphBuilder.RagStatebuilder() .startNode(retrieveNode) // 起始节点是检索 .next(generateNode) // 检索完成后执行生成 .build(); } Bean public Tool ragTool(GraphRagState ragGraph) { return new Tool() { Override public String getName() { return query_knowledge_base; // 工具名称Supervisor将根据这个名称来调用 } Override public String getDescription() { return 查询内部知识库获取与问题相关的权威信息。当用户的问题涉及内部文档、产品手册、历史记录等时使用此工具。; } Override public Object execute(MapString, Object inputs) { // Tool的execute方法被Supervisor调用。 // inputs 参数包含了Supervisor LLM认为应该传递给工具的参数。 // 通常Supervisor会把action_input作为question键的值传过来。 String question (String) inputs.get(question); if (question null || question.isBlank()) { throw new IllegalArgumentException(Tool query_knowledge_base requires a question input.); } // 1. 初始化子图状态 RagState initialState RagState.fromQuery(question); // 2. 执行子图 GraphStateRagState resultState ragGraph.execute(initialState); // 3. 返回子图的最终结果AI生成的答案 return resultState.getState().aiResponse(); } }; } }关键点与踩坑记录1Tool的输入输出这里是我遇到的第一个坑。Tool.execute(Map inputs)方法的inputs参数内容完全依赖于Supervisor内部LLM的理解。你需要确保在Supervisor的提示词或工具描述中清晰地说明这个工具需要什么参数。我最初没有明确说明导致LLM有时传一个Map过来有时只传一个字符串造成类型转换错误。后来在工具描述中明确写“接受一个名为question的字符串参数”才稳定下来。另外execute方法的返回值会成为GraphState的一部分供Supervisor后续判断。这里我直接返回了字符串答案你也可以返回一个更结构化的对象。4. 构建主图与Supervisor实现智能路由决策有了RAG工具接下来我们构建主图核心是创建一个具备路由能力的Supervisor节点。4.1 定义主图状态MainState主图的状态需要包含更丰富的信息以支持多轮对话和工具调用。import java.util.List; import java.util.Map; public record MainState( String userInput, // 用户最新输入 String conversationHistory, // 简化的对话历史实际项目可用更复杂结构 String latestToolResponse, // 上一个工具调用的结果 String supervisorThought, // Supervisor的“思考”过程便于调试 String finalAnswer, // 最终给用户的答案 ListString usedTools // 记录已使用的工具防止循环调用 ) { public static MainState initial(String input) { return new MainState(input, , , , , List.of()); } }4.2 配置SupervisorAgent节点在Spring AI Graph中我们通常通过配置一个ChatClient即LLM并为其提供Tool列表来创建一个Agent这个Agent就可以作为Supervisor节点。import org.springframework.ai.chat.ChatClient; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.ai.graph.api.GraphNode; import org.springframework.ai.graph.api.GraphState; import org.springframework.ai.tool.Tool; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.stream.Collectors; Component public class SupervisorNode implements GraphNodeMainState { private final ChatClient chatClient; // 用于推理的LLM private final ListTool tools; // 可用的工具列表包括我们的RAG Tool private final SystemPromptTemplate systemPrompt; public SupervisorNode(ChatClient chatClient, ListTool tools) { this.chatClient chatClient; this.tools tools; // 构建Supervisor的系统提示词这是路由决策的核心 String toolDescriptions tools.stream() .map(tool - - tool.getName() : tool.getDescription()) .collect(Collectors.joining(\n)); this.systemPrompt new SystemPromptTemplate( 你是一个任务调度员Supervisor。你的职责是分析用户的问题并决定使用哪个工具来解决问题或者直接给出答案。 你可以使用的工具如下 %s 请遵循以下规则 1. 仔细分析用户的问题和对话历史。 2. 如果问题需要查询内部知识、文档、数据请使用query_knowledge_base工具。 3. 如果问题是简单的问候、感谢或无需工具就能回答的常识问题请直接给出友好、简洁的回答。 4. 如果上一个工具调用返回了结果请结合该结果和用户原始问题判断是否需要继续使用其他工具或可以给出最终答案。 5. 你的输出必须是严格的JSON格式且只包含以下两个字段 - thought: 你的思考过程解释你为什么做出这个决定。 - action: 决定执行的动作。如果是使用工具值为工具名称如query_knowledge_base如果是直接回答值为final_answer。 - action_input: 传递给工具或作为最终答案的输入内容。如果是final_answer这里就是你的回答文本。 当前对话历史{history} 上一个工具结果{last_tool_result} 用户当前问题{input} .formatted(toolDescriptions)); } Override public MainState apply(GraphStateMainState graphState) { MainState currentState graphState.getState(); // 构建Prompt Prompt prompt systemPrompt.create(Map.of( history, currentState.conversationHistory(), last_tool_result, currentState.latestToolResponse() ! null ? currentState.latestToolResponse() : 无, input, currentState.userInput() )); // 调用LLM进行决策 String llmResponse chatClient.call(prompt).getResult().getOutput().getContent(); // 解析LLM的JSON输出这里需要简单的JSON解析实际可使用Jackson // 假设我们有一个简单的解析方法 parseLlmResponse(llmResponse)返回一个决策对象Decision Decision decision parseLlmResponse(llmResponse); // 更新状态记录Supervisor的“思考” MainState newState new MainState( currentState.userInput(), currentState.conversationHistory(), currentState.latestToolResponse(), decision.thought(), // 记录思考过程 decision.action().equals(final_answer) ? decision.actionInput() : currentState.finalAnswer(), currentState.usedTools() ); return newState; } // 简化的决策解析逻辑 private Decision parseLlmResponse(String response) { // 实际情况中你需要一个健壮的JSON解析器来处理LLM可能的不稳定输出。 // 这里为演示进行简单字符串匹配。 if (response.contains(\action\: \final_answer\)) { // 提取action_input... return new Decision(直接回答的思考过程, final_answer, 这是最终答案...); } else if (response.contains(\action\: \query_knowledge_base\)) { // 提取action_input作为question... return new Decision(需要查询知识库, query_knowledge_base, 用户的具体问题...); } // 默认回退 return new Decision(无法解析默认直接回答, final_answer, 我暂时无法处理这个问题。); } record Decision(String thought, String action, String actionInput) {} }关键点与踩坑记录2提示词工程与输出解析这是最核心也最容易出问题的地方。提示词必须极其清晰你必须明确告诉LLM输出格式JSON并定义好action字段的可能值你的工具名称和final_answer。模糊的指令会导致解析失败。LLM输出不稳定即使指令清晰LLM偶尔也会输出格式不正确、包含额外解释文字的JSON。因此parseLlmResponse方法必须非常健壮要能处理各种边缘情况比如使用正则表达式提取JSON块或者使用Jackson的JsonNode进行宽松解析。工具描述要准确Tool的getDescription()方法内容至关重要。Supervisor的LLM主要靠这个描述来判断何时调用该工具。描述应简洁说明工具的用途、适用场景和输入参数。4.3 实现工具执行节点与最终回答节点主图中还需要两个节点工具执行节点ToolExecutorNode根据Supervisor的决策action调用对应的Tool。最终回答节点FinalAnswerNode当Supervisor决定action为final_answer时将答案整理并结束流程或返回给用户。// ToolExecutorNode Component public class ToolExecutorNode implements GraphNodeMainState { private final MapString, Tool toolMap; // 工具名称到Tool实例的映射 public ToolExecutorNode(ListTool tools) { this.toolMap tools.stream() .collect(Collectors.toMap(Tool::getName, tool - tool)); } Override public MainState apply(GraphStateMainState graphState) { MainState state graphState.getState(); // 假设上一个节点Supervisor已将决策信息存入state的某个字段这里简化处理。 // 实际中可能需要一个单独的字段如 pendingAction 来传递。 String action query_knowledge_base; // 假设从state中获取 String actionInput state.userInput(); // 假设从state中获取 Tool tool toolMap.get(action); if (tool null) { throw new IllegalStateException(Unknown tool: action); } Object toolResult tool.execute(Map.of(question, actionInput)); // 更新状态记录工具调用结果和已使用工具 ListString newUsedTools new ArrayList(state.usedTools()); newUsedTools.add(action); return new MainState( state.userInput(), state.conversationHistory(), toolResult.toString(), // 工具执行结果 state.supervisorThought(), state.finalAnswer(), newUsedTools ); } } // FinalAnswerNode Component public class FinalAnswerNode implements GraphNodeMainState { Override public MainState apply(GraphStateMainState graphState) { MainState state graphState.getState(); // 这个节点可能只是简单地将最终答案标记为就绪或者进行最后的格式化。 // 在我们的简单流程中Supervisor节点已经将最终答案写入了state.finalAnswer()。 // 所以这个节点可以什么都不做或者记录日志。 System.out.println(最终答案已生成: state.finalAnswer()); return state; // 返回未修改的状态或进行最终处理 } }4.4 组装主图连接Supervisor、工具与决策边最后我们用GraphBuilder将所有这些节点连接起来形成完整的工作流。这里的边路由逻辑是核心。import org.springframework.ai.graph.api.builder.GraphBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MainGraphConfig { Bean public GraphMainState mainSupervisorGraph( SupervisorNode supervisorNode, ToolExecutorNode toolExecutorNode, FinalAnswerNode finalAnswerNode) { return GraphBuilder.MainStatebuilder() .startNode(supervisorNode) // 1. 首先进入Supervisor进行决策 // 2. 根据Supervisor决策的结果需体现在state中进行条件路由 .conditional( state - { // 这里需要根据state中的决策信息来判断 // 例如检查state中某个字段是否为final_answer return shouldGoToFinalAnswer(state); }, finalAnswerNode, // 如果为true前往最终答案节点 toolExecutorNode // 如果为false前往工具执行节点 ) // 3. 从工具执行节点出来后应该再次回到Supervisor让它评估工具结果 .from(toolExecutorNode).next(supervisorNode) // 4. 构建图 .build(); } private boolean shouldGoToFinalAnswer(MainState state) { // 实现你的判断逻辑例如 // return “final_answer”.equals(state.supervisorDecisionAction()); // 这里需要你有一个方法从state中提取出Supervisor的决策动作。 // 这是一个关键设计点决策信息如何存储在state中并在节点间传递 return false; // 示例 } }关键点与踩坑记录3状态管理与路由条件这是第二个大坑。在图中节点之间通过GraphState通信。Supervisor的决策去A工具还是B工具还是直接回答必须被写入GraphState后续的Conditional边才能读取这个决策并正确路由。我最初的设计是让SupervisorNode在state里设置一个nextAction字段。但问题来了Conditional边的判断函数PredicateMainState是在当前节点执行前就被评估用于决定当前节点执行完后该去哪。这意味着从SupervisorNode到Conditional边之间state还没有被SupervisorNode更新解决方案有两种使用Switch边而非ConditionalSwitch边允许根据state的值进行多路分发但其判断逻辑可能仍需仔细设计。将决策作为节点输出的一部分并通过特殊的边类型处理更常见和灵活的模式是让SupervisorNode的输出不仅包含更新后的state还包含一个“建议的下一个节点”的标识。Spring AI Graph的Branch注解或更底层的API支持这种模式。这需要更深入地研究框架的进阶用法。在我的实践中我暂时采用了一种简化方案在SupervisorNode中根据决策直接修改state中的一个routeTo字段然后在Conditional边中判断这个字段。虽然理论上存在上述时序问题但在线性执行且SupervisorNode是唯一修改者的简单场景下可以工作。但这并非最佳实践。5. 调试、常见问题与优化实录将整个图跑起来之后才是真正挑战的开始。下面是我遇到的一些典型问题及解决思路。5.1 问题一Spring AI Graph版本兼容性与Bean注入错误症状启动应用时报错提示GraphBuilder类找不到或者GraphNode注解无法识别或者Tool注入失败。排查检查pom.xml确认spring-ai-graph的版本与spring-ai-core等其它模块版本匹配。实验性模块的版本号可能比较特殊如1.0.0-SNAPSHOT。确保你添加了正确的Spring Snapshot仓库地址如果需要。检查所有GraphNode的实现类是否都被Spring管理即加了Component等注解。ToolBean必须被正确注入到SupervisorNode和ToolExecutorNode中。使用ListTool进行集合注入时确保所有Tool都是Spring Bean。解决锁定一个经过测试的Spring AI版本组合。例如我当时使用的是spring-ai:1.0.0-M5的一系列模块。在SupervisorNode和配置类中使用Autowired或构造器注入ListTool。如果仍有问题尝试将Graph的构建移到Configuration类中并使用Bean方法显式创建节点和边而不是完全依赖类路径扫描。5.2 问题二Supervisor LLM不按预期调用工具症状Supervisor总是输出final_answer即使明显应该使用query_knowledge_base工具。排查检查工具描述Tool.getDescription()是否足够清晰LLM是否理解这个工具的用途尝试将描述写得更具体例如“当用户的问题涉及到公司内部的产品文档、技术规范、历史案例、政策制度等非公开通用知识时使用此工具进行查询。”检查系统提示词给Supervisor的指令是否足够强硬在提示词中明确优先级“首先考虑是否可以使用query_knowledge_base工具。如果问题可能涉及任何内部信息必须先使用该工具。”检查LLM的思考过程将SupervisorNode中解析出的thought字段打印出来或记录到日志。看看LLM到底是怎么“想”的。也许它认为问题太简单或者它误解了“内部知识”的范围。提供少量示例Few-Shot在系统提示词中加入一两个例子展示什么样的问题该调用工具什么样的问题直接回答。解决优化提示词工程。这是Agent类应用的核心。我最终的提示词包含了更明确的规则和例子。考虑使用能力更强的LLM作为Supervisor如从Haiku切换到GPT-4o推理能力有显著提升。在Tool.execute方法开头加日志确认它是否被调用。5.3 问题三子图RAG执行结果未正确返回主图症状ToolExecutorNode调用了ragTool.execute()但主图state中的latestToolResponse为空或不是预期的答案。排查在ragTool.execute()方法内部加日志确认子图ragGraph.execute()是否被成功调用以及返回的resultState.getState().aiResponse()是什么。检查RagState在子图各个节点间的传递是否正确。确保GenerateNode确实将生成的答案写入了state。检查ToolExecutorNode中是否正确地将工具执行结果toolResult.toString()设置到了MainState的对应字段中。解决添加详细的日志记录跟踪GraphState在每个节点执行前后的变化。确保MainState是一个record每次更新都要创建新实例避免状态污染。考虑使用一个共享的上下文对象或ThreadLocal来辅助调试仅用于调试生产环境慎用。5.4 问题四图陷入无限循环症状应用不停执行日志显示在Supervisor-ToolExecutor-Supervisor之间循环无法结束。排查检查结束条件Supervisor在什么情况下应该输出final_answer你的提示词和解析逻辑是否确保了这一点例如当工具返回“根据资料无法回答”时Supervisor是否应该直接给出最终答案而不是再次尝试记录已使用工具我在MainState中设计了usedTools列表。在SupervisorNode的思考中可以加入一条规则“如果同一个工具已被使用过且其返回结果没有提供新信息则倾向于给出最终答案或尝试其他路径。”设置最大迭代次数在图的外部调用层比如你的Controller或Service设置一个循环执行图的计数器超过一定次数如10次后强制跳出并返回超时错误这是一个重要的安全防护。解决在Supervisor的系统提示词中增加防循环指令“注意避免重复调用同一工具处理相同的问题。如果工具未能提供新信息请尝试给出基于现有信息的最佳答案或告知用户能力限制。”在MainState中维护usedTools并在SupervisorNode的提示词模板中传入这个信息。务必在调用图的入口处设置最大步数限制。5.5 性能优化与经验心得向量检索优化RAG子图的性能瓶颈通常在检索。确保你的向量数据库有合适的索引并且检索时限制返回数量topK。对于简单问题topK3可能就够了。提示词模板化将Supervisor和RAGGenerateNode的系统提示词放在配置文件如application.yml或数据库中便于随时调整而无需重新部署。异步执行如果图中某些节点是IO密集型如调用外部API、查询数据库考虑将其改为异步节点可以提高整体吞吐量。Spring AI Graph对异步有一定的支持。状态序列化如果你的应用需要持久化工作流状态例如支持长时间运行的多轮对话GraphState中的对象需要是可序列化的。record类型通常可以但要注意其中包含的复杂对象。测试策略为每个GraphNode编写单元测试模拟输入GraphState验证输出。为整个Graph编写集成测试使用真实的LLM和向量数据库或它们的Mock测试端到端的流程。构建Spring AI Graph应用是一个既需要软件工程思维又需要提示词工程技巧的过程。它不像传统的微服务开发那样有固定的套路很多设计需要根据具体的业务逻辑和LLM的特性进行反复调整和测试。这次从0到1集成RAG子图和Supervisor路由的经历让我深刻体会到在AI工程化的道路上清晰的架构设计、严谨的状态管理和耐心的调试优化与算法模型本身同样重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价