基於阿里雲PAI平臺搭建知識庫檢索增強的大模型對話系統

寫在前面

本方案已在阿里雲線上多個場景落地,將覆蓋阿里雲官方答疑羣聊、研發答疑機器人、釘釘技術服務助手等。線上工單攔截率提升10+%,答疑採納率70+%,顯著提升答疑效率。

本方案最佳實踐已上線阿里雲官網,詳細介紹了使用PAI和向量檢索搭建大模型知識庫對話的具體操作步驟,開始服務雲上客戶使用。詳見:PAI+向量檢索快速搭建大模型知識庫對話

全流程代碼分模塊系統化實現,開源至:

https://github.com/aigc-apps/LLM_Solution

一、背景

ChatGPT和通義千問等大語言模型(LLM),憑藉其強大的自然語言處理能力,正引領着人工智能技術的革命。但LLM在生成回覆時,在“事實性”、“實時性”等方面存在天然的缺陷,很難直接被用於客服、答疑等一些需要精準回答的領域知識型問答場景。因此,如何幫助LLM使用“工具”,藉助外部知識庫生成準確的回覆,成爲解決這類問題的鑰匙。

當前業界主流的解決方案是基於 LangChain,進行LLM檢索增強並生成回覆。其思想是將LLM的能力作爲一個模塊與其他能力組合,藉助向量檢索庫等模塊的檢索能力,對用戶query進行增強,從而充分利用LLM強大的歸納生成能力,生成符合事實的回覆。

LangChain是一套開源框架,可以讓AI開發人員將LLM和外部數據結合起來,從而在儘可能少消耗計算資源的情況下,獲得更好的性能和效果。通過LangChain將輸入的用戶知識庫文件進行處理,存儲在向量檢索庫中。每次推理時,用戶query會先在知識庫中查找相近的答案,並將答案與query一起,輸入部署好的LLM服務中,從而生成基於知識庫的定製答案,解決LLM生成中的“事實性”、“實時性”問題。

下圖展示了基於原始的阿里雲計算平臺產技文檔,搭建一套基於大模型檢索增強答疑機器人的鏈路。

二、模塊化

基於LangChain做檢索增強的原理較爲簡單直觀,但實際落地應用中會發現,整個流程是一套相對複雜的系統工程,包含如下關鍵點。

【文本處理】用戶的原始知識庫格式各式各樣,其中文本通常需要經過仔細的清洗與處理,才適合傳入向量檢索庫中用於檢索。考慮到大模型輸入有長度限制,同時爲切分提取出關鍵信息,需要將清洗後的文本按語義切分爲短chunk,或從中提取出QA,再進行後續的Embedding生成。

【Embedding模型】由於在檢索增強的鏈路中,LLM大多隻負責檢索後的“語句組織與整合”,因此檢索本身是否與query強相關,將直接決定最終的生成效果。實際線上經驗表明,一個好的Embedding模型,對最終結果帶來的影響是決定性的。

【向量檢索庫】不同向量檢索庫性能上存在差異,對於不同規模的外部知識庫,可以選擇與之適應的本地或雲上存儲方案。

【LLM指令微調】不同種類和規模的LLM,其語句組織能力差異明顯。LLM選擇時需要平衡模型大小(效果)與推理延遲(性能)。同時考慮是否需要在領域數據上精調(SFT),以提升領域內泛化回覆的效果。

【Prompt工程】LLM無視query與檢索結果“自由發揮”,或無法精準復現出檢索結果中的關鍵信息,都是LLM生成時常見的問題。通過精心設計的Prompt工程,可以很大概率緩解此類問題。

【推理部署】LLM生成速度太慢是一大瓶頸,在部署爲在線推理服務時,可以通過流式輸出、BladeLLM加速等技術,加快推理速度,提升用戶體驗。

下面我們將基於上述流程中的關鍵點,對各核心模塊進行拆解。

三、向量檢索庫構建

3.1 向量檢索庫選擇

3.1.1 雲上數據庫產品

用戶文檔數較多、且有高併發低延時等檢索要求時,建議存儲在阿里雲自研的雲上數據庫產品。

Hologres

實時數倉Hologres是阿里雲自研一站式實時數倉平臺,其中向量計算深度集成達摩院Proxima向量計算引擎,提供高吞吐、低延時的向量計算能力,支持向量數據實時寫入、實時更新,向量數據寫入即可查,索引構建性能、QPS與召回率性能超過開源向量數據庫數倍,成爲被 OpenAI 和 LangChain 知名大模型社區推薦的向量引擎。支持對接通義千問、ChatGPT、ChatGLM、LLaMA 2等主流大模型,構建企業專屬AI問答知識庫。在AI問答場景下結合大模型進行高效的知識更新,提升問答的速度與準確度,應用於產品技術支持、智能客服、AI導購、公司企業知識庫等場景。

開通Hologres實例並創建數據庫。具體操作,請參見購買Hologres。您需要將已創建的數據庫名稱保存到本地。後續步驟詳見這裏。

Elasticsearch

阿里雲Elasticsearch是基於開源Elasticsearch構建的全託管Elasticsearch雲服務,在100%兼容開源功能的同時,支持開箱即用、按需付費。不僅提供雲上開箱即用的Elasticsearch、Logstash、Kibana、Beats在內的Elastic Stack生態組件,還與Elastic官方合作提供免費X-Pack(白金版高級特性)商業插件,集成了安全、SQL、機器學習、告警、監控等高級特性,被廣泛應用於實時日誌分析處理、信息檢索、以及數據的多維查詢和統計分析等場景。

創建阿里雲Elasticsearch實例。具體操作,請參見創建阿里雲Elasticsearch實例。後續步驟詳見這裏

AnalyticDB

雲原生數據倉庫AnalyticDB PostgreSQL版是一種大規模並行處理(MPP)數據倉庫服務,可提供海量數據在線分析服務。它基於開源項目Greenplum構建,由阿里雲深度擴展,兼容ANSI SQL 2003,兼容PostgreSQL/Oracle數據庫生態,支持行存儲和列存儲模式。既提供高性能離線數據處理,也支持高併發在線分析查詢,是各行業有競爭力的PB級實時數據倉庫方案。

在AnalyticDB PostgreSQL版控制檯上創建實例。具體操作,請參見創建實例。後續步驟詳見這裏

3.1.2 本地數據庫

用戶文檔數較少非高頻場景,可以考慮使用FAISS等本地數據庫方案,輕量級且易於維護。

Faiss

Faiss (Facebook AI Similarity Search) 是FaceBook AI團隊開源的針對大規模相似度檢索的工具,爲稠密向量提供高效相似度搜索和聚類,包含多種搜索任意大小向量集的算法,對10億量級的索引可以做到毫秒級檢索的性能,是目前較成熟的近似近鄰搜索庫。

使用Faiss構建本地向量庫,無需購買線上向量庫產品,也免去了線上開通向量庫產品的複雜流程,更輕量易用。

3.2 文本處理

在構建向量檢索庫前,需要對知識庫文檔進行文本處理。包括數據清洗(文本提取、超鏈替換等)、語義切塊(chunk)、QA提取等。

數據清洗

如果知識庫文本較髒或不規範,需要先做些數據清洗,如將PDF提取爲txt文本,將文本中的超鏈接提取出等。處理代碼需要根據具體文本形式定製。如源知識庫如果是html格式,需要首先按html標題進行切分,保證內容完整性。並對部分文檔類別如常見問題,產品簡介,發佈記錄等進行篩選,單獨處理等。

語義切塊(chunk)

對於非結構化文檔,文本將通過TextSplitter被切分爲固定大小的chunks,這些chunks將被作爲不同的知識條目,用於輔助LLM生成回答。

如果知識庫文本較乾淨或已經過處理,可以直接根據文檔中標題語段進行切分,也可使用LangChain提供的語義切塊接口CharacterTextSplitter,代碼如下:

from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(chunk_size, chunk_overlap)

def split_documents(docs):
    return text_splitter.split_documents(docs)

完成切chunk後,建議爲每個chunk生成對應標題或短摘要,方便後續存入向量庫時作爲查詢主鍵。

QA提取

實際應用中發現,使用QA替代純文本語塊,在向量檢索時效果顯著更好。因此可基於現有非結構化文本,自動生成一些QA對,並將其embedding化後存入向量庫中。

3.3 Embedding模型

這部分根據切分的chunk或提取出的QA,調用開源的embedding模型,以chunk標題或Q生成的embedding作爲索引鍵,chunk或A生成的embedding作爲檢索值。這裏介紹幾種常用的開源embedding模型。

3.3.1 text2vec

text2vec 實現了Word2Vec、RankBM25、BERT、Sentence-BERT、CoSENT等多種文本表徵、文本相似度計算模型,並在文本語義匹配(相似度計算)任務上比較了各模型的效果。

3.3.2 SGPT

SGPT (GPT Sentence Embeddings for Semantic Search) 是一種使用GPT架構生成embedding的方法,模型架構如下,使用 Cross-Encoder 和 Bi-Encoder 兩種方式進行嵌入表示學習。

以非對稱 Bi-Encoder 的 SGPT 爲例,其語義向量檢索代碼如下:

import torch
from transformers import AutoModel, AutoTokenizer
from scipy.spatial.distance import cosine

# Get our models - The package will take care of downloading the models automatically
# For best performance: Muennighoff/SGPT-5.8B-weightedmean-msmarco-specb-bitfit
tokenizer = AutoTokenizer.from_pretrained("Muennighoff/SGPT-125M-weightedmean-msmarco-specb-bitfit")
model = AutoModel.from_pretrained("Muennighoff/SGPT-125M-weightedmean-msmarco-specb-bitfit")
# Deactivate Dropout (There is no dropout in the above models so it makes no difference here but other SGPT models may have dropout)
model.eval()

queries = [
    "I'm searching for a planet not too far from Earth.",
]

docs = [
    "Neptune is the eighth and farthest-known Solar planet from the Sun. In the Solar System, it is the fourth-largest planet by diameter, the third-most-massive planet, and the densest giant planet. It is 17 times the mass of Earth, slightly more massive than its near-twin Uranus.",
    "TRAPPIST-1d, also designated as 2MASS J23062928-0502285 d, is a small exoplanet (about 30% the mass of the earth), which orbits on the inner edge of the habitable zone of the ultracool dwarf star TRAPPIST-1 approximately 40 light-years (12.1 parsecs, or nearly 3.7336×1014 km) away from Earth in the constellation of Aquarius.",
    "A harsh desert world orbiting twin suns in the galaxy’s Outer Rim, Tatooine is a lawless place ruled by Hutt gangsters. Many settlers scratch out a living on moisture farms, while spaceport cities such as Mos Eisley and Mos Espa serve as home base for smugglers, criminals, and other rogues.",
]

SPECB_QUE_BOS = tokenizer.encode("[", add_special_tokens=False)[0]
SPECB_QUE_EOS = tokenizer.encode("]", add_special_tokens=False)[0]

SPECB_DOC_BOS = tokenizer.encode("{", add_special_tokens=False)[0]
SPECB_DOC_EOS = tokenizer.encode("}", add_special_tokens=False)[0]

def tokenize_with_specb(texts, is_query):
    # Tokenize without padding
    batch_tokens = tokenizer(texts, padding=False, truncation=True)   
    # Add special brackets & pay attention to them
    for seq, att in zip(batch_tokens["input_ids"], batch_tokens["attention_mask"]):
        if is_query:
            seq.insert(0, SPECB_QUE_BOS)
            seq.append(SPECB_QUE_EOS)
        else:
            seq.insert(0, SPECB_DOC_BOS)
            seq.append(SPECB_DOC_EOS)
        att.insert(0, 1)
        att.append(1)
    # Add padding
    batch_tokens = tokenizer.pad(batch_tokens, padding=True, return_tensors="pt")
    return batch_tokens

def get_weightedmean_embedding(batch_tokens, model):
    # Get the embeddings
    with torch.no_grad():
        # Get hidden state of shape [bs, seq_len, hid_dim]
        last_hidden_state = model(**batch_tokens, output_hidden_states=True, return_dict=True).last_hidden_state

    # Get weights of shape [bs, seq_len, hid_dim]
    weights = (
        torch.arange(start=1, end=last_hidden_state.shape[1] + 1)
        .unsqueeze(0)
        .unsqueeze(-1)
        .expand(last_hidden_state.size())
        .float().to(last_hidden_state.device)
    )

    # Get attn mask of shape [bs, seq_len, hid_dim]
    input_mask_expanded = (
        batch_tokens["attention_mask"]
        .unsqueeze(-1)
        .expand(last_hidden_state.size())
        .float()
    )

    # Perform weighted mean pooling across seq_len: bs, seq_len, hidden_dim -> bs, hidden_dim
    sum_embeddings = torch.sum(last_hidden_state * input_mask_expanded * weights, dim=1)
    sum_mask = torch.sum(input_mask_expanded * weights, dim=1)

    embeddings = sum_embeddings / sum_mask

    return embeddings

query_embeddings = get_weightedmean_embedding(tokenize_with_specb(queries, is_query=True), model)
doc_embeddings = get_weightedmean_embedding(tokenize_with_specb(docs, is_query=False), model)

# Calculate cosine similarities
# Cosine similarities are in [-1, 1]. Higher means more similar
cosine_sim_0_1 = 1 - cosine(query_embeddings[0], doc_embeddings[0])
cosine_sim_0_2 = 1 - cosine(query_embeddings[0], doc_embeddings[1])
cosine_sim_0_3 = 1 - cosine(query_embeddings[0], doc_embeddings[2])

print("Cosine similarity between \"%s\" and \"%s\" is: %.3f" % (queries[0], docs[0][:20] + "...", cosine_sim_0_1))
print("Cosine similarity between \"%s\" and \"%s\" is: %.3f" % (queries[0], docs[1][:20] + "...", cosine_sim_0_2))
print("Cosine similarity between \"%s\" and \"%s\" is: %.3f" % (queries[0], docs[2][:20] + "...", cosine_sim_0_3))

3.3.3 BGE

BGE (BAAI General Embedding) 是智源開源的中英文語義向量模型,在3億條中英文關聯文本對上訓練。是目前線上表現最好的開源向量模型。

最新開源的BGE v1.5版本,緩解了相似度分佈問題,通過對訓練數據進行過濾,刪除低質量數據,提高訓練時溫度係數 temperature 至 0.02,使得相似度數值更加平穩。

BGE v1.5包含large、base、small三種尺寸的模型,調用速度差距不大,相比LLM生成時間基本可忽略,因此實際線上使用時可以直接使用 bge-large。使用代碼如下:

  from transformers import AutoTokenizer, AutoModel
import torch
# Sentences we want sentence embeddings for
sentences = ["樣例數據-1", "樣例數據-2"]

# Load model from HuggingFace Hub
tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-large-zh-v1.5')
model = AutoModel.from_pretrained('BAAI/bge-large-zh-v1.5')
model.eval()

# Tokenize sentences
encoded_input = tokenizer(sentences, padding=True, truncation=True, return_tensors='pt')
# for s2p(short query to long passage) retrieval task, add an instruction to query (not add instruction for passages)
# encoded_input = tokenizer([instruction + q for q in queries], padding=True, truncation=True, return_tensors='pt')

# Compute token embeddings
with torch.no_grad():
    model_output = model(**encoded_input)
    # Perform pooling. In this case, cls pooling.
    sentence_embeddings = model_output[0][:, 0]
# normalize embeddings
sentence_embeddings = torch.nn.functional.normalize(sentence_embeddings, p=2, dim=1)
print("Sentence embeddings:", sentence_embeddings)

四、LLM訓練及推理

4.1 指令微調

4.1.1 LLM選擇

在當前檢索增強的鏈路中,LLM大多隻負責檢索後的“語句組織與整合”,如根據query與相關檢索結果的拼接,生成針對query,同時蘊含檢索結果的通順回覆。不同種類和規模的LLM,其語句組織能力差異明顯。因此LLM選擇時,會面臨 模型大小(效果)與 推理延遲(性能)之間的平衡。

4.1.2 SFT訓練

如果領域內有批量的QA數據,可以選擇對LLM進行領域相關的精調(SFT),考慮到原始LLM已具備了需要的語句組織生成能力,這一步不是必須的。可以根據具體場景,結合優缺點判斷:

  • 優點:SFT可以提升LLM在領域內的知識能力,使其能更好地理解和處理領域相關的知識、術語和上下文,同時對一些檢索庫中沒有相關內容的問題,具有一定的泛化回覆能力。
  • 缺點:SFT可能一定程度損害LLM本身的文本組織生成能力,使其基於檢索結果的生成效果變差。

如果選擇進行SFT訓練,可以按照以下步驟準備數據,並使用開源的 DeepSpeed-Chat 框架進行SFT訓練。

1.訓練樣本示例(以ChatGLM2的prompt格式爲例)

[Round 1]

問:彈外VVP用戶讀寫Hologres導致JDBC連接數暴漲

答:報錯原因:現在VVP Hologres Connector讀寫Hologres(除了Binlog),默認都使用了JDBC模式,也就是HoloClient,當併發數很高的情況下會佔用很多連接。

解決辦法:可以加上參數useRpcMode = 'true' 切回至Rpc模式。

2.從Github拉取代碼並安裝運行環境;

git clone https://github.com/microsoft/DeepSpeedExamples.git
cd DeepSpeedExamples/applications/DeepSpeed-Chat
pip install -r requirements.txt

3.在 training/utils/data/ 目錄下的 data_utils.py 和 raw_datasets.py 文件中新增自定義數據集,訓練樣本示例如上;
4.在 training/step1_supervised_finetuning/training_scripts 目錄下新建訓練腳本langchain_sft.sh;

OUTPUT=/path/to/save
ZERO_STAGE=2
if [ "$OUTPUT" == "" ]; then
    OUTPUT=./output
fi
if [ "$ZERO_STAGE" == "" ]; then
    ZERO_STAGE=3
fi
mkdir -p $OUTPUT

deepspeed main.py \
   --data_path /path/to/data \
   --data_split 10,0,0 \
   --model_name_or_path /path/to/chatglm2-6b \
   --per_device_train_batch_size 4 \
   --per_device_eval_batch_size 16 \
   --max_seq_len 2048 \
   --learning_rate 9.65e-7 \
   --weight_decay 0. \
   --num_train_epochs 10  \
   --save_per_epoch 5 \
   --gradient_accumulation_steps 4 \
   --lr_scheduler_type cosine \
   --num_warmup_steps 100 \
   --seed 1234 \
   --gradient_checkpointing \
   --zero_stage $ZERO_STAGE \
   --deepspeed \
   --output_dir $OUTPUT \
   |& tee $OUTPUT/training.log

5.執行sh腳本,開始訓練。

cd training/step1_supervised_finetuning/
bash training_scripts/langchain_sft.sh

4.1.3 效果評估

訓練結束後,可以對SFT後的LLM進行效果評估。傳統的NLP文本評價指標包括基於統計的指標,以及基於BERT等預訓練模型的指標。基於統計的方法大多基於N-gram詞重疊率,如 BLEU (Papineni等人,2002年)、ROUGE (Lin,2004年)、METEOR (Banerjee等人,2005年) 等,被廣泛應用於機器翻譯和文本摘要等場景。基於預訓練模型的指標以 BERTScore (Zhang等人,2019年) 爲代表,通過句子相似度計算進行評估,魯棒性較好。

不過這些傳統評估metrics都無法全面衡量出LLM的能力效果。目前主流的LLM評估方法是使用覆蓋各類常見任務的人類試題,來檢驗模型是否具備世界知識(world knowledge),以及解題能力(problem solving)。當前常見的中文LLM Benchmark包括CMMLU、MMCU、C-Eval、GaoKao、SuperCLUE等,英語/多語言LLM Benchmark包括MMLU、HELM、OpenLLM、OpenCompass、MT-bench、AGIEval、BIG-bench等。

下面以 CMMLU 爲例,展示其推理與評估步驟。CMMLU是一個綜合性的中文評估基準,專門用於評估語言模型在中文語境下的知識和推理能力。它涵蓋了從基礎學科到高級專業水平的67個主題,11528道多項選擇題,包括:需要計算和推理的自然科學,需要知識的人文科學和社會科學,以及需要生活常識的中國駕駛規則等。此外,CMMLU中的許多任務具有中國特定的答案,可能在其他地區或語言中並不普遍適用,因此它是一個完全中國化的中文測試基準。

1.從Github拉取代碼與數據集;

git clone https://github.com/haonan-li/CMMLU.git
cd CMMLU/script

2.修改 CMMLU/script/chatglm.sh 中的模型路徑爲微調後的模型checkpoint保存路徑;

...
    --model_name_or_path /path/to/sft_checkpoint \
...

3.運行 chatglm.sh 腳本,模型在各項子領域zero-shot、one-shot、...、five-shot上的分數將會輸出在終端屏幕上。

bash llama2_7b_chat.sh

4.2 Prompt工程

對於不同的專業垂直領域,可以採取不同的prompt策略,以最大化外部知識對LLM的輔助作用。LLM生成時最主要的問題在於其不可控性,包括:LLM無視query與檢索結果“自由發揮”,或LLM無法精準復現出檢索結果中的關鍵信息,如超鏈接、代碼等。我們以下述幾個場景爲例,展示prompt工程構建思路。

場景一:超鏈接精準提取

方案一:Prompt控制

思路:對檢索出來的知識,若內部有網頁鏈接的將其儘可能提取出來,單獨將其額外作爲知識在此強調。同時若知識存在網頁,會在prompt中增加requirement,強調回答的時候“上方提供的知識中可供參考的鏈接有什麼”。需前調上方知識出現的鏈接,以免出現別的鏈接。

import re
prompt = '你是一位智能小助手,請根據下面我所提供的相關知識,對我提出的問題進行回答。回答的內容必須包括其定義、特徵、應用領域以及相關網頁鏈接等等內容,同時務必滿足下方所提的要求!\n 相關知識如下:\n'
for i in range(len(contents)):
    if 'http' in contents[i]:
        prompt += str(i + 1) + '、該知識中包含網頁鏈接!' + '\n' + contents[i] +'。'+ '\n' + '知識中包含的鏈接如下:' 
        pattern = r'([^:]+):(https?://\S+?)(?=\s|$)'
        matches = re.findall(pattern, contents[i])
        # 將鏈接和對應名稱內容存儲到列表中
        links = [(name.strip(), url) for name, url in matches]
        for name, url in links:
            # print("name",name)
            # print("url",url)
            prompt +=  '\n' + name + ':' + url + ';'+'\n'

    else:
        prompt += str(i + 1) + '、' + contents[i] + '\n'
if 'http' in prompt:
    requirement =  '回答的內容要求:若提供的知識中存在“網頁鏈接”,則必須將“網頁鏈接”準確無誤的輸出。不需要輸出知識庫以外的網頁鏈接' 
    prompt += '\n' + requirement + '\n' + '\n' +'問題是:1.' + query_text[0] + '?' + '2. 上方提供的知識中可供參考的鏈接有什麼' + '?\n'
else:
    prompt += '\n' +'問題是:' + query_text[0] + '\n'

效果展示

query: 爲什麼同一個odps sql任務,運行時間差異很大?
向量檢索結果:odps的sql運行時間長怎麼辦:https://help.aliyun.com/document_detail/278738.html
prompt優化前結果(鏈接亂貼):
以下是一些可能有助於解決SQL任務運行時間差異問題的建議:
1. 使用數據庫連接池:<https://help.aliyun.com/document_detail/278738.html>
2. 使用索引:<https://help.aliyun.com/document_detail/278738.html>
3. 優化SQL代碼:<https://help.aliyun.com/document_detail/278738.html>
4. 增加ODPS集羣資源:<https://help.aliyun.com/document_detail/278738.html>
prompt優化後結果(鏈接精準透出):
1. 同一個ODPS SQL任務,運行時間差異很大,可能是由於以下原因:- 數據庫連接時間:數據庫連接時間會直接影響SQL任務的執行時間,如果數據庫連接時間較長,則整個SQL任務的執行時間也會相應增加。- 查詢時間: SQL查詢時間也會對任務的執行時間產生影響。如果查詢時間較長,則整個SQL任務的執行時間也會相應增加。- 輸出數據量:如果SQL任務需要輸出大量的數據,則輸出數據量也會對任務的執行時間產生影響。
2. 上方提供的知識中可供參考的鏈接有:-odps的sql運行時間長怎麼辦:https://help.aliyun.com/document_detail/278738.html

方案二:Map reduce

借鑑LangChain官方文檔中 Map reduce 思想。

 

 

思路:對於一個query,從矢量數據庫從檢索出n條knowledges,將每條知識與query單獨輸入LLM中,得到僅對這個知識下的answer,最後將n個answer一同merge,作爲輸出的結果。

def process(self, db_res):
    for i, reply in enumerate(db_res):
        prompt = '你是一位智能小助手,請根據下面我提供的一條知識,對我提出的若干問題進行回答。\n 提示:我提供的知識一般是由一個問題和答案通過組成。知識如下:\n'
        contents.append(reply)
        if 'http' in contents[i]:
            prompt += str(1) + '、' +'"'+contents[i] +'"'+'。'+ '\n' 
            prompt += '\n'  +'請根據上方所提供的知識,逐一回答以下幾個問題:'+ '\n' +'1. ' + query_text[0] + '?' + '\n'\
                '2. ' + '對於這條知識:"' + contents[i] + '"。' + '這裏面有多少個https?' + '\n'\
                    + '3. 請將這幾個https及其對應的中文描述內容正確的告訴我,請不要額外自行添加別的內容和網頁鏈接。請用:"中文描述+http"這種範式寫出答案。' + '\n' 
        elif '超鏈接' in contents[i]:
            prompt += str(1) + '、' +'"'+ contents[i] +'"'+'。'+ '\n'
            prompt += '\n' + '\n'  +'請根據上方所提供的知識內容,逐一回答以下幾個問題:'+ '\n' +'1. ' + query_text[0] + '?' + '\n'\
            '2. 我提供的知識中有幾個超鏈接?僅告訴我數量,不要人爲去創造鏈接。' + '\n' + '3. 我提供的知識中超鏈接後原文描述是什麼?請求你不要在自行添加別任何的內容和網頁鏈接了!你爲什麼天天要自己製造鏈接?' + '\n'\
                + '對於回答問題3,這裏我給你一個回答例子供你回答參考:比如知識中有一個超鏈接描述是:“[寫出至OSS]<超鏈接<1及以上版本支持_1>>”。則你在回答的時候需要將:[寫出至OSS]<超鏈接<1及以上版本支持_1>>完整輸出。請記住以上這個只是例子,不是知識中的真實內容。' 
        else:
            prompt += str(i + 1) + '、' + contents[i] +'。'+ '\n'
            prompt += '\n' + '\n'  +'請根據上方所提供的知識內容,回答以下問題:'+ '\n' +'1. ' + query_text[0] + '?' + '\n'

場景二:關鍵信息精準還原生成

輸入query後,對於retrieve到的知識的長度,先做一個檢查,可設定一個threshold

  1. 若長度在100個字以內的,就直接讓大模型對知識進行打印
  2. 否則,改變prompt的形式,讓其輸出變得可控
    1. 首先,先判斷知識是否存在http
    2. 若有http,則需要LLM準確打印出http,同時結合兩者回答
    3. 若無http,圍繞LLM自身知識與知識庫內容,進行回答

若沒找到知識,則直接依靠LLM自身知識進行回答

def process(self, db_res):
    contents = []
    for reply in db_res:
        contents.append(reply)

    if len(contents) >=1:
        for i in range(len(contents)):
            if len(contents[i]) < 100:
                prompt = '你是一位智能小助手,請將我提供的內容的原文直接打印出來,不需要你做別的任何解釋與分析!\n 內容如下:\n' + contents[i]
            else:
                if 'http' in contents[i]:
                    prompt = '你是一位智能小助手,請針對我提出的問題進行回答。' + '\n'\
                        '爲了幫助你更好的解答這個問題,對於該問題,本地的知識庫檢索到了相關的標準答案。\n 知識庫中的標準答案是如下:\n'
                    prompt += str(i + 1) + '、' + contents[i] + '。\n' + '現在,要求你,結合知識庫的標準答案與自身知識對提出的問題:\n' + query+',進行回答。'+\
                        '回答的內容要求如下:1.請控制你回答的內容,緊密的圍繞知識庫中的標準答案進行回答!\n 2.知識庫中涉及網頁鏈接,請將該網頁鏈接正確無誤的打印出來!'
                else:
                    prompt = '你是一位智能小助手,請針對我提出的問題進行回答。' + '\n'\
                        '爲了幫助你更好的解答這個問題,對於該問題,本地的知識庫檢索到了相關的標準答案。\n 知識庫中的標準答案是如下:\n'
                    prompt += str(i + 1) + '、' + contents[i] + '。\n' + '現在,要求你,結合知識庫的標準答案與自身知識對提出的問題:\n' + query+',進行回答。'+\
                        '回答的內容要求如下:1.請控制你回答的內容,緊密的圍繞知識庫中的標準答案進行回答!'
    else:
        prompt = '你是一位智能小助手,請根據你自身的知識儲備,對我提出的若干問題進行回答。\n' + '\n' + '回答以下問題:' +'\n' + query

效果展示

query: odps中刪除一個項目?
向量檢索結果:odps常見諮詢【如何刪除項目】:刪除空間 步驟如下: 運維平臺 --> 我的項目 --> 更多 --> 項目下線 請點擊 [運維平臺](https://tesla.alibaba-inc.com/abm/#/odps/portal/workbench/my_project/list)
prompt優化前結果(關鍵信息不完整,超鏈接未透出):要在ODPS中刪除一個項目,請按照以下步驟操作:

1.登錄ODPS控制檯並登錄您的賬戶。
2.導航到“我的項目”頁面,選擇“項目列表”選項卡。
3.找到要刪除的項目並點擊“下線”按鈕。
4.在彈出的提示框中,選擇“確認下線”選項,即可刪除該項目。


請注意,刪除一個項目後,它將不再存在於您的ODPS賬戶中,而且所有相關數據和項目文件都將被永久刪除。因此,在刪除項目之前,請確保備份任何重要數據。
prompt優化後結果(關鍵信息完整,超鏈接準確):針對odps中刪除一個項目的問題,根據知識庫中的標準答案,可以得知刪除項目的步驟如下:


1.打開odps的運維平臺,並登錄賬戶。
2.在導航欄中點擊“我的項目”,進入項目列表頁面。
3.在項目列表頁面中,找到要刪除的項目,並點擊“更多”按鈕。
4.在彈出的選項中,選擇“項目下線”,即可刪除該項目。

知識庫中涉及網頁鏈接爲:https://tesla.alibaba-inc.com/abm/#/odps/portal/workbench/my_project/list

場景三:代碼&超鏈接精準提取

若輸入query後,檢索到答案,對於retrieve到的知識的長度,先做一個檢查,可設定一個threshold

  1. 若長度在100個字以內的,就直接讓大模型對知識進行打印
  2. 否則,改變prompt的形式
    1. 若先判斷知識是否存在“命令”
    2. 有"命令",則需要LLM準確打印出code,同時結合知識庫中的標準答案兩者回答
    3. 若判斷知識是否存在“http”
    4. 有“http”, 則需要LLM準確打印出http,同時結合知識庫中的標準答案兩者回答
    5. 否則,若無http與命令,則LLM結合自身知識與標準答案進行回答

若沒找到知識,則直接依靠LLM自身知識進行回答

def process(self, db_res):
    contents = []
    for reply in db_res:
        contents.append(reply)

    if len(contents) >=1:
        for i in range(len(contents)):
            if len(contents[i]) < 100:
                prompt = '你是一位智能小助手,請將我提供的內容的原文直接打印出來,不需要你做別的任何解釋與分析!\n 內容如下:\n' + contents[i]
            else:
                if '命令' in contents[i]:
                    prompt = '你是一位智能小助手,請針對我提出的問題進行回答。' + '\n'\
                        '爲了幫助你更好的解答這個問題,對於該問題,本地的知識庫檢索到了相關的標準答案。\n 知識庫中的標準答案是如下:\n'

                    prompt += str(i + 1) + '、' + contents[i] + '。\n' + '現在,要求你,結合標準答案與自身知識對提出的問題:\n' + query+',進行回答。'+\
                        '\n回答的內容要求如下:\n1.請控制你回答的內容,請緊密的圍繞標準答案進行回答!\n2.知識庫中涉及命令行和代碼,請將每一行命令行和代碼正確無誤的打印出來!'
                elif 'http' in contents[i]:
                    prompt = '你是一位智能小助手,請針對我提出的問題進行回答。' + '\n'\
                        '爲了幫助你更好的解答這個問題,對於該問題,本地的知識庫檢索到了相關的標準答案。\n 知識庫中的標準答案是如下:\n'

                    prompt += str(i + 1) + '、' + contents[i] + '。\n' + '請結合標準答案與自身知識對以下提出問題進行回答:\n' + '1.' + query +\
                        '\n2.標準答案中存在網頁鏈接,要求你將該網頁鏈接地址正確無誤的打印出來!'
                else:
                    prompt = '你是一位智能小助手,請針對我提出的問題進行回答。' + '\n'\
                        '爲了幫助你更好的解答這個問題,對於該問題,本地的知識庫檢索到了相關的標準答案。\n 知識庫中的標準答案是如下:\n'

                    prompt += str(i + 1) + '、' + contents[i] + '。\n' + '現在,要求你,結合標準答案與自身知識對提出的問題:\n' + query+',進行回答。'+\
                        '\n回答的內容要求如下:\n1.請控制你回答的內容,請緊密的圍繞知識庫中的標準答案進行回答!'
    else:
        prompt = '你是一位智能小助手,請根據你自身的知識儲備,對我提出的若干問題進行回答。\n' + '\n' + '回答以下問題:' +'\n' + query

效果展示

query: odps報錯任務運行如何調整優先級?
向量檢索結果:odps如何設置作業優先級:設置作業優先級的方式如下:運行......命令示例如下。set odps.instance.priority=values;//values取值爲0~9。運行MaxCompute......命令示例如下。bin/odpscmd --config=xxx --project=xxx --instance-priority=x -e ""通過Java SDK設置作業優先級......命令示例如下。import com.aliyun.odps.Instance;import com.aliyun.odps.LogView;import com.aliyun.odps.Odps;import com.aliyun.odps.OdpsException;import com.aliyun.odps.account.Account;import com.aliyun.odps.account.AliyunAccount;import com.aliyun.odps.task.SQLTask;public class OdpsPriorityDemo {public static void main(String args[]) throws OdpsException {Account account = new AliyunAccount("accessId","accessKey");Odps odps = new Odps(account);String odpsUrl = "http://service.odps.aliyun.com/api"; // 公共雲URL。odps.setEndpoint(odpsUrl);odps.setDefaultProject("xxxxxxxxxx");SQLTask task = new SQLTask();task.setName("adhoc_sql_task_1");task.setQuery("select count(*) from aa;");Instance instance = odps.instances().create(task, 5); // 5爲作業優先級。LogView logView = newLogView(odps);System.out.println(logView.generateLogView(instance, 24)); // 打印Logview,用於查看Instance執行狀態,非必須。instance.waitForSuccess(); // 等待Instance執行完成,非必須。}}通過DataWorks......命令示例如下。set odps.instance.priority=x;//x爲優先級取值。
prompt優化前結果(代碼不完整):如果odps任務運行時出現報錯,可以嘗試以下方式調整作業優先級:
1.在odps客戶端中,進入Project空間,設置作業優先級。可以通過以下命令設置MaxCompute作業的優先級:set odps.instance.priority=values; //values取值爲0~9其中,values的取值範圍爲0~9,可以根據具體需求設置。
2.或者,您也可以通過DataWorks的基線管理功能設置作業優先級。該方式常用於保障某個週期性作業以及其上游作業優先產出數據。您可以通過基線管理功能集中設置整條數據鏈路上各個作業的優先級,無需單獨處理每個作業。在DataWorks中,您可以通過以下命令設置作業優先級:set odps.instance.priority=x; //x爲優先級取值其中,x爲優先級取值,可以設置爲1、3、5、7或8,數值越大,優先級越高。當您通過DataWorks的基線管理功能設置MaxCompute作業優先級時,MaxCompute作業優先級=9-DataWorks基線優先級。請注意,以上方式可能會影響任務的執行時間和優先級,需要根據具體情況進行調整。
prompt優化後結果(代碼完整透出):針對odps報錯任務運行如何調整優先級的問題,根據本地知識庫的檢索,可以得出以下解答:
1.如果odps任務運行時遇到錯誤,可以通過重新運行任務來解決。在重新運行任務之前,可以嘗試調整任務優先級,以便任務能夠更優先地運行。具體的調整方式如下:- 在odps客戶端中,運行以下命令行:set odps.instance.priority=values; //values取值爲0~9,設置作業優先級。- 然後運行MaxCompute客戶端並進入Project空間,設置作業優先級。該方式常用於設置臨時查詢作業的優先級。通過這種方式,可以確保任務在出現錯誤時能夠得到更快的處理。
2.如果odps任務運行時遇到錯誤,可以嘗試使用以下代碼來調整任務優先級:
import com.aliyun.odps.Instance;
import com.aliyun.odps.LogView;
import com.aliyun.odps.Odps;
import com.aliyun.odps.OdpsException;
import com.aliyun.odps.account.Account;
import com.aliyun.odps.account.AliyunAccount;
import com.aliyun.odps.task.SQLTask;

public class OdpsPriorityDemo {
    public static void main(String[] args) throws OdpsException {
        Account account = new AliyunAccount("accessId","accessKey");
        Odps odps = new Odps(account);
        String odpsUrl = "http://service.odps.aliyun.com/api"; // 公共雲URL。
        odps.setEndpoint(odpsUrl);
        odps.setDefaultProject("xxxxxxxxxx");

        SQLTask task = new SQLTask();
        task.setName("adhoc_sql_task_1");
        task.setQuery("select count(*) from aa;");

        Instance instance = odps.instances().create(task, 5); // 5爲作業優先級。

        LogView logView = new LogView(odps);
        System.out.println(logView.generateLogView(instance, 24)); // 打印Logview,用於查看Instance執行狀態,非必須。

        instance.waitForSuccess(); // 等待Instance執行完成,非必須。
    }
} ”

以上代碼可以在odps任務運行時動態調整任務優先級,確保任務能夠更優先地運行。

4.3 推理部署

4.3.1 PAI-EAS部署在線服務

模型在線服務 PAI-EAS (Elastic Algorithm Service) 是一種模型在線服務平臺,可支持您一鍵部署模型爲在線推理服務或AI-Web應用。它提供的彈性擴縮容和藍綠部署等功能,可以支撐您以較低的資源成本獲取高併發且穩定的在線算法模型服務。此外,PAI-EAS還提供了資源組管理、版本控制以及資源監控等功能,方便您將模型服務應用於業務。PAI-EAS適用於實時推理、近實時異步推理等多種AI推理場景,並具備自動擴縮容和完整運維監控體系等能力。

在LLM檢索增強鏈路中,一般需要部署2個PAI-EAS在線服務:

  • LangChain主鏈路服務:其中會調用向量檢索,對用戶query與向量檢索後的結果進行拼接,添加prompt工程,並調用LLM在線推理服務得到回覆,後處理後返回給用戶
  • LLM在線推理服務:根據query和檢索結果拼好的prompt,輸入LLM後返回結果

部署PAI-EAS在線服務,需要登陸 PAI控制檯,進入PAI EAS模型在線服務頁面,根據需求配置相關參數,完成部署後即可得到調用地址url與token。

4.3.2 BladeLLM模型加速與流式輸出

BladeLLM 是阿里雲PAI平臺提供的大模型部署框架,支持主流LLM模型結構,並內置模型量化壓縮、 BladeDISC編譯等優化技術用於加速模型推理。使用BladeLLM的預構建鏡像,能夠便捷地在PAI-EAS平臺部署大模型推理服務。

BladeLLM可以在PAI-EAS上很方便地進行部署。以7B參數規模的模型爲例,使用fp16數值精度推理情況下,可以使用 A10 (24GB) 或 V100 (32GB) 規格的單卡GPU實例。服務啓動完成,可通過如下方式調用服務,流式地獲取生成文本:

import json
from websockets.sync.client import connect
with connect("ws://localhost:8081/generate_stream") as websocket: prompt = "What's the capital of Canada?" websocket.send(json.dumps({
        "prompt": prompt,
        "sampling_params": {
                "temperature": 0.9,
                "top_p": 0.9,
                "top_k": 50
            },
        "stopping_criterial":{"max_new_tokens": 100}
}))
while True:
msg = websocket.recv() msg = json.loads(msg) if msg['is_ok']:
if msg['is_finished']: break
print(msg['tokens'][0]["text"], end="", flush=True) print()
    print("-" * 40)

線上測試,使用BladeLLM加速後的鏈路延時,RT可加速約40%。同時支持流式文本生成,大幅減少用戶等待時間,提升用戶體驗。

五、WebUI Demo

以下展示根據上述模塊化搭建 LangChain 檢索增強 LLM 問答的 WebUI Demo,每個模塊均在前端中展示並可進行自定義配置。

5.1 參數配置界面

支持自定義embedding模型、自選向量檢索庫與參數配置、在線EAS服務參數配置。

5.2 用戶知識上傳建庫

切換到Upload選項卡中,按照界面操作指引上傳知識庫文件,然後單擊Upload。

以Hologres爲例,上傳成功後,您可以在Hologres數據庫中查看寫入的數據和向量等信息。具體操作,請參見表。

https://help.aliyun.com/zh/hologres/user-guide/manage-an-internal-table

5.3 多種模式問答

切換到Chat選項卡中,支持 1) 純向量檢索;2) 純LLM生成;3) LLM檢索增強 三種不同模式。選擇不同的查詢模式,推理效果如下。

六、落地案例

阿里雲計算平臺大數據產品智能問答系統

傳統方案

計算平臺中包含了多個大數據產品,這些產品在提供各種能力的同時,也帶來了大量答疑問題。傳統的方案是基於Elasticsearch進行信息檢索,主要存在兩個問題:

1、檢索麪向關鍵詞,缺乏對上下文的理解。

2、強依賴於知識庫的建設質量,需要投入大量人力進行語言,交互等細節上的審覈工作。

優化方案

在本項目中,我們致力於藉助大模型的知識理解和湧現能力,打造面向計算平臺大數據產品的智能問答系統,從而高效且準確地對用戶問題進行答疑,降低人工答疑的成本。爲了解決在強事實性要求的情況下,大模型容易生成“幻覺”的問題,我們參考了LangChain提供的知識庫問答方案,在問題輸入時,給模型提供相關知識和限制,使其聚焦在特定的問題中,從而給出更符合要求的回答,同時針對性地使用高質量的業務數據對模型參數進行全量微調,讓模型掌握更充分的背景知識。整體系統框架如下:

業務結果

在各算法模塊選定和全工程鏈路落地實現後,我們推動了後續兩個階段的業務試點與效果評估。

研發小二試用

在第一階段中,讓研發小二在解決實際問題的過程中試用模型,並給出相應反饋,輔助我們針對性地對模型進行調優。

在一個月的迭代改進中,我們將問題歸爲:檢索問題、模型總結問題、知識缺失 三類。面向不同類別的問題,我們分別採取了針對性的措施,如調整檢索模塊,修改prompt模版,加入規則過濾,補充知識庫內容等,最終經過一個多月的攻堅,小二採納率從最初的不到20%提升到了70+%。

線上灰度結果

在第二階段中,灰度上線,通過真實線上數據變化直觀地對模型結果進行檢驗。主要入口包括:

1) 大數據技術服務助手

2) 研發小蜜答疑機器人

數據表明,在灰度上線的渠道內相比於傳統的檢索式問答,攔截率提升了10+%,有效地降低了大數據產品的人工答疑成本。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章