{status, message, data})实施方案 v2修订说明:本版在 v1(机械包装)基础上新增"灵活产出 + 重试机制"。已确认设计决策:
- 严格按画布声明:data 内字段 = 节点 outputs/variables 声明,多余字段丢弃
- 重试触发:仅结构问题(JSON 解析失败/必填字段缺失)+ 类型问题(无法转换)
- SmartAction 与 LLM 节点统一逻辑:用户在画布声明多个变量,LLM 按描述填充
- 配置粒度:全局默认 + 节点级覆盖
{
"status": 200,
"message": "调用成功",
"data": {
"fieldA": "XXXX", //(字段A输出结果)
"fieldB": 15.83 //(字段B输出结果)
}
}
status:200 成功 / 400 失败message:人类可读结果/失败原因data:节点声明的输出变量集合(严格按画布声明,不收纳多余字段)data 字段内部取变量(不再读扁平 Map 顶层)data 内的子字段(如 data.profile.name)status=400)写入上下文,下游可感知失败状态| 能力 | 适用节点 | 说明 |
|---|---|---|
| 结构化输出 | LLM / SmartAction / Agent / Hermes* | 即使只声明 1 个字段也走 JSON 模式,让 LLM 按字段定义产出 |
| 自动重试 | 同上 | JSON 解析失败、必填字段缺失、类型转换失败时自动重试 |
| 字段填充校验 | 同上 | 必填字段缺失则触发重试或失败 |
actionPrompt(操作要求)作为 user promptNodeOutputEnvelope 值对象文件:backend/.../engine/NodeOutputEnvelope.java
public class NodeOutputEnvelope {
public static final int STATUS_SUCCESS = 200;
public static final int STATUS_FAILED = 400;
private final int status;
private final String message;
private final Map<String, Object> data;
public static NodeOutputEnvelope success(String message, Map<String, Object> data);
public static NodeOutputEnvelope failure(String message);
public static NodeOutputEnvelope of(int status, String message, Map<String, Object> data);
public Map<String, Object> toMap();
public static NodeOutputEnvelope fromObject(Object raw);
public static boolean isEnvelope(Object raw);
}
StructuredOutputHelper 增强(核心)文件:backend/.../engine/StructuredOutputHelper.java
改造点:
强制 JSON 输出:needsStructuredOutput() 改为只要 outputs 非空就返回 true(移除 size > 1 判断),让所有 LLM 类节点统一走结构化模式。
新增 extractWithRetry() 方法:
public static class ExtractionResult {
public final Map<String, Object> data;
public final String errorMessage; // null 表示成功
public final int attempts; // 实际尝试次数
}
public static ExtractionResult extractWithRetry(
ChatClient client,
String userPrompt,
String systemPrompt,
JsonNode outputsDecl,
RetryPolicy retryPolicy
);
流程:
for (attempt = 1; attempt <= maxRetries; attempt++) {
String prompt = userPrompt + buildInstruction(outputsDecl, lastError);
String response = client.prompt().user(prompt).system(systemPrompt).call().content();
Map<String, Object> parsed = parse(response, outputsDecl);
ValidationResult validation = OutputValidator.validate(parsed, outputsDecl);
if (validation.ok) return ExtractionResult.success(parsed, attempt);
lastError = validation.errorMessage;
}
return ExtractionResult.failure(lastError, attempt);
}
buildInstruction() 增强:接受 lastError 参数,重试时在指令中追加"上一次返回错误:xxx,请修正"。
指令强化:在 JSON 模板前增加"严格按要求输出,字段缺失或类型不符将导致流程失败"的强调。
OutputValidator文件:backend/.../engine/OutputValidator.java
public class OutputValidator {
public static class ValidationResult {
public final boolean ok;
public final String errorMessage;
public final List<String> missingRequired; // 缺失的必填字段
public final List<String> typeMismatched; // 类型不匹配的字段
}
public static ValidationResult validate(Map<String, Object> parsed, JsonNode outputsDecl);
}
校验规则:
outputs[i].required == true 但 parsed 中无值或为 null → 加入 missingRequirednumber 但值为字符串且无法转换、声明为 array 但非数组、声明为 object 但非 Map → 加入 typeMismatchednumber + 字符串数字 → 静默转换(不算 mismatch)RetryPolicy (默认重试次数在application.yml中设置,同步更新application.yml.example)文件:backend/.../engine/RetryPolicy.java
public class RetryPolicy {
public final int maxRetries; // 默认 2,节点级可覆盖
public final boolean retryOnParseError; // JSON 解析失败重试,默认 true
public final boolean retryOnMissingRequired; // 必填字段缺失重试,默认 true
public final boolean retryOnTypeMismatch; // 类型不匹配重试,默认 true
public static RetryPolicy fromNodeData(JsonNode data, WorkflowProperties props);
public static RetryPolicy defaultPolicy();
}
配置读取优先级:
data.maxRetries / data.retryOn*application.yml 中 workflow.llm.retry.*application.yml)workflow:
llm:
retry:
max-retries: 2
retry-on-parse-error: true
retry-on-missing-required: true
retry-on-type-mismatch: true
新增 WorkflowProperties 配置类(@ConfigurationProperties("workflow")),通过 Spring 注入。
统一调用模式:
RetryPolicy policy = RetryPolicy.fromNodeData(data, workflowProperties);
StructuredOutputHelper.ExtractionResult extResult = StructuredOutputHelper.extractWithRetry(
client, userPrompt, systemPrompt, outputsDecl, policy);
if (extResult.errorMessage != null) {
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.failure(extResult.errorMessage).toMap());
}
Map<String, Object> data = new LinkedHashMap<>(extResult.data);
data.put("elapsed_time", elapsed); // LLM 节点特有
String msg = "LLM 调用成功" + (extResult.attempts > 1 ? "(重试 " + (extResult.attempts - 1) + " 次)" : "");
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.success(msg, data).toMap());
关键改动:支持 outputs 声明,走与 LLM 一致的结构化输出。
@Override
public NodeExecutionResult execute(...) {
String actionPrompt = TemplateRenderer.render(data.path("actionPrompt").asText(""), workspace.getVariables());
if (actionPrompt.isEmpty()) {
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.failure("智能操作节点的操作要求为空").toMap());
}
JsonNode outputsDecl = data.path("outputs");
if (outputsDecl.isArray() && outputsDecl.size() > 0) {
// 结构化模式:走 LLM 节点的统一流程
return executeStructured(nodeId, data, actionPrompt, workspace, outputsDecl);
}
// 降级模式:无 outputs 声明,按单变量透传(兼容旧工作流)
String result = callLlm(...);
String varName = NodeTypeUtils.resolveOutputVarName(data, "result");
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.success("智能操作执行成功", Map.of(varName, result)).toMap());
}
固定 4 字段,无需 LLM 重试。直接组装 envelope:
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.success("知识检索完成", output).toMap());
按 variables 取值后包装:
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.success("用户输入已收集", collected).toMap());
Map<String, Object> data = Map.of("selectedBranch", sourceHandle);
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.success("条件路由完成:" + sourceHandle, data).toMap(), sourceHandle);
合并所有前置 envelope.data:
Map<String, Object> aggregatedData = new LinkedHashMap<>();
int worstStatus = STATUS_SUCCESS;
List<String> messages = new ArrayList<>();
for (entry : context.getAllNodeScopedOutputs()) {
NodeOutputEnvelope env = NodeOutputEnvelope.fromObject(entry.getValue());
if (env.getStatus() == STATUS_FAILED) {
worstStatus = STATUS_FAILED;
messages.add("[" + entry.getKey() + "] " + env.getMessage());
}
if (env.getData() != null) aggregatedData.putAll(env.getData());
}
// 工作目录文件
aggregatedData.put("_workingDirFiles", files);
aggregatedData.put("_runId", context.getRunId());
String message = worstStatus == STATUS_SUCCESS
? "工作流执行成功"
: "部分节点失败:" + String.join("; ", messages);
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.of(worstStatus, message, aggregatedData).toMap());
WorkflowContext 改造nodeScopedOutputs: Map<nodeId, Map<String, Object>> 的 value 改为 envelope.toMap()。
snapshotAllOutputs() 改为解包 envelope.data 后扁平化(保持 key 形如 {nodeId}__{varName},value 是业务字段值)。
NodeWorkspaceBuilder 改造scopedOutputs 保留 envelope 原貌;variables 扁平表注入解包后的 data 字段。
NodeInputResolver 改造五级匹配全部穿透 envelope.data:
workspace.getScopedOutput(nodeId, fieldName) 先解包 envelopeWorkflowLevelExecutor 失败处理NodeExecutionResult result = executor.execute(...);
Map<String, Object> output = result.getOutput();
if (output != null) {
context.setNodeOutput(nodeId, output);
} else if (result.getStatus() == FAILED) {
Map<String, Object> failEnv = NodeOutputEnvelope.failure(result.getError()).toMap();
context.setNodeOutput(nodeId, failEnv);
}
保留 failStrategy=abort/skip 行为。
useWorkflowRunner.js 新增 helperfunction isEnvelope(o) { return o && typeof o === 'object' && 'status' in o && 'data' in o }
function unwrapNodeOutput(o) { return isEnvelope(o) ? (o.data || {}) : o }
function nodeOutputStatus(o) { return isEnvelope(o) ? o.status : null }
function nodeOutputMessage(o) { return isEnvelope(o) ? o.message : '' }
WorkflowEditor.vue)v-for 遍历 unwrapNodeOutput(r.output) 而非 r.outputNodeOutputEnvelope 类NodeOutputEnvelopeTest 单元测试mvn compile + 测试通过OutputValidatorRetryPolicy(含 fromNodeData 解析)WorkflowProperties 配置类StructuredOutputHelper:强制 JSON 输出 + extractWithRetry + lastError 注入StructuredOutputHelperTest(覆盖重试、降级、类型转换)mvn compile + 单元测试LlmExecutor:使用 extractWithRetry + envelope 包装SmartActionExecutor:新增 outputs 声明支持,复用 extractWithRetryAgentExecutor:按 Skill outputs 走结构化(若 skill 已声明 outputs)HermesAgentExecutor / HermesSmartActionExecutor:同步改造mvn compileKnowledgeRetrievalExecutor:envelope 包装(固定 4 字段)UserInputExecutor:envelope 包装ConditionExecutor:envelope 包装 + selectedBranch 放入 dataOutputExecutor:聚合所有前置 envelope.datamvn compileWorkflowContext.snapshotAllOutputs:解包 envelope.data 扁平化NodeWorkspace.getScopedOutput:穿透 envelopeNodeWorkspaceBuilder.build:注入 variables 时解包NodeInputResolver:五级匹配穿透 dataWorkflowLevelExecutor.executeOneNode:失败兜底为 envelopemvn compile + NodeInputResolverTest 调整useWorkflowRunner.js:新增 unwrap helperWorkflowEditor.vue 运行结果 Tab:适配 envelope + status 徽章 + 重试次数展示WorkflowEditor.vue SmartAction 配置面板:新增 outputs 编辑器 + 重试配置面板WorkflowEditor.vue LLM 配置面板:新增重试配置面板RunHistory.vue:适配 envelopenpm run buildNodeInputResolverTest:mock 数据改为 envelope 结构NodeOutputEnvelopeTest、OutputValidatorTest、RetryPolicyTestmvn test 全量通过npm run build 通过| 风险 | 等级 | 缓解 |
|---|---|---|
| LLM 强制 JSON 模式可能拒绝遵循(小模型) | 中 | 重试 + 类型转换容错;保留单输出降级路径 |
| 重试导致工作流耗时翻倍 | 中 | 全局默认 2 次;用户可节点级关闭;监控 attempts 指标 |
| NodeInputResolver 五级匹配改造引入 bug | 高 | 保留 legacy 兼容分支 + 完整单元测试 |
| SmartAction 新增 outputs 声明兼容旧工作流 | 中 | 检测 data.outputs 是否存在,无声明走降级模式(旧工作流不破坏) |
| Hermes 执行器与内置执行器双路径需同步 | 低 | 提取共享逻辑到工具类 |
ContextPromptHelper(取 workspace.getVariable,已由 NodeWorkspaceBuilder 解包)TemplateRenderer(同上)JsonPathExtractor / VariableConverter(纯工具)ioInference.js / useNodeFields.js(静态推断)data.outputs/inputs 静态渲染)NodeOutputEnvelopeTest / OutputValidatorTest / RetryPolicyTest / StructuredOutputHelperTest 全部通过NodeInputResolverTest 在 envelope 结构下正确解析mvn test 全量通过mvn compile 通过npm run build 通过