Skip to main content

技术栈概览

@Slf4j
@Component
public class MyCustomNode implements WorkflowNode\<MyCustomContext\> {
    // 节点实现
}

快速开始

1. 创建节点文件夹

langchat-workflow/langchat-workflow-core/src/main/java/cn/langchat/workflow/core/node/ 目录下创建新的文件夹,文件夹名称使用小写格式:
mkdir my-custom-node

2. 创建必需的三个Java文件

每个节点都必须包含以下三个文件:
@Slf4j
@Component
public class MyCustomNode implements WorkflowNode<MyCustomContext> {
    // 节点主类实现
}

核心架构

节点接口 (WorkflowNode<T>)

所有节点都必须实现这个核心接口:
public interface WorkflowNode<T extends BaseContext> {
    /**
     * 获取节点名称,默认为类名
     */
    String getNodeName();

    /**
     * 获取上下文类型定义
     */
    Class<T> getContextType();

    /**
     * 节点业务流程
     *
     * @param context 节点的上下文对象
     * @param state 工作流状态
     * @return 更新后的状态
     */
    Map<String, Object> process(T context, Map<String, Object> state);
}

基础上下文 (BaseContext)

提供通用的上下文功能:
@Data
public abstract class BaseContext {
    // 应用状态
    private String appStatus;
    
    // 节点信息
    private String label;
    private String id;
    private String nodeKey;
    
    // 工作流信息
    private String flowId;
    private String chatId;
    private String userId;
    
    // 参数定义和值
    private List<NodeParamSchema> paramSchemas;
    private Map<String, String> params;
    
    // 核心方法
    public String getInput(Map<String, Object> state);
    public <T> T getParamValue(Map<String, Object> state, Enum<?> fieldEnum, Class<T> type);
    public ValidationResult validateParams(Enum<?>... requiredFields);
}

通用参数字段 (CommonParamFields)

定义所有节点共享的参数字段:
@Getter
public enum CommonParamFields {
    /**
     * 输入内容
     */
    INPUT("input", "输入内容");
    
    private final String fieldName;
    private final String description;
}

详细实现

节点主类 (MyCustomNode.java)

这是节点的核心实现类,必须实现 WorkflowNode<T> 接口:
package cn.langchat.workflow.core.node.mycustomnode;

import cn.langchat.workflow.core.WorkflowState;
import cn.langchat.workflow.core.node.common.BaseContext;
import cn.langchat.workflow.core.node.common.CommonParamFields;
import cn.langchat.workflow.core.node.common.WorkflowNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author tycoding
 * @since 2025/8/28
 */
@Slf4j
@Component
public class MyCustomNode implements WorkflowNode<MyCustomContext> {

    @Override
    public String getNodeName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public Class<MyCustomContext> getContextType() {
        return MyCustomContext.class;
    }

    @Override
    public Map<String, Object> process(MyCustomContext context, Map<String, Object> state) {
        // 表单校验
        BaseContext.ValidationResult validationResult = BaseContext.validateParams(
                context.getParamSchemas(),
                context.getParams(),
                CommonParamFields.INPUT
        );
        if (!validationResult.isValid()) {
            throw new RuntimeException("表单校验失败: " + validationResult.getFirstError());
        }

        // 获取输入内容
        String input = context.getInput(state);

        // 实现节点业务逻辑
        String result = processCustomLogic(input);

        // 返回处理结果
        return WorkflowState.addStringMessage(state, result, context.getId());
    }

    private String processCustomLogic(String input) {
        log.info("处理输入内容: {}", input);
        // 在这里实现具体的业务逻辑
        return "处理结果: " + input;
    }
}

节点上下文类 (MyCustomContext.java)

这是节点的上下文类,继承自 BaseContext
package cn.langchat.workflow.core.node.mycustomnode;

import cn.langchat.workflow.core.node.common.BaseContext;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.util.Map;

/**
 * @author tycoding
 * @since 2025/8/28
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class MyCustomContext extends BaseContext {

    /**
     * 获取自定义参数值
     */
    public String getCustomParam(Map<String, Object> state) {
        return getParamValue(state, MyCustomParamFields.CUSTOM_PARAM, String.class);
    }

    /**
     * 获取数值参数
     */
    public Integer getNumberParam(Map<String, Object> state) {
        Integer value = getParamValue(state, MyCustomParamFields.NUMBER_PARAM, Integer.class);
        return value != null ? value : 10; // 默认值
    }

    /**
     * 获取布尔参数
     */
    public Boolean getBooleanParam(Map<String, Object> state) {
        Boolean value = getParamValue(state, MyCustomParamFields.BOOLEAN_PARAM, Boolean.class);
        return value != null ? value : false; // 默认值
    }
}

节点参数字段枚举 (MyCustomParamFields.java)

这是节点参数的字段定义,使用枚举形式:
package cn.langchat.workflow.core.node.mycustomnode;

import lombok.Getter;

/**
 * 自定义节点参数字段枚举
 * 定义MyCustomNode特有的表单字段名
 *
 * @author tycoding
 * @since 2025/8/27
 */
@Getter
public enum MyCustomParamFields {

    /**
     * 自定义参数
     */
    CUSTOM_PARAM("customParam", "自定义参数"),

    /**
     * 数值参数
     */
    NUMBER_PARAM("numberParam", "数值参数"),

    /**
     * 布尔参数
     */
    BOOLEAN_PARAM("booleanParam", "布尔参数"),
    ;

    /**
     * 字段名
     */
    private final String fieldName;

    /**
     * 字段描述
     */
    private final String description;

    MyCustomParamFields(String fieldName, String description) {
        this.fieldName = fieldName;
        this.description = description;
    }

    @Override
    public String toString() {
        return fieldName;
    }
}

高级功能

参数验证

每个节点都应该进行参数验证:
// 验证必需参数
BaseContext.ValidationResult validationResult = BaseContext.validateParams(
    context.getParamSchemas(),
    context.getParams(),
    CommonParamFields.INPUT
);

if (!validationResult.isValid()) {
    throw new RuntimeException("表单校验失败: " + validationResult.getFirstError());
}

// 验证自定义参数
BaseContext.ValidationResult customValidation = context.validateParams(
    MyCustomParamFields.CUSTOM_PARAM,
    MyCustomParamFields.NUMBER_PARAM
);

状态管理

使用 WorkflowState 工具类管理工作流状态:
// 添加字符串消息
return WorkflowState.addStringMessage(state, result, context.getId());

类型转换

系统自动处理前端组件类型到Java类型的转换:
// Input/TextArea → String
String text = context.getParamValue(state, MyCustomParamFields.TEXT_PARAM, String.class);

变量替换

支持在工作流状态中进行变量替换:
import cn.langchat.common.ai.utils.VariableReplacer;

// 自动处理 ${variable} 格式的变量
String processedInput = VariableReplacer.replace(input, state);

完整示例

文件结构

my-custom-node/
├── MyCustomNode.java      # 节点主类
├── MyCustomContext.java   # 节点上下文
└── MyCustomParamFields.java # 参数字段枚举

高级节点实现

@Slf4j
@Component
public class MyCustomNode implements WorkflowNode<MyCustomContext> {

    @Resource
    private SomeService someService; // 注入业务服务

    @Override
    public String getNodeName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public Class<MyCustomContext> getContextType() {
        return MyCustomContext.class;
    }

    @Override
    public Map<String, Object> process(MyCustomContext context, Map<String, Object> state) {
        try {
            // 参数验证
            BaseContext.ValidationResult validationResult = BaseContext.validateParams(
                    context.getParamSchemas(),
                    context.getParams(),
                    CommonParamFields.INPUT
            );
            if (!validationResult.isValid()) {
                throw new RuntimeException("表单校验失败: " + validationResult.getFirstError());
            }

            // 获取参数
            String input = context.getInput(state);
            String customParam = context.getCustomParam(state);
            Integer numberParam = context.getNumberParam(state);
            Boolean booleanParam = context.getBooleanParam(state);

            log.info("开始处理节点: {}, 输入: {}, 自定义参数: {}, 数值: {}, 布尔值: {}", 
                    context.getId(), input, customParam, numberParam, booleanParam);

            // 业务逻辑处理
            String result = processBusinessLogic(input, customParam, numberParam, booleanParam);

            // 记录处理结果
            log.info("节点处理完成: {}, 结果: {}", context.getId(), result);

            // 返回结果
            return WorkflowState.addStringMessage(state, result, context.getId());

        } catch (Exception e) {
            log.error("节点处理失败: {}, 错误: {}", context.getId(), e.getMessage(), e);
            throw new RuntimeException("节点处理失败: " + e.getMessage(), e);
        }
    }

    private String processBusinessLogic(String input, String customParam, Integer numberParam, Boolean booleanParam) {
        // 在这里实现具体的业务逻辑
        StringBuilder result = new StringBuilder();
        
        if (StrUtil.isNotBlank(input)) {
            result.append("输入内容: ").append(input);
        }
        
        if (StrUtil.isNotBlank(customParam)) {
            result.append(", 自定义参数: ").append(customParam);
        }
        
        if (numberParam != null) {
            result.append(", 数值参数: ").append(numberParam);
        }
        
        if (booleanParam != null) {
            result.append(", 布尔参数: ").append(booleanParam);
        }
        
        return result.toString();
    }
}

测试和验证

单元测试

为节点创建单元测试:
@SpringBootTest
class MyCustomNodeTest {

    @Autowired
    private MyCustomNode node;

    @Test
    void testProcess() {
        // 创建测试上下文和状态
        MyCustomContext context = new MyCustomContext();
        Map<String, Object> state = new HashMap<>();
        
        // 设置测试数据
        context.setParams(Map.of(
            "input", "测试输入",
            "customParam", "测试自定义参数",
            "numberParam", "42",
            "booleanParam", "true"
        ));
        
        // 执行节点
        Map<String, Object> result = node.process(context, state);
        
        // 验证结果
        assertNotNull(result);
        assertTrue(result.containsKey("message"));
        // 添加更多断言...
    }

    @Test
    void testProcessWithInvalidInput() {
        // 测试无效输入的情况
        MyCustomContext context = new MyCustomContext();
        Map<String, Object> state = new HashMap<>();
        
        // 不设置必需参数
        context.setParams(new HashMap<>());
        
        // 应该抛出异常
        assertThrows(RuntimeException.class, () -> {
            node.process(context, state);
        });
    }
}

集成测试

在工作流中测试节点的完整流程。

开发规范

注解使用

@Slf4j // 提供日志功能

命名规范

  • 类名使用 PascalCase
  • 文件夹使用小写
  • 枚举值使用 UPPER_SNAKE_CASE
  • 方法名使用 camelCase

异常处理

try {
    // 业务逻辑
    String result = processBusinessLogic(input);
    return WorkflowState.addStringMessage(state, result, context.getId());
} catch (Exception e) {
    log.error("节点处理失败: {}, 错误: {}", context.getId(), e.getMessage(), e);
    throw new RuntimeException("节点处理失败: " + e.getMessage(), e);
}

日志记录

// 记录关键操作
log.info("开始处理节点: {}, 输入: {}", context.getId(), input);
log.debug("处理参数: {}", context.getParams());
log.info("节点处理完成: {}, 结果: {}", context.getId(), result);

故障排除

节点不生效

检查以下项目:
  • 是否正确添加了 @Component 注解
  • 确认包路径是否正确
  • 验证Spring容器是否正确扫描

参数获取失败

可能的原因:
  • ParamFields枚举定义不正确
  • 参数名称不匹配
  • 参数类型转换失败

状态更新失败

常见问题:
  • WorkflowState工具类使用错误
  • 状态键值不正确
  • 返回格式错误

最佳实践

1. 参数验证

始终进行参数验证,确保输入数据的完整性。

2. 异常处理

合理处理异常情况,提供有意义的错误信息。

3. 日志记录

记录关键操作和错误信息,便于调试和监控。

4. 类型安全

使用泛型确保类型安全,避免运行时类型错误。

5. 性能考虑

  • 避免在节点中进行重计算
  • 合理使用缓存
  • 优化数据库查询

更多资源

下一步

创建完后端节点后,您可能还需要:
  1. 创建对应的前端节点组件
  2. 配置节点间的连接关系
  3. 测试完整的工作流流程
  4. 添加单元测试和集成测试
  5. 配置节点参数表单
如果您需要创建前端节点,请参考 创建前端工作流节点 指南。