CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

Solon AI —— 流程编排

说明

Solon 的流程编排,使用了 solon-flow 做流程编排,因此需要先对 solon-flow 有所了解,下面是 Solon flow的一些简单介绍,更具体的介绍可以参考官网 https://solon.noear.org/article/learn-solon-flow

solon-flow

Solon Flow 提供基础的流引擎能力,支持开放式的驱动定制(像 JDBC 有 MySQL 或 PostgreSQL 等不同驱动一样)。可用于业务规则、决策处理、计算编排、流程审批等场景。

核心概念:

  • 链、节点、连接。
  • 链上下文、链驱动器、任务组件接口、条件组件接口。
  • 流引擎。

概念关系:

  • 链(Chain),一个完整的流程,由多个节点(Node)连接(Link)组成。一个链有且只有一个 start 类型的节点。从 start 节点开始,顺着连接(Link)流出。
  • 节点(Node),流程处理的环节,会有多个连接(Link),有流入连接,流出连接。
  • 连接(Link),节点与节点之间的执行顺序。连接向其它节点,称为流出连接。被其它节点连接,称为流入连接。
  • 流引擎,提供执行时的环境(链上下文与对象引用等),驱动链的流动(执行)。链的流转过程,可以有上下文参数(ChainContext),可以被中断(可支持有状态的审批模式)。

通俗些,就是通过点(节点) + 线(连接)来描述一个流程图(链)。因此这里列出节点和连接的属性。

节点 Node 属性

属性 数据类型 需求 描述
id String 节点Id(要求链内唯一)。不配置时,会自动生成
type NodeType 节点类型,类型包括 start,execute,inclusive,exclusive,parallerl,end。不配置时,缺省为 execute 类型,
title String 显示标题
meta Map 元信息(用于应用扩展)
link String or Link String[] or Link[] 连接(支持单值、多值),不配置时,会自动生成。link 全写配置风格为 Link 类型结构;简写配置风格为 Link 的 nextId 值(即 String 类型)
task String 任务描述(会触发驱动的 handleTask 处理)
when String 执行任务条件描述(会触发驱动的 handleTest 处理)

通常可以不配置,或者用简写模式配置即可,只要当Node是包含网关(inclusive)或者排他网关(exclusive)时需要配置全选风格,从而配置条件。

属性 数据类型 需求 描述
nextId String 必填 后面的节点Id
title String 显示标题
meta Map 元信息(用于应用扩展)
condition String 流出条件描述(会触发驱动的 handleTest 处理)

节点类型 NodeType

描述 任务 连接条件 可流入 连接数 可流出 连接数 图例参考
start 开始 / / 0 1
execute 执行节点(缺省类型) 可有 / 1…n 1
inclusive 包容网关 / 支持 1…n 1…n
exclusive 排它网关 / 支持 1…n 1…n
parallel 并行网关 / / 1…n 1…n
end 结束 / / 1…n 0

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-view-enjoy")
implementation("org.noear:solon-ai")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:solon-web-rx")
implementation("org.noear:solon-web-sse")
implementation("org.noear:solon-flow")
implementation("org.dromara.hutool:hutool-all")


testImplementation("org.noear:solon-test")
}

配置

app.yml

这里需要指定配置的流配置文件的位置。

1
2
solon.flow:
- "classpath:flow/*"

流配置

这里尝试用一个模拟一次简单的诊断,从诊断到治疗建议等流程。注意这里只是示例,并非真正的诊断流程,更完整的流程是还需要更多的病人信息(比如病史,检查,检验等),检索相关知识库等。这里只是为了 solon-flow 针对 ai 相关的编排的逻辑。

在诊断的节点,我们使用了deepseek-r1,在治疗环境,我们使用qwen2.5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: "ai-flow-01"
layout:
- id: "开始"
type: "start"
- id: "诊断"
type: "execute"
meta.model: "deepseek-r1:32b"
meta.apiUrl: "http://127.0.0.1:11434/api/chat"
meta.provider: "ollama"
meta.input: "prompt"
meta.output: "intention"
meta.system: "根据用户的描述,判断用户最可能的三个健康问题,只要诊断名称,不需要其他解释,用 Markdown 的列表格式返回。"
task: "@intentionTask"
- id: "治疗建议"
type: "execute"
meta.model: "qwen2.5:7b"
meta.apiUrl: "http://127.0.0.1:11434/api/chat"
meta.provider: "ollama"
meta.input: "intention"
meta.output: "suggestion"
meta.system: "#角色\n你是一个经验丰富的医生\n\n#任务\n根据用户提供的诊断信息,提供治疗建议"
task: "@suggestionTask"
- type: "end"

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demo.ai.llm.controller;

import com.example.demo.ai.llm.service.LlmService;
import com.jfinal.kit.Kv;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.noear.solon.annotation.*;

/**
* @author airhead
*/
@Controller
@Mapping("/llm")
@Api("聊天")
public class LlmController {
@Inject private LlmService service;

@ApiOperation("flow")
@Post
@Mapping("flow")
public Kv flow(String prompt) {
return service.flow(prompt);
}

@ApiOperation("aiFlow")
@Post
@Mapping("aiFlow")
public Kv aiFlow(String prompt) {
return service.aiFlow(prompt);
}
}

这里的 flow 是演示基础的 solon flow 的调用,是我学习solon flow 时编写的一个简单例子,不熟悉的可以先看看这个示例的编写。

aiFlow 是对大模型的一个编排的调用。

Service

这里的重点就是注入 FlowEngine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.demo.ai.llm.service;

import com.jfinal.kit.Kv;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import org.noear.solon.flow.ChainContext;
import org.noear.solon.flow.FlowEngine;

/**
* @author airhead
*/
@Component
public class LlmService {
@Inject private FlowEngine flowEngine;

public Kv flow(String prompt) {

try {
ChainContext chainContext = new ChainContext();
chainContext.put("prompt", prompt);
Kv kv = Kv.create();
chainContext.put("result", kv);
flowEngine.eval("c1", chainContext);

return kv;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

public Kv aiFlow(String prompt) {
try {
ChainContext chainContext = new ChainContext();
chainContext.put("prompt", prompt);
Kv kv = Kv.create();
chainContext.put("result", kv);
flowEngine.eval("ai-flow-01", chainContext);

return kv;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}

IntentionTask

这里是诊断节点的处理,根据 meta 信息和用户的输入构建大模型,然后进行调用,并将结果通过 ChainContext 进行传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.demo.ai.llm.service;

import com.jfinal.kit.Kv;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import org.dromara.hutool.core.regex.ReUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.solon.ai.chat.ChatConfig;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.ChatResponse;
import org.noear.solon.ai.chat.message.ChatMessage;
import org.noear.solon.annotation.Component;
import org.noear.solon.flow.ChainContext;
import org.noear.solon.flow.Node;
import org.noear.solon.flow.TaskComponent;

/**
* @author airhead
*/
@Component("intentionTask")
public class IntentionTask implements TaskComponent {
@Override
public void run(ChainContext context, Node node) throws Throwable {
Kv meta = Kv.create().set(node.meta());
ChatConfig chatConfig = new ChatConfig();
chatConfig.setModel(meta.getStr("model"));
chatConfig.setProvider(meta.getStr("provider"));
chatConfig.setApiUrl(meta.getStr("apiUrl"));
chatConfig.setTimeout(Duration.ofSeconds(600));
ChatModel chatModel = ChatModel.of(chatConfig).build();

List<ChatMessage> chatMessageList = new ArrayList<>();
ChatMessage system = ChatMessage.ofSystem(meta.getStr("system"));
chatMessageList.add(system);

Kv model = Kv.create().set(context.model());
String inputKey = meta.getStr("input");
ChatMessage userMessage = ChatMessage.ofUser(model.getStr(inputKey));
chatMessageList.add(userMessage);
ChatResponse response = chatModel.prompt(chatMessageList).call();
String content = response.getMessage().getContent();

// 去掉think的部分。
String pattern = "<think>.*?</think>";
Pattern p = Pattern.compile(pattern, Pattern.DOTALL);
content = StrUtil.trim(ReUtil.replaceAll(content, p, ""));

String outputKey = meta.getStr("output");
context.put(outputKey, content);

Kv result = context.get("result");
result.set("data", content);
}
}

SuggestionTask

这里是治疗节点的处理,根据 meta 信息和上一级节点的输入构建大模型,然后进行调用,并将结果通过 ChainContext 进行返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.demo.ai.llm.service;

import com.jfinal.kit.Kv;
import java.util.ArrayList;
import java.util.List;
import org.noear.solon.ai.chat.ChatConfig;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.ChatResponse;
import org.noear.solon.ai.chat.message.ChatMessage;
import org.noear.solon.annotation.Component;
import org.noear.solon.flow.ChainContext;
import org.noear.solon.flow.Node;
import org.noear.solon.flow.TaskComponent;

/**
* @author airhead
*/
@Component("suggestionTask")
public class SuggestionTask implements TaskComponent {
@Override
public void run(ChainContext context, Node node) throws Throwable {
Kv meta = Kv.create().set(node.meta());
ChatConfig chatConfig = new ChatConfig();
chatConfig.setModel(meta.getStr("model"));
chatConfig.setProvider(meta.getStr("provider"));
chatConfig.setApiUrl(meta.getStr("apiUrl"));
ChatModel chatModel = ChatModel.of(chatConfig).build();

List<ChatMessage> chatMessageList = new ArrayList<>();
ChatMessage system = ChatMessage.ofSystem(meta.getStr("system"));
chatMessageList.add(system);

Kv model = Kv.create().set(context.model());
String inputKey = meta.getStr("input");
ChatMessage userMessage = ChatMessage.ofUser(model.getStr(inputKey));
chatMessageList.add(userMessage);
ChatResponse response = chatModel.prompt(chatMessageList).call();
String content = response.getMessage().getContent();

String outputKey = meta.getStr("output");
context.put(outputKey, content);

Kv result = context.get("result");
result.set("data", content);
}
}

验证

通过 swagger 直接调用aiFlow。

img

这里是中间环节的输出。

img

小结

Solon AI 通过 Solon Flow 进行编排。通过编排,我们可以整合普通的业务逻辑和大模型的生成式功能实现更复杂、更人性化的业务逻辑,当然要实现真实可用的业务逻辑,需要自己进一步的摸索。

欢迎关注我的其它发布渠道