快轉到主要內容

LangChain 與 LlamaIndex 比較 - RAG 多輪對話

這會是一系列的文章,從不同情境 use case 的實作去比較 LangChain 跟 LlamaIndex 的異同與優缺點,最後再總結

比較版本: LangChain 0.2.0 vs LlamaIndex 0.10.35


前一篇只考慮單次的一問一答。如果問第二個問題,AI 就像失憶:例如接著問「解釋更清楚一點」或「上一個問題怎麼解釋給小學生聽?」,LLM 不會知道你在問什麼,因為他不記得前一次的問答

在多輪對話中要用 RAG 有兩個挑戰

  • 怎麼記得每一輪的問與答?
  • 已經有多輪的問題跟答案了,那接下來面對使用者最新的問題,我要怎麼詢問資料庫?(單獨拿最新的問題?把所有的問題接起來?)

無論什麼框架,我採取 “condense + context” (或 question contextualization 意思一樣)

  1. 把歷史問答 + 新進來的問題用 LLM 「濃縮」成一個問題
  2. 拿濃縮過的問題,在資料庫中搜尋出相關資料,視作知識
  3. 把知識 + 歷史問答 + 新進來的問題交給 LLM 生成回答
Workflow: how to use RAG in multi-round chats
Condense + Context 流程三步驟

對於第一步讓我舉個例子。假設使用者跟 AI 有第一次問答:

人: 什麼是自由市場?
AI: 自由市場是一種經濟體系... (容我省略)

當使用者接著問 請解釋得能讓小學生聽得懂 時,我們希望第一步的 LLM 能彙整產生一個「問題」 (而不是回答問題):

你能用小孩子能聽得懂的方式解釋什麼是自由市場嗎?

實作比較 #

所有的實作都在這個 Github。餵資料的部分不變,所以建議比對 LangChain (la_rag.py) vs. LlamaIndex (ll_rag.py)

只看程式碼的最主要部分:

LangChain

  # Condense (using LangChain's helper function)
  history_aware_retriever = create_history_aware_retriever(
      llm, retriever, condense_prompt
  )

  # Answer (using 2 LangChain's helper function)
  question_answer_chain = create_stuff_documents_chain(
      llm, qa_prompt
  )
  rag_chain = create_retrieval_chain(
      history_aware_retriever, question_answer_chain
  )

  # Manage chat message history for rag_chain
  conversational_rag_chain = RunnableWithMessageHistory(
      rag_chain,
      _get_memory,
      input_messages_key="input",
      history_messages_key="chat_history",
      output_messages_key="answer",
  )

  print("====== conversation pass 1 ======")
  response_1 = conversational_rag_chain.invoke(
      input={"input": "要怎麼申請長照2.0?"},
      config={
          "configurable": {"session_id": "user-24601-conv-1337"}
      }
  )

LlamaIndex

    ...
    memory = _get_memory("user-24601-conv-1337")

    chat_engine = CondensePlusContextChatEngine.from_defaults(
        retriever=retriever,
        llm=llm,
        memory=memory
    )

    print("====== conversation pass 1 ======")
    response = chat_engine.chat("要怎麼申請長照2.0?")
    print(response)

「LlamaIndex 贏了!好短!」

直接講這一句不夠公平,不過的確可以看出一些端倪

LlamaIndex 封裝 #

針對這個運用,LlamaIndex 封裝成一個主要的 class。而 RAG 各個元素,從基礎的 LLM, BaseRetriever, 到 VectorIndexRetriever, BaseNodePostprocessor 等等, 每種可能的要素都是一個類別。乍看之下會比較漂亮,或者有敘述性、比較好追蹤實作。

當然複雜度還是有的:每個 class 裡很多 has-a 關係,而且參數可能傳很深、**kwargs 到地下好幾層。

反之,LangChain 主體就是 chain (Runnable),你也是鏈子我也是鏈子,難一眼看出。而範例內看到好幾個 helper function,目的零碎, 如果不仔細追究很難知道為什麼這個 chain 配上那個 helper 會有這種作用。

也就是,LangChain helper function 太 “helper”、零碎、不夠敘述性,比較 write-only。這種拼裝方式可能會造成很陡的學習曲線

LlamaIndex 一條龍 + 細膩處理 #

LlamaIndex 內建提供了多種對話模式/邏輯, 內建彙整問答的 prompt 文字, 甚至還考慮如果 prompt 太長的話,歷史對話記錄要拿哪幾個給 LLM 生成回答。

LlamaIndex 可以說是從架上挑了喜歡的就走

LangChain 靠的則是強大的官方文件,把很多 use case 情境都整理出來,所以要抄也沒煩惱(這點 LlamaIndex 也一樣就是),但很多地方內建提供的不一定那麼細膩

題外話,LangChain 官方範例提供的 prompt 在我測試之下,第一步並沒有濃縮問題而是回答了問題 – 不過應該調整 prompt 就好,跟 LangChain 本身沒有太大的關係

LangChain magic key 外露 #

我的 LangChain 範例可以看到一些寫死的 “key”,所謂的 magic 字串。

這是由於 LangChain 的某些設計,加上 LCEL Runnable 有種 functional language 的感覺(包括 partials / RunnablePassthrough …),在多變數函式寫起來很「字典」,外露 key 的名字,有些甚至寫死不能改。

LlamaIndex 也會,一般程式也會 – 只不過是化作函數的「參數名字」(signature) 跟物件的「成員變數名字」,大部分的情況下 IDE 都能正確推薦

客製化 #

無論是 LangChain 或 LlamaIndex 都能達到一定的客製化。LangChain 讓使用者拼裝的就不用說,以 LlamaIndex 的 CondensePlusContextChatEngine 為例, 在 constructor 傳進去的各個參數物件,像是 retriever, LLM, memory, prompt, …,都能自己選擇/實作。


總之他們就像樂高

  • LangChain 是很多基礎的積木:身為 workflow engine ,讓開發者拼裝
  • LlamaIndex 是一個產品包,且有不同功用的零件:注重 RAG 常見的情境

不過說到底,上面所講的全都偏向「寫程式喜好的風味」,倒也不是什麼致命的缺點。僅僅是 RAG 的話,我覺得了解抽象/整體的概念,遠比學什麼框架要重要得多


「聽起來你好像很推崇 LlamaIndex?」並不是!(還沒比較 serving 跟 instrumentation 呢)

如果想要作很複雜的流程,例如 RAG 回答後要讓 LLM 再自我反省呢?或是利用工具一步步拆解複雜問題呢?這就是 LangGraph 大顯身手的地方了!請看 下一篇的比較


若您覺得有趣, 請 追蹤我的Facebook 或  Linkedin, 讓你獲得更多資訊!