针对IChatClient的结构化输出可以通过调用如下这些重载的GetResponseAsync<T>扩展方法来完成。具体的实现很简单,这些方法最终会利用指定或者默认的JsonSerializerOptions针对泛型参数T生成一个ChatResponseFormatJson对象,并作为ChatOptions的ResponseFormat属性。这个ResponseFormat承载的JSON Schema将提供给LLM指导它按照定义的格式生成输出内容。当IChatClient接收到LLM的响应结果时,利用匹配的JsonSerializerOptions对响应结果进行反序列化后,封装成一个ChatResponse<T>对象返回给调用方。
csharp
public static class ChatClientStructuredOutputExtensions
{
public static Task<ChatResponse<T>> GetResponseAsync<T>(
this IChatClient chatClient,
IEnumerable<ChatMessage> messages,
ChatOptions? options = null, bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default(CancellationToken));
public static Task<ChatResponse<T>> GetResponseAsync<T>(
this IChatClient chatClient,
string chatMessage,
ChatOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default(CancellationToken));
public static Task<ChatResponse<T>> GetResponseAsync<T>(
this IChatClient chatClient,
ChatMessage chatMessage,
ChatOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default(CancellationToken));
public static Task<ChatResponse<T>> GetResponseAsync<T>(
this IChatClient chatClient,
string chatMessage,
JsonSerializerOptions serializerOptions,
ChatOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default(CancellationToken));
public static Task<ChatResponse<T>> GetResponseAsync<T>(
this IChatClient chatClient,
ChatMessage chatMessage,
JsonSerializerOptions serializerOptions,
ChatOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default(CancellationToken));
public static async Task<ChatResponse<T>> GetResponseAsync<T>(
this IChatClient chatClient,
IEnumerable<ChatMessage> messages,
JsonSerializerOptions serializerOptions,
ChatOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default(CancellationToken));
}
1. 调用GetResponseAsync方法获取结构化输出
在如下的演示程序中,我们定义了一个描述个人基本信息的Profile类。我们利用OpenAIClient创建了一个IChatClient对象,并调用了GetResponseAsync<Profile>方法来从指定的一段文本中提取个人信息。在得到作为响应的Response<Profile>对象后,利用Result属性提取反序列化响应内容生成的Profile对象,调试断言表明这个Profile对象与我们预设的Profile对象是相等的。
csharp
using dotenv.net;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.Diagnostics;
using System.Text.Json;
DotEnv.Load();
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var openAIClient = new OpenAIClient(
credential: new ApiKeyCredential(key: apiKey),
options: new OpenAIClientOptions
{
Endpoint = new Uri(openAIUrl)
});
var chatClient = openAIClient.GetResponsesClient().AsIChatClient(defaultModelId: model);
var profile = new Profile { Name = "张三" , Gender = Gender.Male, Age = 26 };
var prompt = "从下面内容中提取有效的个人信息:我叫张三,男,今年26岁";
var response1 = await chatClient.GetResponseAsync<Profile>(chatMessage: prompt);
Debug.Assert(profile == response1.Result);
public enum Gender
{
Male,
Female,
}
class Profile:IEquatable<Profile>
{
public string? Name { get; set; }
public Gender Gender { get; set; }
public int Age { get; set; }
public bool Equals(Profile? other)
{
if(other is null) return false;
return Name == other.Name && Gender == other.Gender && Age == other.Age;
}
}
对于程序涉及的LLM调用,如下的两段JSON为发送的请求和接收的响应。可以看出针对Profile类型的JSON Schema被包含在发送给LLM的请求中,而LLM生成的响应内容则被成功地反序列化成了一个Profile对象。
json
{
"model": "gpt-5.2-chat",
"text": {
"format": {
"type": "json_schema",
"name": "Profile",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"gender": {
"type": "string",
"enum": [
"Male",
"Female"
]
},
"age": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"name",
"gender",
"age"
]
}
}
},
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "从下面内容中提取有效的个人信息:我叫张三,男,今年26岁"
}
]
}
]
}
json
{
"id": "resp_0962768f225127bc006a002eb6441881978b7b0150d57cdff9",
"object": "response",
"created_at": 1778396854,
"status": "completed",
"background": false,
"completed_at": 1778396858,
"content_filters": [
{
"blocked": false,
"source_type": "prompt",
"content_filter_raw": [],
"content_filter_results": {
"hate": {
"filtered": false,
"severity": "safe"
},
"sexual": {
"filtered": false,
"severity": "safe"
},
"violence": {
"filtered": false,
"severity": "safe"
},
"self_harm": {
"filtered": false,
"severity": "safe"
}
},
"content_filter_offsets": {
"start_offset": 0,
"end_offset": 585,
"check_offset": 0
}
},
{
"blocked": false,
"source_type": "completion",
"content_filter_raw": [],
"content_filter_results": {
"hate": {
"filtered": false,
"severity": "safe"
},
"sexual": {
"filtered": false,
"severity": "safe"
},
"violence": {
"filtered": false,
"severity": "safe"
},
"self_harm": {
"filtered": false,
"severity": "safe"
}
},
"content_filter_offsets": {
"start_offset": 0,
"end_offset": 824,
"check_offset": 0
}
}
],
"error": null,
"frequency_penalty": 0.0,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5.2-chat",
"output": [
{
"id": "rs_0962768f225127bc006a002eb6c400819798b1da517e1a1eda",
"type": "reasoning",
"summary": []
},
{
"id": "msg_0962768f225127bc006a002eba09b48197b34ee13d7b475432",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "{\"name\":\"张三\",\"gender\":\"Male\",\"age\":26}"
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"presence_penalty": 0.0,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "medium",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "json_schema",
"description": null,
"name": "Profile",
"schema": {
"type": "object",
"properties": {
"name": {
"type": [
"string",
"null"
]
},
"gender": {
"type": "string",
"enum": [
"Male",
"Female"
]
},
"age": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"name",
"gender",
"age"
]
},
"strict": true
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 0.85,
"truncation": "disabled",
"usage": {
"input_tokens": 69,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 217,
"output_tokens_details": {
"reasoning_tokens": 192
},
"total_tokens": 286
},
"user": null,
"metadata": {}
}
2. 通过设置ChatOptions的ResponseFormat属性来获取结构化输出
GetResponseAsync<T>方法最终会利用指定或者默认的JsonSerializerOptions针对泛型参数T生成一个ChatResponseFormatJson对象,并作为ChatOptions的ResponseFormat属性。当我们直接调用GetResponseAsync方法时,ChatOptions的ResponseFormat属性返回的JSON Schema将作为调用LLM提示词的一部分,用于指导LLM生成符合结构的输出。上面的演示程序与下面这段其实是完全等效的。
csharp
using dotenv.net;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.Diagnostics;
using System.Text.Json;
DotEnv.Load();
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var openAIClient = new OpenAIClient(
credential: new ApiKeyCredential(key: apiKey),
options: new OpenAIClientOptions
{
Endpoint = new Uri(openAIUrl)
});
var chatClient = openAIClient.GetResponsesClient().AsIChatClient(defaultModelId: model);
var options = new ChatOptions
{
ResponseFormat = ChatResponseFormat.ForJsonSchema<Profile>()
};
var response = await chatClient.GetResponseAsync(chatMessage: prompt, options: options);
var profile2 = JsonSerializer.Deserialize<Profile>(response1.Messages.Last().Text, AIJsonUtilities.DefaultOptions);
Debug.Assert(profile == new Profile { Name = "张三", Gender = Gender.Male, Age = 26 });
public enum Gender
{
Male,
Female,
}
class Profile:IEquatable<Profile>
{
public string? Name { get; set; }
public Gender Gender { get; set; }
public int Age { get; set; }
public bool Equals(Profile? other)
{
if(other is null) return false;
return Name == other.Name && Gender == other.Gender && Age == other.Age;
}
}