概述

工作流是以相对固化的模式来人为的拆解任务,将一个大的任务拆解为一个固化的有多个分支的流程。工作流的优势是确定性强,模型作为流程中的一个结点起到的更多是一个分类决策的职责,因此它更适合意图识别等类别属性强的应用场景。

工作流也有明显的劣势,它要求开发人员对业务流程有深刻的理解,整个流程是由人绘制的,模型在其中更多的只是内容生成、总结、分类识别的作用,并不能最大化利用模型的推理能力,因此很多人诟病这种模式是不够智能的。

用 Spring AI Alibaba Graph 可以轻松开发工作流,声明不同的结点,并将结点串联成一个流程图。下面以一个经典的报告生成流程为例:
该图由Spring AI Alibaba Graph生成

pom依赖

下面添加了开发的基础依赖,如果不想使用阿里百炼平台,可以查看spring ai官网更换其它平台的依赖

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
     <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
 </dependency>
 <dependency>
     <groupId>com.alibaba.cloud.ai</groupId>
     <artifactId>spring-ai-alibaba-graph-core</artifactId>
 </dependency>
 <dependency>
     <groupId>com.google.code.gson</groupId>
     <artifactId>gson</artifactId>
     <version>2.10.1</version>
 </dependency>

模型客户端配置

首先配置百炼平台的key,框架会自动创建DashScopeChatModel,这是最底层的模型类

server:
  port: 9820
spring:
  ai:
    dashscope:
      api-key: sk-xxxxxxxxxxxxxxxxx
      chat:
        options:
          model: qwen-plus-latest
          temperature: 0.7

再创建ChatClient ,我这里采用了自定义的方式创建,这种方式更加灵活,可以配置模型客户端的日志、提示词、记忆等功能

@Bean
public ChatClient qwenPlusNoMemoryClient(){
     ChatClient.Builder builder = ChatClient.builder(dashScopeChatModel);
     // 指定模型
     builder.defaultOptions(DashScopeChatOptions.builder().withModel("qwen-plus-latest").build());
     // 系统提示
     builder.defaultSystem("你是一个乐于助人的智能助手");
     // 日志打印
     SimpleLoggerAdvisor loggerAdvisor = SimpleLoggerAdvisor.builder()
             .requestToString(request -> {
                 List<Message> messageList = request.prompt().getInstructions();

                 List<Map<String,String>> textList = messageList.stream().map(message -> Map.of("type", message.getMessageType().name(), "content", message.getText())).toList();

                 return JSON.toJSONString(textList);
             })
             .responseToString(response -> JSON.toJSONString(response.getResult().getOutput())).build();
     builder.defaultAdvisors(loggerAdvisor);
     return builder.build();
 }

graph 图编写

可以将graph图类比为流程图,它由节点和连接线(边)构成。流程图中包含大量变量,这些变量会被节点或带有判断条件的边所使用。Spring AI Alibaba Graph的工作原理与此相同。

定义变量

可以使用下面的方法,定义graph中的初始变量以及变量的更新策略,目前有追加和替换策略。这些键将贯穿整个工作流,用于在节点之间传递数据

 OverAllStateFactory stateFactory = () -> {
            OverAllState state = new OverAllState();
            state.registerKeyAndStrategy("input", new ReplaceStrategy());   // 输入
            state.registerKeyAndStrategy("outline", new ReplaceStrategy()); // 大纲
            state.registerKeyAndStrategy("report", new ReplaceStrategy());  //  报告
            return state;
        };

定义节点

Spring AI Alibaba Graph 中提供大量预置节点,这些节点可以对标到市面上主流的如 Dify、百炼等低代码平台,方便用户快速串联工作流应用。
典型节点包括 LlmNode(大模型节点)、QuestionClassifierNode(问题分类节点)、ToolNode(工具节点)等,为用户免去重复开发、定义的负担,只需要专注流程串联。

在Graph 中自定义节点十分方便,实现NodeAction接口即可,这个接口只有一个方法apply。它接收当前的OverAllState作为输入,允许节点访问工作流的全部上下文。执行完毕后,它返回一个Map<String, Object>,这个Map中的键值对将被合并到OverAllState中,供后续节点使用

public interface NodeAction {
    Map<String, Object> apply(OverAllState state);
}

下面使用了框架自带的llm节点,节点的作用是调用大模型,返回大模型处理结果

// 接受用户输入,生成写作大纲
LlmNode llmNode1 = LlmNode.builder()
        .systemPromptTemplate("你是一位专业的写作大纲生成专家,根据用户的需求,生成合适的大纲。直接生成可以使用的大纲,不要添加无关信息")
        .userPromptTemplateKey("input")  // 节点的输入变量
        .chatClient(qwenPlusNoMemoryClient)
        .outputKey("outline")  // 指定节点的输出数据存放在outline变量中
        .build();

// 获取大纲,生成详细内容
LlmNode llmNode2 = LlmNode.builder()
        .systemPromptTemplate("你是一位专业的写作专家,根据用户提供的的大纲,编写一个易于阅读的报告")
        .userPromptTemplateKey("outline")
        .chatClient(qwenPlusNoMemoryClient)
        .messages(new ArrayList<>())
        .outputKey("report").build();

定义流程图

下面创建了StateGraph 指定了节点和节点之间的边,需要注意的这里创建了一个variableParsing节点用于处理llm节点返回的AssistantMessage变量,提取其中的输出。

StateGraph graph = new StateGraph("写作工作流", stateFactory)
                // 添加节点 -- 大纲生成节点
                .addNode("llm1", node_async(llmNode1))

                // 添加节点 -- 变量处理节点
                .addNode("variableParsing",node_async(t ->{
                    AssistantMessage message = (AssistantMessage)t.value("outline").orElseThrow();
                    return Map.of("outline",message.getText());
                }))

                // 添加节点 -- 详细内容生成节点
                .addNode("llm2", node_async(llmNode2))
                
                // 定义流程图的节点与节点之间的边,START表示开始节点,END表示结束节点
                .addEdge(START, "llm1")                       // 开始节点 --> 大纲生成节点
                .addEdge("llm1", "variableParsing")  // 生成大纲 --> 变量处理节点
                .addEdge("variableParsing", "llm2")  // 变量处理节点 --> 详细内容生成节点
                .addEdge("llm2", END);                       // 详细内容生成节点 --> 结束节点

下面的方法可以打印graph图结构,使用PLANTUML插件可以进行可视化

 GraphRepresentation graphRepresentation = stateGraph.getGraph(GraphRepresentation.Type.PLANTUML,
                "workflow graph");
        System.out.println("\n\n");
        System.out.println(graph.content());
        System.out.println("\n\n");

最后是编译图

return graph.compile();

完整代码

完整的代码如下

 private ChatClient qwenPlusNoMemoryClient;

 public CompiledGraph demo2() throws GraphStateException {
        OverAllStateFactory stateFactory = () -> {
            OverAllState state = new OverAllState();
            state.registerKeyAndStrategy("input", new ReplaceStrategy());   // 输入
            state.registerKeyAndStrategy("outline", new ReplaceStrategy()); // 大纲
            state.registerKeyAndStrategy("report", new ReplaceStrategy());  //  报告
            return state;
        };

        // 接受用户输入,生成写作大纲
        LlmNode llmNode1 = LlmNode.builder()
                .systemPromptTemplate("你是一位专业的写作大纲生成专家,根据用户的需求,生成合适的大纲。直接生成可以使用的大纲,不要添加无关信息")
                .userPromptTemplateKey("input")
                .chatClient(qwenPlusNoMemoryClient)
                .outputKey("outline").build();

        // 获取大纲,生成详细内容
        LlmNode llmNode2 = LlmNode.builder()
                .systemPromptTemplate("你是一位专业的写作专家,根据用户提供的的大纲,编写合适的报告")
                .userPromptTemplateKey("outline")
                .chatClient(qwenPlusNoMemoryClient)
                .messages(new ArrayList<>())
                .outputKey("report").build();

        // 定义节点与节点之间的边
        StateGraph graph = new StateGraph("写作工作流", stateFactory)
                // 添加节点 -- 大纲生成节点
                .addNode("llm1", node_async(llmNode1))

                // 添加节点 -- 变量处理节点
                .addNode("variableParsing",node_async(t ->{
                    AssistantMessage message = (AssistantMessage)t.value("outline").orElseThrow();
                    return Map.of("outline",message.getText());
                }))

                // 添加节点 -- 详细内容生成节点
                .addNode("llm2", node_async(llmNode2))
                
                // 定义流程图的节点与节点之间的边,START表示开始节点,END表示结束节点
                .addEdge(START, "llm1")                       // 开始节点 --> 大纲生成节点
                .addEdge("llm1", "variableParsing")  // 生成大纲 --> 变量处理节点
                .addEdge("variableParsing", "llm2")  // 变量处理节点 --> 详细内容生成节点
                .addEdge("llm2", END);                       // 详细内容生成节点 --> 结束节点

        // 打印 PlantUML 流程图
        printGraphImage(graph);

        // 编译流程图
        return graph.compile();
    }
private static void printGraphImage(StateGraph stateGraph) {
        GraphRepresentation graphRepresentation = stateGraph.getGraph(GraphRepresentation.Type.PLANTUML,
                "workflow graph");
        System.out.println("\n\n");
        System.out.println(graphRepresentation.content());
        System.out.println("\n\n");
    }

启动

这里使用了graphService.demo2()获取了我们之前创建的编译后的graph图对象,然后使用invoke方法启动流程图,并且传递了流程变量值。

CompiledGraph提供了多种执行方式:
• invoke(): 以阻塞的方式执行整个工作流,并一次性返回最终的状态结果。适用于不需要实时反馈的场景。
• stream(): 以非阻塞的、流式的方式执行工作流。每当一个节点完成(甚至在节点执行过程中,如果节点本身支持流式输出),就会立刻返回一个NodeOutput事件。这对于需要向前端实时展示进度的Web应用来说是绝佳选择。

@GetMapping(value = "/graph/demo2",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
   public Object demo2(String question) throws Exception {
       CompiledGraph compiledGraph = graphService.demo2();
       Optional<OverAllState> invoke = compiledGraph.invoke(Map.of("input", question));
       Map<String, Object> stringObjectMap = invoke.map(OverAllState::data).orElse(new HashMap<>());
       // 这里可以获取流程中全部的变量
       System.out.println(stringObjectMap.get("input"));
       System.out.println(stringObjectMap.get("outline"));
       System.out.println(stringObjectMap.get("report"));
       return JSON.toJSONString(stringObjectMap.get("report"));
   }
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐