LLM 工具調用、Function Calling、Router Agent
在傳統做法中,我們與大型語言模型互動常依賴純文字提示,然後解析模型輸出的文字,這過程充滿不確定性。然而 LLM Function Calling(函式調用) 提供了一種結構化、可靠的途徑,讓 AI 系統能直接調用開發者定義的函式來完成任務。
簡單來說,就是替模型準備好一組「工具函式」,每個工具都有明確的名稱、參數和功能說明,模型在對話中可視需要選擇呼叫相應的函式來執行操作。這種方式為 AI 應用帶來了預期性和可控性:開發者可以明確限定模型能執行哪些動作,以及輸入輸出的結構,減少以往純文字互動中猜測與誤差。
從 OpenAI 的 GPT-4 開始,就支援了 function calling 功能,即模型可以在回答時產生一個 JSON,指定要調用的函數名稱和參數,交由應用程式執行後再將結果傳回模型。透過這種機制,語言模型搖身一變成為應用的中樞調度員:遇到需要外部資訊或操作時,不再拚命「編故事」,而是乾淨俐落地請求我們定義好的函式來完成。
例如,用戶問:「請建立一筆新的客戶聯絡人」時,模型可以直接呼叫我們預先提供的create_contact
函式,而不用我們額外解析文字意圖。這讓 AI 從單純的問答助理,升級成可以動手執行任務的智慧 Agent。
要讓 AI 能夠聽懂我們的指令並動手去操作 Odoo,首先要為它準備好「工具清單」。我們以 Odoo 中常見的三個模組為例:CRM、銷售、庫存,為每個模組設計一兩個實用的操作函式。這些函式將作為 AI 的「能力單元」,讓模型透過函式調用完成相應的任務。
在 OpenAI 的函式調用機制中,每個可用工具需要定義如下資訊:
我們來看範例定義。假設我們希望 AI 具備三項能力:新增潛在客戶(CRM 模組)、建立報價單(Sales 模組)、查詢產品庫存(Stock 模組)。我們可以定義如下的函式清單:
functions = [
{
"name": "create_crm_lead",
"description": "Create a new CRM lead with name and contact info",
"parameters": {
"type": "object",
"properties": {
"lead_name": {"type": "string", "description": "Name of the lead"},
"contact_info": {"type": "string", "description": "Contact details (phone or email)"}
},
"required": ["lead_name"]
}
},
{
"name": "create_sale_order",
"description": "Create a draft sale order with customer and product details",
"parameters": {
"type": "object",
"properties": {
"customer_name": {"type": "string", "description": "Customer's name"},
"product": {"type": "string", "description": "Product to order"},
"quantity": {"type": "number", "description": "Quantity of the product"}
},
"required": ["customer_name", "product"]
}
},
{
"name": "check_stock",
"description": "Check current stock level of a product by name",
"parameters": {
"type": "object",
"properties": {
"product_name": {"type": "string", "description": "Name of the product to check"}
},
"required": ["product_name"]
}
}
]
上述 functions
列表就是要提供給 LLM 模型的工具箱。當 ChatGPT 接收到使用者的請求時,我們會將這個工具列表一併傳給模型,模型就知道可以呼叫哪幾個函式,以及各自需要什麼參數。在對話過程中,模型如果覺得需要執行某個操作(例如「新增一筆潛在客戶」),就會回傳一段 JSON 內容,指明 function_call
要呼叫 create_crm_lead
,並附上填好的參數。應用端接收到這個函式呼叫指令後,便會真的去執行我們在後端實作的 create_crm_lead
函式,完成新增紀錄的動作,再把結果回傳給模型繼續對話。整個流程比起傳統讓 AI 輸出文字再去解析,要可靠、明確得多。
接著,我們看看伺服器端實際執行這些函式的設計。以下是示範的 Python 實作(邏輯略作簡化,著重在概念):
# 假設已取得 Odoo 環境變數 env,可操作 Odoo 模型
def create_crm_lead(lead_name, contact_info):
# 建立 CRM 潛在客戶紀錄
new_lead = env['crm.lead'].create({
'name': lead_name,
'contact_name': contact_info
})
return {"id": new_lead.id, "name": new_lead.name}
def create_sale_order(customer_name, product, quantity):
# 建立銷售訂單(簡化處理客戶與產品查找)
partner = env['res.partner'].search([('name', '=', customer_name)], limit=1)
prod = env['product.product'].search([('name', '=', product)], limit=1)
if not partner or not prod:
return {"error": "Customer or product not found"}
order = env['sale.order'].create({'partner_id': partner.id})
env['sale.order.line'].create({
'order_id': order.id,
'product_id': prod.id,
'product_uom_qty': quantity
})
return {"order_id": order.id, "status": "created"}
def check_stock(product_name):
# 查詢產品目前庫存數量
prod = env['product.product'].search([('name', '=', product_name)], limit=1)
if not prod:
return {"error": "Product not found"}
return {"product": product_name, "stock": prod.qty_available}
如上,我們定義了對應 create_crm_lead
、create_sale_order
、check_stock
的實際處理函式。每個函式透過 Odoo 的 ORM API 來執行資料庫操作,例如 env['crm.lead'].create(...)
新增一筆潛在客戶,或查詢產品的 qty_available
取得現有庫存數量。最後函式將結果組裝成結構化的 Python dict(最終會轉成 JSON)返回。
💡 Gary’s Pro Tip|結構化輸出函式結果
設計工具函式時,務必讓輸出結果結構化。也就是說,不論成功或錯誤,都以約定的 JSON 結構回傳,方便後續處理與模型理解。例如上例中,check_stock 不直接回傳一句「庫存有5件」,而是 {"product": "XYZ", "stock": 5}。這種結構化輸出有助於模型在後續回應時引用正確的資訊。同時,OpenAI 的函式調用機制在2024年8月後推出了**嚴格(Strict)**模式,可確保模型100%輸出符合提供的 JSON Schema。善用這點可以大幅降低解析錯誤,提升整體可靠性。
有了工具列表和對應的後端函式,我們的 AI Agent 雛形就具備「能聽會做」的本事了:當使用者提問需要存取 Odoo 資料或操作時,模型會決定使用哪個工具,一步到位完成任務。接下來,我們討論如何將這套工具機制整合進 Odoo 實際運行,主要介紹兩種典型架構。
第一種方案,是在 Odoo 系統之外建立一個獨立的橋接服務(Bridge Service),例如使用 Python 的 FastAPI 框架來打造。這個服務的角色相當於 AI 中介,集中定義和管理所有工具函式,並通過 API 調用與 Odoo 後端交換資料。
用戶的請求會送到這個 Bridge 服務內的 AI Agent。Agent 使用 OpenAI 的 API (Chat Completion) 與模型對話,並隨時準備好我們前面定義的一系列 functions
列表。當模型決定呼叫某個函式時,Bridge 服務會攔截到模型回傳的 function_call
,由服務內預先綁定的處理函式(例如前述的 create_crm_lead
等)來執行。
這些處理函式透過 Odoo 外部 API 操作 Odoo 資料庫:常見的是使用 XML-RPC/JSON-RPC 介面呼叫 Odoo 的模型方法。例如,我們可以使用 Odoo 提供的 RPC 權限,從 Bridge 服務連線至 Odoo,執行 models.execute_kw
來創建或查詢記錄。Odoo 原生就支援此類外部 API 存取,大部分模型的功能和資料都能透過 XML-RPC 從外部系統使用。
在這種模式下,Bridge 服務可以看作一個統一的大腦,對內管轄多個工具函式,對外與各系統溝通。優點是工具函式的定義與執行邏輯都集中在一處,方便維護和管理,且未來要擴充非 Odoo 的功能(例如再對接另一套系統)也容易,只要在此服務內新增對應函式即可。Agent 可以同時調用跨系統的工具,實現資料的跨域整合。此外,由於 Odoo 僅透過既有的外部API被存取,對 Odoo 本身的穩定性影響較小,不會直接執行未經允許的內部程式碼。
額外的 Bridge 層意味著部署和維護上多了一個服務,系統架構更複雜;而且每次函式調用都涉及網路請求往返 Bridge 服務與 Odoo,若頻率高可能在效能上稍有損耗。不過透過良好的設計(例如使用連線池或批次請求)可將影響降到最低。在企業應用中,這種微服務化的架構反而能提升彈性和擴充性,所以在許多情境下是值得的取捨。
第二種方案,是將上述工具函式直接內嵌在 Odoo 環境中。也就是在 Odoo 本身的自訂模組中定義這些函式,並提供給 AI 使用。相比橋接服務,這種方式讓 AI Agent 更貼近 Odoo 系統本身。
我們可以在 Odoo 模組中選擇兩種路徑來暴露工具函式:一種是利用 HTTP Controller 建立對外的 API 端點,另一種是透過 Python Decorator 直接標註哪些函式可被 AI 調用。
第一種方式,等於我們在 Odoo 裡寫了一組 REST API(或JSON路由)供 AI 呼叫,例如定義一個POST路由 /api/ai/create_lead
,呼叫時由 Odoo 執行對應的函式並回傳結果。這類端點可以設計為只能內網存取,然後由外部的AI服務(或前端介面)呼叫它們。
第二種方式則更進一步,假設我們有一個在 Odoo 內部運行的 Agent(例如利用 Odoo 的Scheduled Action或Server Action觸發AI對話),我們可以用Decorator註記哪些函式允許AI使用,Agent 程式在執行時自動掃描註記,將它們打包成 functions
提供給模型。這有點類似在Odoo內打造一個工具註冊機制,讓模組開發者透過簡單的語法就能把某方法加入AI工具清單。
無論採用哪種實現,在這種Odoo 原生整合模式下,AI 調用函式時其實已經處於 Odoo 環境內部,可以直接存取 ORM 資料,不需要經過外部RPC層,效率上會較佳。同時,因為所有操作發生在 Odoo 中,我們可以更細緻地運用 Odoo 本來的存取控制機制。例如,我們可以讓 AI 呼叫函式時使用特定的 Odoo 用戶身份(環境 env 帶有對應 user),確保該用戶有權限執行那些操作;或是在函式內部檢查 env.user
權限,決定是否允許繼續。這能避免 AI 執行超出許可範圍的動作,保護資料安全。
方法二的優點是在於減少系統耦合:不需要維護額外的服務,Odoo 自身就扮演了 Agent 執行者的角色。對開發者來說,也能直接利用 Odoo ORM 便利地撰寫工具函式。然而,潛在的缺點是 Odoo 本身需要承擔 AI 對話的負荷,包括跟OpenAI的溝通以及執行函式的計算。如果對話頻繁且操作複雜,可能對 Odoo 服務效能帶來壓力。
因此在佈署上,可能考慮將 AI 模組與關鍵業務模組分開不同的 Odoo 執行個體,或使用併發隊列來處理 AI 請求,避免阻塞正常使用者操作。由於本系列聚焦 Odoo x GenAI 實戰概念,我們不深入討論效能優化,但在實務上這也是需要留意的部分。
💡 Gary’s Pro Tip|確保 AI Agent 的操作權限
無論採用何種整合方式,都需要重視 Odoo 權限控管。建議為 AI Agent 專門建立一個具有受限權限的Odoo用戶,並僅授權它可使用我們定義的那些工具操作。例如,只允許 AI 建立客戶、查詢庫存,但不允許刪除資料或執行敏感操作。同時,可以在工具函式層面實作額外的安全檢查,確保關鍵操作需要額外的確認。確保 AI 調用函式時符合安全策略,安全設計不能馬虎,讓 AI 能做事的同時也要看緊它的權限邊界。
當我們提供了多種工具函式給 AI 後,一個現實問題是:模型會不會選錯工具? 尤其當工具數量增加、功能重疊時,如何確保 AI 根據使用者意圖挑對方法來執行,是我們必須考慮的。
這裡就引入所謂的 Router Agent(路由代理) 概念:在整個 Agent 系統中加入一層路由邏輯,用於分析使用者請求並決定後續由哪個工具(或子代理)來處理。
Routing 模式通常由一個分類器或路由代理來判讀使用者的意圖或問題領域,然後將請求導向對應的專業工具或子任務。
這特別適用於「多才多藝」的 AI 助理系統,它面對的提問可能跨越多個領域或功能。舉例來說,一個企業 AI 助理同時具備客服問答、下單處理、庫存查詢等能力,那麼我們希望有個機制先分辨用戶目前是在問客服問題,還是想下訂單、查庫存,之後再走相應流程。
實現 Router Agent 有幾種方式:規則判斷與模型判斷(可參考Day 13:專屬 AI 助理進階版:LLM Agent 模組的深度客製應用)。
規則判斷比較直接,例如可以根據關鍵字或句型來路由——用戶輸入若包含「庫存」「還有多少」等詞就判定屬於庫存查詢,調用庫存工具;出現「訂單」「報價」則判定使用Sales工具;遇到「新增客戶」「潛在客戶」就走CRM工具,以此類推。
這種決策樹可以人工設計,優點是可預測且不會偏離規則,缺點是靈活度有限,可能無法涵蓋所有語意變化。
另一種方法是借助 LLM 自己來做初步分類:我們可以有一個專門的 Router Prompt,把使用者的問題丟給模型,讓它輸出「屬於哪個工具/領域」的標籤。例如提示詞可以是:「請從下列類別中回覆最符合用戶要求的類別:CRM、Sales、Stock、None」,模型輸出一個類別,我們的程式再據此挑選對應工具。然而這種做法有時模型可能分類錯誤或不確定,因此通常會輔以篩選條件或信心評估。
事實上,如果我們善用 OpenAI function calling,模型本身已經會在多個提供的工具之間做選擇。只要我們的每個工具 description 寫得清楚,模型通常能夠用對函式。
但在一些複雜場景下(例如用戶一個問題需要同時調用多個工具才能完成),我們可能還是需要 Router Agent 拆解步驟或在工具之間分配子任務。
下面用一張流程圖來展示 Router Agent 的工作原理,以我們前述的 CRM/Sales/Stock 工具為例:
如上圖,Router Agent 就像一個守門員,先讀懂使用者的請求屬於哪種需求,接著把任務交給最合適的工具去完成。這個判斷過程本身可以由規則或另一個 LLM 來實現。一旦選定工具後,後續的函式執行與結果回傳流程就跟前面介紹的相同了。
實務中,Router Agent 的設計能讓整個 AI 系統更加模組化:我們可以隨時增加新的工具和對應的處理代理,而不必擔心 AI 因工具變多就手忙腳亂。各司其職的代理架構也提升了回應的精準度與效率。
透過函式調用與工具使用的設計,我們為 AI 接入企業系統打造了一條可靠的橋梁。總結而言,LLM 的 Tool Calling 概念讓模型不再只是一個回答問題的被動角色,而是能主動觸發企業內部的各種行動。
這種設計是打造 AI Agent 系統的關鍵基礎:有了它,企業才能放心地讓 AI 去執行實際操作、自動處理流程。在 Odoo 18 本地部署的環境中,我們幾乎沒有太多技術限制,可以充分利用 LLM 強大的函式調用能力,結合 Odoo 完整的 ORM API,實現AI 驅動的業務自動化。