[HttpPost]
public async Task Input([FromForm] string message)
Response.Headers.Add("Content-Type", "text/event-stream");
Response.Headers.Add("Cache-Control", "no-cache");
var completionResult = _openAiService.ChatCompletion.CreateCompletionAsStream(
new ChatCompletionCreateRequest
Messages = new List<ChatMessage>
ChatMessage.FromUser(message)
Stream = true,
MaxTokens = 500,
Model = OpenAI.ObjectModels.Models.ChatGpt3_5Turbo,
await foreach (var completion in completionResult)
if (completion.Successful)
await Response.WriteAsync($"ChatGPT:{completion.Choices.FirstOrDefault()?.Message.Content}");
await Response.Body.FlushAsync();
if (completion.Error == null)
throw new Exception("Unknown Error");
await Response.WriteAsync($"{completion.Error.Code}: {completion.Error.Message}");
await Response.Body.FlushAsync();
我们通过设置Content-Type为text/event-stream,允许服务器向客户端推送数据。通过设置Cache-Control为no-cache,告诉客户端或代理不要缓存响应,而应该每次都从服务器重新获取。
接着把上一节中的ChatCompletion.CreateCompletion改为ChatCompletion.CreateCompletionAsStream,该方法返回IAsyncEnumerable异步迭器:
然后我们可以使用await foreach语法糖,用来异步迭代一个IAsyncEnumerable序列,它类似于foreach,但是它会等待序列中的每个元素异步生成,而不是同步获取。这样可以在等待下一个元素时不阻塞当前线程,提高性能和响应性。
我们使用Response.WriteAsync来异步向HTTP响应的输出流中写入字符串,使用Response.Body.FlushAsync将HTTP响应的输出流中的缓冲数据发送到客户端,所有方法全部使用异步方法,提高性能和响应。
二、Web端js脚本调整
打开Views/Home/Index.cshtml文件,将页面中的js脚本替换为以下内容:
<script type="text/javascript">
//内容显示框
var messagesList = document.getElementById("messagesList")
//消息输入框
var messageInput = document.getElementById("messageInput");
//发送按钮
var sendButton = document.getElementById("sendButton");
// 定义一个XMLHttpRequest对象,用于发送请求和接收响应
var httpRequest = new XMLHttpRequest();
//发送按钮绑定click事件
sendButton.addEventListener("click", function (event) {
var message = messageInput.value;
if (message.length == 0) {
alert('请输入聊天内容');
return;
send(message);
event.preventDefault();
function send(message) {
//修改按钮状态
sendButton.disabled = true;
//向内容显示框中追加发送的内容
var div = document.createElement("div");
div.className = "alert alert-secondary";
div.textContent = `Me:${message}`;
messageInput.value = '';
messagesList.appendChild(div);
//向内容显示框中追加ChatGpt返回的内容
div = document.createElement("div");
div.className = "alert alert-primary";
messagesList.appendChild(div);
div.textContent = "……";
//创建FormData格式消息
var formData = new FormData();
formData.set('message', message);
httpRequest.onprogress = function (progressEvent) {
//处理响应数据
div.textContent = `GPT:${progressEvent.target.responseText}`;
//请求是否成功都会执行
httpRequest.onloadend = function (progressEvent) {
//恢复按钮状态
sendButton.disabled = false;
//打开请求,设置请求方法和地址,并设置异步为true
httpRequest.open("POST", "api/chat", true);
httpRequest.responseType = "text";// 设置请求头为text格式
httpRequest.send(formData);//发送请求
</script>
这里我继续使用XMLHttpReques来实现SSE通信,并没有使用EventSource实现,主要是考虑它的浏览器支持更广泛一些,以下是两者的主要区别:
为了实现打字机效果,我们需要在请求过程中逐步接收数据,而不是等到请求完成时一次性接收所有数据。因此,我们在send方法里使用了httpRequest.onprogress事件,而不是httpRequest.onload事件。
同时,为了防止用户重复点击发送按钮,我们在send方法里对发送按钮进行了锁定。并使用httpRequest.onloadend对按钮解出状态,onloadend事件无论请求是成功还是失败,在请求结束时都会触发。
F5启动项目看一下效果:
在和ChatGPT对话过程中,有时候它的回答并不符合我们的期望,或者我们的输入可能有误,想让它停止回答,然后重新调整对话内容,此时增加一个停止响应的功能很实用,我们来实现一下。
三、服务端优化
为了优化服务端的性能,我们需要对Controllers/ChatController.cs中的Input方法添加一个CancellationToken类型的参数cancellationToken,用于检测HTTP请求是否被客户端或服务器中止了。如果是,cancellationToken的IsCancellationRequested属性将变为true,我们便可以取消正在运行的任务,避免浪费资源。所以我们要把cancellationToken传递给所有异步方法,让它们能够响应取消信号。
四、Web端再次调整
打开Views/Home/Index.cshtml文件,在发送按钮后边增加一个停止响应按钮:
<input type="button" id="stopButton" value="停止响应" class="btn btn-warning" disabled />
找到js脚本中sendButton的定义,增加停止响应按钮代码和事件:
//停止响应按钮
var stopButton = document.getElementById("stopButton");
//停止响应信号
var stopRequest = false;
//停止响应按钮绑定click事件
stopButton.addEventListener("click", function (event) {
stopRequest = true;
event.preventDefault();
在这里我们定义了一个stopRequest变量来接收停止信号,当触发停止响应事件时,stopRequest被设置为true,当请求结束时,stopRequest会重置为false。
在send方法中新增XMLHttpRequest的onreadystatechange事件调用,代码如下:
//监听请求状态
httpRequest.onreadystatechange = function () {
if (stopRequest) {
httpRequest.abort();
stopRequest = false;
onreadystatechange事件是一个属性,它存储了一个函数(或函数名),每当XMLHttpRequest的readyState属性改变时,就会调用该函数,readyState的值有4个,0表示请求未初始化;1表示服务器连接已建立;2表示请求已接收;3表示请求处理中;4表示请求已完成,且响应已就绪。因此我们可以根据它来执行stopRequest的重置任务。
httpRequest.abort方法用于取消已经发送的请求。当一个请求被取消时,readyState属性会被设置为0。
同时增加对停止响应按钮的状态控制,当处理SSE通信时,按钮变为可用,当请求结束时,状态变为不可用。
F5启动项目看一下效果:
今天就先到这里,下节我们继续探索如何实现上下文聊天功能和会话管理功能,请大家多多关注。
//源码地址
https://github.com/ynanech/ChatGPT.Demo
👇感谢阅读,点赞+分享+收藏+关注👇
【.Net/C#之ChatGPT开发系列】一、开发准备及实现与ChatGPT的初次对话前面我们利用了ChatGPT提供的聊天API接口,实现了一个简单的聊天应用,可以与ChatGPT进行基本的对话交互,今天我们继续,还请大家点个关注。????当你使用ChatGPT官网提供的聊天工具时,你会发现,ChatGPT的内容是一个字一个字输出的,而我们实现的却是整句的输入,这是如何实现的呢,这样做又是为什么?(公...
C#.NET串口通信例程源码(18例):
001.利用C#.NET实现PC与PC串口通信(SerialPort控件,查询方式)
002.利用C#.NET实现PC机双串口通信(SerialPort控件,查询方式)
003.C#.NET和USB数据采集模块USB-4711实现实现数字量输入(ActiveDAQ控件)
004.C#.NET和USB数据采集模块USB-4711实现实现数字量输出(ActiveDAQ控件)
005.C#.NET和USB数据采集模块USB-4711实现实现模拟量输入(ActiveDAQ控件)
006.C#.NET和USB数据采集模块USB-4711实现实现模拟量输出(ActiveDAQ控件)
007.C#.NET和51单片机实现数字量输入(SerialPort控件)
008.C#.NET和51单片机实现数字量输出(SerialPort控件)
009.C#.NET和西门子S7-200系列PLC实现模拟量输入(PPI协议,SerialPort控件)
00grb.txt
010.C#.NET和西门子S7-200系列PLC实现模拟量输出(PPI协议,SerialPort控件
# .NET是什么?
.NET是一组技术和框架,它由一个运行时环境和一系列库组成,用于开发、部署和运行应用程序。.NET提供了一套与平台无关的API,这意味着可以在Windows、macOS和Linux等多种操作系统上编写相同的代码。
# C#是什么?
C#是一种现代的面向对象编程语言,它广泛用于开发.NET应用程序。C#是一种类型安全、高效且易于理解的编程语言,它最初由Microsoft开发并于2002年发布。与Java类似,C#运行在虚拟机上,称为公共语言运行时(CLR)。
# 开发使用C#的.NET应用程序
如果想使用C#开发.NET应用程序,可以使用Visual Studio IDE(集成开发环境)来创建、编译和调试应用程序。Visual Studio可以提供自动完成功能、代码重构、调试和集成版本控制等功能,可大大提高开发效率。
另外,开发C#应用程序还需要掌握.NET框架和C#语言本身的知识。对于.NET框架,需要了解它的组件、类库和API,例如ADO.NET、ASP.NET和Windows Presentation Foundation(WPF)等。而对于C#语言知识,则需要了解其基本语法、面向对象编程、常用数据类型和控制流程等。
总之,C#是.NET应用程序中最常用的语言之一,它的高效性和易用性是其受欢迎的原因之一。如果打算使用C#开发.NET应用程序,需要熟悉.NET框架和C#语言本身,并使用Visual Studio进行开发。