不要跟我五四三,回點有用的話來

這篇文章是專門為了繁體中文的 RAG 寫的。
你有覺得聊天機器人一直在打高空嗎?
你有覺得聊天機器人總是瞎掰胡謅嗎?
你有覺得聊天機器人經常答非所問嗎?
如果你使用的 prompt 已經很精準了,卻還常有上述感受,那很有可能你實作的 RAG 出了根本上的問題。
要讓 RAG 務實有以下幾個重點:
- 必須是要懂繁體中文的 embedding
- 要有中文語意的分片器
- 一定要是用繁體中文訓練的 LLM
- 精準的 prompt 連同 system instruction
- 沒做 rerank 的效果都差到不行,但 rerank 要懂繁體中文
能夠做到上述五點,那麼你就可以得到一個穩重且有參考價值的 RAG。
核心概覽
以下我會提供一個使用 langchain 實作的繁體中文萬用套餐,最重要的是:本地運行,完全免費。
既免費又萬用的核心元素是下列:
- 懂中文的 embedding:BGE-M3
- 中文語意的分片器:RecursiveCharacterTextSplitter
- 繁體中文訓練的 LLM:TAIDE
- 懂繁體中文的 rerank:ms-marco-MultiBERT-L-12
上述都是免費的,搭配本地運行的框架:
- Ollama
- Langchain
就可以實作我們要的務實 RAG。
具體實踐
把 Ollama 裝好,有 Python 後就開始吧。
以下我會用一個 Confluence 的爬蟲做示範,但整個腳本可以套用在任何繁體中文的資料源。
1 2 3 4 5 6 7 8
| pip install langchain langchain-community langchain-core langchain-huggingface
pip install atlassian-python-api lxml pytesseract docx2txt
pip install flashrank
pip install faiss-cpu
|
Python 套件都安裝完成後的第一步,我們要先準備給 RAG 使用的資料源,以本例來說是 Confluence。
1 2 3 4 5 6 7 8 9 10 11 12
| from langchain_community.document_loaders import ConfluenceLoader CONFLUENCE_TOKEN = '<your token>' USER = '<your account>' ROOT_URL = '<your domain>' SPACE_KEY = '<your space>' PAGE_ID = '<your page>' loader = ConfluenceLoader( url=ROOT_URL, username=USER, api_key=CONFLUENCE_TOKEN ) documents = loader.load(space_key=SPACE_KEY, page_id=PAGE_ID, include_attachments=True, limit=50)
|
到此,Confluence 的資料就全部抓下來並存進 documents 了。
若你要處理的是別的資料源,就直接從這裡往下,重點是所有資料要放進 documents。接著我們要來做分段,許多英文的教學會使用 token 來做分段,但中文與 token 並沒有很好的對應,效果會非常差。因此,我們依然採用標點符號的分段方式,並且讓上下兩片段是有重疊的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=400, length_function=len, is_separator_regex=False, separators=[ "\n\n", "\n", " ", ".", ",", "\u200b", # Zero-width space "\uff0c", # Fullwidth comma "\u3001", # Ideographic comma "\uff0e", # Fullwidth full stop "\u3002", # Ideographic full stop "", ], ) texts = text_splitter.split_documents(documents)
|
上面程式碼可以看到,我們對中文的全型逗號和句號做了額外處理,想要加入更多字元也行。
另一個關鍵是我們的片段長度是 800 且重疊 400 字,這是我實測下來最理想的長度了,當然也可以根據需求調整。
接著我們準備開始做 embedding,務必記得,embedding 的模型一定要懂繁體中文。現階段,既免費效果又好的當屬 BAAI/bge-m3。
1 2 3 4 5 6 7 8 9 10
| from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS HF_EMBEDDING_MODEL = 'BAAI/bge-m3' hf_embeddings = HuggingFaceEmbeddings( model_name=HF_EMBEDDING_MODEL, model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': False} ) vectordb = FAISS.from_documents(texts, hf_embeddings)
|
現在我們已經有一份完整的向量放在記憶體內了,至於是否需要落地,也是根據需求決定。
最後就是聊天對話了,讓我們用 langchain 的內建功能快速搭建起來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from langchain.retrievers.document_compressors import FlashrankRerank from langchain.retrievers import ContextualCompressionRetriever from langchain_community.llms import Ollama LLM_MODEL = 'cwchang/llama3-taide-lx-8b-chat-alpha1' RERANK_MODEL = 'ms-marco-MultiBERT-L-12' llm = Ollama(model=LLM_MODEL) custom_prompt_template = """ <your system instruction> {context} Question: {question} Helpful Answer:""" CUSTOMPROMPT = PromptTemplate( template=custom_prompt_template, input_variables=["context", "question"] ) retriever = vectordb.as_retriever(search_type="similarity", search_kwargs={"k": 100}) # K1, Top100 Snippets compressor = FlashrankRerank(model=RERANK_MODEL, top_n=5) # K2, Top5 Answers compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever ) qa = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=compression_retriever, return_source_documents=True) ## Inject custom prompt qa.combine_documents_chain.llm_chain.prompt = CUSTOMPROMPT question = "<your question>" answer = qa({"query": question}) print(answer)
|
上面這段 code 有幾個重點:
- 採用 Ollama Hub 內有的 TAIDE 模型 (整合好的比較方便)
- Rerank 模型則是 langchain 整合的
ms-marco-MultiBERT-L-12
- 先用向量的相似性比對找出前 100 名,接著透過 rerank 找出最佳的 5 個
- 雖然
answer 只會有一個答案,但 return_source_documents=True 所以可以看到參考了哪些結果
結論
上面那整段 code 都只需要本地運行,硬碟大概需要 10 GB 左右,但因為不需要 GPU ,所以一般手上的電腦都可以跑。
雖然是以 Confluence 爬蟲作為例子,但相信你們都可以發現,langchain 這個框架其實整合的相當好,大部分的工作都可以簡單幾行 code 就處理掉。即使是需要一定 know-how 的 rerank,也已經有內建的整合可以直接使用。
至於腳本內沒提到的 prompt 和 system instruction,那個必須根據你的用途和語料來動態調整,所以如果使用這個腳本還碰到不夠精準的回答,那多半是這兩點沒調校好。
Originally published on Medium