状态:待用户确认 日期:2026-06-14 作者:架构设计讨论 关联文件:
backend/src/main/java/com/agent/management/engine/WorkflowContext.javabackend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.javabackend/src/main/java/com/agent/management/engine/NodeExecutionResult.javabackend/src/main/java/com/agent/management/engine/executor/*.javafrontend/src/utils/ioInference.js
当前工作流引擎存在前后端契约脱节问题:
ioInference.js:64-66):LLM 节点的输出变量名默认为 resultLlmExecutor.java:59):Map.of(nodeId, result) 把结果写到了 nodeId 名下{{result}} 模板渲染取不到值,只有 {{llm_1}} 能取到——与前端展示给用户的 IO 标签不符同样的 bug 在 5 个 executor 中重复出现:LlmExecutor、AgentExecutor、SmartActionExecutor、HermesAgentExecutor、HermesSmartActionExecutor。
这次不仅是修 bug,而是把节点间数据流从"变量传递"升级为"上下文传递":
| 维度 | 旧契约 | 新契约 |
|---|---|---|
| 节点可见范围 | 自己 inputs 模板里引用的变量 | 整个工作流截至此时的全部状态 |
| 节点产出 | 自己声明的 outputs 变量 | 在原上下文基础上叠加自己的产出 |
| 失败判定 | executor 抛异常 | 前置条件检查:缺文件/缺变量即失败,按 failStrategy 处理 |
| Debug 能力 | 仅看每节点 output | 看每节点执行前后的完整上下文快照 |
| 扩展维度 | variables + workingDir | 还能挂 git 状态、memory 等任意 section |
| 能力 | 实现位置 | 状态 |
|---|---|---|
| 工作目录所有节点共享 | WorkflowContext.workingDir |
✅ |
| 工作流执行时创建临时目录 | WorkflowRunDirManager.createRunDir |
✅ |
| 输入文件/文件夹上传到工作目录 | WorkflowController.run() multipart 支持 + ZIP slip 防护 |
✅ |
| 变量累积传递 | WorkflowContext.variables.putAll(output) |
✅(但 key 错误) |
| 前置条件检查 | WorkflowLevelExecutor.checkPreconditions |
✅(缺类型校验) |
| failStrategy(abort/skip) | WorkflowLevelExecutor |
✅ |
| 输出节点输出所有变量 | OutputExecutor |
✅ |
| 工作目录打包下载 | GET /api/workflows/{id}/runs/{runId}/download |
✅ |
| 能力 | 工作量 |
|---|---|
| 每节点上下文快照(debug 视图) | 中 |
| Context 结构化扩展槽(sections) | 小 |
| 变量 key 按 outputs 规范(5 个 executor 修复) | 小 |
| 前置条件类型校验 | 小 |
| 前端运行历史详情页:上下文快照面板 | 中 |
| 前端输出节点:下载工作目录入口 | 小 |
WorkflowContext(扩展)public class WorkflowContext {
private final Map<String, Object> variables; // 用户变量(扁平累积)
private final Path workingDir; // 工作目录(已有)
private final List<NodeOutput> nodeOutputs; // 累积记录(已有)
private final Map<String, Object> sections; // 【新增】命名空间扩展槽
private NodeStreamSink streamSink; // 已有
// sections 访问器
public Object getSection(String name) { return sections.get(name); }
public void putSection(String name, Object value) { sections.put(name, value); }
}
sections 设计约定:
_ 开头表示系统级(如 _git、_memory)_git → { branch, commit, dirty }_memory → Hermes 长期记忆句柄_runtime → { startTime, nodeCount, ... }NodeExecutionResult(扩展)public class NodeExecutionResult {
private final Map<String, Object> output; // 仅本次输出
private final Map<String, Object> contextSnapshot; // 【新增】执行后完整 variables 快照
// ... 其他字段不变
}
WorkflowRunNode 实体(扩展)public class WorkflowRunNode {
// ... 已有字段
private String output; // 已有:本次输出 JSON
private String contextSnapshot; // 【新增】执行后完整上下文 JSON
}
NodeTypeUtils)适用于所有"产出单一文本结果"的节点(Agent/SmartAction/Hermes 系列)。LlmExecutor 不使用此方法,单独走结构化输出路径(见 3.2.2)。
/**
* 根据节点 data.outputs 声明解析输出变量名。
* 单输出场景:返回 outputs[0].name
* 未声明 outputs:返回 defaultName
* 多输出场景:本期仅取 outputs[0].name,并记录警告日志(其他 outputs 被忽略);留下TODO
* (LLM 节点不走此路径,由 StructuredOutputHelper 单独处理多输出)
*/
public static String resolveOutputVarName(JsonNode data, String defaultName) {
JsonNode outputs = data.path("outputs");
if (outputs.isArray() && outputs.size() > 0) {
String name = outputs.get(0).path("name").asText("");
if (!name.isEmpty()) {
if (outputs.size() > 1) {
log.warn("[NodeTypeUtils] 节点声明了多个 outputs,本期仅取第一个: {}", name);
}
return name;
}
}
return defaultName;
}
/**
* 构造单输出的 Map:{ varName: result }
*/
public static Map<String, Object> singleOutput(String varName, Object result) {
return Map.of(varName, result);
}
StructuredOutputHelper)LLM 节点声明多个 outputs 时(如 summary + detail),通过 Prompt 注入指令让模型按 JSON 格式输出,再解析拆分到各变量。
package com.agent.management.engine;
public class StructuredOutputHelper {
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 判断节点是否需要结构化输出。
* 触发条件:data.outputs 是数组且长度 > 1
*/
public static boolean needsStructuredOutput(JsonNode data) {
JsonNode outputs = data.path("outputs");
return outputs.isArray() && outputs.size() > 1;
}
/**
* 构造结构化输出指令(追加到 userPrompt 末尾)。
* 指令包含:JSON 框架 + 字段说明(name/type/description)。
*/
public static String buildInstruction(JsonNode outputs) {
StringBuilder sb = new StringBuilder();
sb.append("\n\n---\n请将最终结果按以下 JSON 格式输出,仅输出 JSON,"
+ "不要任何额外文字、解释或代码块标记:\n");
sb.append("{\n");
for (JsonNode o : outputs) {
String name = o.path("name").asText("");
String type = o.path("type").asText("string");
sb.append(" \"").append(name).append("\": <").append(type).append(">,\n");
}
// 删除最后一个逗号
int lastComma = sb.lastIndexOf(",\n");
if (lastComma == sb.length() - 2) sb.deleteCharAt(lastComma);
sb.append("}\n\n字段说明:\n");
for (JsonNode o : outputs) {
String name = o.path("name").asText("");
String type = o.path("type").asText("string");
String desc = o.path("description").asText("");
sb.append("- ").append(name).append(" (").append(type).append("): ")
.append(desc.isEmpty() ? "(无描述)" : desc).append("\n");
}
return sb.toString();
}
/**
* 解析 LLM 响应,按 outputs 拆分为 Map<varName, value>。
* 解析失败返回 null(调用方降级处理)。
*/
public static Map<String, Object> parse(String llmResponse, JsonNode outputs) {
if (llmResponse == null || llmResponse.isBlank()) return null;
JsonNode parsed = tryParseJson(llmResponse);
if (parsed == null || !parsed.isObject()) return null;
Map<String, Object> result = new LinkedHashMap<>();
for (JsonNode o : outputs) {
String name = o.path("name").asText("");
if (name.isEmpty()) continue;
JsonNode value = parsed.get(name);
if (value == null || value.isNull()) {
log.warn("[StructuredOutput] 字段 {} 未在 LLM 响应中找到", name);
result.put(name, null);
continue;
}
result.put(name, convertValue(value, o.path("type").asText("string")));
}
return result;
}
/** 三级 JSON 提取:直接解析 → ```json``` 代码块 → 首尾{}子串 */
private static JsonNode tryParseJson(String text) { ... }
/** 按 outputs 声明类型转换值;失败保留字符串 + WARN */
private static Object convertValue(JsonNode value, String targetType) { ... }
}
JSON 解析容错策略(三级降级):
| 级别 | 策略 | 适用场景 |
|---|---|---|
| 1 | 直接 MAPPER.readTree(text) |
模型严格按指令输出 JSON |
| 2 | 提取 json ... 代码块内容再解析 |
模型用 markdown 代码块包裹 |
| 3 | 提取首个 { 到末个 } 之间的子串再解析 |
模型在 JSON 前后加了解释文字 |
三级都失败 → 返回 null,调用方降级:把完整文本塞到 outputs[0].name,并 WARN 日志。
类型映射表(convertValue):
| outputs 声明 type | 转换逻辑 |
|---|---|
string(默认) |
value.asText() |
number |
value.isNumber() ? numberValue() : Double.parseDouble(asText()) |
boolean |
value.isBoolean() ? booleanValue() : Boolean.parseBoolean(asText()) |
array / object |
MAPPER.treeToValue(value, Object.class) |
| 类型转换失败 | 保留 value.toString(),WARN 日志 |
data.outputs 读取声明的变量名outputs[0].nameoutputs[0].name,并 WARN 日志,留下 TODOsetNodeOutput 完成后立即快照 variablesuserInput 节点| 项目 | 内容 |
|---|---|
| 前端契约 | getNodeOutputs(userInput) → data.variables(每个变量作为输出) |
| 默认 outputs | 无(用户在节点上声明的 variables 即为 outputs) |
| 后端实现 | UserInputExecutor.java:39:output.put(name, value) |
| 改动 | ✅ 无需改动——已符合规范 |
llm 节点| 项目 | 内容 |
|---|---|
| 前端契约 | 默认 [{name:'result', type:'string'}],用户可自定义单/多输出 |
| 默认 outputs 变量名 | result(outputs 长度 ≤ 1 时) |
| 后端当前实现 | Map.of(nodeId, result) ❌ |
| 改动 | 分两种场景: • 单输出(outputs ≤ 1): Map.of(NodeTypeUtils.resolveOutputVarName(data, "result"), result)• 多输出(outputs > 1):通过 StructuredOutputHelper 注入指令 + 解析 JSON 拆分到各变量;解析失败降级到 outputs[0].name |
LlmExecutor 改造后伪代码:
public NodeExecutionResult execute(String nodeId, JsonNode data, WorkflowContext context) {
String systemPrompt = TemplateRenderer.render(data.path("systemPrompt").asText(""), context.getVariables());
String userPrompt = TemplateRenderer.render(data.path("userPrompt").asText(""), context.getVariables());
if (userPrompt.isEmpty()) return NodeExecutionResult.failed(nodeId, "用户提示词为空");
JsonNode outputs = data.path("outputs");
boolean structured = StructuredOutputHelper.needsStructuredOutput(data);
if (structured) {
userPrompt = userPrompt + StructuredOutputHelper.buildInstruction(outputs);
}
ChatClient client = resolveChatClient(data);
String result = client.prompt().user(userPrompt)
.system(systemPrompt.isEmpty() ? null : systemPrompt)
.call().content();
if (result == null || result.isBlank()) return NodeExecutionResult.failed(nodeId, "LLM 返回空结果");
Map<String, Object> output;
if (structured) {
Map<String, Object> parsed = StructuredOutputHelper.parse(result, outputs);
if (parsed != null) {
output = parsed;
} else {
String firstName = outputs.get(0).path("name").asText("result");
log.warn("[LLM] 节点 {} 结构化输出解析失败,降级到 {}", nodeId, firstName);
output = Map.of(firstName, result);
}
} else {
output = Map.of(NodeTypeUtils.resolveOutputVarName(data, "result"), result);
}
return NodeExecutionResult.success(nodeId, output);
}
agent 节点(基于 Skill)| 项目 | 内容 |
|---|---|
| 前端契约 | getNodeOutputs(agent) → data.outputs \|\| [](空数组则前端不显示输出标签) |
| 默认 outputs 变量名 | result(与 LLM 对齐) |
| 后端当前实现 | Map.of(nodeId, result) ❌ |
| 改动 | 改为:Map.of(NodeTypeUtils.resolveOutputVarName(data, "result"), result) |
smartAction 节点| 项目 | 内容 |
|---|---|
| 前端契约 | 同 agent |
| 默认 outputs 变量名 | result |
| 后端当前实现 | Map.of(nodeId, result) ❌ |
| 改动 | 同 agent |
HermesAgent 节点(hermes.enabled=true 时替代 agent)| 项目 | 内容 |
|---|---|
| 默认 outputs 变量名 | result(与 agent 一致) |
| 后端当前实现 | Map.of(nodeId, finalText) ❌ |
| 改动 | 同 agent |
HermesSmartAction 节点(hermes.enabled=true 时替代 smartAction)| 项目 | 内容 |
|---|---|
| 默认 outputs 变量名 | result |
| 后端当前实现 | Map.of(nodeId, finalText) ❌ |
| 改动 | 同 agent |
condition 节点| 项目 | 内容 |
|---|---|
| 前端契约 | getNodeOutputs(condition) → getNodeInputs(node)(透传输入作为输出) |
| 默认 outputs | 透传输入字段 |
| 后端当前实现 | Map.of(nodeId, "branch-N") + selectedBranch |
| 现状问题 | 下游激活靠 selectedBranch/sourceHandle,没有任何下游会读 {{nodeId}} 来获取分支号,因此 Map.of(nodeId, ...) 写入毫无意义 |
| 改动 | 改为:Map.of()(空输出),selectedBranch 仍保留用于 DAG 路由 |
output 节点| 项目 | 内容 |
|---|---|
| 前端契约 | getNodeOutputs(output) → [](终端节点,无输出) |
| 后端当前实现 | 收集所有 variables + _workingDirFiles + _runId |
| 改动 | ✅ 无需改动——透传上下文的设计本就符合新契约 |
| Executor | 当前 key | 改后 key | 文件位置 |
|---|---|---|---|
LlmExecutor |
nodeId |
单输出:resolveOutputVarName(data, "result")多输出: StructuredOutputHelper.parse() 拆分 |
LlmExecutor.java:59 |
AgentExecutor |
nodeId |
resolveOutputVarName(data, "result") |
AgentExecutor.java:78 |
SmartActionExecutor |
nodeId |
resolveOutputVarName(data, "result") |
SmartActionExecutor.java:61 |
HermesAgentExecutor |
nodeId |
resolveOutputVarName(data, "result") |
HermesAgentExecutor.java:79 |
HermesSmartActionExecutor |
nodeId |
resolveOutputVarName(data, "result") |
HermesSmartActionExecutor.java:63 |
ConditionExecutor |
nodeId |
空输出 Map.of() |
ConditionExecutor.java:78,83 |
UserInputExecutor |
变量名 | 不变 | - |
OutputExecutor |
全部 variables | 不变 | - |
| # | 决策点 | 最终选择 | 理由 |
|---|---|---|---|
| 1 | Context 结构 | 多 section 容器(B) | 保留扁平 variables,新增 sections 字段隔离扩展 |
| 2 | 变量冲突策略 | 后覆盖前 + 警告日志(A) | 简单直观,日志帮助 debug |
| 3 | 上下文快照粒度 | 全量快照(A) | variables 通常很小(几 KB),N 个节点累加可接受 |
| 4 | SSE 推送策略 | 增量推送 + DB 持久化全量(B) | SSE 维持现状(推 output),全量留作运行历史页面查看 |
| 5 | 前置条件 | 仅加变量类型校验(A) | 不引入硬编码文件名,用 filePath 变量表达更灵活 |
| 6 | 实施节奏 | 一次性全做 | 工作量可控,分阶段反而产生中间不一致状态 |
| 7 | userInput 节点是否承担上传入口 | 否 | 保留 Controller 上传,userInput 仅声明变量名 |
| 8 | 是否本期实现 git section 示范 | 否(仅预留位置) | YAGNI,未来按需扩展 |
| 9 | LLM 多输出实现方式 | Prompt 注入 + JSON 解析 + 三级降级(A) | 兼容性最好;不依赖模型 function calling;解析失败有保底 |
| 10 | 多输出能力适用范围 | 仅 LLM 节点 | Agent/SmartAction/Hermes 内部已有输出格式控制,叠加结构化指令易冲突,留 TODO |
目标:让所有 executor 按 outputs 规范写键;LLM 节点支持多输出结构化输出
NodeTypeUtils 新增 resolveOutputVarName(JsonNode data, String defaultName) 与 singleOutput(varName, result) 方法StructuredOutputHelper 类:
needsStructuredOutput(JsonNode data) 判断是否多输出buildInstruction(JsonNode outputs) 生成 JSON 输出指令parse(String llmResponse, JsonNode outputs) 解析响应并按字段拆分tryParseJson(String text) 三级 JSON 提取(直接/代码块/首尾括号)convertValue(JsonNode value, String targetType) 类型转换LlmExecutor:
Map.of(NodeTypeUtils.resolveOutputVarName(data, "result"), result)StructuredOutputHelper.parse();失败降级到 outputs[0].nameMap.of(nodeId, result) → Map.of(NodeTypeUtils.resolveOutputVarName(data, "result"), result)resolveOutputVarName 取 outputs[0] + WARN,留 TODOConditionExecutor:移除无意义的 Map.of(nodeId, ...),改为 Map.of()验证:
userInput → llm_1 → llm_2(llm_2 引用 {{result}}),确认变量能传递userInput → llm_multi(outputs=[summary, detail]) → output,确认两个变量都被正确拆分outputs[0].name,工作流不报错目标:为 debug 视图和未来扩展打基础
WorkflowContext 新增 sections: Map<String,Object> 字段 + getter/setter/getSection/putSectionNodeExecutionResult 新增 contextSnapshot: Map<String,Object> 字段WorkflowLevelExecutor.executeByLevel 在节点 setNodeOutput 完成后,计算 variables 全量快照塞入 result.contextSnapshotWorkflowContext.setNodeOutput 增加变量覆盖检测:若 key 已存在,记录 WARN 日志验证:
目标:让运行历史能回放每个节点的完整上下文
WorkflowRunNode 实体新增 contextSnapshot: String 字段(JSON 字符串)ALTER TABLE workflow_run_nodes ADD COLUMN context_snapshot TEXTWorkflowLevelExecutor.buildNodeRecord 序列化 contextSnapshot 写入验证:
WorkflowRunNode 记录,每条都有 contextSnapshot 字段目标:补变量类型校验
NodeTypeUtils 新增 isTypeCompatible(String sourceType, String targetType) 方法(参考前端 ioInference.js:127-135)WorkflowLevelExecutor.checkPreconditions 增加类型校验逻辑:
验证:
{name:'count', type:'number'},但上游传字符串 → 前置条件失败目标:让用户能查看每个节点的完整上下文
RunHistory.vue(或新增详情页):
output(本次输出)+ contextSnapshot(执行后完整上下文)WorkflowEditor.vue 运行结果区域:
/runs/{runId}/download)验证:
| 事件 | 触发时机 | payload |
|---|---|---|
node_running |
节点开始执行 | { runId, nodeId } |
node_stream |
流式增量 | { runId, nodeId, kind, content, toolName } |
node_status |
节点完成 | { runId, nodeId, status, output, logs } |
workflow_complete |
工作流完成 | { runId, output } |
workflow_error |
工作流出错 | { runId, error } |
设计取舍:SSE 实时仅推送 output(增量),完整 contextSnapshot 通过运行历史详情 API 查询,避免 SSE 流量爆炸。
GET /api/runs/{runId}/nodes
Response: [
{
nodeId, nodeType, label, status, sortOrder,
output: {...}, // 本次输出
contextSnapshot: {...}, // 【新增】执行后完整 variables
error, selectedBranch, logs
}
]
GET /api/workflows/{id}/runs/{runId}/download
Response: application/zip
| 场景 | 预期 |
|---|---|
userInput(x) → llm_1(输出 result) → llm_2(引用 {{result}}) |
llm_2 能取到 llm_1 的输出 |
同上,llm_1 自定义 outputs 名为 summary |
llm_2 引用 {{summary}} 能取到 |
两个 llm 节点都默认输出 result |
后者覆盖前者,日志记录警告 |
condition 节点不再向 variables 写入 nodeId |
下游 variables 不含 nodeId key |
| 场景 | 预期 |
|---|---|
| llm 节点声明 outputs=[summary, detail],模型严格输出 JSON | 两个变量分别赋值 |
模型用 json... 代码块包裹 JSON |
仍能正确解析(二级容错) |
| 模型在 JSON 前后加解释文字 | 仍能正确解析(三级容错) |
| 模型完全输出纯文本(非 JSON) | 降级:全文写入 outputs[0].name,WARN 日志,工作流不报错 |
| 模型输出 JSON 缺少某个字段 | 该字段赋值 null,WARN 日志,其他字段正常赋值 |
| outputs 声明 number/boolean/array/object 类型 | 按类型转换;转换失败保留字符串 + WARN |
| 场景 | 预期 |
|---|---|
| 跑完 5 节点工作流,查运行历史 | 每个 WorkflowRunNode 都有 contextSnapshot |
| 第 3 个节点的 contextSnapshot | 包含节点 1+2+3 的累积输出 |
| 最后一个节点的 contextSnapshot | 等于工作流最终 variables |
| 场景 | 预期 |
|---|---|
| 节点 input required 变量未提供 | 失败,按 failStrategy 处理 |
| 节点 input filePath 变量指向不存在的文件 | 失败 |
| 节点 input number 类型变量收到字符串 | 失败(Phase 4 后) |
| 场景 | 预期 |
|---|---|
| 运行历史详情页点击任意节点 | 显示该节点 output + contextSnapshot |
| 输出节点点击"下载工作目录" | 浏览器下载 zip |
| 风险 | 影响 | 缓解 |
|---|---|---|
| 存量 graphData 中 outputs 字段缺失 | 节点 key 退化为默认值 result,可能改变存量工作流行为 |
默认值与前端 ioInference 一致(result),实际行为对齐用户预期,不视为破坏性 |
| variables 过大导致 contextSnapshot 消耗 DB 空间 | 长期积累存储压力 | 监控;如需要可加配置开关或定期清理 |
| LLM 不严格按 JSON 格式输出 | 多输出场景拆分失败 | 三级 JSON 提取容错(直接/代码块/首尾括号);解析失败降级到 outputs[0].name 写整段文本,工作流不中断 |
| LLM 输出 JSON 缺字段 | 部分变量未赋值 | 缺失字段赋 null + WARN 日志;其他字段正常赋值;不阻塞工作流 |
| 类型转换失败(如 number 收到字符串 "abc") | 变量类型与声明不符 | 保留字符串原值 + WARN 日志;下游模板渲染按字符串处理 |
| 变量覆盖静默发生难调试 | 下游取到错误值难定位 | 引擎记录 WARN 日志,包含被覆盖的 key 和两个 nodeId |
| Hermes 流式输出与上下文快照混入 SSE | 前端处理混乱 | contextSnapshot 不走 SSE,仅持久化到 DB;SSE 流维持现状 |
| Agent/SmartAction 多输出被忽略 | 用户期望 Agent 单节点产出多个变量 | 本期不支持,WARN 提示,留 TODO;用户可改用 LLM 节点或拆成多个节点 |
| 模块 | 文件数 | 代码行数(估) |
|---|---|---|
NodeTypeUtils 新方法(resolveOutputVarName + singleOutput) |
1 | +30 |
StructuredOutputHelper 新建(含三级 JSON 提取 + 类型转换) |
1 | +120 |
LlmExecutor 改造(单/多输出分支) |
1 | +30 |
| Executor 改造(4 个非 LLM 文本型 + 1 个 condition) | 5 | ~6 行/文件 |
WorkflowContext.sections |
1 | +20 |
NodeExecutionResult.contextSnapshot |
1 | +15 |
WorkflowLevelExecutor 快照逻辑 |
1 | +20 |
WorkflowRunNode 实体 + Repository |
2 | +10 |
checkPreconditions 类型校验 |
1 | +25 |
| 前端 RunHistory / WorkflowEditor 扩展 | 2-3 | +150 |
| 合计 | ~16 | ~430 |
response_format: json_schema(需模型支持),获得更强保证| 文件 | 行号 | 当前代码 | 应改为 |
|---|---|---|---|
LlmExecutor.java |
59 | Map.of(nodeId, result) |
单输出:Map.of(resolveOutputVarName(data, "result"), result)多输出: StructuredOutputHelper.parse() 拆分,失败降级到 outputs[0].name |
AgentExecutor.java |
78 | Map.of(nodeId, result) |
Map.of(resolveOutputVarName(data, "result"), result) |
SmartActionExecutor.java |
61 | Map.of(nodeId, result) |
Map.of(resolveOutputVarName(data, "result"), result) |
HermesAgentExecutor.java |
79 | Map.of(nodeId, runResult.getFinalText()) |
Map.of(resolveOutputVarName(data, "result"), runResult.getFinalText()) |
HermesSmartActionExecutor.java |
63 | Map.of(nodeId, runResult.getFinalText()) |
Map.of(resolveOutputVarName(data, "result"), runResult.getFinalText()) |
ConditionExecutor.java |
78, 83 | Map.of(nodeId, "branch-N") |
Map.of() |
| 节点类型 | getNodeOutputs 默认返回 |
后端 fallback 变量名 |
|---|---|---|
userInput |
data.variables 数组 |
(无 fallback,直接用 variables) |
llm |
data.outputs 或 [{name:'result'}] |
result |
agent |
data.outputs 或 [] |
result |
skill |
data.outputs 或 [] |
result |
condition |
透传 inputs | (路由靠 sourceHandle,无输出) |
output |
[] |
(终端节点) |
前后端 fallback 一致性:本方案确保后端 resolveOutputVarName 的默认值与前端 ioInference.getNodeOutputs 的默认值一致(result)。