ChatGPT for .NET
一个为 .NET 提供的 ChatGPT 集成库,支持 OpenAI 和 Azure OpenAI Service。
安装
该库可在 NuGet 上获取。只需在 Package Manager GUI 中搜索 ChatGptNet,或在 .NET CLI 中运行以下命令:
dotnet add package ChatGptNet
配置
在应用程序启动时注册 ChatGPT 服务:
builder.Services.AddChatGpt(options =>
{
// OpenAI。
//options.UseOpenAI(apiKey: "", organization: "");
// Azure OpenAI 服务。
//options.UseAzure(resourceName: "", apiKey: "", authenticationType: AzureAuthenticationType.ApiKey);
options.DefaultModel = "my-model";
options.DefaultEmbeddingModel = "text-embedding-ada-002";
options.MessageLimit = 16; // 默认值:10
options.MessageExpiration = TimeSpan.FromMinutes(5); // 默认值:1小时
options.DefaultParameters = new ChatGptParameters
{
MaxTokens = 800,
Temperature = 0.7
};
});
ChatGptNet 支持 OpenAI 和 Azure OpenAI 服务,因此根据所选的提供商设置正确的配置是必要的:
OpenAI (UseOpenAI)
- ApiKey:可在 OpenAI 账户的 用户设置 页面获取(必填)。
- Organization:对于属于多个组织的用户,可以指定使用哪个组织。这些 API 请求的使用将计入指定组织的订阅额度(可选)。
Azure OpenAI 服务 (UseAzure)
- ResourceName:你的 Azure OpenAI 资源的名称(必填)。
- ApiKey:Azure OpenAI 提供两种身份验证方法。你可以使用 API 密钥或 Azure Active Directory(必填)。
- ApiVersion:所使用的 API 版本(可选)。允许的值:
- 2023-05-15
- 2023-06-01-preview
- 2023-10-01-preview
- 2024-02-15-preview
- 2024-03-01-preview
- 2024-04-01-preview
- 2024-05-01-preview
- 2024-02-01
- 2024-06-01 (默认)
- AuthenticationType:指定密钥是实际的 API 密钥还是 Azure Active Directory token(可选,默认值:"ApiKey")。
DefaultModel 和 DefaultEmbeddingModel
ChatGPT 可以使用不同的模型来完成对话,无论是在 OpenAI 还是 Azure OpenAI 服务中。通过 DefaultModel 属性,你可以指定默认使用的模型,除非在 AskAsync 或 AsyStreamAsync 方法中传递了明确的值。
即使对于对话来说不是严格必要的,该库也支持 Embedding API,无论是在 OpenAI 还是 Azure OpenAI 中。与对话完成一样,嵌入可以使用不同的模型来完成。通过 DefaultEmbeddingModel 属性,你可以指定默认使用的模型,除非在 GetEmbeddingAsync 方法中传递了明确的值。
OpenAI
当前可用的模型有:
- gpt-3.5-turbo,
- gpt-3.5-turbo-16k,
- gpt-4,
- gpt-4-32k
- gpt-4-turbo
- gpt-4o
- gpt-4o-mini
它们有固定的名称,可在 OpenAIChatGptModels.cs 文件 中找到。
Azure OpenAI 服务
在 Azure OpenAI 服务中,你需要先 部署模型 才能进行调用。当你部署一个模型时,需要为其分配一个名称,该名称必须与 ChatGptNet 使用的名称匹配。
注意 某些模型在所有地区不可用。你可以参考 模型摘要表和地区可用性页面 以检查当前的可用性。
缓存、MessageLimit 和 MessageExpiration
ChatGPT 旨在支持对话场景:用户可以与 ChatGPT 对话,而无需为每次交互指定完整的上下文。然而,对话历史记录并不是由 OpenAI 或 Azure OpenAI 服务管理的,因此我们需要自己保留当前状态。默认情况下,ChatGptNet 使用 MemoryCache 来存储每个对话的消息。可以使用以下属性来设置其行为:
- MessageLimit:指定每个对话保存消息的数量。当达到此限制时,最早的消息会自动被移除。
- MessageExpiration:指定无论消息数量如何,维护消息在缓存中的时间间隔。
若有必要,可以通过实现 IChatGptCache 接口并调用 WithCache 扩展方法来提供自定义缓存:
public class LocalMessageCache : IChatGptCache
{
private readonly Dictionary<Guid, IEnumerable<ChatGptMessage>> localCache = new();
public Task SetAsync(Guid conversationId, IEnumerable<ChatGptMessage> messages, TimeSpan expiration, CancellationToken cancellationToken = default)
{
localCache[conversationId] = messages.ToList();
return Task.CompletedTask;
}
public Task<IEnumerable<ChatGptMessage>?> GetAsync(Guid conversationId, CancellationToken cancellationToken = default)
{
localCache.TryGetValue(conversationId, out var messages);
return Task.FromResult(messages);
}
public Task RemoveAsync(Guid conversationId, CancellationToken cancellationToken = default)
{
localCache.Remove(conversationId);
return Task.CompletedTask;
}
public Task<bool> ExistsAsync(Guid conversationId, CancellationToken cancellationToken = default)
{
var exists = localCache.ContainsKey(conversationId);
return Task.FromResult(exists);
}
}
// 在应用程序启动时注册自定义缓存。
builder.Services.AddChatGpt(/* ... */).WithCache<LocalMessageCache>();
我们还可以在启动时设置用于对话完成的 ChatGPT 参数。查看 官方文档 以获取可用参数及其含义的列表。
使用外部源进行配置
可以使用例如在 appsettings.json 文件中创建的 ChatGPT 部分自动从 IConfiguration 读取配置:
"ChatGPT": {
"Provider": "OpenAI", // 可选。允许的值:OpenAI(默认)或 Azure
"ApiKey": "", // 必填
//"Organization": "", // 可选,只被 OpenAI 使用
"ResourceName": "", // 使用 Azure OpenAI 服务时必填
"ApiVersion": "2024-06-01", // 可选,仅被 Azure OpenAI 服务使用(默认:2024-06-01)
"AuthenticationType": "ApiKey", // 可选,仅被 Azure OpenAI 服务使用。允许的值:ApiKey(默认)或 ActiveDirectory
"DefaultModel": "my-model",
"DefaultEmbeddingModel": "text-embedding-ada-002", // 可选,如果想使用嵌入,请设置该值
"MessageLimit": 20,
"MessageExpiration": "00:30:00",
"ThrowExceptionOnError": true // 可选,默认值:true
//"User": "UserName",
//"DefaultParameters": {
// "Temperature": 0.8,
// "TopP": 1,
// "MaxTokens": 500,
// "PresencePenalty": 0,
// "FrequencyPenalty": 0,
// "ResponseFormat": { "Type": "text" }, // Type 允许的值:text(默认)或 json_object
// "Seed": 42 // 可选(任何整数值)
//},
//"DefaultEmbeddingParameters": {
// "Dimensions": 1536
//}
}
然后使用相应的 AddChatGpt 方法重载:
// 使用 IConfiguration 中的设置添加 ChatGPT 服务。
builder.Services.AddChatGpt(builder.Configuration);
动态配置 ChatGptNet
AddChatGpt 方法还有一个接受 IServiceProvider 作为参数的重载。它可以例如在我们使用 Web API 并且需要支持每个用户有不同的 API Key 这种场景时使用,这些 API Key 可以通过依赖注入访问数据库来获取:
builder.Services.AddChatGpt((services, options) =>
{
var accountService = services.GetRequiredService<IAccountService>();
// 动态从服务中获取 API Key。
var apiKey = "..."
options.UseOpenAI(apiKyey);
});
使用 IConfiguration 和代码配置 ChatGptNet
在更复杂的场景中,可能需要使用代码和 IConfiguration 同时配置 ChatGptNet。当我们想要设置一组通用属性,但同时需要一些配置逻辑时,这会很有用。例如:
builder.Services.AddChatGpt((services, options) =>
{
// 使用 IConfiguration 配置通用属性(消息限制和过期时间,默认参数等)。
options.UseConfiguration(builder.Configuration);
var accountService = services.GetRequiredService
// Dynamically gets the API Key from the service. var apiKey = "..."
options.UseOpenAI(apiKey); });
### 配置 HTTP 客户端
**ChatGptNet** 使用 [HttpClient](https://docs.microsoft.com/dotnet/api/system.net.http.httpclient) 调用聊天完成和嵌入 API。如果需要自定义它,可以使用接受 [Action<IHttpClientBuiler>](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.ihttpclientbuilder) 作为参数的 **AddChatGpt** 方法。例如,如果要为 HTTP 客户端添加弹性功能(例如重试策略),可以使用 [Polly](https://github.com/App-vNext/Polly):
```csharp
// 使用 Microsoft.Extensions.DependencyInjection;
// 需要:Microsoft.Extensions.Http.Resilience
builder.Services.AddChatGpt(context.Configuration,
httpClient =>
{
// 使用 Polly 配置内置 HttpClient 的重试策略。
httpClient.AddStandardResilienceHandler(options =>
{
options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(3);
options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(3);
});
})
有关此主题的更多信息,请参见官方文档。
使用
该库可用于使用 .NET 6.0 或更高版本构建的任何 .NET 应用程序。例如,我们可以通过以下方式创建一个最小 API:
app.MapPost("/api/chat/ask", async (Request request, IChatGptClient chatGptClient) =>
{
var response = await chatGptClient.AskAsync(request.ConversationId, request.Message);
return TypedResults.Ok(response);
})
.WithOpenApi();
// ...
public record class Request(Guid ConversationId, string Message);
如果只想获取响应消息,可以调用 GetContent 方法:
var content = response.GetContent();
注意 如果响应被内容过滤系统过滤,GetContent 将返回 null。因此,在尝试访问实际内容之前,始终应检查
response.IsContentFiltered
属性。
使用参数
使用配置可以为聊天完成设置默认参数。然而,我们也可以使用接受 ChatGptParameters 对象的 AskAsync 或 AskStreamAsync 重载为每个请求指定参数:
var response = await chatGptClient.AskAsync(conversationId, message, new ChatGptParameters
{
MaxTokens = 150,
Temperature = 0.7
});
我们不需要指定所有参数,只需要那些我们想要覆盖的参数。其他参数将从默认配置中获取。
Seed 和系统指纹
ChatGPT 以其不确定性而闻名。这意味着相同的输入可以产生不同的输出。为了尝试控制这种行为,我们可以使用 Temperature 和 TopP 参数。例如,将 Temperature 设置为接近 0 的值会使模型更加确定性,而将其设置为接近 1 的值会使模型更加富有创意。 然而,这并不足以保证相同输入产生相同输出。为了解决这个问题,OpenAI 引入了 Seed 参数。如果指定了这个参数,模型应以确定性方式进行采样,使得具有相同种子和参数的重复请求应该返回相同结果。然而,即使在这种情况下也无法保证确定性,您应参考 SystemFingerprint 响应参数以监视后端的更改。该值的更改意味着后端配置发生了变化,这可能会影响确定性。
如往常一样,Seed 属性可以在默认配置中指定,也可以在接受 ChatGptParameters 对象的 AskAsync 或 AskStreamAsync 重载中指定。
注意 Seed 和 SystemFingerprint 仅支持最新的模型,如 gpt-4-1106-preview。
响应格式
如果想强制响应以 JSON 格式返回,可以使用 ResponseFormat 参数:
var response = await chatGptClient.AskAsync(conversationId, message, new ChatGptParameters
{
ResponseFormat = ChatGptResponseFormat.Json,
});
这样,响应将始终是有效的 JSON。注意,还必须通过系统或用户消息指示模型生成 JSON。如果不这样做,模型将返回错误。
如往常一样,ResponseFormat 属性可以在默认配置中指定,也可以在接受 ChatGptParameters 对象的 AskAsync 或 AskStreamAsync 重载中指定。
注意 ResponseFormat 仅支持最新的模型,如 gpt-4-1106-preview。
处理对话
AskAsync 和 AskStreamAsync(见下文)方法提供了需要 conversationId 参数的重载。如果传递一个空值,则会生成并返回一个随机值。 在后续调用 AskAsync 或 AskStreamAsync 时,我们可以传递这个值,这样该库可以自动检索当前对话的先前消息(根据 MessageLimit 和 MessageExpiration 设置)并将它们发送到聊天完成 API。
这是所有聊天交互的默认行为。如果您希望将特定互动从对话历史中排除,可以将 addToConversationHistory 参数设置为 false:
var response = await chatGptClient.AskAsync(conversationId, message, addToConversationHistory: false);
这样,该消息将被发送到聊天完成 API,但它和 ChatGPT 的相应答案不会添加到对话历史中。
另一方面,在某些情况下,可能需要手动将聊天交互(即一问一答)添加到对话历史中。例如,我们可能想添加由机器人生成的消息。在这种情况下,可以使用 AddInteractionAsync 方法:
await chatGptClient.AddInteractionAsync(conversationId, question: "What is the weather like in Taggia?",
answer: "It's Always Sunny in Taggia");
该问题将作为 user 消息添加,答案将作为 assistant 消息添加到对话历史中。如往常一样,这些新消息(符合 MessageLimit 选项的消息)将在后续调用 AskAsync 或 AskStreamAsync 时使用。
响应流
聊天完成 API 支持响应流。使用此功能时,将像在 ChatGPT 中一样发送部分消息增量。令牌将作为数据仅服务器发送事件在可用时发送。ChatGptNet 提供使用 AskStreamAsync 方法的响应流:
// 请求流响应。
var responseStream = chatGptClient.AskStreamAsync(conversationId, message);
await foreach (var response in responseStream)
{
Console.Write(response.GetContent());
await Task.Delay(80);
}
注意 如果响应被内容过滤系统过滤,foreach 中的 GetContent 方法将返回 null 字符串。因此,在尝试访问实际内容前,始终应检查
response.IsContentFiltered
属性。
响应流通过返回 IAsyncEnumerable 工作,因此即使在 Web API 项目中也可以使用:
app.MapGet("/api/chat/stream", (Guid? conversationId, string message, IChatGptClient chatGptClient) =>
{
async IAsyncEnumerable<string?> Stream()
{
// 请求流响应。
var responseStream = chatGptClient.AskStreamAsync(conversationId.GetValueOrDefault(), message);
// 使用 "AsDeltas" 扩展方法仅检索部分消息增量。
await foreach (var delta in responseStream.AsDeltas())
{
yield return delta;
await Task.Delay(50);
}
}
return Stream();
})
.WithOpenApi();
注意 如果响应被内容过滤系统过滤,foreach 中的 AsDeltas 方法将返回 nulls 字符串。
该库也 100% 兼容 Blazor WebAssembly 应用程序:
有关不同实现的更多信息,请查看 Samples 文件夹。
改变助手的行为
ChatGPT 支持带有 system 角色的消息以影响助手应该如何表现。例如,我们可以告诉 ChatGPT 这样的内容:
- 你是一个有用的助手
- 像莎士比亚一样回答
- 只给我错误答案
- 用押韵回答
ChatGptNet 通过 SetupAsync 方法提供此功能:
var conversationId await = chatGptClient.SetupAsync("Answer in rhyme");
如果在调用 AskAsync 时使用相同的 conversationId,则 system 消息将自动与每个请求一起发送,以便助手知道如何表现。
注意 system 消息不计入消息限制数量。
删除对话
对话历史在达到到期时间(由 MessageExpiration 属性指定)时自动删除。然而,如果需要,可以立即清除历史记录:
await chatGptClient.DeleteConversationAsync(conversationId, preserveSetup: false);
preserveSetup 参数允许决定是否保留通过 SetupAsync 方法设置的 system 消息(默认值:false)。
工具和函数调用
通过函数调用,我们可以描述函数并让模型智能地选择输出包含 调用这些函数的参数的 JSON 对象。这是一种将 GPT 能力更可靠地连接到外部工具和 API 的新方式。
ChatGptNet 通过提供接受函数定义的 AskAsync 方法的重载来完全支持函数调用。如果提供了此参数,模型将决定何时适当地使用这些函数之一。例如:
```csharp
var functions = new List<ChatGptFunction>
{
new()
{
Name = "GetCurrentWeather",
Description = "获取当前天气",
Parameters = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和/或邮政编码"
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "使用的温度单位。从用户的位置推断。"
}
},
"required": ["location", "format"]
}
""")
},
new()
{
Name = "GetWeatherForecast",
Description = "获取N天的天气预报",
Parameters = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市和/或邮政编码"
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "使用的温度单位。从用户的位置推断。"
},
"daysNumber": {
"type": "integer",
"description": "预报的天数"
}
},
"required": ["location", "format", "daysNumber"]
}
""")
}
};
var toolParameters = new ChatGptToolParameters
{
FunctionCall = ChatGptToolChoices.Auto, // 如果存在函数,这是默认值。
Functions = functions
};
var response = await chatGptClient.AskAsync("塔吉亚的天气怎么样?", toolParameters);
我们可以传递任意数量的函数,每个函数都有一个名称、描述和描述函数参数的JSON架构,遵循JSON Schema参考。在后台,函数以模型训练的语法注入系统消息。这意味着函数计入模型的上下文限制,并作为输入令牌计费。
AskAsync方法返回的响应对象提供了一个属性来检查模型是否选择了一个函数调用:
if (response.ContainsFunctionCalls())
{
Console.WriteLine("我已识别出需要调用的函数:");
var functionCall = response.GetFunctionCall()!;
Console.WriteLine(functionCall.Name);
Console.WriteLine(functionCall.Arguments);
}
此代码将打印如下内容:
我已识别出需要调用的函数:
GetCurrentWeather
{
"location": "塔吉亚",
"format": "celsius"
}
请注意,API实际上不会执行任何函数调用。开发人员需要使用模型输出来执行函数调用。
实际执行之后,我们需要调用ChatGptClient上的AddToolResponseAsync方法,将响应添加到对话历史中,就像一个标准消息一样,这样它将自动用于聊天完成:
// 调用远程函数API。
var functionResponse = await GetWeatherAsync(functionCall.Arguments);
await chatGptClient.AddToolResponseAsync(conversationId, functionCall, functionResponse);
像_gpt-4-turbo_这样的新模型支持更通用的函数处理方法,即工具调用。发送请求时,可以指定模型可能调用的工具列表。目前仅支持函数,但将来版本中将提供其他类型的工具。
要使用工具调用而不是直接函数调用,您需要在ChatGptToolParameters对象中设置_ToolChoice_和_Tools_属性(而不是前面的例子中的_FunctionCall_和_Function_):
var toolParameters = new ChatGptToolParameters
{
ToolChoice = ChatGptToolChoices.Auto, // 如果存在函数,这是默认值。
Tools = functions.ToTools()
};
ToTools扩展方法用于将一个ChatGptFunction列表转换为工具列表。
如果使用这种新方法,当然仍需检查模型是否选择了一个工具调用,使用之前展示的相同方法。 然后,实际执行函数后,您需要调用AddToolResponseAsync方法,但在这种情况下,您需要指定响应引用的工具(而不是函数):
var tool = response.GetToolCalls()!.First();
var functionCall = response.GetFunctionCall()!;
// 调用远程函数API。
var functionResponse = await GetWeatherAsync(functionCall.Arguments);
await chatGptClient.AddToolResponseAsync(conversationId, tool, functionResponse);
最后,您需要将原始消息重新发送到聊天完成API,以便模型能够在考虑函数调用响应的情况下继续对话。查看函数调用示例,了解此工作流的完整实现。
内容过滤
使用Azure OpenAI服务时,我们免费获得内容过滤。有关其工作原理的详细信息,请查看文档。在使用API版本2023-06-01-preview
或更高版本的所有场景中,都会返回此信息。ChatGptNet完全支持此对象模型,通过提供ChatGptResponse和ChatGptChoice类中的相应属性。
嵌入
嵌入允许将文本转换为矢量空间。例如,这对于比较两个句子的相似性非常有用。ChatGptNet通过提供GetEmbeddingAsync方法完全支持此功能:
var response = await chatGptClient.GenerateEmbeddingAsync(message);
var embeddings = response.GetEmbedding();
此代码将为您提供一个包含指定消息所有嵌入的浮点数组。数组长度取决于使用的模型:
模型 | 输出维度 |
---|---|
text-embedding-ada-002 | 1536 |
text-embedding-3-small | 1536 |
text-embedding-3-large | 3072 |
像_text-embedding-3-small_和_text-embedding-3-large_这样的新模型允许开发人员在性能和使用嵌入的成本之间进行权衡。具体来说,开发人员可以缩短嵌入,而嵌入仍然保留其概念表示的属性。
对于ChatGPT,可以通过各种方式进行设置:
- 通过代码:
builder.Services.AddChatGpt(options =>
{
// ...
options.DefaultEmbeddingParameters = new EmbeddingParameters
{
Dimensions = 256
};
});
- 使用_appsettings.json_文件:
"ChatGPT": {
"DefaultEmbeddingParameters": {
"Dimensions": 256
}
}
然后,如果您想为特定请求更改维度,可以在GetEmbeddingAsync调用中指定EmbeddingParameters参数:
var response = await chatGptClient.GenerateEmbeddingAsync(request.Message, new EmbeddingParameters
{
Dimensions = 512
});
var embeddings = response.GetEmbedding(); // 数组的长度是512
如果需要计算两个嵌入之间的余弦相似度,可以使用EmbeddingUtility.CosineSimilarity方法。
文档
完整的技术文档请见此处。
贡献
项目在不断发展。欢迎贡献。请随时在代码仓中提交问题和拉取请求,我们将尽可能地解决这些问题和请求。
警告 记住在develop分支上工作,不要直接使用master分支。创建指向develop的拉取请求。