状态:已确认,待实施 关联:workflow-envelope-strict-validation-plan.md、node-context-ondemand-plan.md
LLM 类节点(LLM、智能操作、技能、Hermes 智能体、Hermes 智能操作)需要根据节点画布声明的 outputs 字段,动态生成 JSON 输出约束注入到 prompt,让 LLM 严格按 schema 产出结构化结果,并能优雅处理失败重试。
required=true 字段是否存在且类型匹配StructuredOutputHelper.buildInstruction(StructuredOutputHelper.java:326-356)已根据 data.outputs 动态拼接 JSON 模板,但格式是 "name": <type> 占位符,无示例值OutputValidator.validate(OutputValidator.java:100-152)已实现 strict 校验 + 自动类型转换StructuredOutputHelper.java:91-146)LlmExecutor(LlmExecutor.java:60-75)、SmartActionExecutor(SmartActionExecutor.java:66-83)已完整接入HermesAgentExecutor.java:87-91、HermesSmartActionExecutor.java:74-76、AgentExecutor.java:83-85、SkillExecutor 仅做单字段透传<type> 占位符对小模型不够友好strictAllRequired=true,所有声明字段一律视为必填,无法区分 required/optional| 决策点 | 选择 | 说明 |
|---|---|---|
| 模板格式 | 带示例值 | "output1": "XXX" 形式 |
| Hermes 内层重组 LLM 来源 | 节点 data 中独立字段 reformat_model_id |
区别于节点主模型,允许配置更便宜的重组模型 |
| Hermes 外层重跑 session 策略 | 复用同一 session | 与 Bridge 现有缓存机制一致 |
| strict 兼容期开关 | 不保留,直接改 | 修订后更宽松,存量节点不会因此失败 |
| 扩展字段下游过滤 | 暂不做 | 与 node-context-ondemand-plan.md 解耦 |
| object 子字段约束 | 暂不做 | 后续单独实现 |
Hermes 与普通 LLM 的本质差异:
| 维度 | 普通 LLM | Hermes Agent |
|---|---|---|
| 调用方式 | 单次 prompt→response | 多轮工具调用 + SSE 流 |
| 内层重试代价 | 低(再调一次 LLM) | 极高(重跑整个 Agent 链) |
| 重试可控性 | 高 | 低(Agent 决策路径不可控) |
结论:内层"格式重组"重试不能用 Hermes 重跑,否则一次格式错误就触发完整 Agent 流程,成本不可接受。
外层(runtime retry) ──→ Hermes 完整重跑(同一 session)
↓ finalText
内层(format retry) ──→ 普通 LLM 重组(用 reformat_model_id 配置的模型)
↓ 符合 schema 的 JSON
OutputValidator 校验
职责分离:
HermesAgentExecutor.execute(nodeId, data, context)
│
├─ 1. 构造 userMessage = task + buildInstruction(outputsDecl)
│
├─ 2. hermesBridgeClient.run(userMessage, ...)
│ → SSE 流式(thinking + tool_calls)
│ → finalText(Agent 最终回复)
│
├─ 3. needsStructuredOutput(data)?
│ ├─ 否 → 单字段透传(保持现状)
│ └─ 是 → 进入步骤 4
│
└─ 4. StructuredOutputHelper.extractWithRetryFromText(
finalText,
outputsDecl,
policy,
reformatClient, // 内层重组用,按 reformat_model_id 解析
runtimeRetrySupplier // 外层重试用:hermesBridgeClient 重跑
)
→ envelope.success(data) 或 envelope.failure(...)
StructuredOutputHelper 新增方法:
public static ExtractionResult extractWithRetryFromText(
String rawText,
JsonNode outputsDecl,
RetryPolicy policy,
LlmClient reformatClient, // 内层用,可空(空则跳过内层)
Supplier<String> runtimeRetrySupplier // 外层用,可空(空则不重跑)
) { ... }
复用原则:内部子方法(tryParseJson、buildReformatPrompt、buildInstruction、OutputValidator.validate)与 LLM 路径完全复用,只换"原始文本来源"。
| Executor | 当前 | 改动 |
|---|---|---|
LlmExecutor |
已接入 | 模板格式跟随升级(无逻辑改动) |
SmartActionExecutor |
已接入 | 同上 |
HermesAgentExecutor |
未接入 | 调用 extractWithRetryFromText,传入 hermesBridgeClient::runAgain 作为外层 supplier |
HermesSmartActionExecutor |
未接入 | 同上 |
AgentExecutor |
未接入 | 根据 hermesProperties.enabled 分流到 Hermes 路径或 LLM 路径 |
SkillExecutor |
间接委托 | 委托链不变,跟随上游自动获得能力 |
runtimeMaxRetries=1(Hermes 系列专用)runtimeMaxRetries=2RetryPolicy.fromNodeData 根据节点类型设置默认值StructuredOutputHelper.buildInstruction 重写后产出(以 output1/output2/output3 为例):
{原用户 prompt / 技能执行逻辑输出}
---
**必须按如下 JSON 格式输出最终结果**(仅输出 JSON 本身,不要任何 Markdown 包裹、解释或额外文字):
{
"output1": "XXX", // string 类型
"output2": 0, // number 类型
"output3": {"key": "value"} // object 类型
}
**字段说明**:
- output1 (string, required): {description 或 "(无描述)"}
- output2 (number, required): {description 或 "(无描述)"}
- output3 (object, required): {description 或 "(无描述)"}
**扩展字段**(可选):
- 你可以自由添加业务必要的辅助字段(如 confidence、run_error、user_notice 等)
- 命名规则:snake_case
- 最小化原则:只放置真正必需的辅助信息,不要堆砌
- 不要添加与上述已声明字段语义重叠的字段
**约束**:
- 必须严格使用上述声明的字段名,区分大小写、区分分隔符(禁止中文字段名)
- required 字段缺失或类型不符将触发重试
- 输出必须能被 JSON 直接解析(首尾不能有非 JSON 字符)
| 声明类型 | 模板示例值 | 注释 |
|---|---|---|
string |
"XXX" |
string 类型 |
number |
0 |
number 类型 |
boolean |
true |
boolean 类型 |
array |
["item1", "item2"] |
array 类型 |
object |
{"key": "value"} |
object 类型 |
filePath / directoryPath / file |
"path/to/file" |
视为 string |
for (JsonNode decl : outputsDecl) {
String name = decl.path("name").asText();
boolean required = decl.path("required").asBoolean(false);
String type = decl.path("type").asText("string");
// 规则 1:只校验 required 字段是否存在
if (strict && required && !parsed.containsKey(name)) {
result.addMissing(name);
continue;
}
// 规则 2:字段存在则校验类型(无论 required)
if (parsed.containsKey(name)) {
Object converted = checkAndConvertType(parsed.get(name), type);
if (converted == TYPE_MISMATCH_SENTINEL) {
result.addTypeMismatch(name, type);
}
}
}
// 规则 3:多余字段(含 snake_case 扩展字段)完全不校验
| 场景 | 修订前 | 修订后 |
|---|---|---|
| required 字段缺失 | ❌ 报错 | ❌ 报错(不变) |
| 非 required 字段缺失 | ❌ strict 下报错 | ✅ 不报错 |
多余字段(如 confidence) |
不报错 | ✅ 不报错(明确化) |
| 类型不匹配 | ❌ 报错 | ❌ 报错(不变) |
| required 全存在 + 多余扩展 | ❌ 可能报错 | ✅ 通过 |
strictAllRequired 语义OutputValidatorStrictTest 同步更新改动文件:
engine/StructuredOutputHelper.java:重写 buildInstruction(示例值 + 扩展字段说明)engine/OutputValidator.java:修订 strict 规则(只校验 required)StructuredOutputHelperDualLoopTest、OutputValidatorStrictTest验证标准:
改动文件:
engine/StructuredOutputHelper.java:新增 extractWithRetryFromTextinvokeReformatOnly(内层重组,复用 buildReformatPrompt)验证标准:
改动文件:
engine/executor/HermesAgentExecutor.java:调用 extractWithRetryFromTextengine/executor/HermesSmartActionExecutor.java:同上engine/executor/AgentExecutor.java:根据 hermes 启用状态分流engine/RetryPolicy.java:增加节点类型感知,Hermes 系列默认 runtimeMaxRetries=1engine/hermes/HermesBridgeClient.java:可能需要"复用 session 重跑"接口验证标准:
| 风险 | 等级 | 缓解 |
|---|---|---|
| Hermes 内层重组需要 LLM 客户端 | 中 | 节点 data 配置 reformat_model_id,缺失时回退到 AiModelService.getDefaultModel() |
| Hermes 外层重跑成本高 | 高 | 默认 runtimeMaxRetries=1;监控告警 |
| strict 语义变化影响存量 | 低 | 修订后更宽松,已通过的节点不会失败 |
| 扩展字段污染下游 NodeInputResolver | 低 | YAGNI,本阶段不处理,由 node-context-ondemand-plan.md 后续覆盖 |
| 阶段 | 工时 |
|---|---|
| 阶段 1 | 3-4 小时 |
| 阶段 2 | 3-4 小时 |
| 阶段 3 | 6-8 小时 |
| 总计 | 12-16 小时 |