LangChain 與 LlamaIndex 比較 - ReAct agent
目錄
這會是一系列的文章,從不同情境 use case 的實作去比較 LangChain 跟 LlamaIndex 的異同與優缺點,最後再總結
- Naive RAG
- Conversational RAG
- Simple agent / tool use (這篇)
- 人類半規範的 Agentic flow
- 總結 (敬請期待)
比較版本: 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)
他們將兩者合併,藉由不斷執行「思考、行動、觀察」三步循環,逐步拆解複雜問題或面對未知世界
為了達到這個效果,論文提到了 in-context learning 與 finetuning。前者,白話來說就是 在 prompt 的前面加一些「思考行動觀察」的例子當作示範。 讓我舉例怎麼在 LlamaIndex 上實作
實踐的想法 - prompt engineering #
ReAct prompting 是在 system prompt 裡面解釋 ReAct 手法、有哪些工具、以及「思考行動觀察」的例子 (請看 ll_agent_with_custom_prompt.py 跟 system 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,我們期待
- (assistant message) LLM 輸出思考 + 行動指令
- 我們/框架讀 LLM 的行動指令,去呼叫工具 function
- (user message) 我們/框架把工具 function 的結果給 LLM 當作觀察
一直循環,直到 LLM 覺得可以結束並回答。也就是,為了回答一個使用者的問題,上面這三步可能執行非常多輪!
三步循環中,第一步是 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 的時機:
- OpenAI API 回傳 Assistant message 的
content
是空的,但附有tool_calls
告訴你 LLM 期待你去呼叫哪個 tool 以及呼叫的參數 - 你/框架去呼叫 tool,得到結果
- 你/框架傳送給 OpenAI API 一個 Tool message,包括 tool 輸出的結果,以及是對應前面哪一個期待
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
,執行到這就結束一輪。
- 有一個特殊 node 叫
- Edge: 「決定流程」:node 可以從 edge(s) 連到任何 node;一個 node 執行完,流程就隨著 edge 到下一個 node 準備接下來執行
- 可以有 conditional edge : 例如設定 n1 可能連到 n2 或 n3,我們定義好條件(通常是根據 state 裡的資料)
- 之後 n1 執行完以後會跑這個條件函數,判斷流程要去 n2 還是 n3
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 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.py 跟 llamaindex_try.py 吧
這篇內容有點多,大部分講 ReAct,再加上 LangChain 與 LlamaIndex 對於 Agent 的基礎建設
我對 LangChain agent / LangGraph 的概念與實作做了 很詳盡的三部曲 過去看看吧!