Deno Fresh OpenAI 實(shí)現(xiàn)智能搜索
大家好,我是Echa。
在上個月Deno 官方的SaasKit受到如此積極的歡迎之后,官方與Suabase合作,為大家?guī)砹肆硪豢頓eno Fresh首發(fā)產(chǎn)品。這次,Deno 官方使用OpenAI Text Completion API創(chuàng)建了一個自定義ChatGPT風(fēng)格的文檔搜索。
在線體驗(yàn):https://supabase-openai-doc-search.deno.dev/
Suabase的免費(fèi)托管PostgresDB非常適合與OpenAI的GPT-3一起使用,因?yàn)樵摂?shù)據(jù)庫帶有擴(kuò)展pgvector,允許您存儲嵌入并執(zhí)行向量相似性搜索。這兩者都是構(gòu)建GPT-3應(yīng)用程序所必需的。接下來咱們看看怎么實(shí)現(xiàn)。
技術(shù)細(xì)節(jié)
構(gòu)建您自己的自定義ChatGPT需要四個步驟:
- ?? GitHub操作預(yù)處理知識庫(docs文件夾中的.mdx文件)。
- ?? 使用pgvector在Postgres中嵌入GitHub Action Store。
- 運(yùn)行時執(zhí)行向量相似性搜索,以查找與問題相關(guān)的內(nèi)容。
- 運(yùn)行時將內(nèi)容注入OpenAI GPT-3文本完成提示并流式響應(yīng)到客戶端。
GitHub操作
步驟1。和2。無論何時我們對主分支進(jìn)行更改,都可以通過GitHub Action進(jìn)行。
合并main時,將執(zhí)行此生成嵌入腳本,該腳本將執(zhí)行以下任務(wù):
- 使用.mdx文件預(yù)處理知識庫
- 使用OpenAI生成嵌入
- 將嵌入內(nèi)容存儲在Suabase中
以下是所發(fā)生情況的工作流程圖:
我們可以在GitHub Actions中使用setup deno GitHub Action通過deno執(zhí)行TypScript腳本。此操作還允許我們使用npm說明符。
Github Action:https://github.com/marketplace/actions/setup-deno
這是GitHub Action yml文件:
name: Generate Embeddings
on:
push:
branches:
- main
workflow_dispatch:
jobs:
generate-embeddings:
runs-on: ubuntu-latest
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- run: deno task embeddings
除了存儲嵌入之外,此腳本還會為每個.mdx文件生成一個校驗(yàn)合并將其存儲在另一個數(shù)據(jù)庫表中,以確保只有在文件更改時才重新生成嵌入。
運(yùn)行時
步驟3。和4。無論何時用戶提交問題,都會在運(yùn)行時發(fā)生。發(fā)生這種情況時,將執(zhí)行以下任務(wù)序列:
- Edge函數(shù)接收查詢并使用OpenAI為查詢生成嵌入
- 嵌入向量用于使用pgvector對Supadase進(jìn)行向量相似性搜索,返回相關(guān)文檔
- 文檔和查詢被發(fā)送到OpenAI,響應(yīng)被流式傳輸?shù)娇蛻舳?/li>
下面是一個工作流程圖,詳細(xì)描述了這些步驟:
在代碼中,用戶提示以SearchDialog 開始。
然后,向量搜索API端點(diǎn)生成嵌入,然后在Supabase上執(zhí)行向量搜索。當(dāng)它得到相關(guān)文檔的響應(yīng)時,它會組裝OpenAI的提示:
const prompt = codeBlock`
${oneLine`
You are a very enthusiastic Supabase representative who loves
to help people! Given the following sections from the Supabase
documentation, answer the question using only that information,
outputted in markdown format. If you are unsure and the answer
is not explicitly written in the documentation, say
"Sorry, I don't know how to help with that."
`}
Context sections:
${contextText}
Question: """
${sanitizedQuery}
"""
Answer as markdown (including related code snippets if available):
`;
const completionOptions: CreateCompletionRequest = {
model: "text-davinci-003",
prompt,
max_tokens: 512,
temperature: 0,
stream: true,
};
// The Fetch API allows for easier response streaming over the OpenAI client.
const response = await fetch("https://api.openai.com/v1/completions", {
headers: {
Authorization: `Bearer ${OPENAI_KEY}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(completionOptions),
});
// Proxy the streamed SSE response from OpenAI
return new Response(response.body, {
headers: {
...corsHeaders,
"Content-Type": "text/event-stream",
},
});
最后,SearchDialog使用EventSource web API處理從OpenAI API返回的服務(wù)器發(fā)送事件。這使我們能夠?qū)㈨憫?yīng)流式傳輸?shù)娇蛻舳耍驗(yàn)樗菑腛penAI生成的:
const onSubmit = (e: Event) => {
e.preventDefault();
answer.value = "";
isLoading.value = true;
const query = new URLSearchParams({ query: inputRef.current!.value });
const eventSource = new EventSource(`api/vector-search?${query}`);
eventSource.addEventListener("error", (err) => {
isLoading.value = false;
console.error(err);
});
eventSource.addEventListener("message", (e: MessageEvent) => {
isLoading.value = false;
if (e.data === "[DONE]") {
eventSource.close();
return;
}
const completionResponse: CreateCompletionResponse = JSON.parse(e.data);
const text = completionResponse.choices[0].text;
answer.value += text;
});
isLoading.value = true;
};
最后
不知道小伙們看明白了,希望大家動手也嘗試一下看看能否實(shí)現(xiàn)。遇到問題歡迎在評論區(qū)留言。