用户观察到的现象:
节点 envelope = {
message: "Hermes Agent 执行成功",
data: { result: "API call failed after 3 retries: HTTP 429: 该模型当前访问量过大..." }
}
工作流最终状态 = SUCCESS
LLM 实际调用失败(HTTP 429),但工作流报告成功。根因是 跨语言契约缺失 + envelope 双重语义混淆,问题分布在 4 个层级:
| 层级 | 文件 | 问题 |
|---|---|---|
| 1. Python Agent | backend/hermes-agent/run_agent.py |
AIAgent.run() 在 LLM 失败时不抛异常,错误描述作为 final_response 字符串返回 |
| 2. Python Bridge | backend/hermes-bridge/hermes_bridge.py::_run_agent_stream |
把 Agent 返回的错误描述当作正常 done 事件发出 |
| 3. Java Client | backend/.../engine/hermes/HermesBridgeClient.java::parseSseResponse |
case "done" 分支把错误文本当作正常回复累积到 finalResponse |
| 4. Java Executor | backend/.../engine/executor/HermesAgentExecutor.java |
只判断 finalText 是否空白,非空就返回 NodeExecutionResult.success + envelope.success |
| 5. Engine | backend/.../engine/WorkflowLevelExecutor.java::onNodeCompleted |
只读外层 result.getStatus(),不看内层 envelope.status;外层 SUCCESS 时即便 envelope.status=400 也不进入失败分支 |
| 方案 | 改动半径 | 是否在本期实施 |
|---|---|---|
| A: Python Bridge / hermes-agent 显式区分 done / error | 跨语言 + 第三方代码 | ❌ 不做(hermes-agent 是上游代码) |
| B: Java Client 做错误文本嗅探,命中则抛异常 | HermesBridgeClient 单文件 |
✅ 实施(含可配置菜单) |
| C: Executor 同步外层 NodeExecutionResult 与 envelope.status | HermesAgentExecutor + HermesSmartActionExecutor |
✅ 实施 |
| D: 引擎层 envelope.status=400 自动升级为节点 FAILED | WorkflowLevelExecutor 单文件 |
✅ 实施 |
整体目标:只要业务结果实质失败,无论失败在哪一层被识别,工作流都必须感知。
改动文件:backend/src/main/java/com/agent/management/engine/WorkflowLevelExecutor.java
改动位置:onNodeCompleted 方法的 synchronized (ctx.recordsLock) 块(约 309-330 行)
当前逻辑:
if (result.getStatus() == NodeExecutionResult.Status.FAILED) {
// ... failStrategy 处理
}
改造后逻辑:
NodeExecutionResult effectiveResult = result;
// D: envelope 失败自动升级 —— SUCCESS 但 envelope.status=400 时,按 FAILED 处理
if (result.getStatus() == NodeExecutionResult.Status.SUCCESS
&& NodeOutputEnvelope.isEnvelope(result.getOutput())) {
NodeOutputEnvelope env = NodeOutputEnvelope.fromObject(result.getOutput());
if (env != null && env.isFailure()) {
String originalMsg = env.getMessage();
String errMsg = String.format("节点 %s 业务执行失败(envelope.status=400): %s",
nodeId, originalMsg == null || originalMsg.isEmpty() ? "未提供失败原因" : originalMsg);
effectiveResult = NodeExecutionResult.failed(nodeId, errMsg, result.getLogs());
// 关键:把失败 envelope 写入上下文,让下游节点能通过 envelope.status 感知(不能丢)
context.setNodeOutput(nodeId, effectiveResult.getOutput() == null
? NodeOutputEnvelope.failure(errMsg).toMap()
: effectiveResult.getOutput());
}
}
// 后续判断与记录都用 effectiveResult
关键点:
executeOneNode 之外(即 onNodeCompleted)做这个升级,因为 executeOneNode 已经写入 output 到 context,这里只补"如果 envelope 是 failure,按 FAILED 走 failStrategy"workspace.getOutputStatus(prevId) 感知output 类型节点(其 envelope 永远是 success/failure,failure 时也按失败传播)Status.FAILED 的节点零影响(路径不变)改动文件:
backend/src/main/java/com/agent/management/engine/executor/HermesAgentExecutor.javabackend/src/main/java/com/agent/management/engine/executor/HermesSmartActionExecutor.java当前问题:
} catch (Exception e) {
return NodeExecutionResult.success(nodeId,
NodeOutputEnvelope.failure("Hermes Agent 调用失败: " + e.getMessage()).toMap());
}
外层 success + 内层 failure 是语义不一致的。WorkflowLevelExecutor 当前只看外层,所以这种节点失败不会触发 failStrategy。
改造(两处对称修改):
} catch (Exception e) {
log.error("[HermesAgent] 节点 {} 调用失败: {}", nodeId, e.getMessage());
// C: 外层状态与 envelope.status 保持一致 —— 抛给引擎的 NodeExecutionResult 也 FAILED
return NodeExecutionResult.failed(nodeId,
"Hermes Agent 调用失败: " + e.getMessage());
}
同时把"返回空结果"分支也改为 failed(原来的 success+failure envelope 也是反模式):
if (runResult.getFinalText() == null || runResult.getFinalText().isBlank()) {
return NodeExecutionResult.failed(nodeId, "Hermes Agent 返回空结果", runResult.getLogs());
}
兜底机制:executeOneNode 在 result.getStatus() == FAILED 且 output == null 时自动构造 failure envelope 写入 context(已有逻辑,不变),下游节点可通过 envelope.status 感知。
"Agent 节点未关联 Skill"、"加载 Skill 失败" 等早期失败分支 也一并改为 failed。
新增实体:HermesErrorPattern
public class HermesErrorPattern {
private Long id;
private String name; // 模式名称,如 "LLM 429 限流"
private String pattern; // 匹配文本(contains,大小写不敏感)
private Boolean enabled; // 启用/禁用
private Boolean isBuiltIn; // 内置不可删除标记
private String description; // 可选说明
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
匹配规则:使用 String#toLowerCase().contains(pattern.toLowerCase()),简单稳定,避免正则转义陷阱。匹配范围限定在 finalText 的前 N 个字符(默认 500)以避免对超长回复做无效扫描。
设计权衡:曾考虑正则表达式,但 (a) 用户配置正则易出错;(b) 错误描述大多是固定前缀(
"API call failed after"、"HTTP 4"),子串匹配已足够;(c) 子串匹配对前后台一致性更友好。如果未来需要更复杂匹配,可在 pattern 增加前缀regex:区分。
按现有项目模式(AiModelRepository + AiModelService + AiModelController)实现:
HermesErrorPatternRepository extends AbstractJsonRepository<HermesErrorPattern>,文件名 hermes-error-patterns.jsonHermesErrorPatternService 提供:list()、create(req)、update(id, req)、delete(id)、getActivePatterns()、isErrorResponse(String finalText)HermesErrorPatternController,路径 /api/hermes-error-patternsController 风格:对齐 AiModelController(POST /{id}/edit、/{id}/delete、/{id}/toggle 动词式 path)。
改动文件:backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java
改动位置:parseSseResponse 方法的 case "done" 分支后、return new HermesRunResult(...) 前。
private HermesRunResult parseSseResponse(...) {
// ... 现有逻辑
String finalText = finalResponse.toString();
// B: 错误响应嗅探
if (errorPatternService != null) {
ErrorDetectionResult detection = errorPatternService.isErrorResponse(finalText);
if (detection.isError()) {
String errMsg = String.format("Hermes Agent 返回的响应被识别为错误(命中模式「%s」): %s",
detection.getMatchedPatternName(),
truncate(finalText, 200));
LLM_HTTP.warn("[Hermes-RESP] 错误响应嗅探命中: pattern={}, response={}",
detection.getMatchedPatternName(), truncate(finalText, 500));
throw new HermesErrorResponseException(errMsg, detection.getMatchedPatternName());
}
}
return new HermesRunResult(finalText, logs);
}
为什么抛 HermesErrorResponseException 而非 RuntimeException:
HermesAgentExecutor 的 catch 块能区分"网络/进程异常"(重试类)与"业务错误响应"(不可重试)依赖注入:HermesBridgeClient 构造器新增 HermesErrorPatternService 参数(可为 null,方便测试)。
通过 HermesErrorPatternService.@PostConstruct initBuiltInPatterns() 实现:
hermes-error-patterns.json 不存在或为空,写入以下内置模式(isBuiltIn=true):
"API call failed after" — LLM SDK 重试耗尽"HTTP 4" — 4xx 客户端错误"HTTP 5" — 5xx 服务端错误"rate limit" — 限流(英文)"访问量过大" — 智谱 AI 限流(中文)"该模型当前访问量过大" — 智谱 AI 限流(更具体)"quota" / "额度" / "余额不足" — 配额耗尽"authentication" / "invalid api key" / "无效的 api" — 认证失败为什么不用 application.yml 作为内置来源:用户的诉求就是"做一个菜单页进行列表配置",所以默认值放 JSON 仓库里更直接。yml 配置适合运维固定参数,不适合动态列表。
菜单:在 frontend/src/components/layout/AppSidebar.vue 的"系统管理"或底部独立分组追加:
// 在 menuItems 中追加
{ key: '/hermes-errors', label: '错误模式管理', icon: WarningOutline }
路由:frontend/src/router/index.js 追加:
{
path: '/hermes-errors',
name: 'HermesErrorPatterns',
component: () => import('../views/system/HermesErrorPatterns.vue'),
meta: { title: '错误模式管理' }
}
API:frontend/src/api/hermesError.js
import request from '../utils/request'
export function getHermesErrorPatterns() {
return request.get('/hermes-error-patterns')
}
export function createHermesErrorPattern(data) {
return request.post('/hermes-error-patterns', data)
}
export function updateHermesErrorPattern(id, data) {
return request.post(`/hermes-error-patterns/${id}/edit`, data)
}
export function deleteHermesErrorPattern(id) {
return request.post(`/hermes-error-patterns/${id}/delete`)
}
export function toggleHermesErrorPattern(id) {
return request.post(`/hermes-error-patterns/${id}/toggle`)
}
页面:frontend/src/views/system/HermesErrorPatterns.vue
主要功能:
NDataTable 列表展示,列:启用状态(switch)、名称、匹配模式、说明、内置标记(tag)、操作(编辑/删除/复制)NForm 含名称、匹配模式、说明、启用UI 风格:与 ModelManagement.vue 一致(NDataTable + NModal + NForm + useMessage)。
按依赖关系:
1. 方案 D:WorkflowLevelExecutor envelope 失败自动升级
(独立改造,为后续方案兜底)
2. 方案 C:HermesAgentExecutor + HermesSmartActionExecutor 外层状态对齐
(依赖 D 把 SUCCESS+failure envelope 也视为失败)
3. 方案 B 后端:实体 + Repository + Service + Controller + Client 集成
4. 方案 B 前端:菜单 + 路由 + API + 页面
5. 验证:编译 + 单元测试 + 手工触发 429 场景
新增测试覆盖:
WorkflowLevelExecutorEnvelopeUpgradeTest:D 方案的核心测试
HermesErrorPatternServiceTest:B 方案的匹配测试
HermesBridgeClientErrorDetectionTest:B 方案的集成测试
HermesErrorResponseExceptionHermesRunResult| 风险 | 缓解 |
|---|---|
| 错误模式误判(用户合法回复里恰好包含 "rate limit" 等关键词) | (a) 模式由用户精细管理;(b) 内置模式偏激进可禁用;(c) 嗅探只看 finalText,不影响 tool 中间步骤 |
| D 方案改动影响其他节点 | 只对 SUCCESS + envelope.status=400 生效,FAILED 路径不变;output 类型节点失败时也合理传播 |
| C 方案改变历史行为 | 原本"Hermes 调用失败但工作流继续"的行为本就是 bug,对依赖此 bug 的工作流是 break-change。但比起继续假装成功,让 failStrategy 接管是更正确的语义 |
| Python Bridge 仍可能漏报错误 | 方案 A 留作后续;B 方案的列表可动态扩展,遇到新错误描述可立即配置 |