首先說明一下來龍去脈,我是使用餘弦相似來找出Embedding向量最近的前兩筆資料,但是當Embedding向量資料量很多時,這種方式找出的前兩筆向量和其對應的文本內容不一定是跟我的QA提問有關聯(有可能有相關或是可用於回答QA提問的文本在餘弦排名下,只排在第四或第五甚至是更後面,因此前兩名的文本可能跟QA問題完全沒有關係),這導致QA機器人回答時的質量變得很低或是我明明有準備和訓練好QA文本但是怎麼問,QA機器人就是答不出來等等的問題發生。
那麼知道問題點後,我該如何解決這個問題?就在蹲廁所時靈光一閃,既然餘弦排名的結果不一定是我要的文本,那就由ChatGPT自己去推測和篩選出這些排名中,哪些文本可以用作回答QA提問,然後再組合起來當作參考文本不就好了嗎!Let's Go直接實作!
ChatGPT推測和篩選文本實作
首先開始前需要先建立兩個提示工程的方法:
QATextUserPrompt
這個方法內的提示跟上一篇的差不多,不過有稍做微調,大家可以參考著用或是自己調整也OK
public string QATextUserPrompt()
{
return "Answer the question as truthfully as possible using the provided context. " +
"If there is code, please wrap it in markdown syntax. If the answer is not contained within the text below or the text is empty " +
"say \"抱歉,您的提問未納入QA問題集,因此我無法回答您的問題\".\n\nContext:\n";
}
QATextUseSourcePrompt
這個方法的提示主要告訴ChatGPT每個文本都有對應的TextGuid、TextName和TextContent,然後根據QA提問選出最適合用來回答問題的文本,依據所選的TextGuid和TextName輸出成我指定的JSON資料結構如以下:「[{textGuid: "xxx", textName: "zzz"}]」
public string QATextUseSourcePrompt()
{
return "Each context has a TextGuid and TextName and TextContent followed by the actual message. " +
"Before providing an answer as accurately as possible, you must select multiple sources that can be used to answer the question and keep the TextGuid and TextName of the selected sources then output the result in the following format, " +
"e.g. [{textGuid: \"xxx\", textName: \"zzz\"}], only output the format that I have specified. " +
"If no sources are selected, please output an empty array.\n\nSources:\n";
}
接下來需要新增四個新的方法,可以使用服務注入的方式實作:
GetGuidsFromQAResultAsync
這個方法是Call ChatGPT API並且將回傳結果解析並輸出成List<Guid>物件
private async Task<List<Guid>> GetGuidsFromQAResultAsync(string prompt)
{
var qaResult = await _openAIHttpService.GetCustomChatGPTResponse(prompt, 0.0, 200);
var regex = new Regex(@"\[(.*?)\]");
var match = regex.Match(qaResult);
var guids = new List<Guid>();
if (match.Success)
{
var idsList = JsonConvert.DeserializeObject<List<UsingTextModel>>(match.Value);
guids.AddRange(idsList.Select(id => Guid.Parse(id.textGuid)));
}
return guids;
}
GetTextGuidsAsync
這個方法主要在組合所有向量對應的文本提示,以及對應的提示所需要的資料:TextGuid、TextName和TextContent,並且依據Token數量組成一個大的Prompt然後請ChatGPT選出最適合當參考文本的Guid,最後加入到List<Guid>物件並且回傳
private async Task<List<Guid>> GetTextGuidsAsync(Guid[] vectors, string inputText)
{
var guids = new List<Guid>();
var prompt = _openAIPrompt.QATextUseSourcePrompt();
var tokenizer = _openAIGPTToken.GetGPT3Tokenizer(prompt);
var qaPrompt = $"\n\nQuestion:{inputText}";
foreach (var vector in vectors)
{
var tbTextData = await _dbContext.TB_Texts.FirstOrDefaultAsync(text => text.EmbeddingId == vector);
if (tbTextData != null)
{
var textContent = FilterNewLines(tbTextData.TextContent);
var tempPrompt = $"TextGuid:\"{tbTextData.Id}\"\nTextName:\"{tbTextData.Name}\"\nTextContent:\"{textContent}\"\n\n";
var currentTextTokenizer = _openAIGPTToken.GetGPT3Tokenizer(tempPrompt);
int tokensNeeded = tokenizer.tokens + currentTextTokenizer.tokens + 200;
if (tokensNeeded > totalTokenSize)
{
guids.AddRange(await GetGuidsFromQAResultAsync(prompt + qaPrompt));
prompt = _openAIPrompt.QATextUseSourcePrompt();
tokenizer = _openAIGPTToken.GetGPT3Tokenizer(prompt + qaPrompt);
}
tokenizer.tokens += currentTextTokenizer.tokens;
prompt += tempPrompt;
}
}
guids.AddRange(await GetGuidsFromQAResultAsync(prompt + qaPrompt));
return guids;
}
💡 這邊會有點抽象,如果看不懂沒關係,後面會有輸出後的結果可以看
GenerateQAPromptAsync
這個方法是根據文本和問題,生成一個最終用於問答的Prompt。在給定的Guid列表中循環逐一處理每個Guid對應的文本。它會檢查是否有足夠的Token數量可用於添加當前文本,並在超過總Token數時停止添加。如果Guid列表為空,則在Prompt中添加一個"empty"字串。最後把Prompt和使用到的文本列表(usingTextList)一併輸出成一個QAGenerateModel物件。private async Task<QAGenerateModel> GenerateQAPromptAsync(List<Guid> guids, string inputText)
{
var prompt = _openAIPrompt.QATextUserPrompt();
var qaPrompt = $"\n\nQuestion:{inputText}";
var tokenizer = _openAIGPTToken.GetGPT3Tokenizer(prompt + qaPrompt);
var usingTextList = new List<UsingTextModel>();
foreach (var guid in guids)
{
var tbTextData = await _dbContext.TB_Texts.FirstOrDefaultAsync(text => text.Id == guid);
if (tbTextData != null)
{
var textContent = FilterNewLines(tbTextData.TextContent);
var currentTextTokenizer = _openAIGPTToken.GetGPT3Tokenizer(textContent);
int tokensNeeded = tokenizer.tokens + currentTextTokenizer.tokens + qaGPTCompletionMaxTokenSize;
if (tokensNeeded > totalTokenSize)
{
break;
}
tokenizer.tokens += currentTextTokenizer.tokens;
prompt += $"{textContent}\n\n";
usingTextList.Add(new UsingTextModel
{
textGuid = tbTextData.Id.ToString(),
textName = tbTextData.Name
});
}
}
if (guids.Count == 0)
{
prompt += "empty";
}
prompt += qaPrompt;
return new QAGenerateModel
{
prompt = prompt,
usingText = usingTextList
};
}
UseTop5TextGeneratePrompt
這個方法就只是組合GetTextGuidsAsync和GenerateQAPromptAsync而已
private async Task<QAGenerateModel> UseTop5TextGeneratePrompt(Guid[] vectors, string inputText)
{
var guids = await GetTextGuidsAsync(vectors, inputText);
return await GenerateQAPromptAsync(guids, inputText);
}
GetTop5TextsByEmbedding
最後來修改上一篇的「GetSimilarWordsByEmbedding」將其名稱調整為「GetTop5TextsByEmbedding」,這裡我改成取得Top 5的向量資料,和調整使用的Function名稱
public async Task<QAGenerateModel> GetTop5TextsByEmbedding(string inputText)
{
.
.
.
.
以上程式都沒變
var sortedSimilarities = cosineSimilarities.OrderByDescending(x => x.Value).Take(5).ToDictionary(x => x.Key, x => x.Value);
var top5Vectors = sortedSimilarities.Select(x => x.Key).ToArray();
return await UseTop5TextGeneratePrompt(top5Vectors, inputText);
}
💡 不一定只選Top 5筆,可以自由決定。當然越多的文本當作篩選依據當然是更好,但相對的,API的費用自然也會增加哦!
執行API
完成後來執行API,在前面「GetTextGuidsAsync」方法我覺得很難解釋,所以直接下一個中斷點來看看提供給AI篩選文本的Prompt組合後的樣子
💡 為什麼有兩個是因為每一次請求時有Token的限制(我設定4096),因此才會變成兩次提問(第一次包含三個文本,第二次包含兩個)。
送出請求後可以在「GetGuidsFromQAResultAsync」方法看到該提示ChatGPT執行後,確實的幫我篩選出了TextGuid和TextName,並且輸出成我指定的格式
💡 可以看到ChatGPT篩選出2個文本作為參考答案
最後就都一樣的流程,只需要執行ChatGPT API請它依據你經過AI篩選後的所有QA文本,作為回答的基準,就完成了QA聊天機器人的實作囉!
0 Comments
張貼留言