# 工作流节点自动输入输出与变量转换规范

> 说明：本文档基于当前代码实现梳理，覆盖后端执行器（`backend/src/main/java/com/agent/management/engine/executor`）与前端 IO 推断逻辑（`frontend/src/utils/ioInference.js`）。
> 文中“转换器”一栏既描述当前运行时的实际行为，也给出推荐的统一转换规则，供后续在 `TemplateRenderer` / `ContextPromptHelper` 或前置条件校验中实现。

---

## 1. 术语与上下文机制

### 1.1 变量存储

工作流上下文 `WorkflowContext` 内部维护一张**扁平变量表** `variables: ConcurrentHashMap<String, Object>`：

- 运行开始时，把 `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<RagEvidence>` | 检索结果 | 命中的证据列表，每个元素含 `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:<id>` / `graph:<id>` / `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<string> | 工作目录扫描 | 工作目录中经 `.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<RagEvidence>`）

这是用户问题中重点示例的场景。

| 下游目标类型 | 转换器行为 |
|-------------|-----------|
| `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>）

| 下游目标类型 | 转换器行为 |
|-------------|-----------|
| `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 时看到自动转换规则。
