# 工作流节点自动输入输出与变量转换规范 > 说明:本文档基于当前代码实现梳理,覆盖后端执行器(`backend/src/main/java/com/agent/management/engine/executor`)与前端 IO 推断逻辑(`frontend/src/utils/ioInference.js`)。 > 文中“转换器”一栏既描述当前运行时的实际行为,也给出推荐的统一转换规则,供后续在 `TemplateRenderer` / `ContextPromptHelper` 或前置条件校验中实现。 --- ## 1. 术语与上下文机制 ### 1.1 变量存储 工作流上下文 `WorkflowContext` 内部维护一张**扁平变量表** `variables: ConcurrentHashMap`: - 运行开始时,把 `request.inputs` 全部放入。 - 每个节点执行完成后,`WorkflowLevelExecutor` 调用 `context.setNodeOutput(nodeId, output)`,把 `output` 中的每个 key-value 写入 `variables`。 - 同名 key 会覆盖上游值,并打印 WARN 日志。 ### 1.2 变量消费方式 | 消费方式 | 触发位置 | 当前行为 | |---------|---------|---------| | `{{变量名}}` 模板渲染 | `TemplateRenderer` | 从 `variables` 取值后调用 `value.toString()` 替换;变量不存在时保留占位符。 | | 自动系统提示注入 | `ContextPromptHelper` | 把全部 variables 按 `- key:value` 格式拼接为系统提示,value 同样走 `toString()`,超长截断至 2000 字符。 | | 前置条件校验 | `WorkflowLevelExecutor.checkPreconditions` | 仅校验存在性与类型兼容性,不做类型转换。 | | Agent / Skill 用户消息 | `AgentExecutor.buildUserMessage` / `HermesAgentExecutor.buildUserMessage` | 按 Skill.inputs 定义取值后 `toString()` 拼接。 | | 条件分支判断 | `ConditionExecutor` | 把全部 variables 的 `toString()` 拼成上下文摘要,交给 LLM 判断分支编号。 | ### 1.3 类型系统 前后端统一的 IO 类型定义: ``` string | number | boolean | array | object | filePath | directoryPath ``` 来源: - 后端:`com.agent.management.model.dto.IOField` - 前端:`frontend/src/utils/ioInference.js` 的 `IO_TYPES` --- ## 2. 节点类型总览 | 节点 type | 中文名 | 后端执行器 | 前端组件 | |----------|--------|-----------|---------| | `userInput` | 用户输入 | `UserInputExecutor` | `InputNode.vue` | | `llm` | 大模型处理 | `LlmExecutor` | `LLMNode.vue` | | `agent` | 智能体 | `AgentExecutor`(hermes=false)/ `HermesAgentExecutor`(hermes=true) | `AgentNode.vue` | | `skill` | 技能 | `SkillExecutor`(委托给 agent 执行器) | `SkillNode.vue` | | `smartAction` | 智能操作 | `SmartActionExecutor`(hermes=false)/ `HermesSmartActionExecutor`(hermes=true) | `SmartActionNode.vue` | | `knowledgeRetrieval` | 知识库检索 | `KnowledgeRetrievalExecutor` | `KnowledgeRetrievalNode.vue` | | `condition` | 条件分支 | `ConditionExecutor` | `ConditionNode.vue` | | `output` | 输出 | `OutputExecutor` | `OutputNode.vue` | --- ## 3. 各节点自动输入输出字段 ### 3.1 userInput(用户输入节点) **自动输入**:无。节点本身只负责把运行时已经注入到 `variables` 中的初始输入值注册为可用变量。 **自动生成输出**: | 输出字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `variables[i].name` | 由前端声明的类型决定 | `request.inputs` 中同名 key | 节点 data.variables 数组里每个条目的 name 字段;若运行时未提供该 key,则不写入。 | > 例如:data.variables = `[{name:"question", type:"string"}]`,则输出变量名为 `question`。 --- ### 3.2 llm(大模型节点) **自动输入**: | 输入字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `systemPrompt` 中的 `{{var}}` | string | 上游 variables | 模板渲染后作为系统提示。 | | `userPrompt` 中的 `{{var}}` | string | 上游 variables | 模板渲染后作为用户提示。 | | `data.inputs[]` 声明的字段 | 任意 | 上游 variables | 用于前端边映射校验与 `checkPreconditions` 前置条件校验。 | | 全部上游 variables | object/map | 上下文 | 通过 `ContextPromptHelper.merge(...)` 自动追加到系统提示词末尾。 | **自动生成输出**: | 输出字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `outputs[0].name` 或 `result` | string | LLM 响应文本 | 单输出场景。若 `data.outputs` 为空,默认变量名为 `result`。 | | `outputs[i].name`(i≥0) | 由 outputs[i].type 决定 | LLM 响应经 JSON 解析 | 多输出场景(`outputs` 长度 > 1):由 `StructuredOutputHelper` 注入 JSON 指令并解析。 | 多输出字段类型转换见第 4 节。 --- ### 3.3 agent / skill(智能体 / 技能节点) 两者对外接口一致;`SkillExecutor` 把 `skillId` 复制为 `agentId` 后委托给 agent 执行器。 **自动输入**: | 输入字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | 关联 Skill 的 `inputs[]` | 由 Skill 声明 | 上游 variables | `buildUserMessage` 按 Skill 输入定义从 context 取值。 | | 全部上游 variables | object/map | 上下文 | 通过 `ContextPromptHelper.merge(...)` 追加到 SKILL.md 系统提示后。 | > 注意:当前 agent/skill 节点的前端默认数据中没有 `inputs/outputs` 数组,依赖 Skill 元数据中的 `inputs`/`outputs`。 **自动生成输出**: | 输出字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `outputs[0].name` 或 `result` | string | Agent 执行返回的 finalText | 当前仅支持单输出,多 outputs 时也只取第一个。 | --- ### 3.4 smartAction(智能操作节点) **自动输入**: | 输入字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `actionPrompt` 中的 `{{var}}` | string | 上游 variables | 模板渲染后作为操作要求。 | | `data.inputs[]` 声明的字段 | 任意 | 上游 variables | 用于前端边映射校验与前置条件校验。 | | 全部上游 variables | object/map | 上下文 | 通过 `ContextPromptHelper` 自动注入系统提示。 | **自动生成输出**: | 输出字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `outputs[0].name` 或 `result` | string | LLM / Hermes 执行结果 | 单输出场景,默认变量名 `result`。 | --- ### 3.5 knowledgeRetrieval(知识库检索节点) **自动输入**: | 输入字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `query` 中的 `{{var}}` | string | 上游 variables | 模板渲染后作为检索语句。 | | `data.inputs[]` 声明的字段 | 任意 | 上游 variables | 用于前端边映射校验与前置条件校验。 | | `source` / `topK` / 各类 sourceIds | 配置项 | 节点 data | 决定检索范围与来源类型。 | **自动生成输出**: | 输出字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `evidences` | `List` | 检索结果 | 命中的证据列表,每个元素含 `id / sourceType / evidenceType / title / content / score / sourceName / sourceRef / payload / metadata`。 | | `evidenceCount` | number | 检索结果 | `evidences.size()`。 | | `sourceType` | string | 检索结果 | `DOCUMENT` / `STRUCTURED_DATA` / `GRAPH` / `HYBRID`。 | | `diagnostics` | object | 检索结果 | 各来源的诊断信息,key 可能为 `datasource:` / `graph:` / `error:...`。 | | `knowledgeBaseId` | number | 节点 data(仅 hybrid) | 混合检索时指定的知识库 ID。 | | `enabledSources` | object/map | 后端结果(仅 hybrid) | 各来源是否启用。 | > 默认输出变量:若用户未在 `data.outputs` 中自定义,前端默认暴露 `evidences / evidenceCount / sourceType / diagnostics`。 --- ### 3.6 condition(条件分支节点) **自动输入**: | 输入字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `conditions[i].expression` 中的 `{{var}}` | string | 上游 variables | 条件表达式模板。 | | `data.inputs[]` 声明的字段 | 任意 | 上游 variables | 用于前端边映射校验。 | | 全部上游 variables | object/map | 上下文 | `ConditionExecutor` 把 variables 摘要后交给 LLM 判断分支。 | **自动生成输出**: 条件节点**不向 `variables` 写入任何变量**。它通过 `NodeExecutionResult.selectedBranch` 返回分支句柄(如 `branch-0`、`branch-1`),由 `WorkflowLevelExecutor.activateDownstream` 按 `edge.sourceHandle` 过滤下游激活节点。 --- ### 3.7 output(输出节点) **自动输入**: | 输入字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | `data.fields[]` 声明的字段 | 任意 | 上游 variables | 用于前端选择最终输出字段。 | | 全部上游 variables | object/map | 上下文 | 节点执行时直接收集整个 variables 表。 | **自动生成输出**: | 输出字段 | 类型 | 来源 | 说明 | |---------|------|------|------| | 全部上游 variables | 混合 | 上下文 | `OutputExecutor` 把 `context.getVariables()` 原样放入输出。 | | `_workingDirFiles` | array | 工作目录扫描 | 工作目录中经 `.agentignore` 过滤后的相对文件路径列表。 | | `_runId` | string | 上下文 | 本次运行 ID。 | --- ## 4. 输出字段 → 下游输入字段转换器规范 ### 4.1 通用转换规则 当前运行时**没有**按目标类型做自动转换的组件,统一走 `Object.toString()`。为支持工作流节点之间的类型安全连接,推荐引入以下转换器语义。 #### 基础类型转换表 | 源实际值 | 目标类型 string | 目标类型 number | 目标类型 boolean | 目标类型 object | 目标类型 array | 目标类型 filePath / directoryPath | |---------|----------------|----------------|-----------------|----------------|---------------|----------------------------------| | `String` | 原值 | 尝试 `Double.parseDouble`;失败取 `NaN` | `"true"`/`"1"`/`"yes"` 为 true,其余 false | 尝试 JSON 解析为对象;失败封装为 `{value: 原值}` | 尝试 JSON 解析为数组;失败封装为 `[原值]` | 若值对应工作目录中合法路径则通过;否则保留字符串 | | `Number` | `toString()` | 原值 | `!= 0` 为 true | `{value: 数字}` | `[数字]` | 不适用 | | `Boolean` | `toString()` | true→1,false→0 | 原值 | `{value: true/false}` | `[true/false]` | 不适用 | | `Map / POJO` | `ObjectMapper.writeValueAsString` | 若 map 仅含数值字段可尝试;否则 NaN | 非空 map 为 true | 原值 | `[Map.Entry]` 列表 | 不适用 | | `Collection / Array` | 拼接为字符串(推荐 JSON) | 长度值 | 非空为 true | 若长度为 1 取首元素;否则 `{items: 数组}` | 原值 | 不适用 | | `RagEvidence`(检索证据) | `content` 字段内容 | 无意义 | 非空为 true | 将 POJO 转为 Map(含 title/content/score...) | `[证据Map]` | 不适用 | #### 兼容性原则(与现有代码保持一致) 前后端已有的类型兼容规则: - `filePath` / `directoryPath` / `number` / `boolean` → 可隐式转为 `string`。 - `array` → 可隐式赋值给 `object`。 - 其他组合视为不兼容。 ### 4.2 各节点输出字段的推荐转换器 #### 4.2.1 LLM / Agent / SmartAction 的 `result`(string) | 下游目标类型 | 转换器行为 | |-------------|-----------| | `string` | 直接传递完整文本。 | | `number` | 尝试提取文本中的第一个数字;无数字返回 `NaN`。 | | `boolean` | 文本为 `"true"`/`"1"`/`"yes"` 返回 true; `"false"`/`"0"`/`"no"` 返回 false;其余按非空判断。 | | `object` | ① 尝试整段 JSON 解析为 Map;② 失败则取文本前 2000 字符封装为 `{result: text}`。 | | `array` | ① 尝试 JSON 解析为 List;② 失败则按行/段落拆分后封装为 `["line1", "line2", ...]`。 | #### 4.2.2 LLM 结构化多输出字段 由 `StructuredOutputHelper.convertValue` 已提供基础转换: - `number`:`value.isNumber() ? value.numberValue() : Double.parseDouble(value.asText())` - `boolean`:`value.isBoolean() ? value.booleanValue() : Boolean.parseBoolean(value.asText())` - `array` / `object`:`MAPPER.treeToValue(value, Object.class)` - 默认:文本或 `toString()` #### 4.2.3 KnowledgeRetrieval 的 `evidences`(`List`) 这是用户问题中重点示例的场景。 | 下游目标类型 | 转换器行为 | |-------------|-----------| | `string` | 取 Top-1 证据的 `content` 字段;若列表为空返回 `""`。推荐可配置为 `"top1"` 或 `"concat"` 两种模式。 | | `number` | 返回 `evidenceCount`(即列表长度)。 | | `boolean` | 列表非空返回 true。 | | `object` | 取 Top-1 证据,将其字段(id/sourceType/evidenceType/title/content/score/sourceName/sourceRef/payload/metadata)转为 Map。 | | `array` | 取 Top-K(默认全部)证据列表,每条证据转为 Map。 | | `filePath` / `directoryPath` | 不适用,转换失败。 | #### 4.2.4 KnowledgeRetrieval 的 `evidenceCount`(number) | 下游目标类型 | 转换器行为 | |-------------|-----------| | `string` | `String.valueOf(count)` | | `number` | 原值 | | `boolean` | `count > 0` | | `object` | `{evidenceCount: count}` | | `array` | `[count]` | #### 4.2.5 KnowledgeRetrieval 的 `sourceType`(string) | 下游目标类型 | 转换器行为 | |-------------|-----------| | `string` | 原值,如 `"DOCUMENT"` / `"GRAPH"` | | `object` | `{sourceType: value}` | | `array` | `[value]` | #### 4.2.6 KnowledgeRetrieval 的 `diagnostics`(Map) | 下游目标类型 | 转换器行为 | |-------------|-----------| | `string` | `ObjectMapper.writeValueAsString(diagnostics)` | | `object` | 原 Map | | `array` | Map 的 entry 列表 `[{key, value}, ...]` | #### 4.2.7 userInput 变量(按声明类型) 由于前端已声明类型,转换器按声明类型直接透传;若下游目标类型与声明类型不兼容,再按 4.1 通用规则转换。 #### 4.2.8 Output 节点收集的 `_workingDirFiles`(array) | 下游目标类型 | 转换器行为 | |-------------|-----------| | `string` | 取第一条路径;空数组返回 `""`。 | | `array` | 原数组 | | `object` | `{files: array}` | --- ## 5. 当前代码实现速查 | 关注点 | 文件路径 | |-------|---------| | 节点执行器接口 | `backend/src/main/java/com/agent/management/engine/NodeExecutor.java` | | 执行器注册与调度 | `backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java` | | 上下文与变量存储 | `backend/src/main/java/com/agent/management/engine/WorkflowContext.java` | | `{{变量}}` 模板渲染 | `backend/src/main/java/com/agent/management/engine/TemplateRenderer.java` | | 上下文系统提示注入 | `backend/src/main/java/com/agent/management/engine/ContextPromptHelper.java` | | 单/多输出变量名解析 | `backend/src/main/java/com/agent/management/engine/NodeTypeUtils.java` | | LLM 结构化输出解析 | `backend/src/main/java/com/agent/management/engine/StructuredOutputHelper.java` | | 前置条件类型校验 | `backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java:294` | | 前端 IO 推断与映射 | `frontend/src/utils/ioInference.js` | | 前端节点默认数据 | `frontend/src/utils/workflowNode.js` | | 证据数据结构 | `backend/src/main/java/com/agent/management/rag/model/RagEvidence.java` | ### 5.1 前置数据关联范围 前端 `WorkflowEditor.vue` 中节点配置面板的"关联方式"下拉选项由 `inputMappingOptions` 计算得出,其范围: - **不限于直接上游**:从当前选中节点反向 BFS 遍历边图(`getReachablePredecessors(targetId, allNodes, allEdges)`),收集所有**可达前序节点**的输出字段。 - 这意味着:上游的上游、分叉合并的另一支、跨多层跳跃的前序节点输出,都可在"关联方式"中显式选择。 - 该范围与运行时 `NodeWorkspace` 的可达前驱定义一致(详见 `docs/workflow-variable-scope-design.md` 4.1 节),保证编辑期选项与运行期可见变量对齐。 涉及的实现: | 函数 | 位置 | 作用 | |------|------|------| | `getReachablePredecessors` | `frontend/src/utils/ioInference.js` | 反向 BFS 计算可达前驱节点列表 | | `inputMappingOptions` | `frontend/src/views/workflow/WorkflowEditor.vue` | 列出可达前驱 × 输出字段作为关联选项 | | `getNodeOutputs` | `frontend/src/utils/ioInference.js` | 提取单个节点的输出字段 | --- ## 6. 待实现建议 1. **统一转换器注册表**:在 `TemplateRenderer` 或新增 `VariableConverter` 中按“源字段 + 目标类型”注册转换函数,替代目前简单的 `value.toString()`。 2. **KnowledgeRetrieval `evidences` 转 string 策略可配置**:支持 `top1`、`concat`、`json` 等模式,满足不同下游节点需求。 3. **前置条件校验增强**:在 `checkPreconditions` 中引入转换器尝试,使 array→object、number→string 等兼容组合真正可用,而非仅做类型检查。 4. **前端类型提示**:在 `ioInference.js` 的 `isTypeCompatible` 基础上,增加转换器说明,让用户在连线和配置 mapping 时看到自动转换规则。