技术栈概览
Copy
@Slf4j
@Component
public class MyCustomNode implements WorkflowNode\<MyCustomContext\> {
// 节点实现
}
快速开始
1. 创建节点文件夹
在langchat-workflow/langchat-workflow-core/src/main/java/cn/langchat/workflow/core/node/ 目录下创建新的文件夹,文件夹名称使用小写格式:
Copy
mkdir my-custom-node
2. 创建必需的三个Java文件
每个节点都必须包含以下三个文件:Copy
@Slf4j
@Component
public class MyCustomNode implements WorkflowNode<MyCustomContext> {
// 节点主类实现
}
核心架构
节点接口 (WorkflowNode<T>)
所有节点都必须实现这个核心接口:Copy
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)
提供通用的上下文功能:Copy
@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)
定义所有节点共享的参数字段:Copy
@Getter
public enum CommonParamFields {
/**
* 输入内容
*/
INPUT("input", "输入内容");
private final String fieldName;
private final String description;
}
详细实现
节点主类 (MyCustomNode.java)
这是节点的核心实现类,必须实现WorkflowNode<T> 接口:
Copy
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:
Copy
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)
这是节点参数的字段定义,使用枚举形式:Copy
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;
}
}
高级功能
参数验证
每个节点都应该进行参数验证:Copy
// 验证必需参数
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 工具类管理工作流状态:
Copy
// 添加字符串消息
return WorkflowState.addStringMessage(state, result, context.getId());
类型转换
系统自动处理前端组件类型到Java类型的转换:Copy
// Input/TextArea → String
String text = context.getParamValue(state, MyCustomParamFields.TEXT_PARAM, String.class);
变量替换
支持在工作流状态中进行变量替换:Copy
import cn.langchat.common.ai.utils.VariableReplacer;
// 自动处理 ${variable} 格式的变量
String processedInput = VariableReplacer.replace(input, state);
完整示例
文件结构
Copy
my-custom-node/
├── MyCustomNode.java # 节点主类
├── MyCustomContext.java # 节点上下文
└── MyCustomParamFields.java # 参数字段枚举
高级节点实现
Copy
@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();
}
}
测试和验证
单元测试
为节点创建单元测试:Copy
@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);
});
}
}
集成测试
在工作流中测试节点的完整流程。开发规范
注解使用
Copy
@Slf4j // 提供日志功能
命名规范
- 类名使用 PascalCase
- 文件夹使用小写
- 枚举值使用 UPPER_SNAKE_CASE
- 方法名使用 camelCase
异常处理
Copy
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);
}
日志记录
Copy
// 记录关键操作
log.info("开始处理节点: {}, 输入: {}", context.getId(), input);
log.debug("处理参数: {}", context.getParams());
log.info("节点处理完成: {}, 结果: {}", context.getId(), result);
故障排除
节点不生效
检查以下项目:
- 是否正确添加了
@Component注解 - 确认包路径是否正确
- 验证Spring容器是否正确扫描
参数获取失败
可能的原因:
- ParamFields枚举定义不正确
- 参数名称不匹配
- 参数类型转换失败
状态更新失败
常见问题:
- WorkflowState工具类使用错误
- 状态键值不正确
- 返回格式错误
最佳实践
1. 参数验证
始终进行参数验证,确保输入数据的完整性。2. 异常处理
合理处理异常情况,提供有意义的错误信息。3. 日志记录
记录关键操作和错误信息,便于调试和监控。4. 类型安全
使用泛型确保类型安全,避免运行时类型错误。5. 性能考虑
- 避免在节点中进行重计算
- 合理使用缓存
- 优化数据库查询
更多资源
- 查看现有节点实现作为参考
- 阅读 LangGraph4j 文档 了解工作流概念
- 参考 Spring Boot 文档 了解依赖注入
- 查看 Hutool 工具类文档 了解可用工具
下一步
创建完后端节点后,您可能还需要:- 创建对应的前端节点组件
- 配置节点间的连接关系
- 测试完整的工作流流程
- 添加单元测试和集成测试
- 配置节点参数表单
如果您需要创建前端节点,请参考 创建前端工作流节点 指南。

