快轉到主要內容

LangChain 與 LlamaIndex 比較 - ReAct agent

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

比較版本: LangChain 0.2.0 vs LlamaIndex 0.10.35


前兩篇比較了 RAG,接下來比較 agent / agentic flow 跟環境互動!

本篇從 ReAct 模式三本柱,到 ReAct 的實踐,再示範 LangChain 與 LlamaIndex 宣稱的 ReAct agent (tool use) 實作,最後比較兩者對 agentic flow 的設計

所有的實作都在這個 Github, 務必看 README 與本文了解每個檔案的用途

ReAct: 推理 + 行動 #

ReAct (Reasoning + Acting) 是 Shunyu Yao 等人 2022 發表的,讓 LLM 回答更準確的一個方法論

  • 工具 (tool) 能讓 LLM 跟環境互動,或者獲得本身未知的知識
  • 讓 LLM 自我思考推論也能幫助處理複雜的問題 (e.g. CoT)

他們將兩者合併,藉由不斷執行「思考、行動、觀察」三步循環,逐步拆解複雜問題或面對未知世界

The concept and flow of ReAct
ReAct 概念與流程

為了達到這個效果,論文提到了 in-context learning 與 finetuning。前者,白話來說就是 在 prompt 的前面加一些「思考行動觀察」的例子當作示範。 讓我舉例怎麼在 LlamaIndex 上實作

實踐的想法 - prompt engineering #

ReAct prompting 是在 system prompt 裡面解釋 ReAct 手法、有哪些工具、以及「思考行動觀察」的例子 (請看 ll_agent_with_custom_prompt.pysystem prompt 範例

一個「思考行動觀察」的例子大概長這樣

問題:長方形 X 長寬各為 6, 9,正方形 Y 邊長 5,兩者面積總共多少?

思考:答案是兩個未知的面積加起來,
      所以我要先算出兩者的面積,然後再加起來。
      先算 X 的面積,長方形面積是長乘以寬
行動:呼叫 multiply (6,9)
觀察:54

思考:已知 X 面積為 54,所以接下來算 Y 的面積
      正方形面積是邊長乘以邊長
行動:呼叫 multiply (5,5)
觀察:25

思考:已知 X 面積為 54,Y 面積為 25,
      所以我用加法算出兩者總共的面積
行動:呼叫 add (54,25)
觀察:79

思考:已經可以回答
行動:回答 79

有了 system prompt 以後,將使用者的問題放在 user message,我們期待

  1. (assistant message) LLM 輸出思考 + 行動指令
  2. 我們/框架讀 LLM 的行動指令,去呼叫工具 function
  3. (user message) 我們/框架把工具 function 的結果給 LLM 當作觀察

一直循環,直到 LLM 覺得可以結束並回答。也就是,為了回答一個使用者的問題,上面這三步可能執行非常多輪!

The trace of running ReAct
ReAct 執行步驟

三步循環中,第一步是 LLM 生成,LlamaIndex 會幫你做第二步跟第三步。所以設定好 LlamaIndex 的 agent,你只需要把最初的問題丟過去就自動解決!

# 設定可用工具,創造 ReAct agent
agent = ReActAgent.from_tools(
    [multiply_tool, add_tool], llm=llm,
)

# 使用自己客製的 system prompt
agent.update_prompts(
    {"agent_worker:system_prompt": react_system_prompt}
)

# 呼叫,收工
response = agent.chat("What is 20+(2*4)?")

不過!ReAct 就品質來說,我實際測的結果不是每次都很美好,有時候:

  • 「思考」與「行動指令」矛盾:思考認為可以給答案了,但行動卻不回答而是使用工具
  • 「觀察」不會反應在下一步的「思考」:當我故意把 乘法 的運算弄錯時,思考卻用了正確的乘法結果,就像 LLM 自己知道怎麼做乘法,而不理會觀察

有可能是因為四則運算過於簡單,不像搜尋之類的。總之要根據實際情境去測試

原論文 #

原汁原味的 prompt 和實作請參考原作程式原論文的 Appendix C

他們在那個時代不是用 chat API, 而是用 completion (沒有角色的文字接龍) 並設定暫停條件:如果看到了 觀察: 就停下來,因為這時候要呼叫工具了

兩框架 #

  • LlamaIndex 已經有 class 可直接用。然而預設的 prompt 不夠原汁原味且無引導「思考」;每一次的思考產生都是「我應該用工具幫我」 – 好險客製化不難。
  • LangChain 我一時找不到現成的可套用

不過呢,兩者皆在官方範例放了另一種實作方法:LLM 的 tool use / function calling

實踐的想法 - tool use #

一些公司像是 OpenAI 提供的 API 直接支援 tool use (function call):你不用像之前把 tool 資訊寫在 prompt,而是把 tool 敘述與用法直接給 API

這 API 背後的原理大概也是他們的程式幫你寫 prompt 。手法雖類似,總之你不用自己刻 prompt 了

以 OpenAI 為例,訊息的種類除了 system, user, assistant 以外,還有一種 “tool” 訊息。 在對話的過程中,如果碰到 OpenAI 覺得需要利用到 tool 的時機:

  1. OpenAI API 回傳 Assistant message 的 content 是空的,但附有 tool_calls 告訴你 LLM 期待你去呼叫哪個 tool 以及呼叫的參數
  2. 你/框架去呼叫 tool,得到結果
  3. 你/框架傳送給 OpenAI API 一個 Tool message,包括 tool 輸出的結果,以及是對應前面哪一個期待
The message trace when tool-use is triggered
Tool use 執行步驟 (其實可以一次多個 tool messages)

LangChain 跟 LlamaIndex 都把這視作 ReAct – 雖然單純看這模式,只有行動 (Act) 而沒有顯性的思考推論 (Reasoning), 不過反過來說,思考也可以跟行動脫鉤,而且我也不知道 OpenAI 背後從 API 到 LLM 有沒有其他魔法

題外話,偶爾這也有「觀察」不會反應在下一步的「思考」的缺陷

框架實作比較 #

Tool use 的實踐:兩個框架都是用他們的「Agent 設計」去實作

假設使用支援 tool use 的 LLM API(以 OpenAI 為例),兩個框架的主要程式比較:

LangChain / la_agent_with_tool_llm.py

  llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.001)
  graph = create_react_agent(
      llm, tools=[multiply, add],
      checkpointer=MemorySaver()
  )

  question = "What is 20+(2*4)? Calculate step by step"
  response = graph.invoke(
      {"messages": [HumanMessage(content=question)]},
      config={"configurable": {"thread_id": "user-24601-conv-1337"}}  # magic strings
  )

LlamaIndex / ll_agent_with_tool_llm.py

  llm = OpenAI(model="gpt-3.5-turbo", temperature=0.001)
  agent = OpenAIAgent.from_tools(
      [multiply_tool, add_tool], llm=llm
  )

  question = "What is 20+(2*4)? Calculate step by step"
  response = agent.chat(question)

幾乎一樣吧!(表面上)

工具的部分,都可以自訂 function。只要 docstring 好好描述這 function 的用途,兩個框架都是多加一行程式

# LangChain: 多加 decorator
@tool
def add(a: int, b: int) -> int:
    """Add two integers and returns the result integer"""
    return a + b


# LlamaIndex: 多包一層
def add(a: int, b: int) -> int:
    """Add two integers and returns the result integer"""
    return a + b

add_tool = FunctionTool.from_defaults(fn=add)

因為 tool use 很常用,兩個框架都包起來讓你方便呼叫 – 表面上沒什麼差別,實際上兩者背後的實作理念大不相同

兩者對於 Agent 的設計理念 #

大致上,“Agent” 是指有智慧的代理人:能計畫、使用工具、跟使用者與環境互動、有記憶等等,來完成你的目標

從這角度看,ReAct 模式的 LLM 可說是個 agent ; 但一個 agent 的模式不一定是 ReAct, 可以是非常複雜的流程,無論是 LLM 發想的流程,或是人類預先設定好的流程

而 LangChain 跟 LlamaIndex 用不同的設計去想辦法實踐 agent。簡單說

  • LangChain 是靠 LangGraph,用的是 graph 控制流程
  • LlamaIndex 是用 Todo list/queue 的方式控制流程

LangChain agent #

LangGraph 是 LangChain 生態系的一環,也是 v0.2 主打 (Update: 請看我的 LangGraph 教學介紹

利用 nodes (節點) 跟 edges (有向邊) 組成 graph,來處理你的 state

  • State: 你感興趣的資料,例如訊息歷史、工具結果、中介資料、計數…
  • Node: 「做事情」:上面綁 Runnable / function,例如呼叫 LLM, 工具搜尋網路…
    • 有一個特殊 node 叫 END,執行到這就結束一輪。
  • Edge: 「決定流程」:node 可以從 edge(s) 連到任何 node;一個 node 執行完,流程就隨著 edge 到下一個 node 準備接下來執行
    • 可以有 conditional edge : 例如設定 n1 可能連到 n2 或 n3,我們定義好條件(通常是根據 state 裡的資料)
    • 之後 n1 執行完以後會跑這個條件函數,判斷流程要去 n2 還是 n3
Illustration of LangGraph on ReAct
以 ReAct / tool use 為例的 LangGraph

Graph 是預先定義好需要 compile 的,所以比較動態的流程決定,會需要 conditional edge

和 LangChain 類似,LangGraph 是進階版的 workflow engine

  • 可以循環 (cyclic directed graph)
  • 對全域 state 操作
  • 有 memory 對不同使用者儲存每個流程跑到哪 (checkpointer)
  • 因為是有 memory 的 state machine,可以人工後悔、重跑、更改 state 後重跑等等
  • Whole-graph snapshot resolve (這詞我亂掰的, 請看 langgraph_try.py 吧)

LlamaIndex agent #

LlamaIndex 裡的 agent 是「老闆 - 工人」架構,以 TODO list (step_queue) 變相達到流程控制

  • 老闆 AgentRunner: 只監督維護,例如擁有 state, step_queue, memory 等等
  • 工人 AgentWorker: 真正做事的:執行任務(呼叫 LLM, 工具…),產生未來要做的任務、決定是否該停止一輪了…
    • 也就是,無論「做事」或「產生未來做什麼」都是 worker 的事

一個 AgentRunner 只有一個 AgentRunner 物件

LlamaIndex high level flow
LlamaIndex agent 設計概念

LlamaIndex agent 有一個設計我覺得要小心的是,step_queue 是放在 Runner

  • 每次 Runner 會從 queue pop 一個任務給 Worker 做,但不檢查 queue 空了沒(也就是有可能例外噴錯)
  • Worker 執行完一個任務後,去產生未來要做的任務,傳回去給 Runner 放到 queue 裡
  • Worker 回傳給 Runner 時,要順便決定是否「流程該結束了」(就算 queue 裡還有任務)
  • Worker 看不見摸不到 Runner 的 queue

雖然所有的任務都是 Worker 執行並決定,但在實作時就要很小心;如果流程有 diamond shape 的話也要留意

但不是你想像的 #

你以為的 graph, 你以為的 todo queue 並不一定是兩個框架真實的實作

我就不在這篇解釋了,而是用 LangChain 跟 LlamaIndex 各別寫了玩具範例,有興趣的話看看 langgraph_try.pyllamaindex_try.py


這篇內容有點多,大部分講 ReAct,再加上 LangChain 與 LlamaIndex 對於 Agent 的基礎建設

我對 LangChain agent / LangGraph 的概念與實作做了 很詳盡的三部曲 過去看看吧!


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