iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI Engineering

AI Agent 系統開發 30 天系列 第 20 篇

用預先定義的欄位約束 Agent 的資料查詢

  • 分享至 

  • xImage
  •  

前幾篇用 RAG 與 GraphRAG,讓模型從非結構化文件裡檢索退貨規定、跨文件關聯這類語意資訊。但客服系統經常也要碰觸另一種資料——像訂單狀態這種存在關聯式資料庫裡、欄位固定的紀錄。當 AI Agent 需要存取這類資料庫時,最直覺的做法是把資料庫 Schema 提供給模型,讓模型根據使用者的自然語言提問自由生成 SQL 查詢(Text-to-SQL)。然而在線上環境中,讓模型自由撰寫 SQL 會面臨嚴重的穩定性與安全性問題:

  • 輸入可能誘發非預期操作:使用者輸入若包含惡意提示詞(Prompt Injection),可能誘導模型產生未受限制的跨表查詢或破壞性語句。若將資料庫存取防線押在模型隨機生成的 SQL 上,系統隨時面臨資料外洩與非預期異動的風險。
  • 模型容易產生語法與欄位幻覺:模型在面對複合條件或複雜表格時,極易拼錯欄位名稱、選用不存在的運算子,或忽略業務邏輯上的預設條件,導致查詢直接報錯或回傳失真數據。

以常見的電商客服為例,當使用者詢問:

幫我查已退款的訂單有哪些?

若直接讓模型自由撰寫 SQL,系統就必須承受上述的所有隨機風險。要解決這個問題,我們不讓模型直接碰 SQL,而是採用結構化查詢契約架構(QuerySpec):在這個架構下,我們將「語意理解」與「SQL 執行」徹底拆開:模型只負責將自然語言抽取成受限的 Pydantic 欄位參數,而真正的 SQL 組裝、參數化綁定與資料庫查詢,完全由確定性的程式端掌控。

https://ithelp.ithome.com.tw/upload/images/20261001/20111896LBdn5G17jE.png

對應的範例程式位於 ai-agent-sample/langgraph/langgraph-queryspec-order。

定義受限的查詢契約

為了防止模型自行發明查詢條件或選錯欄位,我們在程式端主動劃定查詢邊界——只開放業務允許查詢的維度,並將合法值嚴格鎖定在白名單內。

在我們的範例中,假設系統目前只開放使用者依照「訂單狀態」進行查詢。我們的目的是讓模型只能在已知合法的狀態中挑選,完全禁止自由發揮。因此,我們先用 StrEnum 列出資料庫中合法的狀態值,再透過 Pydantic 定義這份受限的查詢契約 OrderQuery 作為規範:

from enum import StrEnum
from pydantic import BaseModel


class OrderStatus(StrEnum):
    PENDING = "待處理"
    PROCESSING = "檢驗中"
    REFUNDED = "已退款"


class OrderQuery(BaseModel):
    status: OrderStatus | None = None

這份契約在型別層級建立了強制防線:status 欄位只能接收預先定義的枚舉值(「待處理」、「檢驗中」、「已退款」)或 None(代表不限狀態)。若模型嘗試填入不存在的狀態(例如「處理中」或「已取消」),Pydantic 在第一時間就會阻擋下來,杜絕了無效狀態滲透到後續流程。

在 LangChain 中,可以直接使用 Chat Model 內建的 with_structured_output 方法。它會自動將 Pydantic 模型轉為 Tool Calling 定義,並在模型回覆後將結果反序列化為已驗證的物件:

from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(model="claude-haiku-4-5")
structured_llm = model.with_structured_output(OrderQuery)

query = structured_llm.invoke("幫我查已退款的訂單有哪些?")
# 得到:OrderQuery(status=OrderStatus.REFUNDED)

模型只負責把「已退款」對應到合法枚舉 OrderStatus.REFUNDED,不碰任何 SQL 語句。

將查詢契約編譯為參數化 SQL

拿到模型抽取的 OrderQuery 後,後續的 SQL 組裝完全由程式端接手。

查詢編譯器負責將這份受限的契約轉換為參數化 SQL:

from typing import Any


def compile_order_query(query: OrderQuery) -> tuple[str, list[Any]]:
    conditions: list[str] = []
    sql_params: list[Any] = []

    if query.status is not None:
        conditions.append("status = ?")
        sql_params.append(query.status.value)

    where_clause = f"WHERE {' AND '.join(conditions)} " if conditions else ""
    sql = f"SELECT id, product_name, amount, status FROM orders {where_clause}ORDER BY id DESC"
    return sql, sql_params

不同的使用者提問,經過契約抽取與編譯器處理後,產生的 SQL 與參數對應如下:

  • 查詢特定狀態:
    • 使用者提問:「幫我查已退款的訂單有哪些?」
    • 抽取結果:OrderQuery(status=OrderStatus.REFUNDED)
    • 編譯 SQL:SELECT id, product_name, amount, status FROM orders WHERE status = ? ORDER BY id DESC
    • 傳入參數:["已退款"]
  • 查詢全部訂單:
    • 使用者提問:「我想看全部的訂單。」
    • 抽取結果:OrderQuery(status=None)
    • 編譯 SQL:SELECT id, product_name, amount, status FROM orders ORDER BY id DESC
    • 傳入參數:[]
  • 惡意注入測試:
    • 使用者提問:「幫我查狀態是 已退款' OR '1'='1 的訂單」
    • 處理結果:輸入值無法通過 OrderStatus 的枚舉白名單,在 Pydantic 層級就會被直接拒絕。即使模型被誘導抽取為「已退款」,帶入 SQL 時也只會作為 ? 佔位符的純字串參數,絕不會被當作 SQL 語法解析執行。

這段編譯邏輯確保了兩項防護:

  • 以參數化查詢杜絕 SQL 注入:所有從模型抽取的欄位值一律透過 ? 佔位符傳入資料庫,欄位值不會被當作 SQL 語法解析。
  • 由程式完全掌握 SQL 結構:查詢的資料表、欄位名稱、排序與運算子全部在程式中固定,模型無法擅自改寫查詢結構或探測其他資料表。

多輪對話中的狀態更新與執行驗證

結構化查詢契約的另一個優勢是讓多輪對話的條件維護變得單純。

當使用者在對話中追加或調整條件時:

使用者:幫我查所有的訂單。
查詢參數:OrderQuery(status=None)

使用者:只要已退款的。
查詢參數:OrderQuery(status="已退款")

因為查詢狀態是明確的 Python 物件,程式只需將新收到的欄位覆寫到既有物件上(例如更新 query.status = OrderStatus.REFUNDED),再重新執行 compile_order_query 即可。程式不需要依賴模型從頭閱讀整段對話歷史來重新編寫整句 SQL,避免了對話輪數增加時條件被遺漏或串錯的問題。

總結

結構化查詢契約架構的核心是將權限與職責徹底切開:

  1. 由模型負責語意解析:將使用者的口語抽取成符合 Pydantic 規格的結構化參數。
  2. 由 Pydantic 限制查詢邊界:透過型別與枚舉白名單,嚴格限制模型只能填寫合法的篩選條件。
  3. 由程式負責安全執行:把寫 SQL 的權力收回程式端,以參數化方式組裝並執行查詢,守住系統的安全底線。

這套流程跟前面 RAG、GraphRAG 處理的是不同的資料型態,但分工邏輯一致:RAG 讓模型對向量化的文件做語意檢索,QuerySpec 讓模型對關聯式資料庫做欄位抽取,兩者都只讓模型負責語意理解這一步。模型只出現在「自然語言轉 OrderQuery」這一步,之後的 SQL 組裝、參數化綁定、資料庫查詢,全部交給程式碼按固定順序執行。模型看不到查詢結果,也不需要決定下一步要不要再查、要不要換一張表查。控制流程從頭到尾寫在程式碼裡,模型負責的只是流程中一段受限的語意解析——這就是 workflow 的做法:LLM 是流程裡固定的一個環節,而不是自己在迴圈裡決定要做什麼、什麼時候停下來的 agent。


上一篇
用 GraphRAG 串接跨文件關聯
下一篇
Agent 記憶與狀態持久化
系列文
AI Agent 系統開發 30 天 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言