现象
使用某些模型网关(实测:Cloudflare Workers AI,经 new-api 中转)时,Eino 各 agentic 模式(单代理 / Deep / Supervisor / Plan-Execute)在模型发起并行工具调用的轮次必现失败:
- 工具卡片先正常出现(如
skill (1/2)、exec (2/2)),随后整轮报 执行失败
- 错误文案:
[NodeRunError] exceeds max retries: last error: model output rejected by ShouldRetry at attempt 5,UI 显示「模型调用重试已耗尽:模型未返回原始错误;输出被重试策略拒绝」
- 单工具调用场景一切正常;失败几乎总出现在第一次并行调用时
根因
-
Provider 行为:该类网关在并行 tool_calls 的 SSE 流中,会在两个工具调用之间插入纯空白文本帧。实测原始流(节选):
data: {"choices":[{"delta":{"tool_calls":[{"id":"call_a","index":0,...,"function":{"name":"exec","arguments":""}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"id":null,"index":0,...,"function":{"arguments":"{\"command\":\"whoami\"}"}}]}}]}
data: {"choices":[{"delta":{"content":"\n"}}]} <-- 插在两个工具调用之间的文本帧
data: {"choices":[{"delta":{"tool_calls":[{"id":"call_b","index":1,...,"function":{"name":"exec","arguments":""}}]}}]}
-
eino-ext 缺陷(已报上游 cloudwego/eino-ext#975,修复 PR cloudwego/eino-ext#976):acl/openai 的 chunkConverter.advanceContent 在工具调用开始后,给交错文本块分配了与工具调用块相同的 StreamingMeta.Index。
-
错误被掩盖:schema.ConcatAgenticMessages 按 index 分组时类型冲突报错(content block type mismatch: expected 'function_tool_call', but got 'assistant_gen_text');eino ADK 的 typedConsumeStream 吞掉该错误返回 nil 消息,CyberStrikeAI 的 ShouldRetry 空输出判定(internal/multiagent/eino_model_resilience.go isRetryableEmptyAgenticModelOutput)将其判为 empty_model_output,重试 5 次后耗尽——真实错误完全被掩盖。
最小复现:
block := func(idx int, b *schema.ContentBlock) *schema.AgenticMessage {
b.StreamingMeta = &schema.StreamingMeta{Index: idx}
return &schema.AgenticMessage{Role: schema.AgenticRoleTypeAssistant, ContentBlocks: []*schema.ContentBlock{b}}
}
frames := []*schema.AgenticMessage{
block(0, schema.NewContentBlock(&schema.AssistantGenText{Text: "\n\n"})),
block(1, schema.NewContentBlock(&schema.FunctionToolCall{CallID: "call_a", Name: "exec"})),
block(1, schema.NewContentBlock(&schema.FunctionToolCall{Arguments: `{"command":"whoami"}`})),
block(1, schema.NewContentBlock(&schema.AssistantGenText{Text: "\n"})), // 与工具调用块同 index → 冲突
block(2, schema.NewContentBlock(&schema.FunctionToolCall{CallID: "call_b", Name: "exec"})),
block(2, schema.NewContentBlock(&schema.FunctionToolCall{Arguments: `{"command":"id"}`})),
}
_, err := schema.ConcatAgenticMessages(frames)
// err: content block type mismatch: expected 'function_tool_call', but got 'assistant_gen_text'
修复方案
在 newEinoAgenticChatModelFactory 的模型实例外包一层修复包装器(与既有 streamToolCallIndexRepairModel 同一防御思路):检测流式帧中跨块类型的 StreamingMeta.Index 冲突,把冲突块改编到负数 index(不与 converter 后续 index 相撞),内容完整保留。覆盖 OpenAI 兼容与 Claude 两条 agentic 路径,eino 单代理 / Deep / Supervisor / Plan-Execute 均受益。
该补丁为框架边界防御,对正常流零影响;待 eino-ext 上游修复发版、依赖升级后可移除。
随附 PR 实现 + 单元测试。
现象
使用某些模型网关(实测:Cloudflare Workers AI,经 new-api 中转)时,Eino 各 agentic 模式(单代理 / Deep / Supervisor / Plan-Execute)在模型发起并行工具调用的轮次必现失败:
skill (1/2)、exec (2/2)),随后整轮报执行失败[NodeRunError] exceeds max retries: last error: model output rejected by ShouldRetry at attempt 5,UI 显示「模型调用重试已耗尽:模型未返回原始错误;输出被重试策略拒绝」根因
Provider 行为:该类网关在并行 tool_calls 的 SSE 流中,会在两个工具调用之间插入纯空白文本帧。实测原始流(节选):
eino-ext 缺陷(已报上游 cloudwego/eino-ext#975,修复 PR cloudwego/eino-ext#976):
acl/openai的chunkConverter.advanceContent在工具调用开始后,给交错文本块分配了与工具调用块相同的StreamingMeta.Index。错误被掩盖:
schema.ConcatAgenticMessages按 index 分组时类型冲突报错(content block type mismatch: expected 'function_tool_call', but got 'assistant_gen_text');eino ADK 的typedConsumeStream吞掉该错误返回 nil 消息,CyberStrikeAI 的ShouldRetry空输出判定(internal/multiagent/eino_model_resilience.goisRetryableEmptyAgenticModelOutput)将其判为empty_model_output,重试 5 次后耗尽——真实错误完全被掩盖。最小复现:
修复方案
在
newEinoAgenticChatModelFactory的模型实例外包一层修复包装器(与既有streamToolCallIndexRepairModel同一防御思路):检测流式帧中跨块类型的StreamingMeta.Index冲突,把冲突块改编到负数 index(不与 converter 后续 index 相撞),内容完整保留。覆盖 OpenAI 兼容与 Claude 两条 agentic 路径,eino 单代理 / Deep / Supervisor / Plan-Execute 均受益。该补丁为框架边界防御,对正常流零影响;待 eino-ext 上游修复发版、依赖升级后可移除。
随附 PR 实现 + 单元测试。