Azure OpenAI の Responses API を .NET で使う方法
Azure OpenAI Responses API のドキュメントで Responses API の使い方が紹介されていますが、.NET 向けのサンプルコードがなかったため、.NET での利用方法を紹介します。
前提条件
NuGet の Azure.AI.OpenAI
パッケージの 2.3.0-beta.2
を使用しています。バージョンが変わると API が変わる可能性があるため、適宜ドキュメントを確認してください。
Responses API を呼ぶためのクライアントの作成方法
まずは、Responses API を呼ぶためのクライアントを作成します。
これは簡単で AzureOpenAIClient
クラスの GetOpenAIResponseClient
メソッドを呼ぶだけです。
このメソッドは評価目的のみで提供されているためエラーになります。そのため #pragma warning disable OPENAI001
を定義する必要がある点に注意してください。
// Responses API のクライアントは評価目的のみで提供されているため、これを書かないとエラーになる
#pragma warning disable OPENAI001
using Azure.AI.OpenAI;
using Azure.Identity;
// 普通に AOAI クライアントを作る
var aoaiClient = new AzureOpenAIClient(
new("https://<Your AOAI Endpoint>"),
new AzureCliCredential());
// GetOpenAIResponseClient で Responses API クライアントを作る
var responseClient = aoaiClient.GetOpenAIResponseClient("gpt-5");
今後のコードは responseClient
がすでにあるものとしてコードを書いていきます。
Responses API の呼び出し
では単純にメッセージを投げてみます。
メッセージを投げるには CreateResponseAsync
メソッドを呼びます。
var response = await responseClient.CreateResponseAsync("こんにちは!!");
Console.WriteLine(response.Value.GetOutputText());
これで AI からの応答が得られます。
会話を続ける
Responses API はステートフルな API で、既定では各応答(Response オブジェクト)が 30 日間保存されます。保存期間内であれば、直前の応答 ID を previous_response_id
として渡すことで、前回までのやり取りを引き継いで会話を継続できます。C# の SDK では ResponseCreationOptions
クラスの PreviousResponseId
プロパティに応答 ID をセットします。
以下は 1 回目の応答の ID を 2 回目に引き継いで、コンテキストを保持したまま会話を続ける例です。
// using OpenAI.Responses; が必要
// 1 回目の応答
var first = await responseClient.CreateResponseAsync("こんにちは。私の名前は Kazuki Ota です。");
Console.WriteLine(first.Value.GetOutputText());
Console.WriteLine("---------------------------");
// 2 回目。previous_response_id に 1 回目の ID を渡す
var second = await responseClient.CreateResponseAsync(
"私の名前を答えてください。",
new ResponseCreationOptions
{
PreviousResponseId = first.Value.Id,
});
Console.WriteLine(second.Value.GetOutputText());
実行すると以下のような結果になります。ちゃんと前の会話で伝えた私の名前を憶えていますね。
Kazuki Otaさん、こんにちは。ご連絡ありがとうございます。今日はどのようにお手伝いできますか?
お呼びする際は「太田さん」「カズキさん」など、ご希望はありますか?
---------------------------
あなたのお名前は Kazuki Ota です。
previous_response_id
を使わずに、毎回「これまでのユーザー/アシスタントのやり取り」をまとめて入力に含める方法でも会話を継続できます。後述するステートレス運用(store=false
パラメーター)や、アプリ側で会話コンテキストを明示的に管理したい場合に有効です。
やりかたは簡単で従来の Chat Completions API と同じように履歴をリストなどで管理して、それをまとめて渡すだけです。ちょっと戸惑うのは ResponseItem
でメッセージを管理するのですが、これの作成方法が ResponseItem
クラスに定義されている各種ファクトリーメソッドを使うという点です。そこさえ気を付ければそんなに難しくありません。
// 会話履歴を保持するためのリスト
List<ResponseItem> messages = [
ResponseItem.CreateDeveloperMessageItem("""
あなたは猫型アシスタントです。
猫っぽく振舞うために語尾には必ず「にゃん」をつけてください。
""")
];
// 1 回目の応答
messages.Add(ResponseItem.CreateUserMessageItem("こんにちは。私の名前は Kazuki Ota です。"));
var first = await responseClient.CreateResponseAsync(messages);
Console.WriteLine(first.Value.GetOutputText());
Console.WriteLine("---------------------------");
// 2 回目の応答
// AI からの回答と新しいユーザーメッセージを会話履歴に追加してから再度応答を取得する
messages.Add(ResponseItem.CreateAssistantMessageItem(first.Value.GetOutputText()));
messages.Add(ResponseItem.CreateUserMessageItem("私の名前はなんですか?"));
var second = await responseClient.CreateResponseAsync(messages);
Console.WriteLine(second.Value.GetOutputText());
実行すると以下のような結果になります。ちゃんと前の会話で伝えた私の名前を憶えていますね。
こんにちはにゃん。Kazuki Otaさん、はじめましてにゃん。今日はどんなお手伝いができるにゃん?
---------------------------
あなたの名前は Kazuki Ota さんですにゃん
レスポンスの削除
レスポンスは 30 日間保存されますが、DeleteResponseAsync
メソッドを使うことで任意のタイミングで削除できます。
先ほどのコードの続きとして、以下のように書くことでレスポンスを削除できます。
// 応答の削除
var deleteFirstResult = await responseClient.DeleteResponseAsync(first.Value.Id);
var deleteSecondResult = await responseClient.DeleteResponseAsync(second.Value.Id);
Console.WriteLine($"deleteFirstResult.Value.Deleted: {deleteFirstResult.Value.Deleted}");
Console.WriteLine($"deleteSecondResult.Value.Deleted: {deleteSecondResult.Value.Deleted}");
実行すると以下のような結果になります。
deleteFirstResult.Value.Deleted: True
deleteSecondResult.Value.Deleted: True
ステートレスにする
そもそもサーバーサイドにレスポンスを残したくない場合は、store=false
パラメーターを使うことでステートレスにできます。C# の SDK では ResponseCreationOptions
クラスの StoredOutputEnabled
プロパティに false
をセットします。
// 1 回目の応答
messages.Add(ResponseItem.CreateUserMessageItem("こんにちは。私の名前は Kazuki Ota です。"));
var first = await responseClient.CreateResponseAsync(messages,
new ResponseCreationOptions
{
StoredOutputEnabled = false, // 応答を保存しない
});
Console.WriteLine(first.Value.GetOutputText());
Console.WriteLine("---------------------------");
// 2 回目の応答
// AI からの回答と新しいユーザーメッセージを会話履歴に追加してから再度応答を取得する
messages.AddRange(first.Value.OutputItems);
messages.Add(ResponseItem.CreateUserMessageItem("私の名前はなんですか?"));
var second = await responseClient.CreateResponseAsync(messages,
new ResponseCreationOptions
{
StoredOutputEnabled = false, // 応答を保存しない
});
Console.WriteLine(second.Value.GetOutputText());
// 応答の削除
var deleteFirstResult = await responseClient.DeleteResponseAsync(first.Value.Id);
var deleteSecondResult = await responseClient.DeleteResponseAsync(second.Value.Id);
Console.WriteLine($"deleteFirstResult.Value.Deleted: {deleteFirstResult.Value.Deleted}");
Console.WriteLine($"deleteSecondResult.Value.Deleted: {deleteSecondResult.Value.Deleted}");
実行すると、そもそもサーバーサイドに保存していないレスポンスを削除しようとすることになるので、以下のようにエラーになります。
System.ClientModel.ClientResultException: HTTP 404 (invalid_request_error: )
Response with id 'resp_68b3e646d34c819eb3aeebbc0e855141028e3ee5ec2b4150' not found.
at OpenAI.ClientPipelineExtensions.ProcessMessageAsync(ClientPipeline pipeline, PipelineMessage message, RequestOptions options)
at OpenAI.Responses.OpenAIResponseClient.DeleteResponseAsync(String responseId, RequestOptions options)
at OpenAI.Responses.OpenAIResponseClient.DeleteResponseAsync(String responseId, CancellationToken cancellationToken)
at Program.<Main>$(String[] args) in D:\source\ConsoleApp25\ConsoleApp25\Program.cs:line 48
ちゃんと思った通りに動いてくれています。サービスに履歴の管理をお任せしたい場合は StoredOutputEnabled = true
(デフォルト) にして、アプリ側で明示的に履歴を管理したい場合は StoredOutputEnabled = false
にすると良いでしょう。個人的には、あんまりサーバーサイドに履歴を保存したくないので、StoredOutputEnabled = false
にして、アプリ側で会話履歴を管理するかなぁと思っていますが実際のところどっちがいいんでしょう…。
その他のパラメーター
Responses API には、会話継続以外にも知っておくと便利なパラメーターがあります。ここでは Reasoning(思考過程)まわりの代表的なものを紹介します。
Reasoning サマリー
最新の Reasoning 対応モデルでは、思考過程そのもの(いわゆる chain-of-thought)を露出せず、要約(summary)のみ取得できます。説明性の向上やデバッグに役立ちます。
以下のように ResponseCreationOptions
クラスの ReasoningOptions
プロパティに ResponseReasoningOptions
オブジェクトをセットし、その中で ReasoningSummaryVerbosity
プロパティに Detailed
をセットすることで、詳細なサマリーを取得できます。
// 会話履歴を保持するためのリスト
// しっかり考えないとダメそうな質問をする
List<ResponseItem> messages = [
ResponseItem.CreateDeveloperMessageItem("""
ユーザーからの質問に対して、よく考えて簡潔に回答してください。
""")
];
messages.Add(ResponseItem.CreateUserMessageItem("""
押すと開くドアがあります。
そのドアから部屋に入り、体を真後ろに回転させてドアの方を向き、そのままドアを通って部屋から出ます。
その後、その姿勢のまま後ろ向きに部屋に入ります。
この時ドアを閉めるためには押せばいいでしょうか、引けばいいでしょうか。
"""));
// Reasoning 系パラメーターを設定して呼び出す
var response = await responseClient.CreateResponseAsync(messages,
new ResponseCreationOptions
{
// 応答を保存しない
StoredOutputEnabled = false,
// 出力トークン数
MaxOutputTokenCount = 1024 * 4,
// Reasoning 系のパラメーターはここで設定する
ReasoningOptions = new()
{
ReasoningEffortLevel = ResponseReasoningEffortLevel.Low,
ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Detailed,
},
});
JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerOptions.Web)
{
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};
// 出力をダンプ
foreach (var item in response.Value.OutputItems)
{
Console.WriteLine("==============================");
Console.WriteLine($"Item type: {item.GetType()}");
Console.WriteLine(item switch
{
// 通常のメッセージ
MessageResponseItem message => GetMessageContent(message),
// AI の思考の過程のアイテム
ReasoningResponseItem reasoning => GetReasoningContent(reasoning),
_ => $"Not supported item type: {item.GetType()}",
});
}
string GetReasoningContent(ReasoningResponseItem reasoning) =>
// 思考の過程のテキストを表示
string.Join('\n', reasoning.SummaryParts.OfType<ReasoningSummaryTextPart>().Select(x => x.Text));
string GetMessageContent(MessageResponseItem message) =>
// コンテンツを表示
$"""
{JsonSerializer.Serialize(message.Content, jsonSerializerOptions)}
""";
OpenAIResponse
(CreateResponseAsync
の戻り値の Value
プロパティ) の OutputItems
プロパティに ResponseItem
のリストが入っており、ReasoningResponseItem
型のアイテムが思考過程に関する情報を持っています。上記コードでは ReasoningResponseItem
型のアイテムを見つけたら、SummaryParts
プロパティから思考過程の要約を取り出して表示しています。MessageResponseItem
型のアイテムは通常のメッセージなので、Content
プロパティを表示しています。
実行すると以下のような結果になります。
==============================
Item type: OpenAI.Responses.ReasoningResponseItem
**Evaluating door orientation**
Okay, if I rotate my body 180 degrees to face the door, I interpret "そのままドアを通って部屋から出ます" as moving forward through the door. To push the door outward, I think the hinge doesn’t allow this if it's a single-swing door. It could be a double-action door instead. I'm also considering whether "押すと開くドアがあります" suggests it opens from both sides. After exiting, I'll need to turn around to face the door again.
**Analyzing door orientation and movement**
When he moves forward through the doorway, "そのままドアを通って部屋から出ます" suggests he'll exit while still facing the door. So, once he's outside, he’ll be looking back at the door. If he’s backing into the room, while maintaining that orientation, he’ll approach the door from the opposite way. The question then arises: to close the door while backing in, should he push or pull it? This creates a bit of confusion considering his position.
**Considering door mechanics while backing in**
He wants to close the door while entering backwards without turning around. The door swings into the room, so he would usually push it to open from the outside. Earlier, he pushed the door to enter, which opens inward. Now, while backing in, he needs to figure out if he should push or pull the door. Since he’s facing the door and moving backwards, it makes sense for him to reach behind and pull the door towards himself to open it.
**Clarifying door movement and orientation**
The door's hinge direction means he can only push it open inward from outside. Since he's backing into the room, he'll need to push the door with his back to open it as he enters. After moving inside, he'll be facing outward toward the door. Now, to close it while continuing to face outside, he would need to pull the door towards the jamb. So, to answer the question, to close the inward-swinging door from inside, he should pull it towards him.
**Deciding how to close the door**
I’m considering how to close a door that swings inward. When standing inside, to close it, I typically pull it toward me to shut it. However, pushing might actually be the correct action. Since the door opens inward, when I'm inside facing the door, I would push it to close. After backing in and still facing outside, I conclude that he should push the door to close it. The answer is “押す,” meaning push!
==============================
Item type: OpenAI.Responses.InternalResponsesAssistantMessage
[
{
"kind": 4,
"text": "押します。\n\n理由:\n- 外側から「押すと開く」=内開きドア。\n- 内側では「開ける」は引く方向、「閉める」は押す方向。\n- あなたは後ろ向きで部屋に入った後は室内側にいるので、閉めるには押す。",
"inputImageFileId": null,
"inputImageDetailLevel": null,
"inputFileId": null,
"inputFilename": null,
"inputFileBytes": null,
"inputFileBytesMediaType": null,
"outputTextAnnotations": [],
"refusal": null
}
]
ツール呼び出し
ツールも呼び出し可能です。Chat Completions API と同じように tools
パラメーターでツールを登録して呼び出しモードに Auto を設定することで必要に応じて読んでくれるようになります。例えば関数を渡す場合は以下のような流れになります。天気を質問して、天気を返すための getWeather
関数を登録して呼び出す例です。
// 会話履歴を保持するためのリスト
List<ResponseItem> messages = [
ResponseItem.CreateDeveloperMessageItem("""
あなたはユーザーの質問に答えるアシスタントです。
""")
];
messages.Add(ResponseItem.CreateUserMessageItem("""
2025-09-01 の東京の天気を教えて。
"""));
// Reasoning 系パラメーターを設定して呼び出す
var first = await responseClient.CreateResponseAsync(messages,
new ResponseCreationOptions
{
// 応答を保存しない
StoredOutputEnabled = false,
ToolChoice = ResponseToolChoice.CreateAutoChoice(),
Tools =
{
ResponseTool.CreateFunctionTool(
"getWeather",
"現在の天気を取得します",
BinaryData.FromString("""
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The name of the city or location to get the weather for."
},
"date": {
"type": "string",
"format": "date",
"description": "The date for the weather forecast in YYYY-MM-DD format."
}
},
"required": ["location", "date"],
"additionalProperties": false
}
"""),
true),
},
});
JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerOptions.Web)
{
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};
// 出力をダンプ
foreach (var item in first.Value.OutputItems)
{
Console.WriteLine("==============================");
Console.WriteLine($"Item type: {item.GetType()}");
Console.WriteLine(item switch
{
// 通常のメッセージ
MessageResponseItem message => GetMessageContent(message),
// AI の思考の過程のアイテム
ReasoningResponseItem reasoning => GetReasoningContent(reasoning),
FunctionCallResponseItem functionCall => GetFunctionCallContent(functionCall),
_ => $"Not supported item type: {item.GetType()}",
});
}
// 2025/08/31 時点では暗号化された ReasoningResponseItem を返すための include パラメーターは SDK で internal になっているため
// ReasoningResponseItem を使うためには StoredOutputEnabled = true にする必要があるため false の場合は指定できない。
// そのため、ここでは ReasoningResponseItem を除外して次のリクエストを作成する。
messages.AddRange(first.Value.OutputItems.Where(x => x is not ReasoningResponseItem));
// 関数呼び出しの結果を追加
var functionCallResult = first.Value.OutputItems.OfType<FunctionCallResponseItem>().First();
messages.Add(ResponseItem.CreateFunctionCallOutputItem(functionCallResult.CallId, "東京の天気は雷雨です。"));
// 最終回答を取得
var second = await responseClient.CreateResponseAsync(messages,
new ResponseCreationOptions
{
// 応答を保存しない
StoredOutputEnabled = false,
});
// 出力をダンプ
foreach (var item in second.Value.OutputItems)
{
Console.WriteLine("==============================");
Console.WriteLine($"Item type: {item.GetType()}");
Console.WriteLine(item switch
{
// 通常のメッセージ
MessageResponseItem message => GetMessageContent(message),
// AI の思考の過程のアイテム
ReasoningResponseItem reasoning => GetReasoningContent(reasoning),
FunctionCallResponseItem functionCall => GetFunctionCallContent(functionCall),
_ => $"Not supported item type: {item.GetType()}",
});
}
string GetFunctionCallContent(FunctionCallResponseItem functionCall) =>
// 関数呼び出しの内容を表示
$"""
Function Call:
Name: {functionCall.FunctionName}
Arguments: {functionCall.FunctionArguments.ToString()}
""";
string GetReasoningContent(ReasoningResponseItem reasoning) =>
// 思考の過程のテキストを表示
string.Join('\n', reasoning.SummaryParts.OfType<ReasoningSummaryTextPart>().Select(x => x.Text));
string GetMessageContent(MessageResponseItem message) =>
// コンテンツを表示
$"""
{JsonSerializer.Serialize(message.Content, jsonSerializerOptions)}
""";
このコードを実行すると以下のような結果になります。ちゃんと天気を取得して回答していますね。
==============================
Item type: OpenAI.Responses.ReasoningResponseItem
==============================
Item type: OpenAI.Responses.FunctionCallResponseItem
Function Call:
Name: getWeather
Arguments: {"location":"東京","date":"2025-09-01"}
==============================
Item type: OpenAI.Responses.ReasoningResponseItem
==============================
Item type: OpenAI.Responses.InternalResponsesAssistantMessage
[
{
"kind": 4,
"text": "2025年9月1日の東京の天気は雷雨の予報です。外出の際は雷や強い雨にご注意ください。",
"inputImageFileId": null,
"inputImageDetailLevel": null,
"inputFileId": null,
"inputFilename": null,
"inputFileBytes": null,
"inputFileBytesMediaType": null,
"outputTextAnnotations": [],
"refusal": null
}
]
ステートフルでやる場合には、ローカルで管理しなくてよくなるぶん少しコードがシンプルになります。実行結果は変わらないためコードだけ載せておきます。
// 会話履歴を保持するためのリスト
List<ResponseItem> messages = [
ResponseItem.CreateDeveloperMessageItem("""
あなたはユーザーの質問に答えるアシスタントです。
""")
];
messages.Add(ResponseItem.CreateUserMessageItem("""
2025-09-01 の東京の天気を教えて。
"""));
// Reasoning 系パラメーターを設定して呼び出す
var first = await responseClient.CreateResponseAsync(messages,
new ResponseCreationOptions
{
// 応答を保存しない
StoredOutputEnabled = true,
ToolChoice = ResponseToolChoice.CreateAutoChoice(),
Tools =
{
ResponseTool.CreateFunctionTool(
"getWeather",
"現在の天気を取得します",
BinaryData.FromString("""
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The name of the city or location to get the weather for."
},
"date": {
"type": "string",
"format": "date",
"description": "The date for the weather forecast in YYYY-MM-DD format."
}
},
"required": ["location", "date"],
"additionalProperties": false
}
"""),
true),
},
});
JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerOptions.Web)
{
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};
// 出力をダンプ
foreach (var item in first.Value.OutputItems)
{
Console.WriteLine("==============================");
Console.WriteLine($"Item type: {item.GetType()}");
Console.WriteLine(item switch
{
// 通常のメッセージ
MessageResponseItem message => GetMessageContent(message),
// AI の思考の過程のアイテム
ReasoningResponseItem reasoning => GetReasoningContent(reasoning),
FunctionCallResponseItem functionCall => GetFunctionCallContent(functionCall),
_ => $"Not supported item type: {item.GetType()}",
});
}
// 状態をサーバーサイドで管理してくれているため、 ID を指定して続きの会話ができる
var functionCallResult = first.Value.OutputItems.OfType<FunctionCallResponseItem>().First();
var secondMessage = ResponseItem.CreateFunctionCallOutputItem(functionCallResult.CallId, "東京の天気は雷雨です。");
// 最終回答を取得
var second = await responseClient.CreateResponseAsync([secondMessage],
new ResponseCreationOptions
{
// 応答を保存しない
StoredOutputEnabled = true,
// 前の ID を指定して会話を続ける
PreviousResponseId = first.Value.Id,
});
// 出力をダンプ
foreach (var item in second.Value.OutputItems)
{
Console.WriteLine("==============================");
Console.WriteLine($"Item type: {item.GetType()}");
Console.WriteLine(item switch
{
// 通常のメッセージ
MessageResponseItem message => GetMessageContent(message),
// AI の思考の過程のアイテム
ReasoningResponseItem reasoning => GetReasoningContent(reasoning),
FunctionCallResponseItem functionCall => GetFunctionCallContent(functionCall),
_ => $"Not supported item type: {item.GetType()}",
});
}
string GetFunctionCallContent(FunctionCallResponseItem functionCall) =>
// 関数呼び出しの内容を表示
$"""
Function Call:
Name: {functionCall.FunctionName}
Arguments: {functionCall.FunctionArguments.ToString()}
""";
string GetReasoningContent(ReasoningResponseItem reasoning) =>
// 思考の過程のテキストを表示
string.Join('\n', reasoning.SummaryParts.OfType<ReasoningSummaryTextPart>().Select(x => x.Text));
string GetMessageContent(MessageResponseItem message) =>
// コンテンツを表示
$"""
{JsonSerializer.Serialize(message.Content, jsonSerializerOptions)}
""";
まとめ
一旦、少し気になるところだけ Responses API を .NET SDK を使って試してみました。
まだ、いくつかのパラメーターは SDK 側でサポートされていなかったりするので、機能が足りない部分もありますが、基本的には Chat Completions API と似たような感じで使えるため、.NET での利用もそんなに難しくないと思います。
次は、これを Microsoft.Extensions.AI
で使ってみようと思います。
Discussion