工作流节点实现
在上一篇文章中,讲述了我选择Pipeline架构来实现工作流,其链表的数据结构特性能够很好的实现逻辑链路,其inbonnd 与 outbound 的处理逻辑能很好的处理不同分支逻辑。
现在我们需要着手实现内部业务逻辑,包括分析当前工作流节点的抽象分类,以及内部细节处理。
上图中可以看到整个流程是有三次与AI 对话(六边形逻辑)同时还有四次的系统代码逻辑,系统业务逻辑主要是上下文的补充以及对过程中AI生成的数据进行业务处理
本篇文章不详细说明如何部署模型以及如何实现Open AI 协议来与模型对话
再上一篇文章中我们抽象了每个节点的基础功能,也就是inbound
入站 与outbound
出站的逻辑接口
csharp
public interface INodeHandler {
/**
* 入站处理
*/
void inboundHandler(ChatHandlerContext chc, ChatRequest request) throws Exception;
/**
* 出站处理
*/
void outboundHandler(ChatHandlerContext chc, ChatRspDto response) throws Exception;
}
同时我们确定了一个工作流的头节点的出站逻辑负责SSE响应至客户端,尾节点的入站逻辑逻辑是与AI再进行最后一次对话
添加上下文与提示词-实现系统上下文与用户需求
我们如果粗暴的将所有AI可能需要的信息全部一次性的添加到上下文中,会导致AI分析内容过多而耗时较长,同时即使是一次最简单的数据查询功能,也会消耗大量的tokens。后期的上下文管理也会变的非常臃肿和复杂,我们需要一个类来实现特定的内容补充,这样可以将上下文分割,只补充当前流程需要的内容。
我们需要一个抽象的添加对话上下文类,通过对这个类的继承来实现不同类型的上下文添加,比如用户需求补充以及系统上下文补充和系统服务模块的内容补充。
java
public abstract class IMessageHandler implements INodeHandler {
/**
* 向链中添加一个Message
*/
public abstract List<Message> addMessages(ChatHandlerContext chcChatRequest, ChatRequest request);
@Override
public void inboundHandler(ChatHandlerContext chc, ChatRequest request) throws Exception {
// 在请求阶段添加消息
List<Message> messages = addMessages(chc, request);
request.getMessages().addAll(messages);
chc.fireRequest(request);
}
@Override
public void outboundHandler(ChatHandlerContext chc, ChatRspDto response) throws Exception {
// 默认直接传递响应,不做任何处理
chc.fireResponse(response);
}
}
为了更好的管理上下文提示词,设计一个包含角色,静态构造方法的结构体是很有必要的
typescript
@Data
public class Message {
private String role;
private String content;
public Message() {}
public Message(String role, String content) {
this.role = role;
this.content = content;
}
// 静态工厂方法,提供更好的API
public static Message system(String content) {
return new Message("system", content);
}
public static Message user(String content) {
return new Message("user", content);
}
public static Message assistant(String content) {
return new Message("assistant", content);
}
// 常用角色常量
public static final class Role {
public static final String SYSTEM = "system";
public static final String USER = "user";
public static final String ASSISTANT = "assistant";
}
// 便捷的角色判断方法
public boolean isSystemMessage() {
return Role.SYSTEM.equals(role);
}
public boolean isUserMessage() {
return Role.USER.equals(role);
}
public boolean isAssistantMessage() {
return Role.ASSISTANT.equals(role);
}
// 内容验证
public boolean isValid() {
return role != null && !role.trim().isEmpty()
&& content != null && !content.trim().isEmpty();
}
}
基于上下文补充类的实现
用户需求添加,通过匿名内部类完成
typescript
freshPipeline.addLast("userMessage", new IMessageHandler() {
@Override
public List<Message> addMessages(ChatHandlerContext chc, ChatRequest request) {
return Collections.singletonList(Message.user(chatMessage.getMessage() +"/no-think"));
}
});
系统上下文概括
scala
/**
* function call 信息收集节点
* 添加系统功能的上下文
*/
@Component
public class InformationCollectionHandler extends IMessageHandler {
@Override
public List<Message> addMessages(ChatHandlerContext chc, ChatRequest request) {
List<ServerInfo> serverInfoList = SystemServerContext.getServerInfoList();
// 构建系统功能上下文信息
StringBuilder systemPrompt = new StringBuilder();
systemPrompt.append("系统可用功能列表:\n");
for (ServerInfo serverInfo : serverInfoList) {
systemPrompt.append("\n服务名称:").append(serverInfo.getServerName());
systemPrompt.append("\n服务描述:").append(serverInfo.getDesc());
systemPrompt.append("---\n");
}
systemPrompt.append("\n请根据用户需求,从上述功能中选择合适的服务和方法来完成任务。/no-think");
Message systemMessage = Message.system(systemPrompt.toString());
return Collections.singletonList(systemMessage);
}
}
模块详细内容,参数规则,接口列表
scala
/**
* 系统功能上下文过多,没办法一次性提供给llm,所以需要让llm决定使用哪个模块后再去查询功能列表以及参数
*/
@Component
@Slf4j
public class InformationSelectHandler extends IMessageHandler {
@Override
public List<Message> addMessages(ChatHandlerContext chc, ChatRequest request) {
String serverName = chc.getAttrs("serverName").toString();
BaseControllerTemplate serviceTemplate = SpringContextUtil.getBean(serverName, BaseControllerTemplate.class);
ServerInfo serverInfo = SystemServerContext.getServerInfoList().stream().filter(o -> o.getServerName().equals(serverName)).findFirst().get();
serverInfo.setFormRule(serviceTemplate.getService().getFormRule());
String promptTemplate =
"功能列表如下:\n" +
"服务名称:" + serverInfo.getServerName() + "\n" +
"服务描述:" + serverInfo.getDesc() + "\n"+
"可用方法:" + serverInfo.getMetas().toString() + "\n" +
"入参规则:" + serverInfo.getFormRule() + "\n" +
"基于上面内容继续构造functionCall请求," +
"如果查询为列表查询且没有指定分页数量的情况下,默认从第0页pageNum = 0,每页数量为10个 pageSize = 10";
return Collections.singletonList(Message.system(promptTemplate));
}
}
系统执行结果以及提示词 也是用上下文补充类完成的
swift
@Component
@Slf4j
public class FunctionCallExecuteHandler extends IMessageHandler {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public List<Message> addMessages(ChatHandlerContext chc, ChatRequest request) {
// 仅保留用户的需求即第一个消息,省去中间的检查和其他不必要的信息,减少上下文token
ArrayList<Message> newMessage = new ArrayList<>();
newMessage.add(request.getMessages().get(0));
request.setMessages(newMessage);
return Collections.singletonList(Message.system(
"如果系统没有正常执行功能,你需要告诉用户系统内部错误,联系管理员\n" +
"如果用户只是需要简单查询数据内容,你需要将结构化的执行结果 使用表格进行规整化输出所有字段内容,不允许遗漏任何字段\n" +
"如果数据为列表则不允许折叠内容" +
"同时将字段翻译为中文,除非该字段内容不明 \n" +
"严格按照下面的规范返回文本内容 \n" +
"| 字段1 | 数值1 | 换行! \n" +
"| 字段2 | 数值3 | 换行! \n" +
"| 字段3 | 数值3 | 换行!\n"+
"(如果数据类型为列表则用--进行切割) |--------------------|-----------------------------------|。\n" +
"| 字段1 | 数值1 | 换行! \n" +
"| 字段2 | 数值3 | 换行! \n" +
"| 字段3 | 数值3 | 换行!\n"+
"对齐格式:确保每行的 | 符号垂直对齐。\n" +
"\n" +
"空值处理:用空格显式表示空单元格(避免完全省略内容)。\n" +
"如果用户需要基于数据再进行内容分析判断处理,你需要按照用户需求进行数据分析和处理\n" +
"系统执行函数结果:"+ handleFinalRequestWithLLM(chc)));
}
private String handleFinalRequestWithLLM(ChatHandlerContext chc){
FunctionCallRequest callRequest = (FunctionCallRequest) chc.getAttrs("functionCallRequest");
BaseControllerTemplate serviceTemplate = SpringContextUtil.getBean(callRequest.getServerName(), BaseControllerTemplate.class);
String methodName = callRequest.getMethod();
Object[] params = callRequest.getParams();
Object result;
try {
// 通过方法名和参数数量查找方法
Method method = findMethodByNameAndParamCount(serviceTemplate.getClass(), methodName, params.length);
if (method == null) {
return "未找到匹配的方法: " + methodName;
}
// 直接通过方法参数类型反序列化参数
Object[] convertedParams = deserializeParameters(params, method, serviceTemplate);
result = method.invoke(serviceTemplate, convertedParams);
} catch (Exception e) {
log.error("服务调用异常", e);
return "服务调用异常: " + e.getMessage();
}
return result.toString();
}
xxx 详细的反序列化参数较多不一一列出,可以由AI生成,不过更加推荐用HTTP调用。
AI处理节点,生成结构化响应 - 让工作流富有灵魂
不可能所有的流程都只需要通过上下文的补充就可以实现,还需要融入AI进行分析判断。这部分逻辑主要是要求AI进行结构化返回数据,然后后端再进行数据处理。因此我们有两个步骤,一个是要求AI进行结构化生成,第二个就是结构化的数据再处理逻辑。
这个抽象父类的实现逻辑中我们用到范型,子类只需要返回java类即可要求AI按照该类进行数据返回。同时我们还需要写好提示词,这个提示词只能让AI规划化输出内容,至于内容的生成逻辑则需要子类进行重写补充。
最后不要忘记还需要提供一个解析失败的接口,进行一个逻辑兜底。默认实现是将AI返回的msg直接进行outbound输出,因为大部分情况下我们不再需要进行逻辑处理,只是告知用户AI认为错误的处理原因是什么。
java
/**
* Chat处理器抽象类
* 专门用于与LLM进行chat的抽象父类
*/
@Slf4j
@Component
public abstract class IChatHandler<T> implements INodeHandler {
@Autowired
protected ChatService chatService;
protected final ObjectMapper objectMapper = new ObjectMapper();
/**
* 获取期望的数据类型
* 子类必须实现此方法返回期望的数据结构类型
* @return 期望LLM返回的数据类型
*/
public abstract Class<T> getDataClass();
/**
* 反序列化成功的抽象接口
* 子类必须实现此方法处理解析成功的数据
* @param chc 上下文
* @param chatRspDto 解析成功的数据
* @throws Exception 处理异常
*/
public abstract void onParseSuccess(ChatHandlerContext chc, ChatRspDto<T> chatRspDto, ChatRequest chatRequest) throws Exception;
public void onParseFail(ChatHandlerContext chc, ChatRspDto<T> chatResponse) throws Exception{
chc.fireResponse(chatResponse);
}
/**
* 处理聊天请求 - INodeHandler接口实现
* 默认实现:与LLM进行结构化交互
*/
@Override
public void inboundHandler(ChatHandlerContext chc, ChatRequest request) throws Exception {
// 构建包含结构化提示词的请求
request.getMessages().add(Message.system(buildPrompt(chc)));
// log.info("当前消息上下文: {}", SessionContext.getMessageList());
// 调用LLM进行chat
ChatResponse aiResponse = chatService.chatNonStream(request);
// 将 LLM的回复内容添加至上下文中
SessionContext.addMessage(aiResponse.getFirstMessage());
// 使用正则表达式提取JSON内容,去除LLM的思考过程文本
String responseContent = aiResponse.getChoices().get(0).getMessage().getContent();
String jsonContent = extractJsonContent(responseContent);
if (jsonContent == null) {
// 没有匹配到JSON格式,调用onParseFail
ChatRspDto<T> failDto = new ChatRspDto<>();
failDto.setSuccess(false);
failDto.setMsg("LLM返回内容中未找到有效的JSON格式数据");
onParseFail(chc, failDto);
return;
}
// 尝试解析提取到的JSON内容为ChatRspDto
try {
// 创建ChatRspDto的参数化类型
ChatRspDto<T> chatRspDto = objectMapper.readValue(jsonContent,
objectMapper.getTypeFactory().constructParametricType(ChatRspDto.class, getDataClass()));
onParseSuccess(chc, chatRspDto, request);
} catch (Exception parseException) {
// JSON解析失败,说明LLM没有按照ChatRspDto格式返回
log.error("LLM返回格式错误,解析失败:" + parseException.getMessage() +
"\n提取的JSON内容:" + jsonContent +
"\n原始回复:" + responseContent+
parseException);
ChatRspDto<T> failDto = new ChatRspDto<>();
failDto.setSuccess(false);
failDto.setMsg("JSON解析失败:" + parseException.getMessage());
onParseFail(chc, failDto);
}
}
/**
* 处理聊天响应 - INodeHandler接口实现
* 默认实现:直接传递response
*/
@Override
public void outboundHandler(ChatHandlerContext chc, ChatRspDto chatRspDto) throws Exception {
// 默认直接传递响应
chc.fireResponse(chatRspDto);
}
/**
* 构建提示词
* 告知LLM必须返回ChatRspDto结构的字符串,不能有多余的字符
*/
protected String buildPrompt(ChatHandlerContext chc) {
StringBuilder prompt = new StringBuilder();
prompt.append("请严格按照以下JSON结构返回数据,不要包含任何文字或说明:\n");
prompt.append(generateClassStructure());
prompt.append("\n\n重要说明:");
prompt.append("\n1. 必须返回有效的JSON格式,返回内容必须只能包含一个{}, 不能有多个json对象");
prompt.append("\n2. 不能包含任何多余的字符或说明");
prompt.append("\n3. success字段:true表示成功返回体,false表示失败返回体");
prompt.append("\n4. msg字段:成功时可为空,失败时必须说明原因");
prompt.append("\n5. data字段:成功时包含具体业务数据,失败时为null");
prompt.append("\n6. data字段的结构必须严格按照以下格式:");
prompt.append("\n\n数据结构示例:{success: true, msg: "", data: {}}");
prompt.append("\n").append(generateDataClassStructure(getDataClass()));
return prompt.toString();
}
/**
* 生成ChatRspDto类结构描述
* 让LLM知道必须返回ChatRspDto格式
*/
protected String generateClassStructure() {
StringBuilder structure = new StringBuilder();
structure.append("{\n");
structure.append(" "success": true,\n");
structure.append(" "msg": "成功或错误信息",\n");
structure.append(" "data": {\n");
structure.append(" // 这里是具体的业务数据结构\n");
structure.append(" }\n");
structure.append("}");
return structure.toString();
}
/**
* 生成具体数据类结构描述
* 解析子类指定的数据类型结构
*/
protected String generateDataClassStructure(Class<?> clazz) {
// 如果是基本类型或String,直接返回简单描述
if (isPrimitiveOrString(clazz)) {
return getPrimitiveTypeDescription(clazz);
}
StringBuilder structure = new StringBuilder();
structure.append("{\n");
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
structure.append(" "").append(field.getName()).append("": ");
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
structure.append(""string类型的值"");
} else if (fieldType == Integer.class || fieldType == int.class) {
structure.append("数字");
} else if (fieldType == Boolean.class || fieldType == boolean.class) {
structure.append("true或false");
} else if (fieldType == Double.class || fieldType == double.class) {
structure.append("小数");
} else {
structure.append(""对象"");
}
if (i < fields.length - 1) {
structure.append(",");
}
structure.append("\n");
}
structure.append("}");
return structure.toString();
}
/**
* 判断是否为基本类型或String
*/
private boolean isPrimitiveOrString(Class<?> clazz) {
return clazz.isPrimitive() ||
clazz == String.class ||
clazz == Integer.class ||
clazz == Boolean.class ||
clazz == Double.class ||
clazz == Float.class ||
clazz == Long.class ||
clazz == Short.class ||
clazz == Byte.class ||
clazz == Character.class;
}
/**
* 获取基本类型的描述
*/
private String getPrimitiveTypeDescription(Class<?> clazz) {
if (clazz == String.class) {
return ""string类型的值"";
} else if (clazz == Integer.class || clazz == int.class) {
return "数字";
} else if (clazz == Boolean.class || clazz == boolean.class) {
return "true或false";
} else if (clazz == Double.class || clazz == double.class ||
clazz == Float.class || clazz == float.class) {
return "小数";
} else if (clazz == Long.class || clazz == long.class) {
return "长整型数字";
} else {
return ""基本类型值"";
}
}
/**
* 提取JSON内容
* 使用正则表达式从LLM的回复中提取出JSON格式的内容
*/
private String extractJsonContent(String responseContent) {
if (responseContent == null || responseContent.trim().isEmpty()) {
return null;
}
// 使用贪婪匹配的正则表达式,匹配从第一个{到最后一个}的完整JSON
// DOTALL模式让.能匹配换行符
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\{.*\}", java.util.regex.Pattern.DOTALL);
java.util.regex.Matcher matcher = pattern.matcher(responseContent);
if (matcher.find()) {
String jsonContent = matcher.group();
log.debug("提取的JSON内容: {}", jsonContent);
return jsonContent;
}
return null;
}
}
结构化输出的最大影响因素的大模型的能力与参数,代码对返回体进行了数据结构提取,一些不太听话的模型输出的内容并不是一个可以解析的json,可能还附带了别的东西。
基于强大的AI分析类的实现
用户需求分析与系统模块匹配
swift
/**
* 信息分析处理器
* 负责分析用户输入的需求,并从系统服务列表中找到匹配的serverName
*/
@Component
@Slf4j
public class InformationAnalysisHandler extends IChatHandler<FunctionCallRequest> {
@Override
public Class<FunctionCallRequest> getDataClass() {
return FunctionCallRequest.class;
}
@Override
public void onParseSuccess(ChatHandlerContext chc, ChatRspDto<FunctionCallRequest> chatRspDto, ChatRequest chatRequest) throws Exception {
if (!chatRspDto.getSuccess()) {
chc.fireResponse(chatRspDto);
return;
}
// 解析LLM返回的serverName
FunctionCallRequest functionCallRequest = chatRspDto.getData();
String serverName = functionCallRequest.getServerName();
log.info("LLM分析结果,匹配的服务名称: {}", serverName);
// 验证serverName是否存在于系统服务列表中
List<ServerInfo> serverInfoList = SystemServerContext.getServerInfoList();
boolean serverExists = serverInfoList.stream()
.anyMatch(serverInfo -> serverInfo.getServerName().equals(serverName));
if (serverExists) {
// 将匹配的serverName存储到上下文中,供后续Handler使用
chc.setAttrs("serverName", serverName);
chc.fireRequest(chatRequest);
} else {
// 服务不存在,返回错误信息
ChatRspDto<String> errorResponse = new ChatRspDto<>();
errorResponse.setSuccess(false);
errorResponse.setMsg("抱歉,未找到匹配您需求的服务,请重新描述您的需求或选择以下可用服务:" +
getAvailableServiceNames(serverInfoList));
chc.fireResponse(errorResponse);
}
}
@Override
protected String buildPrompt(ChatHandlerContext chc) {
String promptTemplate =
"请分析用户的需求,并从用户消息中提到的系统服务列表中找到最匹配用户需求的服务名称。\n\n" +
"分析要求:\n" +
"1. 仔细理解用户的需求描述\n" +
"2. 从提供的服务列表中选择最匹配的服务\n" +
"3. 如果找到匹配的服务,请在data字段中返回对应的serverName(必须与列表中的完全一致)method params 等内容后续补充,目前不需要提供\n" +
"4. 如果用户需求不明确或没有匹配的服务,请返回失败状态并在msg中说明原因\n" +
"5. 只能选择一个最匹配的服务,不能返回多个\n\n";
return promptTemplate + super.buildPrompt(chc);
}
/**
* 获取可用服务名称列表,用于错误提示
*/
private String getAvailableServiceNames(List<ServerInfo> serverInfoList) {
return serverInfoList.stream()
.map(ServerInfo::getServerName)
.reduce((s1, s2) -> s1 + "、" + s2)
.orElse("无可用服务");
}
}
必要的参数检查
scala
/**
* 执行参数检查节点
* 负责检查执行参数的有效性和完整性
* 该节点可以在Pipeline中用于验证用户输入的参数是否符合预期格式
*/
@Component
public class InputParamsCheckHandler extends IChatHandler<FunctionCallRequest> {
@Override
public Class<FunctionCallRequest> getDataClass() {
return FunctionCallRequest.class;
}
@Override
public void onParseSuccess(ChatHandlerContext chc, ChatRspDto<FunctionCallRequest> chatRspDto, ChatRequest chatRequest) throws Exception {
if(!chatRspDto.getSuccess()){
chc.fireResponse(chatRspDto);
}else{
chc.setAttrs("functionCallRequest", chatRspDto.getData());
chc.fireRequest(chatRequest);
}
}
@Override
protected String buildPrompt(ChatHandlerContext chc) {
return "请检查以下用户输入的参数是否与接口要求一致,参数是否完整且格式正确。"+
"如果参数不完整或格式不正确,请返回错误返回体,并在msg字段中充分说明用户缺少哪些字段信息" +
"如果当前参数正确,请返回成功返回体,内容与上一次一致除非params不一样" +
super.buildPrompt(chc);
}
}
构建AI chat 请求体
不要忘记最开始时我们需要构建一个Open AI 协议的请求体,让大模型环境知道我们请求的什么模型与参数,需要注意这个节点必须配置为第一个处理节点,负责初始化ChatRequest
java
@Slf4j
public abstract class IRequestBuilder implements INodeHandler {
/**
* 构造聊天请求体
*/
public abstract ChatRequest buildChatRequest();
@Override
public void inboundHandler(ChatHandlerContext chc, ChatRequest request) throws Exception {
// 在管道开始时构建请求
ChatRequest builtRequest = buildChatRequest();
// 清空消息列表,避免包含历史对话
builtRequest.setMessages(new ArrayList<>());
log.info("构建新的ChatRequest,已清空历史消息");
chc.fireRequest(builtRequest);
}
@Override
public void outboundHandler(ChatHandlerContext chc, ChatRspDto response) throws Exception {
// 在管道结束时处理响应
chc.fireResponse(response);
}
}
匿名内部类实现
scss
// 构建请求头
super.addFirst("requestHeader", new IRequestBuilder() {
@Override
public ChatRequest buildChatRequest() {
return ChatRequest.builder()
.userId(SessionContext.userSessionLocal.get())
.model("openai/gpt-oss-20b")
.maxTokens(8000)
.temperature(0.7)
.build();
}
});
gpt-oss-20b 非常强的小模型,指令服从度相当高
代码只展示了关键的部分,其中上下文管理,SSE部分,AI Chat server 部分 可自己选择实现,总体不难,不做展示
尾声
分析工作流中的节点内容,我们可以概括为两类节点,一种是负责上下文补充节点,包括系统功能信息,接口详细信息以及系统执行结果,这些主要都是为下一次Chat进行上下文补充。第二种则是AI处理节点,通过提示词让AI返回结构化文本然后节点再进行解析处理,是工作流的灵魂部分。在抽象类进行逻辑的高度分离,封装是减轻子类代码逻辑的重要部分,虽然工作流不能与Dify这种标杆AI 工作流相比,但仍能满足当初的需求,后续还可以实现RAG接口,实现项目文档内容的补充,让用户对不熟悉的业务可以直接通过AI掌握业务配置逻辑。