LCEL 與 Agent

LCEL 全名 LangChain Expression Language, 是一種描述 LangChain 架構的語言。最大的特徵就是看到 A = B | C | D 這種表示法 – 說明了 LCEL 串接多個可執行單元的特性,是 LangChain 的進階實現。而 ‘|’ 的理解和 Linux 的 pipe 很像,它是非同步的串接。

舉例來說,一個 LLM 的 LangChain 可以表示為:

chain = prompt | LLM model | Output Parser

依序做好這三件事:有方向性的提示、強大的大語言模型、友善的輸出格式,就可以提升使用者體驗。

但是顯然這樣還不太夠,比方說,LLM 需要查一筆它沒被訓練過的資料,在上面的 pipe 就無法做到。換個例子,我想幫 AI 助理取個名字,它也會說好。但是一轉眼什麼山盟海誓都忘光了!

顯然,我們要有個負責的 agent,把 function call 的能力加進來;而且 call 完之後,還要餵給 LLM 做 “人性化" 的自然語言潤飾。這是一個循環的路徑,直到 AI 判斷它不再需要任何外部工具,就可以結束迴圈,跟使用者報告最終結果了。

那麼這個 code 長什麼樣子? 首先把基本元件準備好:

# 初始化模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 將工具打包成列表
tools = [get_current_weather] # 以問天氣的函式為例

# 給出配合 function 的 prompt
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是天氣助理,請根據工具的結果來回答問題。"),
        ("placeholder", "{chat_history}"),  # 預留給歷史訊息
        ("human", "{input}"), # 真正的輸入
        ("placeholder", "{agent_scratchpad}"), # 於思考和記錄中間步驟
    ]
)

創建 agent

# 創建 Tool Calling Agent
# 將 LLM、Prompt 和 Tools 組合起來,處理 Function Calling 的所有複雜流程
agent = create_tool_calling_agent(llm, tools, prompt)

執行 agent

# 創建執行器, 跑 模型思考 -> 呼叫工具 -> 再次思考 -> 輸出答案的循環
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

設計人機介面

def run_langchain_example(user_prompt: str):
    print(f"👤 用戶提問: {user_prompt}")
    
    # 使用 invoke 運行 Agent,LangChain 會自動管理多輪 API 呼叫
    result = agent_executor.invoke({"input": user_prompt})
    
    print(f"🌟 最終答案: {result['output']}")

測試案例

# 示範一:Agent 判斷需要呼叫工具
run_langchain_example("請問新竹現在的天氣怎麼樣?請告訴我攝氏溫度。")

# 示範二:Agent 判斷不需要呼叫工具,直接回答
run_langchain_example("什麼是 LLM?請用中文簡短回答。")

這邊要留意的是,第一個問天氣的問題,LLM 顯然要用到 tool 去外面問,第二個問題不需要即時的資料,所以它自己回答就好。

意猶未盡的讀者可能好奇 function 長怎樣? 其實就很一般,主要的特色是用 @tool decorator。這樣可以獲得很多好處,最重要的一點是 agent 都認得它的 JSON 輸出,方便資料異步流動。

# LangChain 會自動將這個 Python 函數轉換成模型可理解的 JSON Schema
from langchain_core.tools import tool

@tool
def get_current_weather(location: str, unit: str = "celsius") -> str:
    if "新竹" in location or "hsinchu" in location.lower():
        return "風大啦! 新竹就是風大啦!"
    else:
        return f"我不知道 {location} 的天氣啦!"

另外,追根究柢的讀者可能想問,code 裡面怎麼沒看到 ‘|’? 它跑那裡去了? 沒錯,上面講的都是 agent,它比較進階,可以動態跑流程。反而 LCEL 只是 LangChain 的實行方式,它就是一個線性的 chain。

我們由奢返儉,回過頭來看 LCEL,它不能跟 agent 比,只能打敗傳統的 LangChain。標為紅色的是 LCEL 特色的 coding style。

def demonstrate_legacy_chain():
 
    # 定義模型
    llm = ChatOpenAI(temperature=0)
    
    # 定義 Prompt Template
    template = "Translate English text to Chinese: {text}"
    prompt = PromptTemplate(template=template, input_variables=["text"])
    
    # 建立 Chain (透過類別組合)
    # 缺點:語法較冗長,看不到資料流向,且 output 通常包含原始 meta data
    chain = LLMChain(llm=llm, prompt=prompt)
    
    # 執行
    input_text = "Make American Great Again!"
    result = chain.run(input_text)

VS

def demonstrate_lcel():
    
    # 定義模型
    model = ChatOpenAI(temperature=0)
    
    # 定義 Prompt Template, 使用更現代的 ChatPromptTemplate
    prompt = ChatPromptTemplate.from_template("Translate English text to Chinese: {text}")
    
    # 定義 Output Parser (將 AI Message 轉為純字串)
    output_parser = StrOutputParser()
    
    # 建立 Chain (使用 Pipe '|' 運算符)
    # 優點:Unix 風格管道,由左至右邏輯清晰,易於修改和擴展
    chain = prompt | model | output_parser
    
    # 執行
    input_text = "Make American Great Again!"
    result = chain.invoke({"text": input_text})

這兩個都是一次性的 Q&A。

  1. Functions, Tools and Agents with LangChain

LLM 學算術

上週一位前同事貼了篇文章:

看一個YouTube 影片測試Gemini and Chatgpt, 一個簡單的邏輯問題,一瓶汽水一元,兩個空瓶可以換一瓶新的汽水,請問20元可以喝到幾瓶汽水,兩個都答錯,

原本以為寫程式是個邏輯能力的展現,但是GenAI 邏輯不好為何可以寫程式呢?

然後臉友們試了各個 LLM, 沒有一個可以正確解答這題. 我試的 copilot 一開始答錯, 算出 30 瓶也就罷了. 提示它這是個遞迴的問題, 它竟然回答:

你說得對,這確實是一個遞迴的問題。讓我們再次計算一下,…

總結來說,你可以喝到無限多瓶汽水,只要你不停地用兩個空瓶換取新的汽水。

希望這個解答對你有幫助!

那麼正解是什麼呢? 簡單的解法是使用 chain of thought (COT), 很像是建構式數學. 我們要按照人類的思路一步一步地教它. 雖然我下面的描述提到一點 Action, 而不是純 Reaseon, 但 RecAct [3] 才是終極版結合 reason 和 action 的提示方式.

  1. 首先 20 元可以買 20 瓶汽水.
  2. 喝完 20 瓶汽水得到 20 個空瓶, 可以換 10 瓶新的汽水.
  3. 喝完 10 瓶汽水又得到 10 個空瓶, 可以換 5 瓶汽水.
  4. 喝完 5 瓶汽水又得到 5 個空瓶, 可以換 2 瓶汽水, 奇數的汽水產生一個空瓶還沒有兌換.
  5. 喝完 2 瓶汽水又得到 2 個空瓶, 可以換 1 瓶汽水.
  6. 喝完 1 瓶汽水又得到 1 個空瓶, 加上還沒兌換的 1 個空瓶, 又可以換一瓶汽水.
  7. 喝完 1 瓶汽水又得到 1 個空瓶 沒有還沒兌換的空瓶, 不能再喝了.

However, Copilot 無法學會. 繼續堅持 30 瓶的答案. 這招對它無效. 但是 ChatGPT 學得會 20 元的例子. 但改問它 10 元, 30 元可以喝幾瓶就沒辦法了.

根據我的理解, LLM 不是 encoder only, decoder only, 就是 encoder + decoder. 無論哪一類模型, 都要把文字轉向量. 在向量空間中, 類似的詞都有類似的向量. 所以狗, 犬, 㹴的向量都彼此靠近, 和貓距離比較遠. 以 encoder 類 LLM 來說, training 時隨機遮掉句子中的一個詞, 讓 LLM 學習填空. 學得再不完美, 關鍵字也會是同義字.

但是數字 1,2,3,4… 就沒有相關性了. 它本身沒意義, 很多個 1 也比 1 個 9 大 (11111…111 > 9). 因此看到數學很容易胡說八道, 但背公式可以.

想要徹底解決 LLM 數學不好的問題, 是採用 program-aid Language (PAL) model [1]. 也就是說, 我們叫 LLM 寫個小程式, 把原理寫成註解, 計算寫成數學式. 把這整個當作 prompt, LLM 應該就能學會了.

我不打算暗示 LLM 怎麼寫出 Python script, 於是我試著這樣問:

一瓶汽水一元,兩個空瓶可以換一瓶新的汽水,請問20元可以喝到幾瓶汽水?

# 一瓶汽水一元

Buy_bottle = money / 1

# 喝汽水

Used_bottle = buy_bottle

#  假如 used_bottle != 0, 兩個空瓶可以換一瓶新的汽水

While  used_bottle >= 2, do the following calculation

If used_bottle %2 = 0, buy_bottle += use_bottle / 2, used_bottle = 0

Else if used_bottle %2 = 1,  buy_bottle += use_bottle / 2,  used_bottle = 1

Answer = buy_bottle

結果 ChatGPT 算出 45 瓶, 因為它換過的汽水瓶又拿去換. 反倒是 Copilot 比較好一點. 雖然功敗垂成, 但是好歹它有聽進去我的話了~~~ 當然我的提示也有容易誤解的地方, 當我想寫得更詳細, 它卻答得更差. 所以我只能保留這一版.

這個問題是一個經典的換瓶問題,可以通過迭代計算來解決。讓我們根據你提供的算法來計算一下:

  1. 首先,用20元可以買到20瓶汽水。
  2. 喝完這些汽水後,你會有20個空瓶。
  3. 根據算法,每兩個空瓶可以換一瓶新的汽水,所以你可以換10瓶新的汽水。
  4. 現在你又有10個空瓶,可以再換5瓶新的汽水。
  5. 接著你又有5個空瓶,可以換2瓶新的汽水,並且還剩下一個空瓶。
  6. 最後,你可以用剩下的3個空瓶(之前剩下的1個加上剛換的2瓶的空瓶)換1瓶新的汽水。

所以,總共可以喝到的汽水數量是:

20+10+5+2+1=38 瓶汽水。

因此,用20元可以喝到38瓶汽水。🥤

自己都說是經典了還這樣. 那麼還有別招嗎?

有的. 這個技術的總稱叫做 LangChain [2]. LangChain 是個模組化的架構, 讓 LLM 更容易和應用程式結合. 它可以有很多 agent, 每個 agent 有自己的 memory, tools, 和 prompt template. 可以針對複雜的 prompt 拆成不同的屬性, 各自用適合的 tool 去處理. 至於 tool 最後會做什麼事情, 不是預先設定好的, 而是根據 LLM 的輸出決定的.

既然 LangChain 有個 chain 字, 我就從網路上找個 agent 內部有 chain 的圖示意.

本圖取材自 https://www.langchain.cn/t/topic/35

再回溯 Chain of Thought (COT) 和另外一個 Tree of Thought (TOT), 前者也提到chain, 他們和 LangChain 又是甚麼關係呢? 它們都只是輔助 LLM 推理的方法, COT 就是 step by step, TOT 就是找不到方法是會換一條思路. 它們都只是強調推理 (reason) 的部分, 有 action 才會去執行指令, 做成可互動的應用程式 (ReAct) [3].

[REF]

  1. https://arxiv.org/abs/2211.10435
  2. https://www.langchain.com/
  3. https://react.dev/