ใช้ GPT-3 เพื่อความสนุกและไม่หวังผลกำไร

ฉันเป็นแท็บบิสต์แบบอนุกรม ฉันยอมรับมัน.

ขณะนี้ฉันมีแท็บประมาณ 460 แท็บที่เปิดอยู่ในหน้าต่างที่กล้าหาญ 5 บาน อย่าเพิ่งเริ่มต้นที่บุ๊กมาร์กด้วยซ้ำ

“พ-ข-แต่ พวกมันทั้งหมดจำเป็น! ความรู้เพียบ! มีลิงค์ดีๆ มากมาย!”
- ผู้สะสมภายในของฉัน

ใช่แล้ว ฉันเหมือนกับหนูแฮมสเตอร์ข้อมูล ฉันแค่สะสมแท็บทั้งหมดต่อไปจนกว่าฉันจะมีเวลามากพอที่จะอ่าน ทุกสิ่ง — และเปิดอ่านเพิ่มเติมระหว่างทาง และอย่างที่ใครๆ คาดคิดไว้ การมีแท็บจำนวนมากอาจดูล้นหลามเกินไป ไม่ว่าเมื่อฉันต้องการค้นหาบางสิ่งและหายไปเกินขอบเขตของแถบแท็บ หรือเมื่อฉันแค่ดูหน้าจอและรู้สึกกังวลว่า "มี มีอะไรให้ทำมากมาย” — แม้ว่าจะไม่มีอะไรให้ทำก็ตาม

ดังนั้น ในฐานะที่ฉันเป็นแฮ็กเกอร์ที่ขี้เกียจ แทนที่จะจัดเรียงพวกมันจริงๆ แล้วทำความสะอาดพวกมัน หรือ *อึก* แค่ปิดมันทั้งหมด ฉันสงสัยว่า ทำไมไม่ปล่อยให้เครื่องจักรทำงานล่ะ ฉันสามารถมีวิธีแก้ไขปัญหาทั้งหมดด้วยการคลิกเพียงครั้งเดียวได้หรือไม่

ฉันสามารถ Marie-Kondo ผู้สะสมภายในของฉันส่งโดยใช้รหัสได้หรือไม่?

โชคดีสำหรับเรามีโมเดลภาษาขนาดยักษ์มูลค่าหลายพันล้านดอลลาร์ที่รอคอยที่จะทำงานนี้อย่างกระตือรือร้น

แนวคิดนั้นง่ายมาก: มอบรายการสินค้าให้กับ GPT-3 และขอให้ส่งคืนรายการหมวดหมู่ของสินค้าเหล่านั้น รวมทั้งหมดนั้นไว้เป็นส่วนขยายของ Chrome และปล่อยให้ความมหัศจรรย์เกิดขึ้น

งั้นเรามาถอดรหัสนิ้วของเราแล้วเข้ารหัสกันดีกว่า.. หรือ.. โอ้… รอก่อน..

รสชาติอันหอมหวานของความซับซ้อน

มาเหยียบหลังกันสักหน่อย แผนของเราฟังดูง่ายพอแล้ว แต่เนื่องจากโดยปกติแล้วจะเกี่ยวข้องกับซอฟต์แวร์ เราจึงพลาดรายละเอียดสำคัญบางประการที่อาจจะทำให้ขอบเขตและงบประมาณของเราพังทลายลงหากเราไม่ได้คิดถึงสิ่งเหล่านั้นอย่างเหมาะสม

ประเด็นสำคัญบางประการที่ต้องคำนึงถึงก่อนที่เราจะเจาะลึกเรื่อง Code Head ก่อนและพบว่าตัวเองอยู่ในโลกแห่งความเสียใจคือ:

  • ขีดจำกัดโทเค็นพร้อมท์ —โมเดลภาษาของ OpenAI มีขีดจำกัดโทเค็น — โทเค็น 2048 หรือ 4096 เนื่องจากแต่ละโทเค็นมีความยาวประมาณ 4 อักขระ ซึ่งจำกัดขนาดพรอมต์และการตอบกลับของเราไว้ที่ 8192/16384 อักขระตามลำดับ มีหลายวิธีที่เราจะสามารถแก้ไขปัญหานี้ได้ (เราจะกล่าวถึงทั้งหมด): การตัดข้อความแจ้งออกเป็นส่วนๆ เพิ่มประสิทธิภาพข้อมูลที่ส่งเพื่อลดจำนวนโทเค็น และปรับแต่งแบบจำลองสำหรับงานของเรา
  • ความปลอดภัยของคีย์ API —เนื่องจาก OpenAI API เรียกเก็บเงินการเรียก API ตามโทเค็นที่ใช้ คีย์ API ของเราจึงต้องซ่อนอยู่ที่ไหนสักแห่งที่ปลอดภัย ฮาร์ดโค้ดในส่วนขยายของเรานั้นเป็นสิ่งที่ไม่ควรทำ เว้นแต่ว่าเราต้องการจ่ายเงินจำนวนหลายล้านดอลลาร์ให้กับ OpenAI เพราะสคริปต์ที่เบื่อหน่ายบางตัวตัดสินใจขูดคีย์ของเรา
  • ความเป็นส่วนตัวของผู้ใช้ —ชื่อแท็บและ URL สามารถเปิดเผยสิ่งที่ละเอียดอ่อนได้ เช่น เอกสารส่วนตัว ลิงก์ รหัสเซสชัน และข้อมูลจำนวนมากเกี่ยวกับบุคคล เราต้องการให้ผู้ใช้สามารถเชื่อถือส่วนขยายได้ ดังนั้นเราจึงต้องการเปิดแหล่งที่มา ให้สร้างและปรับใช้จากแหล่งที่มานั้น และทำให้ง่ายต่อการปรับใช้สำหรับผู้อื่น
  • อัปเดตได้ง่าย —เนื่องจาก LLM อาจไม่แน่นอนกับการตอบสนอง และ OpenAI API อาจทำให้เกิดต้นทุนการใช้งานที่สูงลิ่วเนื่องจากความผิดพลาดง่ายๆ เราจึงต้องการควบคุมการอัปเดต แทนที่จะปล่อยให้ผู้ใช้ดำเนินการตามใจชอบ นั่นหมายความว่ารหัสที่สำคัญที่สุดของเราไม่สามารถอยู่ในส่วนขยายได้

เราจะแก้ไขปัญหาเหล่านั้นได้อย่างไร?

เราจะใช้เส้นทางง่ายๆ — แทนที่จะเขียนตรรกะทั้งหมดในส่วนขยาย เราจะซ่อนมันไว้เบื้องหลัง API — เราจะสร้างบริการแบ็กเอนด์แบบง่ายๆ ที่จะรับข้อมูลแท็บจากส่วนขยาย ตามคำแนะนำของเรา สื่อสารกับ API ของ OpenAI และลดข้อมูลกลับเป็นการตอบสนองเดียว สิ่งนี้ช่วยให้เราสามารถรักษาความปลอดภัยคีย์ของเรา ควบคุมการอัปเดตของเรา และโอเพ่นซอร์สส่วนขยายโดยไม่ต้องให้โทเค็นลับของเราออกไป

ในการดำเนินการนี้ เราจะใช้ Rust โดยมี Axum เป็นเฟรมเวิร์กแบ็กเอนด์ของเรา, Shuttle เป็นแพลตฟอร์มการปรับใช้ของเรา และ GitHub Actions เป็น CI ของเรา

ดังนั้น ก่อนที่เราจะเริ่มเขียนโค้ด เรามาสเก็ตช์ Napkin เพื่อดูภาพรวมของสิ่งที่เรากำลังสร้างกันดีกว่า:

ขั้นตอนที่ 1: สร้างส่วนขยาย

ส่วนขยาย Chromium นั้นค่อนข้างง่ายในการสร้าง — โดยพื้นฐานแล้วเป็นเพียงหน้าเว็บเล็กๆ ที่อยู่ในเบราว์เซอร์ของคุณและ (ด้วยการอนุญาตที่เหมาะสม) จะได้รับสิทธิ์ในการเข้าถึงเบราว์เซอร์ของคุณโดยใช้ API ของเบราว์เซอร์ของคุณ

เราจะใช้ "Chrome API" ซึ่งเป็น API ที่ Google Chrome ใช้ และเบราว์เซอร์ตามโปรเจ็กต์ "Chromium" จำนวนมากที่เปิดเผย (เช่น "Brave" ที่ฉันใช้ และแม้แต่ Edge ด้วย เนมสเปซที่แตกต่างกัน)

เบราว์เซอร์อื่นๆ เช่น Firefox และ Safari ไม่ได้สร้างมาจากโปรเจ็กต์ Chromium แต่มีส่วนขยาย API ที่ค่อนข้างคล้ายกัน หากคุณต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับความแตกต่างระหว่างทั้งสอง ฉันขอแนะนำบทความ MDN นี้

โดยเฉพาะอย่างยิ่ง เราจะมุ่งเน้นไปที่ API ทั้งสองนี้:

  • chrome.tabs - ช่วยให้เราสามารถสืบค้นแท็บที่ผู้ใช้ของเราเปิดอยู่ในปัจจุบัน
  • chrome.tabGroups - ช่วยให้เราสามารถสืบค้นกลุ่มที่มีอยู่ สร้างกลุ่มใหม่ และย้ายแท็บภายในกลุ่มเหล่านั้น

มาดูส่วนของอาคารกันดีกว่า

ในการบูตส่วนขยายของเรา เราจะใช้ Chrome extension CLI — มันจะสร้างโครงสร้างโปรเจ็กต์เริ่มต้นที่เราต้องการ

ดังนั้นกดเทอร์มินัลด้วย:

npm install -g chrome-extension-cli
chrome-extension-cli bookie-js
cd bookie-js

ทำตามคำแนะนำในตอนท้ายและโหลดโฟลเดอร์บิลด์เป็นส่วนขยาย ซึ่งจะช่วยให้คุณโหลดและทดสอบส่วนขยายของคุณผ่านการรีโหลดแบบ hot reload ดังนั้นการเปลี่ยนแปลงทุกอย่างจะมองเห็นได้ทันที

ทีนี้ ลองดูภายในโครงสร้างที่สร้างขึ้น — ส่วนใหญ่อธิบายได้ในตัว

├── README.md
├── config
│   ├── paths.js
│   ├── webpack.common.js
│   └── webpack.config.js
├── node_modules
├── package-lock.json
├── package.json
├── pbcopy
├── public
│   ├── icons
│   ├── manifest.json
│   └── popup.html
└── src
    ├── background.js
    ├── contentScript.js
    ├── popup.css
    └── popup.js

ตอนนี้เราสนใจไฟล์เพียงสามไฟล์เป็นส่วนใหญ่:

public/manifest.json ไฟล์ Manifest คือไฟล์ JSON ที่ให้ข้อมูลเกี่ยวกับส่วนขยายของคุณแก่เบราว์เซอร์ เช่น ชื่อ ความสามารถของส่วนขยาย วิธีการเริ่มต้น ไฟล์ที่จะแสดง สคริปต์ที่จะเรียกใช้บนหน้าเว็บ และ อื่นๆ อีกมากมาย ฟิลด์บางส่วนที่ควรทราบสำหรับเรา:

  • default_popup - ไฟล์ HTML ที่จะแสดงเมื่อมีการคลิกไอคอนส่วนขยาย
  • permissions - เราต้องการให้พวกเขาเข้าถึงบางส่วนของ Chrome API
  • host_permissions - ชุดรูปแบบ URL ที่ส่วนขยายของคุณสามารถเข้าถึงได้

สำหรับตอนนี้ เราจะทิ้งทุกอย่างไว้เหมือนเดิมแล้วกลับมาใหม่ในภายหลัง

src/popup.html — จุดเริ่มต้นของ UI ของเรา HTML นี้จะปรากฏขึ้นเมื่อเราคลิกปุ่มส่วนขยายในเบราว์เซอร์ ดังนั้นเราจะใช้มันเพื่อสร้างอินเทอร์เฟซที่เรียบง่ายที่นี่

เราจะมีปุ่ม 'เรียงลำดับ' ที่เรียกจุดสิ้นสุด /sort ของ API ของเรา และส่งคืนผลลัพธ์ แถบการโหลด และกล่องข้อผิดพลาดแบบง่ายในกรณีที่มีสิ่งผิดปกติเกิดขึ้น
สำหรับการแก้ไขจุดบกพร่อง เรายังสามารถมี "แสดง" ปุ่ม” ที่จะแสดงรายการแท็บทั้งหมดของเรา เรามาเขียน HTML ง่ายๆ กัน:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Bookie JS</title>
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <div class="app">
      <div class="button-container">
        <!-- This will call our API -->
      <button id="sortBtn" class="button">Sort my mess</button>
      <div id="loading" class="loading"></div>
      <div id="error" class="error"></div>
    </div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

src/popup.jsนี่คือที่ที่ JS ของเราจะอาศัยอยู่ เราจะไม่ใช้เฟรมเวิร์ก JavaScript SSSR ของ CRISPR ที่ป้องกันกระสุนทางไซเบอร์ที่หรูหราใดๆ เลย มันจะเป็น "Vanilla JS" ธรรมดาๆ ของเรา ในการอัปเดต UI เราจะใช้ฟังก์ชัน render(state) แบบธรรมดาที่จัดการองค์ประกอบ DOM โดยใช้ฟังก์ชัน show และ hide แบบง่ายบางฟังก์ชัน (โดยการเปลี่ยน element.style.display เป็น block/none)

ตอนนี้ เรามาเขียนกระบวนการคิดของเราโดยการเขียนลงในฟังก์ชัน:

'use strict';
import './popup.css';

(function () {
const SORT_BTN = 'sortBtn';
const LOADING = 'loading';
const ERROR = 'error';
    
// get tabs & groups from the API
async function getTabsAndGroups(){};
// call backend with the data
async function callBackendToSort(tabsAndGroups){};
// apply result to browser
async function applySort(sortedCategories){};
//runs our app   
async function run(){
//get tabs
let tabsAndGroups = await getTabsAndGroups();
render({loading: false, error: null}
let btn = document.getElementById('sortBtn')
//on click, call the API, show loading and apply the results when done 
 btn.addEventListener('click',async ()=> {
     render({loading: true, error: null}
      try {
        let result = await callBackendToSort(tabsAndGroups)
        await applySort(result)
        render({loading: false, error: undefined})
      }catch (e){
        render({loading: false, error: e})
      }
 })
}
//load our run function when the content loads
document.addEventListener('DOMContentLoaded', run);
    
})();

ขั้นตอนแรกของเราจะค้นหา Chrome API สำหรับแท็บและกลุ่ม ดังที่เราเห็นในเอกสาร เราสามารถใช้ chrome.tabs.query เพื่อบรรลุเป้าหมายนี้ได้

เรามาลองดูกัน:

async function getTabsAndGroups() {
    let chromeTabs = await chrome.tabs.query({})
    console.log(chromeTabs)
  }

ไม่ทำงาน? ตอนนี้จำไฟล์ public/manifest.json นั้นได้ไหม? และวัตถุ permissions?

ในการเข้าถึงแท็บ ชื่อ และกลุ่ม เราจะต้องเพิ่มการอนุญาตที่ตรงกัน ดังนั้นให้เปิด manifest.json และต่ำกว่า permissions เพิ่ม "tabs", "tabGroups" ขณะนี้ขณะติดตั้ง Chrome สามารถตรวจสอบสิทธิ์ส่วนขยายของคุณและแจ้งให้ผู้ใช้ทราบว่าคุณกำลังเข้าถึงอะไรอยู่
แต่เพื่อให้สามารถเข้าถึง API ของแท็บได้ เราจะต้องได้รับอนุญาตพิเศษอีกหนึ่งรายการที่เรียกว่า host-permissions

โดยจะแจ้งให้ผู้ใช้ทราบว่าส่วนขยายนั้นเปิดใช้งานให้ทำงานบนเว็บไซต์ใด ดังนั้นหากเราต้องการให้สามารถใช้งานได้ในทุกแท็บ เราจะต้องเพิ่มรูปแบบ URL ที่ถูกต้อง ดังนั้นให้เพิ่มคุณสมบัติใหม่ให้กับ manifest.json ที่เรียกว่า host-permissions โดยมีรูปแบบที่อนุญาตให้ตรงกับ URL ทั้งหมด เช่น "host_permissions": ["*://*/*"] ในที่สุด ตอนนี้เราสามารถเข้าถึงแท็บและกลุ่มของผู้ใช้ทั้งหมดได้แล้ว

ตอนนี้ใช้งานได้แล้ว ข้อมูลที่เมธอด chrome.tabs.query ส่งกลับจะมีบางสิ่งที่เราต้องการ: id, title และ groupId เราจะใช้ id และ title สำหรับการเรียงลำดับ และ groupId เพื่อค้นหากลุ่มที่มีอยู่ ดังนั้นก่อนอื่น เราจะแมปออบเจ็กต์ที่ส่งคืนกับเวอร์ชันที่เรียบง่ายของวัตถุนั้น โดยใช้เฉพาะคุณสมบัติที่เราต้องการเท่านั้น

หากต้องการข้อมูลเพิ่มเติมเกี่ยวกับกลุ่ม เราจะสร้างฟังก์ชัน tabsForGroups ซึ่งจะค้นหากลุ่มที่ไม่ซ้ำกันทั้งหมดและค้นหา Chrome API โดยใช้ chrome.tabGroups.get(id) เพื่อรับชื่อของแต่ละกลุ่ม

async function tabsToGroups(tabs){
  //get all existing groupIds from tabs
  let groupIds = tabs
      .map( (it)=>it.groupId)
      .filter((it)=>it!==null && it!==undefined && it!==-1);
  
  //push them into a set to get unique ones
  let groups = new Set(groupIds)
//query chrome API for data about each tab group
  return await Promise.all([...groups]
      .map(async (it) => {
      let item = await chrome.tabGroups.get(it)
        return {
          id: item.id,
          title: item.title
        }
    }));
  }
// now our function can return us all of our tabs and groups
async function getTabsAndGroups() {
    let chromeTabs = await chrome.tabs.query({})
    let tabs = await mapTabs(chromeTabs)
    let tabsWithGroups = await tabsToGroups(tabs)
    let groups =  tabsWithGroups.filter((it)=>it.title.length !== 0);
    return {
      items: tabs,
      categories: groups
    }
  }

บูม เพียงไม่กี่ขั้นตอนง่ายๆ เราก็มีรายการกลุ่มและแท็บที่มีอยู่แล้ว

ฟังก์ชั่นการเรียก API นั้นค่อนข้างง่ายเช่นกัน เนื่องจาก API ของเรายังไม่มีอยู่ เราจะเขียนคำขอ POST ทั่วไปไปยัง localhost:

async function callBackendToSort(data){    
 return await fetch('http://127.0.0.1:8000/sort',{
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        items: data.items,
        categories: data.categories
      })
    })
}

ฟังก์ชันการเรนเดอร์ของเราก็ค่อนข้างง่ายเช่นกัน — เราแค่ตรวจสอบสถานะและเปลี่ยน UI ของเราตามนั้น

function render(state){
    if(state.loading){
      show(LOADING)
      hide(SORT_BTN)
      hide(ERROR)
    }else{
      hide(LOADING)
      show(SORT_BTN,true)
    }
    if(state.loading!==true &&
      (state.error!==undefined && state.error!=null)){
      show(ERROR)
      showError(state.error)
    }else
      hide(ERROR)
}

สิ่งที่ต้องทำตอนนี้คือใช้ฟังก์ชัน applySort ซึ่งจะนำหมวดหมู่ใหม่ของเราไปใช้กับเบราว์เซอร์

แนวคิดคือ:

  • ตรวจสอบว่ามีกลุ่มอยู่หรือไม่
  • หากไม่เป็นเช่นนั้น ให้สร้างมันขึ้นมา
  • อัปเดตรายการแท็บและชื่อเรื่อง

สำหรับสิ่งนี้ เรามีการวิจัย API เล็กน้อยที่ต้องทำ — เอกสารที่ครอบคลุมส่วนนี้ค่อนข้างน่าสับสนเล็กน้อย คุณคาดหวังว่าจะมีบางอย่างเช่น
chrome.tabGroups.create หรือ chrome.tabGroups.update ซึ่งจะเปลี่ยนแท็บในกลุ่ม แต่... นั่นเป็นความคิดที่ไร้เดียงสา

ในการสร้างกลุ่ม เราใช้การเรียก API chrome.tabs.group โดย NOT ผ่าน chrome.tabs.group a groupId จากนั้นกลุ่มจะถูกสร้างขึ้นและ groupId ใหม่จะส่งคืนให้คุณ นี่เป็นการโทรที่แปลกประหลาดโดยทีมงาน Chrome หากกลุ่มเป็นเพียงคอนเทนเนอร์ของแท็บ ทำไมแท็บจึงมีความรู้และควบคุมแท็บเหล่านั้นได้

ไม่ควรสร้างและจัดการกลุ่มผ่าน API ของกลุ่มใช่ไหม

โอ้ นอกจากนี้ หากคุณต้องการเพิ่มแท็บในกลุ่ม คุณใช้การโทรเดียวกันและส่งผ่านอาร์เรย์ของแท็บผ่าน tabIds 'เฮ้ ฉันสามารถส่งต่อชื่อได้เช่นกันเนื่องจากเรากำลังสร้างและอัปเดตออบเจ็กต์ผ่านการเรียก API นี้แล้ว' ไม่ คุณจะต้องใช้การเรียก chrome.tabGroups.update API

ฉันคิดว่าไวยากรณ์แปลก ๆ นี้เป็นเพราะกลุ่มเป็นส่วนเสริมในภายหลังใน Chrome ดังนั้นจึงมีการปรับปรุงการสนับสนุนในแท็บ API เอง ลองทดสอบสมมติฐานนั้นดู เมื่อดูที่ "ความมุ่งมั่น" ที่เพิ่มกลุ่มลงใน Tabs API เราจะพบการสนทนาแบบเดียวกันในความคิดเห็น ซึ่งนำเราไปสู่ ​​"ข้อเสนอกลุ่มแท็บ API" ดูเหมือนว่าทีมงานตัดสินใจแบ่งความรับผิดชอบระหว่างการจัดการแท็บและการจัดการกลุ่ม เนื่องจากการย้ายแท็บถือเป็นการจัดการแท็บ ความรับผิดชอบจึงอยู่ใน Tabs API

มีการพูดคุยถึงข้อเสนอทางเลือกอื่นด้วย (วางความรับผิดชอบนั้นไว้ใน TabGroups API) พร้อมด้วยข้อดีและข้อเสีย:

จากมุมมองของฉัน (ในฐานะผู้ใช้ API) รายการข้อเสียไม่ได้ดูแย่ขนาดนั้น แท็บไม่จำเป็นต้องทราบเกี่ยวกับกลุ่ม ความปลอดภัยของผู้ใช้จะเพิ่มขึ้น (ส่วนขยายต้องการเพียง tabGroups การอนุญาต ลดพื้นที่ที่ส่วนขยายอาจนำไปใช้ในทางที่ผิด) และจะ ซ่อนรายละเอียดการใช้งาน โดยแทนที่ด้วย API ที่ใช้งานง่าย ซึ่งเป็นสิ่งที่เป็นนามธรรมทั้งหมด การตัดสินใจที่แปลกอย่างไรก็ตาม

แต่พอพูดถึงสปาเก็ตตี้แล้ว มาเขียนกันต่อดีกว่า

function applySort(sortedCategories){
/* The response object we want looks like: 
{ categories: [
	{ category_id: int, category_title: string, items: [int] }
    ] }
*/
for (i = 0; i < sortedCategories.categories.length; i++) {
     let category = sortedCategories.categories[i]
     let categoryId = category.category_id
     //check if the group with ID exists
     let groupExists = await chrome.tabGroups.get(categoryId)
     					.catch((e)=>undefined);
      let groupId;
      if(groupExists === undefined)
         //if it doesnt, the chrome.tabs.group returns us an ID
         groupId = await chrome.tabs.group({ tabIds: category.items });
      else {
          
        //if it does, we use the existing one
       	groupId = groupExists.id
        await chrome.tabs.group({groupId: groupId,
                                tabIds: category.items});
      }
// Set the title of all groups and collapse them
      await chrome.tabGroups.update(groupId, {
        collapsed: true,
        title: category.title
      });
})
}

ด้วยเหตุนี้ MVP ส่วนขยาย JS ของเราจึงเสร็จสิ้น

  • เรารวบรวมแท็บและกลุ่ม
  • เราส่งพวกเขาไปที่ API
  • เราใช้การเรียงลำดับที่ส่งคืน

ตอนนี้เรายังไม่มี API แล้วเราจะทดสอบมันได้อย่างไร?

เราควรจดการทดสอบหน่วยไว้บ้าง แต่เอาไว้วันอื่นดีกว่า (ไม่หรอก — เราจะพิจารณาการทดสอบส่วนขยายของ Chrome ด้วย Jest ในบางโพสต์)

ในตอนนี้ เราสามารถปลอมการส่งคืนฟังก์ชัน callBackendToSort เพื่อรวมบางหมวดหมู่และรหัสแท็บบางส่วน - บางอย่างเช่นนี้ (แต่ด้วยรหัสแท็บของคุณ):

{
	"categories": [{
		"category_id": 837293848,
		"category_name": "Hacker News",
		"items": [1322973609, 1322973620]
	}, {
		"category_id": 837293850,
		"category_name": "Science",
		"items": [1322973618, 1322973617, 1322973608]
	}, {
		"category_id": 837293851,
		"category_name": "GitHub",
		"items": [1322973619]
	}, {
		"category_id": 837293852,
		"category_name": "Web Development",
		"items": [1322973612, 1322973613, 1322973615, 1322973616]
	}, {
		"category_id": 837293853,
		"category_name": "Web APIs",
		"items": [1322973646]
	}]
}

ตอนนี้เราไปยังส่วนที่สนุกสนานได้แล้ว — การสร้าง API, การเพิ่มประสิทธิภาพโดยทันที, การหมดเวลาของ GPT และแก้ไขข้อผิดพลาดที่เราจะทำในวันข้างหน้าในอนาคต

โอ้ และเรายังจะเพิ่มความซับซ้อนและฟีเจอร์ครีปให้มากขึ้น แต่จะเพิ่มเติมในภายหลัง

ตอนนี้ เราจะมาเจาะลึกภาษาที่ร้อนแรงที่สุดในกลุ่มตอนนี้ — Rust

ฉันไม่คิดว่าจะต้องอธิบายว่า Rust คืออะไร แม้ว่าคุณจะอาศัยอยู่ใต้ก้อนหิน แต่คุณคงเคยได้ยินชื่อ Rust มาก่อน — ชุมชนการพัฒนาต่างยกย่องมันจนสูงส่ง — มันมีความเร็วที่ C, ความปลอดภัยของ Java และระบบการยืมพร้อมทักษะการเลี้ยงดูด้วยเฮลิคอปเตอร์ของ เฮลิคอปเตอร์โจมตีอาปาเช่ เอเอช-64

แต่ — ไวยากรณ์นั้นเรียบร้อย ประสิทธิภาพก็ยอดเยี่ยม มาโครก็เจ๋ง และถึงแม้ว่าส่วนใหญ่จะเข้มงวดเกี่ยวกับหน่วยความจำ แต่ก็ยังให้คุณเข้าถึงพอยน์เตอร์ดิบและให้คุณไป !unsafe

ดังนั้นเพื่อให้เข้าใจถึงภาษานั้น เรามาลองสนุกไปกับมันกันดีกว่า

เราจะสร้างบริการง่ายๆ ที่จะใช้คอลเลกชันแท็บของเรา ลดความซับซ้อนลงเล็กน้อย พูดคุยกับ API ของ OpenAI และหวังว่าจะไม่มีอาการประสาทหลอน แยกวิเคราะห์การตอบสนองเป็นสิ่งที่ส่วนขยายของเราสามารถใช้ได้

ระหว่างทาง เราจะพบกับอุปสรรคบางประการ ตั้งแต่การมีแท็บมากเกินไปและการเสียเงินไปจนถึง Silicon Valley ที่ตื่นขึ้นและทุบ OpenAI API ให้ถูกลืมเลือน

บริการของเราจะค่อนข้างเรียบง่าย — เราจะเปิดเผยวิธีการหนึ่ง /sort ซึ่งเราจะ POST แท็บและหมวดหมู่ที่มีอยู่ของเรา ในการสร้างมันขึ้นมา เราจะหันไปพึ่ง "กรอบงาน Axum" ซึ่งช่วยให้เราสามารถเริ่มต้นเซิร์ฟเวอร์ด้วยจุดสิ้นสุด /sort ได้อย่างง่ายดาย และในการปรับใช้ เราจะใช้ รถรับส่ง เพื่อให้เราสามารถหมุนเซิร์ฟเวอร์ Rust ได้อย่างง่ายดายโดยไม่ต้องผ่านการกำหนดค่า AWS มากมาย เขียนโปรไฟล์ หรือสร้างอิมเมจนักเทียบท่า

เราจะใช้มันเพื่อนั่งร้านโปรเจ็กต์ของเราด้วยซ้ำ ดังนั้นมาเริ่มด้วยการติดตั้งกันเลย
ก่อนอื่น เราจะต้องมีคาร์โก้ ตัวจัดการแพ็คเกจสนิม — หากคุณยังไม่ได้ติดตั้ง ให้ทำตามขั้นตอน ที่นี่ . ประการที่สอง เราจำเป็นต้องมีบัญชี รถรับส่ง ไม่ต้องกังวล คุณสามารถสมัครใช้งาน Github ได้ในคลิกเดียว ไม่จำเป็นต้องกรอกแบบฟอร์ม

ตอนนี้ ให้เปิดเทอร์มินัล ol’ แล้วกด cargo install cargo-shuttle && cargo shuttle login ตามด้วย cargo shuttle init หลังจากที่คุณตรวจสอบสิทธิ์แล้ว

ทำตามคำแนะนำเพื่อตั้งชื่อโปรเจ็กต์และที่ตั้ง และในเมนูให้เลือก axum เป็นเฟรมเวิร์กของคุณ สิ่งนี้จะสนับสนุนโปรเจ็กต์ axum ใหม่ให้เป็นห้องสมุด โดยมีรถรับส่งเป็นตัวอ้างอิง

โฟลเดอร์ของเราตอนนี้ควรมีลักษณะเช่นนี้

├── Cargo.lock
├── Cargo.toml
└── src
    └── lib.rs

โครงสร้างค่อนข้างเรียบง่าย — เรามี cargo.toml ซึ่งเป็นเวอร์ชันสนิมของ manifest.json หรือ package.json ประกอบด้วยข้อมูลเมตาเกี่ยวกับแพ็คเกจของคุณ การขึ้นต่อกัน คุณสมบัติการคอมไพล์ และอื่นๆ cargo.lock เป็นเพียงรายการการขึ้นต่อกันแบบฮาร์ดโค้ดที่ระบุ เพื่อให้มั่นใจว่ามีการสร้างที่สอดคล้องกันในสภาพแวดล้อมต่างๆ

รหัสเซิร์ฟเวอร์หลักของเราจะอยู่ภายใน src/lib.rs มาดูตอนที่ยังสดและสวยงามกันดีกว่า:

use axum::{routing::get, Router};
use sync_wrapper::SyncWrapper;
async fn hello_world() -> &'static str {
    "Hello, world!"
}
#[shuttle_service::main]
async fn axum() -> shuttle_service::ShuttleAxum {
    let router = Router::new().route("/hello", get(hello_world));
    let sync_wrapper = SyncWrapper::new(router);
    Ok(sync_wrapper)
}

บางสิ่งที่ควรทราบที่นี่:

  • ไม่มีเมธอด main เนื่องจากโปรเจ็กต์เหล่านี้ถูกทำเครื่องหมายเป็น [lib]rary จึงไม่จำเป็นต้องมีจุดเริ่มต้นที่กำหนดไว้ล่วงหน้า
  • router — 'จุดเริ่มต้น' สำหรับบริการ Axum ของคุณ คำขอจะถูกส่งผ่านที่นี่และโค้ดอธิบายได้ในตัวมันเอง - คุณจับคู่เส้นทางกับฟังก์ชันที่จัดการมัน เช่น supercoolservice.com/hello ของเราจะส่งกลับข้อความ 'Hello, world!' แบบง่ายๆ ข้อความ.
  • SyncWrapper — ล้อมรอบอ็อบเจ็กต์เราเตอร์ของเรา เพื่อให้มั่นใจว่าจะปลอดภัยในการเข้าถึงผ่านเธรดต่างๆ
  • #[shuttle_service::main] - นี่คือมาโครสนิม - ให้คิดว่ามันเป็นคำอธิบายประกอบเวอร์ชันที่ทรงพลังกว่านี้ หากคุณรู้ว่ามันคืออะไร มันช่วยให้คุณเขียนโค้ดที่เขียนโค้ด - แต่นั่นเป็นคำอธิบายที่ขี้เกียจ เอ่อ.. ฉันคิดว่าเราจำเป็นต้องเปลี่ยนทางด่วนที่นี่

การเปลี่ยนเส้นทางอย่างรวดเร็วสู่อาณาจักรแห่งมาโครอันมหัศจรรย์

ตอนนี้ ก่อนที่เราจะพูดถึงมาโคร ฉันต้องขึ้นต้นด้วยคำเตือน: นี่ไม่ใช่คำอธิบาย 100% ของมาโครและวิธีการทำงานของมาโคร [ใส่ภาษาโปรดของคุณที่นี่] ด้วยเหตุนี้จึงมีหนังสือ คู่มือ และบทความหลายร้อยเล่ม

แต่สำหรับผู้อ่านสุ่มๆ ที่เข้ามาที่นี่และไม่ต้องการอ่านบทความสไตล์ “monad is a monoid in the category of endofunctors” ที่อธิบายแมโคร เราจะมาเจาะลึกเข้าไปในโพรงกระต่ายที่สวยงามกัน ของมาโคร

ลองจินตนาการว่าเรากำลังทำงานในภาษาในจินตนาการที่เรียกว่า Bust

Bust เป็นภาษาใหม่สุดเจ๋งที่ Twitter กำลังพูดถึง และพวกเขาบอกว่ามันจะเป็นภาษาของแอป metaverse AI web4 แต่เนื่องจากเป็นภาษาใหม่ มันยังเร็วเกินไปและมีไลบรารีไม่มากนัก ตัวอย่างเช่น ยังไม่มีไลบรารีซีเรียลไลเซชัน JSON ดังนั้นคุณต้องเขียนโค้ดทั้งหมดสำหรับภาษานั้นด้วยตนเอง ดังนั้นทุกครั้งที่คุณสร้างโครงสร้าง คุณจะต้องเขียนรหัสซีเรียลไลซ์จำนวนมากด้วย

ชอบ:

struct ReallyBigModel {
   id: String,
   name: String,
   isReal: Bool,
   ...
   stuff: AnotherBigModel
   }

impl ToJson for ReallyBigModel {
    fn toJson() -> String {
          return mapOf { "id" to id, 
                "name" to name,
                "isReal" to isReal,
                ..., 
                "stuff" to stuff.toJson())
              }.toJson() 
        }
}

น่ารำคาญใช่ไหม?

ไม่มีใครอยากเขียนบทความสำเร็จรูปมากขนาดนี้ทุกวัน

แต่วันหนึ่ง คุณอ่านบันทึกการเปลี่ยนแปลงล่าสุดว่าตอนนี้รองรับสิ่งใหม่ที่เรียกว่ามาโครแล้ว มาโครมีหลายประเภท แต่ใน Bust Macros เป็นวิธีพิเศษที่คุณสามารถกำหนดได้ซึ่งประกอบด้วยสองสิ่ง:

  • แอตทริบิวต์แมโคร
  • ฟังก์ชันมาโคร

attribute เปรียบเสมือนเครื่องหมายที่คุณสามารถนำไปใส่โค้ดอื่นได้

ลองนึกภาพว่ามันเป็น X สีแดงขนาดใหญ่เหนือคลาสหรือเมธอด ดังนั้นเมื่อคอมไพเลอร์ของคุณทำการคอมไพล์ ถ้ามันสะดุดกับฟังก์ชันที่มีเครื่องหมาย X สีแดงขนาดใหญ่อยู่เหนือหัว มันก็รู้ว่ามันควรจะเรียกใช้ฟังก์ชันมาโครของคุณ

macro function ได้รับโค้ดที่มีเครื่องหมาย attribute ตัดสินใจว่าจะทำอย่างไรกับโค้ดนั้น จากนั้นส่งคืนโค้ดใหม่ไปยังคอมไพเลอร์ จากนั้นมันจะรวมกลับในตำแหน่งที่มีฟังก์ชันที่ทำเครื่องหมายไว้

ดังนั้น หากในตัวอย่างของเรา เราสร้างมาโคร toJson เราสามารถเพิ่มแอตทริบิวต์ toJson ไว้เหนือโครงสร้างใดก็ได้ และมันจะเขียนโค้ดนั้นให้เรา ดังนั้นโค้ดด้านบนจึงจะกลายเป็น:

#[toJson]
struct ReallyBigModel {
   id: String,
   name: String,
   isReal: Bool,
   ...
   stuff: AnotherBigModel
}

แล้วมาโครของเราจะเป็นอย่างไร?

มันจะเป็นฟังก์ชันที่รับโค้ดที่ทำเครื่องหมายไว้ (แสดงเป็นโทเค็น) และส่งคืนโค้ดใหม่ที่จะแทนที่

#[toJson]
#[toJson] fn addToJsonTrait(input: TokenStream) -> TokenStream { 
  let tree = parseIntoAST(input) 
  let nodes = ast.data.asStruct();
  let name = tree.identity
   // Get all the children that are properties
   // Map them into format: $name to name 
  let properties = nodes
    .filter((child)=>child.isProperty)
    .map((property) => "\"${property.name}\" to ${property.name}")
    .joinToString(",\n") 
  // Write the toJson trait body
  let body = quote! { //this is also a kind of macro!
     impl ToJson for #name { 
      fn toJson() -> String { mapOf { properties }.toJson()}; 
    }
   }
   return body.intoTree().intoStream() 
}

หมายเหตุ: นี่คือ Bust ซึ่งเป็นภาษาในจินตนาการ ทุกภาษามีการใช้งานมาโครของตัวเอง และนี่เป็นเพียงการนำเสนออย่างง่ายของภาษาหนึ่ง ดังนั้นบทความจะได้ไม่ยาวจนเกินไป

ดังนั้นเมื่อคอมไพเลอร์ของเรามาถึงคลาสที่มีเครื่องหมาย #[toJson] มันจะเรียกใช้เมธอด addToJsonTrait ส่งโค้ดให้กับคลาสและรอจนกว่าจะส่งคืนโค้ดใหม่ก่อนที่จะทำการคอมไพล์ต่อ

และเช่นเดียวกัน เราประหยัดเวลาได้มากโดยใช้ฟังก์ชันมาโคร และตอนนี้ก็สามารถเป็นนักพัฒนา Bust ที่มีประสิทธิผลอย่างที่เราอยากเป็นมาโดยตลอด!

ตอนนี้ อย่าเพิ่งตื่นเต้นเกินไป นี่เป็นเพียงการใช้งานในจินตนาการ
มีเรื่องน่ารู้มากมายเกี่ยวกับมาโคร และฉันขอแนะนำให้คุณเจาะลึกเข้าไปในโพรงกระต่าย — สนิมนั้นเอง มีประเภทของมาโครที่แตกต่างกันสองสามประเภท มาโคร เป็นหนึ่งใน "เหตุผล" ที่ผู้คนชื่นชอบ "Lisp" มาก มี "มาโครที่ถูกสุขลักษณะและไม่ถูกสุขลักษณะ" ส่วนขยายประเภทต่างๆ และมีเวทมนตร์อีกมากมายซ่อนอยู่ในส่วนลึก

ตอนนี้เราเข้าใจเรื่องนั้นแล้ว เรามาเริ่มสร้าง API ของเรากันดีกว่า

ไปรษณีย์

เราจะซ่อนความมหัศจรรย์อันเรียบง่ายของบริการของเราไว้เบื้องหลังวิธี /sort POST ดังนั้นให้ลบสวัสดีชาวโลกนั้นออกและแทนที่เราเตอร์ด้วยวิธีหนึ่งที่จัดการคำขอ /sort - Router::new().route("/sort", post(sort_items)) และวิธี sort_items ที่จะจัดการคำขอ:

async fn sort_items(Json(payload): Json<SortRequestPayload>)
                                       -> impl IntoResponse {
 (StatusCode::OK, Json("ok")).into_response()
}

เมธอดนี้จะได้รับ Json wrapper ของโครงสร้างคำขอของเรา และจะส่งคืนการใช้งานลักษณะ IntoResponse ซึ่งเซิร์ฟเวอร์ของเรารู้วิธีจัดการ

โดยเฉพาะอย่างยิ่ง เราจะส่งคืนในรูปแบบทูเพิล StatusCode,T ซึ่งเซิร์ฟเวอร์รู้วิธีแปลงเป็นการตอบกลับที่เหมาะสม

อีกสิ่งหนึ่งที่เราต้องดำเนินการคือโครงสร้างข้อมูลคำขอของเรา ดังนั้นแทนที่จะให้พวกมันอยู่ในไฟล์เดียวกัน เรามาเปิดไฟล์ใหม่ชื่อ models.rs ในโฟลเดอร์ src กันดีกว่า แล้วสร้างคำจำกัดความพื้นฐานขึ้นมา

เราจะต้องมี SortRequestPayload ซึ่งเป็นกระดาษห่อที่เราจะได้รับ ควรมีรายการหมวดหมู่และรายการต่างๆ ดังนั้นเราจึงต้องมีโครงสร้างสำหรับหมวดหมู่เหล่านั้นด้วย - Category และ Item ดังนั้นมาเพิ่มสิ่งเหล่านั้นด้วย

และเราจะต้องมีรายการหมวดหมู่ กับ รายการ เพื่อให้เราสามารถมีหมวดหมู่ที่มีรายการที่เป็นของที่จะส่งคืนและ wrapper สำหรับรายการเหล่านั้น นอกจากนี้ เราจะเพิ่ม ErrorResponse เพื่อให้เรารู้ว่าปัญหาอยู่ที่ไหน

//in models.rs
pub(crate) struct SortRequestPayload {
    pub(crate) categories: Vec<Category>,
    pub(crate) items: Vec<Item>,
}

pub(crate) struct Category {
    pub(crate) id: usize,
    pub(crate) title: String,
}

pub(crate) struct Item {
    pub(crate) id: usize,
    pub(crate) title: String,
}

pub(crate) struct CategoryWithItems {
    pub category_id: usize,
    pub category_name: String,
    pub items: Vec<usize>
}

pub(crate) struct Categories {
    pub categories: Vec<CategoryWithItems>
}

pub(crate) struct ErrorResponse {
    pub message: String,
}

แต่เรามีปัญหาหนึ่ง — เราต้องการให้โครงสร้างของเราสามารถ (ยกเลิก) อนุกรมจาก/เข้าสู่ JSON ได้อย่างง่ายดาย — เพื่อสิ่งนั้น เราจะใช้ไลบรารีชื่อ Serde และใช้มาโครของมัน (คล้ายกับมาโครที่เราสร้างก่อนหน้านี้) เพื่อให้เปิดได้ เพิ่มไฟล์ cargo.toml ของคุณและเพิ่ม serde และ serde_json เป็นการขึ้นต่อกัน:

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

ตอนนี้ เราสามารถทำเครื่องหมายโครงสร้างของเราด้วยมาโคร #[derive(Deserialize)] ของ serde เพื่อให้เฟรมเวิร์กรู้วิธีดีซีเรียลไลซ์ JSON ที่ได้รับลงในโครงสร้างของเรา

//in models.rs
#[derive(Deserialize)]
pub(crate) struct SortRequestPayload {
    pub(crate) categories: Vec<Category>,
    pub(crate) items: Vec<Item>,
}

#[derive(Deserialize)]
pub(crate) struct Category {
    pub(crate) id: usize,
    pub(crate) title: String,
}

#[derive(Deserialize)]
pub(crate) struct Item {
    pub(crate) id: usize,
    pub(crate) title: String,
}

#[derive(Deserialize)]
#[derive(Serialize)]
pub(crate) struct CategoryWithItems {
    pub category_id: usize,
    pub category_name: String,
    pub items: Vec<usize>
}

#[derive(Deserialize)]
#[derive(Serialize)]
pub(crate) struct Categories {
    pub categories: Vec<CategoryWithItems>
}

#[derive(Serialize)]
pub(crate) struct ErrorResponse {
    pub message: String,
}

เมื่อทำสิ่งนี้เสร็จแล้ว เราก็สามารถกลับเข้าสู่โค้ดของเราได้

มาตรวจสอบแผนของเรากัน:

1. Get the items
2. Assign items to categories
3. Slice the prompt into chunks
4. A recursive sort:
    4.1. Take existing categories and a chunk, turn them into a prompt
    4.2. Ask OpenAI to sort it
    4.3. Deserialize the response
    4.4. Add to existing categories
    4.5. While chunks remain, back to 4.1
5. Return the result

และจัดโครงสร้างเป็นวิธีการ:

//in lib.rs
...
fn create_chunks_for_prompting(items: Vec<Item>) -> Vec<Vec<Item>
fn sort_recursively(sorted_categories: Vec<CategoryWithItems>,
                    remaining: Vec<Vec<Item>>) -> Result<Categories, Error>
fn build_prompt(items: Vec<Item>,categories: Vec<CategoryWithItems>) -> String
fn prompt_open_ai(prompt: String) -> Result<String, String>

นอกจากนี้ เราต้องการการแจ้งเตือน ดังนั้นเรามาลองทำสิ่งนี้กัน เราบอก GPT3 ว่าจะได้รับรายการของรายการและกำหนดรูปแบบ จากนั้นจึงฝังรายการ

จากนั้น เราจะอธิบายรูปแบบ JSON ที่ถูกต้องเพื่อส่งคืนและส่งผ่านในหมวดหมู่ที่มีอยู่ ท้ายที่สุดแล้ว เราก็บอกให้ส่งคืนในรูปแบบ JSON ที่ถูกต้อง หวังว่ามันจะเป็นไปตามรูปแบบ JSON และไม่ทำให้เกิดภาพหลอน แต่เราจะปรับแต่งมันในโพสต์ต่อๆ ไป

ในตอนนี้ ดูเหมือนว่าการระบุรูปแบบ JSON ที่ถูกต้องใกล้กับส่วนท้ายของพรอมต์และการกล่าวถึง "รูปแบบ JSON ที่ถูกต้อง" ในตอนท้ายทำให้รูปแบบดังกล่าวมีพื้นฐานค่อนข้างดี

You will receive list of items with titles and id's in form of [title,id].
Based on titles and urls, classify them into categories, 
by using existing categories or making new ones.
Tabs are:
[$tabName, $tabId].
Valid JSON format to return is:
{ "categories": [ { 
    "category_id":"id here",
    "category_name": "name here", 
    "items":[tab_id here] } 
]}.
Existing categories are: 
$categories
A new more detailed list of categories (existing and new) with items, in valid JSON format is:

ฟังดูดีสำหรับฉัน!

มาแบ่งมันเป็นค่าคงที่ที่เราสามารถใช้ภายในโค้ดของเราได้

const PROMPT_TEXT_START: &str = "You will receive list of items with titles and id's in form of [title,id].
Based on titles and urls, classify them into categories, by using existing categories or making new ones.";
const PROMPT_TEXT_MIDDLE: &str = "\nValid JSON format to return is:
{ \"categories\": [ { \"category_id\":\"id here\", \"category_name\": \"name here\", \"items\":[tab_id here] } ]}.
Existing categories are:";
const PROMPT_TEXT_ENDING: &str = "A new more detailed list of categories (existing and new) with tabs, in valid JSON format is:";

ในที่สุด เราก็เข้าสู่วิธี sort_items และเริ่มกรอกข้อมูลทั้งหมดได้ ขั้นแรก เราจะเป็นเจ้าของข้อมูลของเราและแบ่งออกเป็นส่วนๆ:

let items = payload.items;
let categories = payload.categories.iter().map(|it| {
    CategoryWithItems {
        category_id: it.id,
        category_name: it.title.to_owned(),
        items: Vec::new(),
    }
}).collect();
let prompt_slices = create_chunks_for_prompting(items_with_indexes);

ทำไมต้องเป็นชิ้น?

เพราะหากเราเพิ่มรายการทั้งหมดลงในพรอมต์ ขนาดพรอมต์ของเราอาจมีมากกว่า 4,096 โทเค็น ซึ่งเป็นโมเดลที่เราจะใช้รองรับเป็นความยาวสูงสุดสำหรับพรอมต์และความสมบูรณ์

ดังนั้นเราจึงต้องหาทางแยกมันออกเป็นขนาดที่เหมาะสมและมีบัฟเฟอร์สำหรับการทำงานให้เสร็จด้วย เราจะเหลือบัฟเฟอร์ไว้ 50% โดยเหลือขนาดพร้อมท์ไว้ที่ 2048

เพื่อให้บรรลุเป้าหมายดังกล่าว ฟังก์ชัน create_chunks_for_prompting ของเราจะต้องดำเนินการสองสิ่ง:

  • นับจำนวนโทเค็นในพรอมต์ฐานของเรา
  • นับจำนวนโทเค็นในข้อมูลที่เราส่งไปยัง API
  • คำนวณจำนวนชิ้นที่เราต้องการโดยการแบ่งขนาดของโทเค็นทั้งหมดด้วย 2048 ลบด้วยขนาดพร้อมท์ฮาร์ดโค้ดของเรา

ตามเอกสารของ OpenAI เราสามารถคาดเดาได้ว่าโทเค็นมีขนาดประมาณ 4 อักขระ

ขณะนี้ มีหลายวิธีในการนับโทเค็น และเพื่อให้ถูกต้อง เราจะต้องทำอะไรมากกว่าการแบ่งความยาวเป็น 4 เล็กน้อย — วิธีที่ดีที่สุดคือใช้ลัง Rust tokenizers และของพวกเขา โทเค็นไนเซอร์ GPT2

แต่เนื่องจากสิ่งนั้นทำให้เราตกหลุมกระต่ายอีกแห่งหนึ่ง เราจะข้ามมันไป ในตอนนี้ และจะทำเคล็ดลับง่ายๆ — เมธอด split_whitespace ซึ่งจะทำให้เราได้ประมาณความยาวของโทเค็น

fn create_chunks_for_prompting(items: Vec<Item>) -> Vec<Vec<Item>> {
   
  //tokens in our data
  let json_size = serde_json::to_string(&items).unwrap()
      .split_whitespace()
      .collect_vec()
      .len();
  
  // get the size of our hardcoded prompt
  let hardcoded_prompt = format!("{a}{b}{c}",
                                 a =String::from(PROMPT_TEXT),
                                 b = String::from(PROMPT_TEXT_APPEND),
                                 c= String::from(PROMPT_TEXT_ENDING));
  
  let hardcoded_prompt_size = hardcoded_prompt
      .split_whitespace()
      .len();
  //find the number of chunks we should split the items into
  let chunks_to_make = json_size / (2048 - hardcoded_prompt_size);
  
  //split the vector up into N vectors
  let chunk_size = items.chunks(items.len() /
                                    (if chunks_to_make > 0 {
                                    chunks_to_make
                                    } else { 1 }));
                                    
  //return the list of chunks
  return chunk_size.map(|s| s.into()).collect();
  }

ตอนนี้ มาดูฟังก์ชัน build_prompt ของเรากันดีกว่า

เพื่อสร้างพร้อมท์ของเรา เราจำเป็นต้องมีรายการของรายการที่จะเรียงลำดับและหมวดหมู่ที่มีอยู่

เราจะนำรายการและ format! ไปยังสตริงในรูปแบบ [title,id] จากนั้น เราจะเปลี่ยนหมวดหมู่ให้เป็น JSON และใช้มาโคร format! เพื่อรวมหมวดหมู่ทั้งหมดไว้ในข้อความแจ้งเดียว

fn build_prompt(items: Vec<Item>,
                categories: Vec<CategoryWithItems>) -> String {
  //map items into [title,id] then join them all into a string
    let items_joined = items.iter().map(|item| format!(
                                        "[{title},{id}]",
                                        title = item.title,
                                        id = item.id))
                                .collect()
                                .join(",");
    let categories_json = serde_json::to_string(&categories).unwrap();
    
    format!("{prompt}\n{tabs}{middle}{categories}\n{ending}",
            prompt = String::from(PROMPT_TEXT_START),
            tabs = items_joined,
            middle = String::from(PROMPT_TEXT_MIDDLE),
            categories = categories_json,
            ending = String::from(PROMPT_TEXT_ENDING))
}

ตอนนี้ หากต้องการส่งข้อความดังกล่าวไปยัง OpenAI เราจำเป็นต้องมีไคลเอ็นต์ HTTP

เพื่อสิ่งนั้น เราจะใช้ลัง reqwest ซึ่งมอบไคลเอ็นต์ HTTP ระดับสูงพร้อมฟังก์ชันอะซิงก์ธรรมดาที่เราสามารถใช้เพื่อพูดคุยกับ OpenAI API และมีคุณสมบัติ JSON ที่ช่วยให้เราทำการซีเรียลไลซ์/ดีซีเรียลไลซ์ได้ง่าย

มาเพิ่มลงในไฟล์ Cargo.toml ของเรากันดีกว่า:

[dependencies]
...
reqwest = { version = "0.11", features = ["json"] }

เมื่อใช้สิ่งนี้ เราสามารถสร้างไคลเอนต์ HTTP ของเราผ่านรูปแบบตัวสร้างเก่าที่ดีได้

let client = Client::builder()
    .http2_keep_alive_timeout(Duration::from_secs(120))
    .timeout(Duration::from_secs(120))
    .build()
    .unwrap();

แต่หากเราสร้างไคลเอ็นต์ภายในฟังก์ชัน prompt_open_ai ของเรา เราจะสร้างอินสแตนซ์ไคลเอ็นต์สำหรับแต่ละคำขอที่เราทำ ดังนั้นเรามาสร้างการขึ้นต่อกันแทนและเพิ่มโค้ดไคลเอ็นต์ลงในฟังก์ชัน sort_items ของเรา จากนั้นส่งต่อเป็น na argument ลงใน sort_recursively ฟังก์ชันและฟังก์ชัน prompt_open_ai

ด้วยวิธีนี้ เราจะใช้อินสแตนซ์เดียวของไคลเอนต์ HTTP ต่อการเรียก /sort หนึ่งครั้ง และฟังก์ชัน prompt_open_ai ของเราสามารถมุ่งเน้นที่การเรียก API จริงเท่านั้น และส่งคืนผลลัพธ์ให้เรา

เรามาสร้างการโทร POST แบบง่ายๆ แล้วดูว่าเราจะรับหมายเลข Result ได้อย่างไร

เพื่อรักษาความสะอาด เราจะสร้างโมดูลแยกต่างหากภายในโครงสร้างของเรา - โมดูลคือคอนเทนเนอร์สำหรับโค้ดของคุณ (คล้ายกับแพ็คเกจ) ทำให้คุณสามารถสร้างการแยกระหว่างส่วนต่างๆ ของโค้ดของคุณได้

สร้างโฟลเดอร์ใหม่ชื่อ openai และไฟล์ใหม่สองไฟล์ในนั้น:

  • a mod.rs สำหรับโค้ดของเรา
  • a models.rs สำหรับรุ่นของเรา

เปิด models.rs และเพิ่มโครงสร้างที่เราต้องการเพื่อสื่อสารกับ OpenAI Completion API ของเรา:

use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub(crate) struct AskGPT {
    pub prompt: String,
    pub model: String,
    pub max_tokens: usize,
    pub stream: bool,
    pub temperature: usize,
    pub top_p: usize,
    pub n: usize,
}
#[derive(Deserialize)]
pub(crate) struct Completion {
    pub model: String,
    pub choices: Vec<Choices>,
}
#[derive(Deserialize)]
pub(crate) struct Choices {
    pub text: String,
    pub index: usize,
}

และใน mod.rs เราสามารถสร้างเมธอด prompt_open_ai ของเราได้ด้วยคำขอ POST ซึ่งจะส่งโมเดล AskGPT ที่สร้างขึ้นใหม่ของเราไปยังจุดสิ้นสุด /completions

ขณะนี้ มีฟิลด์สำคัญสองสามฟิลด์อยู่ที่นี่ — ฟิลด์ prompt ที่อธิบายตนเองได้ model ซึ่งให้เราเลือกว่าโมเดลใดจะดำเนินการให้เสร็จสิ้น (ในขณะที่เขียน text-davinci-003 เป็นฟิลด์ที่มีประสิทธิภาพดีที่สุดสำหรับงานนี้) max_tokens ซึ่ง เราจะตั้งค่าเป็น 4096 (สูงสุด, d'oh), n ซึ่งควบคุมจำนวนการตอบกลับ และ temperature ซึ่งเป็นวิธีที่จะบอกว่าความน่าจะเป็นใดที่ต้องพิจารณา - ยิ่งสูงเท่าใด การเสร็จสิ้นก็จะยิ่งสุ่มมากขึ้นเท่านั้น - เราจะใช้ 0 ดังนั้นเอาต์พุตของเราจึงสุ่มน้อยลง

หมายเหตุ: ในส่วนนี้ คุณจะต้องมี คีย์ OpenAI API ซึ่งคุณสามารถหาได้ที่นี่

async fn prompt_open_ai(prompt_txt: String,
                        client: &Client) -> Result<String, String> {
    let token =  String::from("YOUR_API_KEY_HERE")
    let auth_header = format!("Bearer {}",token);
    let req = client.post("https://api.openai.com/v1/completions")
        .header("Authorization", auth_header)
        .json(&AskGPT {
            prompt: prompt_txt,
            model: String::from("text-davinci-003"),
            max_tokens: 4096,
            n: 1,
            stream: false,
            temperature: 0,
        }).send().await;
}

ในที่สุด Result!

แต่เราจะทำอย่างไรกับมัน?

เราสามารถเพิ่ม ? ต่อท้ายการรอคอย ซึ่งจะให้ Response ทันที แต่นั่นไม่สนุกเลย ดังนั้นเราจะใช้หนึ่งในคุณสมบัติสนิมที่ฉันชื่นชอบ นั่นก็คือ match อันโด่งดัง

คำสั่ง match ถือเป็นหัวใจสำคัญของประสบการณ์นักพัฒนาซอฟต์แวร์ ช่วยให้คุณมีความสามารถในการจับคู่รูปแบบที่มีประสิทธิภาพ เพื่อให้แน่ใจว่าโค้ดของคุณครอบคลุมเส้นทางทั้งหมด

แต่เอียน มีอะไรพิเศษเกี่ยวกับเรื่องนี้บ้าง? ไม่ใช่แค่ if/else บนสเตียรอยด์ใช่ไหม?

โอ้ ไม่นะ มันมากกว่านั้นมาก

ต่างจากชุดคำสั่ง if/else หรือ switch ตรงที่ match บังคับให้คุณตรวจสอบความเป็นไปได้ทั้งหมด เพื่อให้มั่นใจว่าคุณจะครอบคลุมทั้งเส้นทางที่มีความสุขและเศร้าที่โค้ดของคุณสามารถทำได้

เหตุใดจึงมีพลังพิเศษเช่นนี้?

เพราะมันช่วยลดโอกาสที่จะเกิดจุดบกพร่องเนื่องจากกรณีที่ไม่ได้รับการจัดการ และบังคับให้คุณครอบคลุมทุกกรณีที่เป็นไปได้ และปรับปรุงโค้ดของคุณทันที เป็นหนึ่งในเครื่องมือหายากที่สามารถปรับปรุงความสามารถในการอ่านโค้ดของคุณ แก้ไขจุดบกพร่อง และเพิ่มการบำรุงรักษาได้ในคราวเดียว

เรามาลองใช้มันกันดีกว่า — ไวยากรณ์นั้นง่าย ทางด้านซ้ายมือคือรูปแบบที่คุณกำลังจับคู่ และทางด้านขวามือคือบล็อคโค้ดที่ต้องดำเนินการ

ขั้นแรก เราจะตรวจสอบว่าคำขอเกิดขึ้นจริงหรือไม่โดยตรวจสอบ Result ที่เราได้รับ

match req {
        Ok(response) => {
          //request actually happened, we can access response safely
        }
        Err(error) => {
            //TODO handle error
        }
    }

ตอนนี้ในสาขา Ok ของเรา เราสามารถเข้าถึงออบเจ็กต์การตอบกลับของเราได้อย่างปลอดภัย โดยรู้ว่าเราได้รับกรณีข้อผิดพลาดด้วยเช่นกัน และมันจะไม่ทำให้เกิดข้อผิดพลาดรันไทม์

เราสามารถตรวจสอบต่อไปได้ว่าคำขอนั้นสำเร็จจริงหรือไม่ โดยเพียงแค่ตรวจสอบว่ารหัสสถานะคือ 200 OK หรือไม่

match response.status() {
    StatusCode::OK => {
      // smashing success 
    }
    other => {
      // TODO handle error
    }
}

และสุดท้าย สำหรับขั้นตอนหลัก หากคำขอประสบความสำเร็จ เราควรลองและดีซีเรียลไลซ์เนื้อหาไปยังโครงสร้าง Completion ของเรา แต่เนื่องจากสิ่งนั้นสามารถล้มเหลวได้เช่นกัน เราจึงควรดำเนินการ match อย่างรวดเร็วที่นี่ด้วย และแยกการตอบกลับจากออบเจ็กต์การกรอกข้อมูลของเรา:

match response.json::<Completion>().await {
    Ok(parsed) => {
        //We know there is always at least 1 item in choices 
        //due to our request param n==1 so we'll just live wild and unwrap
        let choices = parsed.choices.first().unwrap();
        let json: &str = choices.text.borrow();
        Ok(String::from(json))
    }
    Err(err) => {
            return Err(Parsing);
        }
}

ตอนนี้ เพื่อจัดการกับข้อผิดพลาด เราจะเพิ่ม enum ที่จะแสดงข้อผิดพลาดประเภทต่างๆ ที่เราอาจมี (ใช่ ฉันจะย่อข้อผิดพลาดที่เป็นไปได้ทั้งหมดให้กับทั้งสามประเภทนี้ สิ่งที่อาจผิดพลาดได้...) — ข้อผิดพลาดในการเชื่อมต่อ ข้อผิดพลาดในการตอบสนองของเซิร์ฟเวอร์และข้อผิดพลาดในการแยกวิเคราะห์ กระโดดขึ้นไปที่ models.rs และเพิ่ม:

#[derive(Debug)]
pub(crate) enum OpenAiError {
    Connection,
    Parsing,
    Server,
}
match req {
    Ok(response) => {
        match response.status() {
            StatusCode::OK => {
                match response.json::<Completion>().await {
                    Ok(parsed) => {
                        //there is always at least 1 due to our request
                        let choices = parsed.choices.first().unwrap();
                        let json: &str = choices.text.borrow();
                        Ok(String::from(json))
                    }
                    Err(err) => Err(Parsing);
                }
            }
            other => Err(Server)          
        }
    }
    Err(err) => Err(Connection)
}

ยินดีด้วย! เราได้ดำเนินการตามคำขอของเราอย่างปลอดภัยและครอบคลุมเส้นทางที่น่าเศร้าและมีความสุขระหว่างทาง

ด้วยคำขอของเราที่ดังขึ้น เราก็ในที่สุดก็สามารถเริ่มทำงานกับฟังก์ชัน sort_recursively ของเราได้ ทำไมต้องเรียกซ้ำที่นี่? เนื่องจากโดยพื้นฐานแล้วเรากำลังลดรายการลงในตัวมันเองโดย GPT3 ทำหน้าที่เป็นฟังก์ชันตัวลดของเรา

แม้ว่าเราจะทำการวนซ้ำที่นี่และเรียกเมธอดนี้ได้ n ครั้ง แต่ก็หมายความว่าเราจะต้องกลายพันธุ์ตัวแปรนอกลูปด้วย (ซึ่งมีหมวดหมู่ของเรา) เมื่อรู้สึกว่าสกปรก เราจะทำมันให้สะอาดและใช้งานได้จริงโดยใช้การเรียกซ้ำเพื่อนเก่าของเรา

เรามาเปิด main.rs ของเราและเริ่มเข้าสู่ฟังก์ชัน sort_recursively กันดีกว่า

ขั้นแรก เราจะสร้างพรอมต์ของเรา จากนั้นส่งไปที่ prompt_open_ai และพยายามดีซีเรียลไลซ์การตอบกลับ ถ้ามันสำเร็จ เราจะรวมมันเข้ากับหมวดหมู่ที่มีอยู่แล้วส่งต่ออีกครั้งไปที่ sort_recursively พร้อมกับชิ้นที่เหลือ จนกว่าเราจะไม่มีชิ้นนั้น

async fn sort_recursively(
                        sorted_categories: Vec<CategoryWithItems>,
                        remaining: Vec<Vec<Item>>,
                        client: Client) -> Result<Categories, Error> {
    let prompt = build_prompt(remaining.first().unwrap().to_vec(),
                             sorted_categories);
    let ai_response = prompt_open_ai(prompt, &client).await.unwrap();
    let json = ai_response.as_str();
    //try to deserialize it
    let generated = serde_json::from_str::<Categories>(json);
    
    match ai_response_result {
        Ok(response) => {
           let parsed = serde_json::
                           from_str::<Categories>(ai_response.as_str());
           match parsed {
               Ok(res) => match res {
                   Ok(wrapper) => {
                       let mut new_categories = wrapper
                                   .categories.to_owned();
                       //remove the processed chunk
                       let mut next_slice = remaining.to_owned();
                       next_slice.remove(0);
                       //join the categories
                       next_categories.append(&mut new_categories);
                       //if we're not done yet recurse
                       if next_slice.len() != 0 {
                        let next = sort_recursively(next_categories,
                                                    next_slice, 
                                                    client).await;
                        match next {
                            Ok(cats) => Ok(cats),
                            Err(e) => Err(String::from("Sort failed"))
                        }
                       } else {
                           Ok(Categories { categories: next_categories })
                       }
                   }
                   Err(msg) => Err(msg)
               }
               Err(parsing) => Err("Parsing response error".to_string())
           }
        }
        Err(err) => Err(err)
    }}

ด้วยการจับคู่ทั้งหมดนี้ โค้ดของเราเริ่มดูน่าเกลียดทีเดียว วิธีหนึ่งในการหลีกเลี่ยงการจับคู่นรกแบบซ้อนคือการใช้ส่วนขยาย map,map_err และ and_then โดยส่วนขยายจะทำงานทางด้านซ้าย (map) หรือด้านขวา (map_err) ของ Result ซึ่งช่วยให้เราสามารถหลีกเลี่ยงการซ้อนนรกได้โดยเพียงแค่ผูกมัดส่วนขยายเหล่านี้เข้าด้วยกัน เวอร์ชันที่กระชับและอ่านง่าย

ข้อมูลจะถูกส่งผ่านตัวถูกดำเนินการที่เกี่ยวข้องเท่านั้น เพื่อให้เราสามารถแมปข้อมูลและข้อผิดพลาดของเราให้อยู่ในรูปแบบที่เหมาะสมได้อย่างปลอดภัย

เราจะใช้มันเพื่อลดการจับคู่ที่ซ้อนกันชุดแรก และเราจะปล่อยให้การจับคู่สุดท้ายเป็นการจับคู่ ทำไม เพราะดูเหมือนว่า "การปิดแบบ async ยังไม่เสถียร" ใน Rust เราจะแมปข้อผิดพลาดทั้งหมดให้อยู่ในรูปแบบ Err(String) เพื่อให้ส่งคืนได้อย่างถูกต้อง:

async fn sort_recursively(sorted_categories: Vec<CategoryWithItems>,
                          remaining: Vec<Vec<Item>>,
                          client: Client) -> Result<Categories, String> {
let mut next_categories = Vec::from(sorted_categories.deref());
    let prompt = build_prompt(remaining.first().unwrap().to_vec(),
                              sorted_categories);
    let ai_response_result = prompt_open_ai(prompt, &client).await;
    let res = ai_response_result
        .map_err(|e|
                format!("Error communicating with OpenAI - {:?}", e))
        .and_then(|ai_response|
            serde_json::from_str::<Categories>(ai_response.as_str())
                .map_err(|_| "Parsing response error".to_string()));
    match res {
        Ok(wrapper) => {
            let mut new_categories = wrapper.categories.to_owned();
            //remove the processed chunk
            let mut next_slice = remaining.to_owned();
            next_slice.remove(0);
            //join the categories
            next_categories.append(&mut new_categories);
            //if we're not done yet recurse
            if next_slice.len() != 0 {
                sort_recursively(next_categories, 
                                next_slice,
                                client).await
                    .map_err(|e| 
                        format!("Sorting failed, reason: {}", e))
            } else {
                Ok(Categories { categories: next_categories })
            }
        }
        Err(msg) => Err(msg)
    }
}

นั่นแหละ — เราเรียก API อย่างปลอดภัย ไร้ข้อผิดพลาด รอสักครู่….

มันไม่ได้รวบรวม

สิ่งหนึ่งที่เราไม่ได้คิดคือการเรียกซ้ำแบบอะซิงก์

เหตุใดจึงเป็นปัญหาเช่นนี้?

เนื่องจากวิธีการ async/await ถูกนำไปใช้ใน Rust (และภาษาอื่น ๆ อีกมากมาย) ภายใต้ประทุนจึงสร้างประเภทเครื่องสถานะพร้อมฟิวเจอร์สทั้งหมดในวิธีการ

แต่ตอนนี้เมื่อเราเพิ่มการเรียกซ้ำที่นี่ ประเภทที่สร้างขึ้นจะเริ่มอ้างอิงถึงตัวเอง ดังนั้นภายใต้ประทุน มันจึงขยายออกเป็นประเภทที่เรียกซ้ำอย่างไม่มีที่สิ้นสุด และคอมไพเลอร์ไม่สามารถระบุขนาดของประเภทได้

เพื่อป้องกันไม่ให้มันระเบิด เราจะต้องแก้ไขการเรียกซ้ำเพื่อส่งคืน Box'd Future ซึ่งจะให้ตัวชี้แก่เราไปยังฮีปแทนที่จะเป็นวัตถุทั้งหมด ป้องกันการอ้างอิงตนเองที่ไม่มีที่สิ้นสุดภายใต้ประทุน

ฉันขอแนะนำให้อ่านเพิ่มเติมเกี่ยวกับปัญหานี้ ที่นี่ และติดตามโพรงกระต่ายนี้ให้ลึกลงไปอีก — ซึ่งครอบคลุมคำถามและแนวคิดการออกแบบภาษามากมายที่ปรากฏในหลายภาษา แต่สำหรับตอนนี้ สิ่งที่เราจะทำคือใช้ลัง async_recursion ดังนั้นไปที่ Cargo.toml ของคุณและเพิ่มเข้าไปที่นั่น:

[dependencies]
..
async-recursion = "1.0.2"

และทำเครื่องหมายฟังก์ชันของคุณด้วยมาโคร #[async_recursion] เพื่อให้สามารถ Box ให้คุณได้

ด้วยวิธีนี้ เราจึงสามารถกลับมาใช้วิธี sort_items เดิมของเรา และตอบสนองต่อคำขอ API นั้นได้ในที่สุด ครั้งล่าสุดที่เราออกจากที่นั่น เราได้เพิ่มอินสแตนซ์ Client ดังนั้น เพียงลงไปด้านล่างแล้วเรียกใช้เมธอด sort_recursively ใช้ map_err เพื่อจับคู่ข้อผิดพลาดเข้ากับโครงสร้าง ErrorResponse ของเรา รวมไว้ใน JSON แล้วส่งคืนเป็นการตอบกลับ และใช้ map เพื่อเปลี่ยนผลลัพธ์ Ok ของเราให้เป็นการตอบสนองที่เหมาะสม:

sort_recursively(categories, prompt_slices, client).await
        .map_err(|e| 
            (StatusCode::INTERNAL_SERVER_ERROR, 
            Json(ErrorResponse { message: e })).into_response())
        .map(|wrapper| {
            let new_categories = wrapper.categories.iter().map(|item| {
                CategoryWithItems {
                    category_id: item.category_id.to_owned(),
                    category_name: item.category_name.to_owned(),
                    items: item.items.to_owned(),
                }
            }).collect::<Vec<CategoryWithItems>>();
            (StatusCode::OK, Json(Categories {
                categories: new_categories
            })).into_response()
        })

และเมื่อดำเนินการเสร็จแล้ว การบริการของเราก็สิ้นสุดลงแล้ว!

เรารับการตอบกลับ จัดรูปแบบ แจ้ง และส่งคืนให้กับผู้ใช้ แผนของเราปลอดภัย ที่เหลือก็แค่ปรับใช้ — แต่เราไม่ต้องคิดถึงการจัดเตรียมอินสแตนซ์ การตั้งค่ากลุ่มความปลอดภัย หรือการเขียน dockerfiles เนื่องจากเราให้บริการโดยนั่งร้านผ่านรถรับส่ง เราจึงปรับใช้ได้อย่างง่ายดายเพียงแตะเทอร์มินัล

เปิดโฟลเดอร์โครงการของคุณในเชลล์ที่คุณเลือกแล้วพิมพ์:

cargo shuttle deploy

ตอนนี้ ลุกขึ้น หายใจเข้าสักเล็กน้อย จิบกาแฟ และก่อนที่คุณจะรู้ตัว เซิร์ฟเวอร์ของคุณก็เริ่มทำงานที่: https://projectname.shuttleapp.rs/

ตอนนี้เอ่อ… ทำไมเราถึงทำเช่นนี้?

โอ้ใช่แล้ว เรากำลังเขียนส่วนขยาย JS เมื่อเซิร์ฟเวอร์ของเราทำงาน ก็ใกล้จะเสร็จสิ้นแล้ว เพียงเปิดไปที่ส่วนขยายและแทนที่ตำแหน่งข้อมูลโฮสต์ภายในเครื่องด้วยตำแหน่งข้อมูลจริงที่คุณเพิ่งได้รับจาก shuttle

ตอนนี้ โหลดส่วนขยายลงในหน้าต่างเล็ก ๆ เพื่อทดสอบ กดปุ่มจัดเรียง รอสักครู่แล้ว — BAM! แท็บของคุณควรถูกจัดเรียงเป็นกลุ่มที่เหมาะสมอย่างน่าอัศจรรย์! ในที่สุด!

มาลองใช้ในหน้าต่างจริงของเรากัน — หน้าต่างที่มี ..เอ่อ ตอนนี้มีแท็บเกือบ 600 แท็บแล้ว ดังนั้นเราจะกดปุ่มจัดเรียงและ — รอสักครู่...

…รอ..

…..รออีกสักหน่อย….

…… ว้าาา มันมาแล้ว…

…. มันใช้เวลานานกว่า 60 วินาที…

…โอ้เดี๋ยวก่อน…

.. ข้อผิดพลาด?

อ๊ะ — เราถึงขีดจำกัดโทเค็นแล้ว!

ทำไม ยังไง? เราไม่ได้ทำสิ่งที่เป็นชิ้น ๆ ทั้งหมดเพื่อให้มันพอดีใช่ไหม?

Weeeeell ดูเหมือนว่าเราจะต้องคำนวณขนาดพร้อมท์ให้ดีขึ้น

นอกจากนี้ การเรียกซ้ำของเรายังก่อให้เกิดปัญหา — การเพิ่มหมวดหมู่ก่อนหน้าทั้งหมดลงในแต่ละพร้อมท์ทำให้ขนาดขยายใหญ่ขึ้น และใช้เวลานานมากในการทำให้ห่วงโซ่ทั้งหมดเสร็จสิ้น — นานกว่า 60 วินาที

และสุดท้าย หมวดหมู่ก็ค่อนข้าง… แย่จัง

นี่เป็นเรื่องดี เนื่องจากทำให้เรามีหลายสิ่งที่ต้องทำในการวนซ้ำครั้งถัดไป เราจะมาดูวิธีกำจัดการเรียกซ้ำนี้ วิธีใช้ GPT tokenizer และฝังไฟล์พจนานุกรมลงในไบนารี่ และใช้บริการ shuttle's static folder สำหรับ แทนที่จะทำให้เวลาในการสร้างของเราพังทลายลง

นอกจากนี้ เรายังจะทดลอง "ปรับแต่ง" โมเดลด้วย ซึ่งจะให้ผลลัพธ์ที่ดีกว่าโดยใช้โทเค็นน้อยลง และเนื่องจากเราขี้เกียจ เราจึงจะสร้างข้อมูลการฝึกโดยใช้ GPT เอง

หากคุณมาไกลขนาดนี้ ขอขอบคุณที่อ่าน และไม่ต้องกังวล เรามีฟีเจอร์ครีปและปัญหาที่อาจเกิดขึ้นอีกมากมายให้ค้นหาบนเส้นทางของเรา ดังนั้นแล้วพบกันใหม่ในตอนต่อไปของ “Human vs Machines”