」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 使用 fetch 串流 HTTP 回應

使用 fetch 串流 HTTP 回應

發佈於2024-07-31
瀏覽:783

Streaming HTTP Responses using fetch

这篇文章将着眼于使用 JavaScript Streams API,它允许进行 fetch HTTP 调用并以块的形式接收流响应,这允许客户端开始更多地响应服务器响应快速构建像 ChatGPT 这样的 UI。

作为一个激励性的例子,我们将实现一个函数来处理来自 OpenAI(或任何使用相同 http 流 API 的服务器)的流式 LLM 响应,不使用 npm 依赖项,仅使用内置的 fetch。完整的代码在这里,包括指数退避重试、嵌入、非流式聊天以及用于与聊天完成和嵌入交互的更简单的 API。

如果您有兴趣了解如何将 HTTP 流返回给客户端,请查看这篇文章。

完整示例代码

这是完整的示例。我们将看看下面的每一个部分:

async function createChatCompletion(body: ChatCompletionCreateParams) {
  // Making the request
  const baseUrl = process.env.LLM_BASE_URL || "https://api.openai.com";
  const response = await fetch(baseUrl   "/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer "   process.env.LLM_API_KEY,
    },
    body: JSON.stringify(body),
  });
  // Handling errors
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Failed (${response.status}): ${error}`,
  }
  if (!body.stream) { // the non-streaming case
    return response.json();
  }
  const stream = response.body;
  if (!stream) throw new Error("No body in response");
  // Returning an async iterator
  return {
    [Symbol.asyncIterator]: async function* () {
      for await (const data of splitStream(stream)) {
        // Handling the OpenAI HTTP streaming protocol
        if (data.startsWith("data:")) {
          const json = data.substring("data:".length).trimStart();
          if (json.startsWith("[DONE]")) {
            return;
          }
          yield JSON.parse(json);
        }
      }
    },
  };
}

// Reading the stream  
async function* splitStream(stream: ReadableStream) {
  const reader = stream.getReader();
  let lastFragment = "";
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Flush the last fragment now that we're done
        if (lastFragment !== "") {
          yield lastFragment;
        }
        break;
      }
      const data = new TextDecoder().decode(value);
      lastFragment  = data;
      const parts = lastFragment.split("\n\n");
      // Yield all except for the last part
      for (let i = 0; i 



请参阅此处的代码,了解具有流式和非流式参数变体的良好类型重载的版本,以及重试和其他改进。

这篇文章的其余部分是关于理解这段代码的作用。

提出请求

这部分其实很简单。流式 HTTP 响应来自普通 HTTP 请求:

const baseUrl = process.env.LLM_BASE_URL || "https://api.openai.com";
const response = await fetch(baseUrl   "/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer "   process.env.LLM_API_KEY,
  },
  body: JSON.stringify(body),
});

HTTP 标头按平常方式发送,无需特别设置任何内容即可启用流式传输。您仍然可以利用常规缓存标头进行 HTTP 流式传输。

处理错误

关于客户端错误的故事对于 HTTP 流来说有点不幸。好处是,对于 HTTP 流式传输,客户端会在初始响应中立即获取状态代码,并可以检测到故障。 http 协议的缺点是,如果服务器返回成功,但随后在流中中断,则协议级别没有任何内容可以告诉客户端流已中断。我们将在下面看到 OpenAI 如何在最后编码“全部完成”哨兵来解决这个问题。

if (!response.ok) {
  const error = await response.text();
  throw new Error(`Failed (${response.status}): ${error}`,
}

读取流

为了读取 HTTP 流响应,客户端可以使用 response.body 属性,该属性是一个 ReadableStream,允许您使用 .getReader() 方法迭代从服务器传入的块。 1

const reader = request.body.getReader();
try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      const text = TextDecoder().decode(value);
      //... do something with the chunk
    }
} finally {
  reader.releaseLock();
}

这会处理我们返回的每一位数据,但对于 OpenAI HTTP 协议,我们期望数据是由换行符分隔的 JSON,因此我们将拆分响应正文并“生成”每一行。重新完成。我们将进行中的行缓冲到lastFragment中,并且只返回由两个换行符分隔的完整行:

// stream here is request.body
async function* splitStream(stream: ReadableStream) {
  const reader = stream.getReader();
  let lastFragment = "";
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Flush the last fragment now that we're done
        if (lastFragment !== "") {
          yield lastFragment;
        }
        break;
      }
      const data = new TextDecoder().decode(value);
      lastFragment  = data;
      const parts = lastFragment.split("\n\n");
      // Yield all except for the last part
      for (let i = 0; i 



如果你不熟悉这个 function* 和yield 语法,只需将 function* 视为可以在循环中返回多个内容的函数,并将yield视为从函数中多次返回内容的方式。

然后您可以循环此 splitStream 函数,例如:

for await (const data of splitStream(response.body)) {
  // data here is a full line of text. For OpenAI, it might look like
  // "data: {...some json object...}" or "data: [DONE]" at the end
}

如果这个“for wait”语法让您感到困惑,那么它使用了所谓的“异步迭代器”——就像您在 for 循环中使用的常规迭代器一样,但每次它获取下一个值时,都会等待它。

对于我们的示例,当我们从 OpenAI 获取一些文本并且正在等待更多文本时,for 循环将等待,直到 splitStream 产生另一个值,这将在 wait reader.read() 返回一个完成的值时发生一行或多行文本。

接下来我们将研究另一种返回异步迭代器的方法,该迭代器不是 splitStream 等函数,因此调用者可以使用“for wait”循环来迭代此数据。

返回一个异步迭代器

现在我们有一个返回整行文本的异步迭代器,我们可以只返回 splitStream(response.body),但我们希望拦截每一行并转换它们,同时仍然让函数的调用者进行迭代。

该方法类似于上面的 async function* 语法。这里我们将直接返回一个异步迭代器,而不是调用时返回一个的异步函数。不同之处在于类型是 AsyncIterator 而不是需要首先调用的 AsyncGenerator。 AsyncIterator 可以通过某个命名函数来定义:Symbol.asyncIterator.2

      return {
        [Symbol.asyncIterator]: async function* () {
          for await (const data of splitStream(stream)) {
            //handle the data
            yield data;
          }
        },
      };

当您想要返回与来自 splitStream 的数据不同的内容时,这非常有用。每次从流式 HTTP 请求中传入新行时,sp​​litStream 都会生成它,该函数将在数据中接收它,并可以在将其生成给调用者之前执行一些操作。

接下来我们将看看如何在 OpenAI 的流式聊天完成 API 的情况下具体解释这些数据。

处理 OpenAI HTTP 流协议

OpenAI 响应协议是一系列以 data: 或 event: 开头的行,但我们只处理数据响应,因为这是完成聊天的有用部分。如果流完成,则有一个 [DONE] 标记,否则它只是 JSON。

for await (const data of splitStream(stream)) {
  if (data.startsWith("data:")) {
    const json = data.substring("data:".length).trimStart();
    if (json.startsWith("[DONE]")) {
      return;
    }
    yield JSON.parse(json);
  } else {
    console.debug("Unexpected data:", data);
  }
}

将所有内容整合在一起

既然您了解了 HTTP 流,您就可以放心地直接使用流 API,而无需依赖 sdk 或库。这使您可以隐藏延迟,因为您的 UI 可以立即开始更新,而不会因为多个请求而消耗更多带宽。您可以像使用官方 openai npm 包一样使用上述功能:

  const response = await createChatCompletion({
    model: "llama3",
    messages: [...your messages...],
    stream: true,
  });
  for await (const chunk of response) {
    if (chunk.choices[0].delta?.content) {
      console.log(chunk.choices[0].delta.content);
    }
  }

请参阅此处的代码,它还允许您创建一些实用函数,通过预先配置模型并提取 .choices[0].delta.content:
使这变得更加容易

const response = await chatStream(messages);
for await (const content of response) {
  console.log(content);
}

在复制代码之前,尝试自己实现它作为异步函数的练习。

更多资源

  • 有关从您自己的服务器端点返回 HTTP 流数据的信息,请查看关于 AI Chat with HTTP Streaming 的这篇文章,该文章既将数据从 OpenAI(或类似的)流式传输到您的服务器,又同时将其流式传输到客户端,同时执行自定义逻辑(例如将块保存到数据库)。
  • MDN 文档一如既往地很棒。除了上面的链接之外,这里还有关于可读流 API 的指南,它展示了如何将可读流连接到 使用 fetch 串流 HTTP 回應 标签以在图像请求中进行流式传输。注意:本指南使用 response.body 作为异步迭代器,但目前尚未广泛实现,并且不在 TypeScript 类型中。

  1. 注意:一次只能有一个流的读取器,因此您通常不会多次调用 .getReader() - 在这种情况下您可能需要 .tee() ,并且如果您想使用 .由于某种原因多次 getReader() ,请确保首先拥有第一个 .releaseLock() 。 ↩

  2. 或者,如果您不熟悉 Symbol,它的用途是在对象中包含非字符串或数字的键。这样,如果您添加了名为 asyncIterator 的键,它们就不会发生冲突。您可以使用 myIterator[Symbol.asyncIterator]() 访问该函数。 ↩

版本聲明 本文轉載於:https://dev.to/ianmacartney/streaming-http-responses-using-fetch-1fm2?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 如何在Java中執行命令提示命令,包括目錄更改,包括目錄更改?
    如何在Java中執行命令提示命令,包括目錄更改,包括目錄更改?
    在java 通過Java通過Java運行命令命令可能很具有挑戰性。儘管您可能會找到打開命令提示符的代碼段,但他們通常缺乏更改目錄並執行其他命令的能力。 solution:使用Java使用Java,使用processBuilder。這種方法允許您:啟動一個過程,然後將其標準錯誤重定向到其標準輸出...
    程式設計 發佈於2025-03-26
  • 如何將MySQL數據庫添加到Visual Studio 2012中的數據源對話框中?
    如何將MySQL數據庫添加到Visual Studio 2012中的數據源對話框中?
    在Visual Studio 2012 儘管已安裝了MySQL Connector v.6.5.4,但無法將MySQL數據庫添加到實體框架的“ DataSource對話框”中。為了解決這一問題,至關重要的是要了解MySQL連接器v.6.5.5及以後的6.6.x版本將提供MySQL的官方Visual...
    程式設計 發佈於2025-03-26
  • 如何檢查對像是否具有Python中的特定屬性?
    如何檢查對像是否具有Python中的特定屬性?
    方法來確定對象屬性存在尋求一種方法來驗證對像中特定屬性的存在。考慮以下示例,其中嘗試訪問不確定屬性會引起錯誤: >>> a = someClass() >>> A.property Trackback(最近的最新電話): 文件“ ”,第1行, AttributeError: SomeClass...
    程式設計 發佈於2025-03-26
  • 如何實時捕獲和流媒體以進行聊天機器人命令執行?
    如何實時捕獲和流媒體以進行聊天機器人命令執行?
    在開發能夠執行命令的chatbots的領域中,實時從命令執行實時捕獲Stdout,一個常見的需求是能夠檢索和顯示標準輸出(stdout)在cath cath cant cant cant cant cant cant cant cant interfaces in Chate cant inter...
    程式設計 發佈於2025-03-26
  • 為什麼Microsoft Visual C ++無法正確實現兩台模板的實例?
    為什麼Microsoft Visual C ++無法正確實現兩台模板的實例?
    The Mystery of "Broken" Two-Phase Template Instantiation in Microsoft Visual C Problem Statement:Users commonly express concerns that Micro...
    程式設計 發佈於2025-03-26
  • 為什麼PYTZ最初顯示出意外的時區偏移?
    為什麼PYTZ最初顯示出意外的時區偏移?
    與pytz 最初從pytz獲得特定的偏移。例如,亞洲/hong_kong最初顯示一個七個小時37分鐘的偏移: 差異源利用本地化將時區分配給日期,使用了適當的時區名稱和偏移量。但是,直接使用DateTime構造器分配時區不允許進行正確的調整。 example pytz.timezone(&#...
    程式設計 發佈於2025-03-26
  • 如何克服PHP的功能重新定義限制?
    如何克服PHP的功能重新定義限制?
    克服PHP的函數重新定義限制在PHP中,多次定義一個相同名稱的函數是一個no-no。嘗試這樣做,如提供的代碼段所示,將導致可怕的“不能重新列出”錯誤。 但是,PHP工具腰帶中有一個隱藏的寶石:runkit擴展。它使您能夠靈活地重新定義函數。 runkit_function_renction_...
    程式設計 發佈於2025-03-26
  • 如何處理PHP文件系統功能中的UTF-8文件名?
    如何處理PHP文件系統功能中的UTF-8文件名?
    在PHP的Filesystem functions中處理UTF-8 FileNames 在使用PHP的MKDIR函數中含有UTF-8字符的文件很多flusf-8字符時,您可能會在Windows Explorer中遇到comploreer grounder grounder grounder gro...
    程式設計 發佈於2025-03-26
  • Android如何向PHP服務器發送POST數據?
    Android如何向PHP服務器發送POST數據?
    在android apache httpclient(已棄用) httpclient httpclient = new defaulthttpclient(); httppost httppost = new httppost(“ http://www.yoursite.com/script.p...
    程式設計 發佈於2025-03-26
  • 如何將PANDAS DataFrame列轉換為DateTime格式並按日期過濾?
    如何將PANDAS DataFrame列轉換為DateTime格式並按日期過濾?
    Transform Pandas DataFrame Column to DateTime FormatScenario:Data within a Pandas DataFrame often exists in various formats, including strings.使用時間數據時...
    程式設計 發佈於2025-03-26
  • 您如何在Laravel Blade模板中定義變量?
    您如何在Laravel Blade模板中定義變量?
    在Laravel Blade模板中使用Elegance 在blade模板中如何分配變量對於存儲以後使用的數據至關重要。在使用“ {{}}”分配變量的同時,它可能並不總是最優雅的解決方案。 幸運的是,Blade通過@php Directive提供了更優雅的方法: $ old_section =...
    程式設計 發佈於2025-03-26
  • 大批
    大批
    [2 數組是對象,因此它們在JS中也具有方法。 切片(開始):在新數組中提取部分數組,而無需突變原始數組。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    程式設計 發佈於2025-03-26
  • HTML格式標籤
    HTML格式標籤
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    程式設計 發佈於2025-03-26
  • 最大的組合與位,大於零
    最大的組合與位,大於零
    2275。最大的組合與位和大於零 [2 [2 [2 例如,對於nums = [1,5,3],位於位,等於1&5&3 = 1。 ,位且為7。 您有一系列積極的整數候選者。評估每個候選數字的每個的的。每種組合中的每個數字都只能在中使用。 比0 cantidates = [16,17,71,62,...
    程式設計 發佈於2025-03-26
  • 如何同步迭代並從PHP中的兩個等級陣列打印值?
    如何同步迭代並從PHP中的兩個等級陣列打印值?
    同步的迭代和打印值來自相同大小的兩個數組使用兩個數組相等大小的selectbox時,一個包含country代碼的數組,另一個包含鄉村代碼,另一個包含其相應名稱的數組,可能會因不當提供了exply for for for the uncore for the forsion for for ytry...
    程式設計 發佈於2025-03-26

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3