【Bug已解决】BedrockService.GetStreamingResponseAsyncreturns Amazon.BedrockRuntime.Model.ValidationException 解决方案
一、现象长什么样
你在 .NET 里用 AWS SDK 的BedrockService.GetStreamingResponseAsync(即AmazonBedrockRuntimeClient的流式调用)请求 Claude,抛:
Amazon.BedrockRuntime.Model.ValidationException;- 或流还没开始就被校验拒绝;
- 你用
InvokeModelWithResponseStreamAsync/GetStreamingResponseAsync包装了请求,但 body 不合规; - 报错比同步
InvokeModel更难读,因为流式请求的 body 结构和字段名仍有要求; - 你确认模型 ID 对、key 对,但流式就是 ValidationException;
- 你怀疑流式 API 和同步 API 的请求格式不同。
一句话:GetStreamingResponseAsync底层仍是 Bedrock 的流式推理请求,body 必须符合 Claude on Bedrock 的格式契约(顶层anthropic_version、正确的max_tokens层级、system数组、content 块结构)。你构造的流式请求 body 某处不合规,于是被ValidationException拒绝。
二、背景
AWS SDK for .NET 里,AmazonBedrockRuntimeClient提供:
InvokeModelAsync:同步(整段响应);InvokeModelWithResponseStreamAsync/ 经BedrockService封装的GetStreamingResponseAsync:流式(逐块Chunk)。
两者都要求请求 body 符合模型格式。对 Claude on Bedrock,流式 body 与同步 body 格式一致(都是那段 JSON),只是响应以流形式返回。所以"流式报 ValidationException"和"同步报 malformed"根因相同——都是 body 字段不对(见 934)。
常见流式专属坑:
- 用
InferenceConfig但字段名错(MaxTokens大小写、或放在错误层级); - 流式的
RequestStream里塞了非 JSON 或编码错误; ModelId在流式调用时用了简写(同 936 问题)。
三、根因
根因是流式请求的 body 不符合 Claude on Bedrock 格式:
// 错误:流式 body 里 system 是字符串、缺 anthropic_version var request = new InvokeModelWithResponseStreamRequest { ModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0", Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { max_tokens = 256, system = "你是助手", // 应为 [{text:...}] messages = new[] { new { role="user", content=new[]{new{text="hi"}} } } }))) }; // -> ValidationException// 正确 var body = new { anthropic_version = "bedrock-2023-05-31", max_tokens = 256, system = new[] { new { text = "你是助手" } }, messages = new[] { new { role="user", content=new[]{new{text="hi"}} } } };四、最小可运行复现
下面用 C# 伪代码演示"流式请求 body 校验":
using Amazon.BedrockRuntime; using Amazon.BedrockRuntime.Model; var client = new AmazonBedrockRuntimeClient(region: RegionEndpoint.USWest2); var body = new { anthropic_version = "bedrock-2023-05-31", max_tokens = 256, system = new[] { new { text = "你是助手" } }, messages = new[] { new { role = "user", content = new[] { new { text = "你好" } } } } }; var req = new InvokeModelWithResponseStreamRequest { ModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0", ContentType = "application/json", Accept = "application/json", Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes( System.Text.Json.JsonSerializer.Serialize(body))) }; // 逐块读流式响应 var resp = await client.InvokeModelWithResponseStreamAsync(req); await foreach (var chunk in resp.Body) { // chunk.Bytes 是 SSE/JSON 片段 Console.Write(System.Text.Encoding.UTF8.GetString(chunk.Bytes.ToArray())); }若body里system是字符串或漏anthropic_version,InvokeModelWithResponseStreamAsync直接抛ValidationException。
五、解决方案(第一层:最小直接修复)
最小修复是构造合规的流式 body(顶层anthropic_version+system数组 + 正确max_tokens):
var body = new { anthropic_version = "bedrock-2023-05-31", max_tokens = 256, system = new[] { new { text = "你是助手" } }, messages = new[] { new { role = "user", content = new[] { new { text = "你好" } } } } }; var req = new InvokeModelWithResponseStreamRequest { ModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0", ContentType = "application/json", Accept = "application/json", Body = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(body)), }; using var resp = await client.InvokeModelWithResponseStreamAsync(req); await foreach (var chunk in resp.Body) { // 解析 chunk 里的 delta.text 并输出(参考 940 的 SignalR 推送) }要点:流式不改变 body 格式要求;system必须是数组、anthropic_version必填。
六、解决方案(第二层:结构化改进)
把"流式请求 body 校验"做成策略,提交前自检,避免ValidationException:
from dataclasses import dataclass, field from typing import Any, Dict, List @dataclass(frozen=True) class BedrockStreamingValidationPolicy: """Bedrock 流式请求策略:消灭 ValidationException。 规则: - anthropic_version 必填 - system 必须是文本块数组 - max_tokens 为合法正数 - messages content 为块数组 """ def build(self, *, model_id: str, system: str, messages: List[Dict], max_tokens: int = 256) -> Dict: if not model_id or "anthropic.claude" not in model_id: raise ValueError("model_id 必须是完整 Bedrock identifier") if not isinstance(system, str) or len(system) == 0: raise ValueError("system 不能为空") if not messages: raise ValueError("messages 不能为空") if not (1 <= max_tokens <= 8192): raise ValueError("max_tokens 必须 1~8192") return { "anthropic_version": "bedrock-2023-05-31", "max_tokens": max_tokens, "system": [{"text": system}], "messages": messages, } def demo() -> None: policy = BedrockStreamingValidationPolicy() b = policy.build( model_id="anthropic.claude-3-5-sonnet-20241022-v2:0", system="你是助手", messages=[{"role": "user", "content": [{"text": "hi"}]}], ) assert b["anthropic_version"] == "bedrock-2023-05-31" print("流式 body 合规") if __name__ == "__main__": demo()七、解决方案(第三层:断言 / CI 守护)
import pytest from your_module import BedrockStreamingValidationPolicy def test_build_ok(): policy = BedrockStreamingValidationPolicy() b = policy.build(model_id="anthropic.claude-x:0", system="s", messages=[{"role": "user", "content": [{"text": "y"}]}]) assert b["system"] == [{"text": "s"}] def test_missing_version_in_body(): # 策略在 body 内置版本,但 model_id 仍需校验 policy = BedrockStreamingValidationPolicy() with pytest.raises(ValueError): policy.build(model_id="claude-x", system="s", messages=[{"role": "user", "content": [{"text": "y"}]}]) def test_empty_system(): policy = BedrockStreamingValidationPolicy() with pytest.raises(ValueError): policy.build(model_id="anthropic.claude-x:0", system="", messages=[{"role":"user","content":[{"text":"y"}]}]) def test_bad_max_tokens(): policy = BedrockStreamingValidationPolicy() with pytest.raises(ValueError): policy.build(model_id="anthropic.claude-x:0", system="s", messages=[{"role":"user","content":[{"text":"y"}]}], max_tokens=0) def test_empty_messages(): policy = BedrockStreamingValidationPolicy() with pytest.raises(ValueError): policy.build(model_id="anthropic.claude-x:0", system="s", messages=[]) def test_version_present(): policy = BedrockStreamingValidationPolicy() b = policy.build(model_id="anthropic.claude-x:0", system="s", messages=[{"role":"user","content":[{"text":"y"}]}]) assert b["anthropic_version"] == "bedrock-2023-05-31"CI 里加一条:在调用GetStreamingResponseAsync前跑build自检,确保 body 合规再发流,避免ValidationException。
八、排查清单
- 流式 body 是否带了
anthropic_version: bedrock-2023-05-31? system是否文本块数组(非字符串)?max_tokens是否合法正数、层级正确?ModelId是否完整 Bedrock identifier(非简写)?- 流式响应是否逐块解析
chunk.Bytes?别把整段当同步处理。 - 是否混淆了同步
InvokeModel与流式InvokeModelWithResponseStreamAsync的 body(其实格式一致)?
九、小结
BedrockService.GetStreamingResponseAsync抛ValidationException,根因是流式请求 body 不符合 Claude on Bedrock 格式(顶层anthropic_version、system数组、max_tokens合法、content 块数组),与同步调用同源。最小修复是构造合规的流式 body(系统数组化、补全版本、完整 model ID)再发流;结构化做法是抽成BedrockStreamingValidationPolicy,提交前自检;最后用 pytest 守护"body 合规才可发流",杜绝 ValidationException。