什麼是 Model Context Protocol (MCP)?PostgreSQL 打造即時 AI 應用的關鍵技術
模型上下文協定(Model Context Protocol, MCP)是由 Anthropic 釋出的開放標準,它就像 AI 界的 USB-C,提供了一個統一的協定,讓大型語言模型(LLM)能夠安全、即時地連接外部資料來源與開發工具。
如果要求 LLM 針對正式環境資料庫寫 SQL,寫出來的結構通常看起來正確,但可能引用了不存在的資料表、半年前就改名的欄位,或憑空捏造 JOIN 條件。這會導致查詢直接失敗,甚至面臨更糟情況:指令成功執行,卻回傳了錯誤的數據。
為了解決 LLM 亂寫 SQL 的問題,目前的作法是透過 MCP,利用 PostgreSQL 的原生架構來確保查詢的準確性。
技術背景與初步嘗試
在 2024 年初還沒有 MCP 或 AI Agent 的概念。為了讓聊天機器人能串接 API 取得即時資料,當時通常採用比較拼湊的流程:
- 把 OpenAPI spec 餵給 LLM。
- 要求 LLM 只能回傳
curl指令。 - 透過 Go 函式去執行這個
curl指令。 - 把執行的結果再丟回給 LLM,讓它轉換成一般人看得懂的回答。
User: "What's the CPU usage and total DB connections right now?"
-> LLM + API Spec -> curl -X GET https://portal.curiousone.in/api/v1/metrics?names=cpu,db_conn
-> execute() -> { cpu: 72%, conn: 84 }
-> LLM: "CPU is at 72% and there are 84 active DB connections."
這做法雖然可行,但完全無法擴充。每增加一個新資料來源,就得寫一套新的 parser;而且 Prompt 工程脆弱,必須針對每一種 spec 或 schema 重新調整。
後來也發現不少團隊在開發這類應用時,也都面臨一模一樣的挑戰。直到 Anthropic 釋出 MCP作為開放標準,問題才得到解決。
為何 LLM 總是寫錯 SQL?
LLM 其實看得懂 SQL 語法,也能寫出包含 CTE、子查詢的複雜指令。但它無法得知資料表名稱、欄位名稱或關聯性,只能單純根據訓練資料的盲猜。
對比如下:
| LLM 生成的內容 | 正式環境實際存在的內容 |
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id | SELECT c.full_name, o.total_price FROM app_customers c JOIN customer_orders o ON c.id = o.customer_id |
結果: ERROR: relation “users” does not exist | 結果: SUCCESS: 47 rows returned |
LLM 猜測資料表叫 users,但實際上是 app_customers;猜測欄位叫 name,但實際上是 full_name;猜測外鍵(Foreign Key)是 o.user_id,但實際上是 o.customer_id。它的每一次猜測聽起來都很合理,但結果全都是錯的。
上下文斷層(Context Gap)
這類錯誤歸根究底,都是因為缺乏上下文所導致的:
| 錯誤類型 | 範例 | 缺少的上下文 |
| 資料表錯誤 | FROM users | 實際的資料表是 app_customers |
| 欄位錯誤 | SELECT email FROM orders | 該資料表根本沒有這個欄位 |
| JOIN 錯誤 | ON users.id = orders.id | 應該要是 orders.user_id |
| 型態錯誤 | WHERE created_at = '2024' | 該欄位是 timestamptz,而不是字串(String) |
| 資料庫語系錯誤 | DATEADD(day, 7, now()) | 這是 SQL Server 的語法,不是 Postgres |
解決這個問題的關鍵並不是去寫更好的 Prompt,而是要在 LLM 進行查詢的當下,直接讓它讀取到真實的資料庫 Schema。
解決方案:Model Context Protocol (MCP)
MCP 是一種開放標準,主要用來連接 AI 模型與外部資料來源和工具。
在技術架構上的核心特點是:單一 MCP 用戶端可以連接多個 MCP 伺服器,而每個伺服器各自封裝不同的資料來源。以 Postgres 運作來說,代表同一個 AI 助理可以同時存取多個資料來源,例如:
┌─ Postgres MCP Server ──→ PostgreSQL (live schema & queries)
│
MCP Client ─────────┼─ Filesystem MCP Server ─→ PG Logs (log file analysis)
(Claude, IDEs) │
└─ Prometheus MCP Server ─→ Telemetry (metrics & alerting data)
在這個架構中,Client 端就是 AI 應用程式,而每個 Server 端則提供工具供 LLM 呼叫,彼此透過 stdio 或 SSE 傳輸協定來跑 JSON-RPC 2.0。LLM 會根據使用者的問題,自己決定要呼叫哪一個 Server。
舉例來說:如果詢問的是 Schema 相關問題,它會去找 Postgres MCP;但如果問的是「為什麼昨天晚上資料庫變慢?」,就可能同時呼叫 Filesystem MCP(去看 Log 紀錄)和 Prometheus MCP(去抓指標數據)。
核心組成要素
在 MCP 的三種基本元件中,資料庫應用最核心的是 Tools:
- Tools: 這是供 LLM 呼叫的函式,會直接回傳即時結果。例如
execute_sql、list_schemas、list_objects等,這類工具回傳的是最新狀態,而不是過期的快照。 - Resources: 透過 URI 提供的唯讀靜態資料。如果把資料庫 Schema 當成這種靜態資源發布,資訊很快就會過期,因此並不適合用來處理動態的資料庫元資料。
- Prompts: 封裝了上下文與指令的重複使用範本。雖然實用,但在資料庫的操作中屬於輔助角色。
PostgreSQL 結合 MCP:pg-airman-mcp
pg-airman-mcp 是一款針對 PostgreSQL 設計的 MCP 伺服器,能讓 LLM 即時存取資料庫的結構與資料。以下是它所提供的核心工具:
| 工具名稱 | 功能說明 |
| list_schemas | 列出所有資料庫 Schema,並自動區分為系統或使用者,是讓 LLM 探索資料庫結構的起點。 |
| list_objects | 列出特定 Schema 內的資料表、檢視表、序列、函式以及相關註解。 |
| get_object_details | 獲取特定物件的詳細資訊,包含欄位、資料型態、條件約束、索引以及註解。 |
| explain_query | 執行 EXPLAIN 執行計畫,並能在不實際建立索引的情況下,透過 HypoPG 模擬虛擬索引的效果。 |
| execute_sql | 執行 SQL 查詢,具備可自訂的存取控制、唯讀模式與安全的 SQL 解析機制。 |
| analyze_query_indexes | 評估上千種可能的索引組合,找出最符合當前工作負載的最佳化配置。 |
查詢流程
當使用者詢問「幫我列出營收前五名的客戶」時,LLM 不再用盲猜的方式,而是依序執行以下步驟:
- 呼叫
list_schemas: 確認目前有哪些可用的 Schema。 - 呼叫
list_objects: 取得真實的資料表與檢視表(View)名稱。 - 呼叫
get_object_details: 掌握欄位、資料型態、條件約束與索引。 - 呼叫
explain_query: 驗證執行計畫並檢查效能。 - 呼叫
execute_sql: 在具備存取控制與安全解析的機制下執行查詢。
到了第 3 步,LLM 已經明確知道資料表叫 app_customers、欄位是 full_name,且外鍵是 customer_id。整個過程完全不需要任何猜測。
透過 pg_catalog 讓 Postgres 自行維護元資料
這套架構最核心的優勢,在於 Postgres 本身就已經完整記錄了所有需要的元資料。MCP 伺服器完全不需要依賴外部的設定檔、Schema 傾印檔案,或是常常忘記更新的系統文件。它只需要直接查詢 Postgres 的系統目錄:
- pg_class:記錄所有的資料表、檢視表與序列。
- pg_attribute:記錄欄位名稱與資料型態。
- pg_constraint:記錄主鍵(PK)、外鍵(FK)與 CHECK 條件約束。
- pg_index:記錄用於查詢最佳化的索引資訊。
- pg_namespace:記錄 Schema。
- pg_description:記錄
COMMENT ON的註解內容(這能直接當作 LLM 的文件說明)。
在底層運作上,MCP 伺服器就是透過執行這類的查詢來獲取即時資訊:
-- list_schemas
SELECT
schema_name, schema_owner,
CASE
WHEN schema_name LIKE 'pg_%' THEN 'System Schema'
WHEN schema_name = 'information_schema' THEN 'System Info Schema'
ELSE 'User Schema'
END AS schema_type
FROM information_schema.schemata
ORDER BY schema_type, schema_name;
-- list_objects
SELECT
CASE c.relkind
WHEN 'r' THEN 'table'
WHEN 'v' THEN 'view'
END AS object_type,
n.nspname AS table_schema,
c.relname AS table_name,
d.description AS comment
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_catalog.pg_description d
ON d.objoid = c.oid AND d.objsubid = 0
WHERE n.nspname = $1 AND c.relkind IN ('r','v')
ORDER BY c.relname;
-- get_object_details
SELECT
column_name, data_type,
is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = $1
AND table_name = $2
ORDER BY ordinal_position;
這些查詢是直接針對線上資料庫即時執行。不論欄位是新增、改名或刪除,MCP 伺服器都能在第一時間同步反應最新狀態。
透過 EXPLAIN 最佳化查詢
掌握正確的資料表與欄位是第一步,接下來還必須確保寫出來的查詢具備良好的執行效率。
透過 explain_query 工具,系統會把 LLM 生成的 SQL 先丟給 Postgres 的查詢規劃器(Query Planner)進行評估。如果 EXPLAIN 的結果顯示該執行計畫不夠理想,LLM 就能在幾毫秒內、在使用者完全沒有察覺的情況下,重寫並修正語法,才正式執行。
例如,以下是 LLM 的第一次嘗試:
SELECT c.full_name, SUM(o.total_price)
FROM app_customers c
JOIN customer_orders o ON c.id = o.customer_id
GROUP BY c.full_name
ORDER BY total DESC;
透過 EXPLAIN 的分析發現:這段查詢會對約 50 萬筆資料進行全表掃描,且沒有設定日期篩選(導致掃描了歷史至今的所有訂單),執行時間長達近 3,000 毫秒。
LLM 讀取 EXPLAIN 的輸出結果後,便會主動重寫 SQL:
SELECT c.full_name, SUM(o.total_price)
FROM app_customers c
JOIN customer_orders o ON c.id = o.customer_id
WHERE o.created_at >= NOW() - INTERVAL '90 days'
GROUP BY c.id, c.full_name
ORDER BY total DESC
LIMIT 10;
優化後轉為索引掃描,掃描筆數縮減至約 3,000 筆,執行時間縮短到 20 毫秒,速度提升了約 150 倍。
在這個過程中,LLM 透過分析 EXPLAIN 執行計畫的結果,主動加上了日期篩選條件、修正了 GROUP BY(納入 c.id 以確保資料正確性),並加上了 LIMIT。這完全是基於即時的執行計畫評估,而不是死背硬記的最佳化規則。
安全防護機制(Guardrails)
要讓 LLM 能夠存取資料庫,必須部署多層安全機制。以下是推薦的標準配置方法:
首先,從配置唯讀帳號開始,並且只授予 GRANT SELECT 權限。這樣一來,即使 LLM 生成了 DROP TABLE 這類的指令,Postgres 也會在資料庫引擎層級直接拒絕執行。
CREATE ROLE mcp_reader LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE mydb TO mcp_reader;
GRANT USAGE ON SCHEMA public TO mcp_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_reader;
針對多租戶架構的資料庫,可以啟用資料列級安全性(Row-Level Security, RLS)。這能直接在 Postgres 引擎層級強制隔離資料,即使面臨 SQL 注入(SQL Injection)攻擊也無法繞過這層防禦。
ALTER TABLE customer_orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON customer_orders
FOR SELECT USING (
tenant_id = current_setting('app.tenant_id')::int
);
execute_sql 工具本身也提供了可自訂的存取控制、強制唯讀模式、安全的 SQL 解析機制,以及針對長時間異常查詢的 statement_timeout 限制。系統也會為每筆查詢記錄包含上下文的 Log,並強制執行速率限制。
這些安全機制並非選配,而是將 MCP 應用在任何真實資料庫時都必須具備的基本防線。
開始使用 pg-airman-mcp
只花幾分鐘,就能完成 pg-airman-mcp 的設定。請將以下內容新增到 MCP 用戶端設定檔中:
{
"mcpServers": {
"postgres": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"AIRMAN_MCP_DATABASE_URL",
"enterprisedb/pg-airman-mcp",
"--access-mode=unrestricted"
],
"env": {
"AIRMAN_MCP_DATABASE_URL": "postgresql://mcp_reader:pass@localhost:5432/mydb"
}
}
}
}
在正式環境中,請使用 --access-mode=restricted(受限模式)。完成後重新啟動你的 MCP 用戶端,接下來就能自由進行任何查詢了。(更多設定説明)
更棘手的問題:悄悄出錯的查詢
Schema 探索機制只能解決表面上的明顯錯誤,例如寫錯資料表、漏掉欄位或語法錯誤的 JOIN。但還有另一個問題是它無法解決的。
當你詢問 LLM:「我們這個月的營收是多少?」時,SQL 順利執行了,跑出來的數據看起來也非常合理。但實際上,那個數字是錯的。
-- What the LLM generates
SELECT SUM(o.total_price)
FROM customer_orders o
WHERE o.created_at >= '2026-04-01'
這段查詢能正常執行,也會回傳一個數字。但這個數字卻把「已取消的訂單」、「已退款的訂單」,以及 is_deleted = true 的軟刪除(Soft-deleted)資料全都算進去了。
正確的查詢應該是:
-- What the query should be
SELECT SUM(o.total_price)
FROM customer_orders o
WHERE o.created_at >= '2026-04-01'
AND o.status != 'cancelled'
AND o.is_deleted = false
AND o.is_refunded = false
這就是所謂的「內部潛規則」。這些邏輯只存在於開發人員的腦袋裡,根本沒有寫進 Schema。不論再怎麼去查 pg_catalog,系統目錄也絕對不會告訴你:只要查詢 customer_orders,就必須一律加上 is_deleted = false 這個篩選條件。
解決方案:利用 FastMCP 客製化 MCP 伺服器
解決這個問題的方法,是將業務邏輯直接封裝進 MCP 工具中。讓 LLM 直接呼叫一個早已寫好規則的工具,而不是讓它從頭開始盲寫 SQL。
from fastmcp import FastMCP
import psycopg2
mcp = FastMCP("revenue-server")
@mcp.tool()
def get_monthly_revenue(month: str, year: int) -> dict:
"""Get actual revenue excluding cancelled,
refunded, and soft-deleted orders."""
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
cur.execute("""
SELECT SUM(o.total_price)
FROM customer_orders o
WHERE DATE_TRUNC('month', o.created_at)
= DATE_TRUNC('month', %s::date)
AND o.status != 'cancelled'
AND o.is_deleted = false
AND o.is_refunded = false
""", [f"{year}-{month}-01"])
result = cur.fetchone()
return {"revenue": float(result[0] or 0)}
if __name__ == "__main__":
mcp.run()
透過 Python 的 Docstring(文件字串),我們可以直接告訴 LLM 這個工具的功能;而真實的業務規則則已經寫死在函式實作中。這樣一來,當 LLM 遇到營收相關的問題時,它會直接呼叫 get_monthly_revenue,而不是自己瞎猜語法。
混合模式
在實際應用中,這兩種做法缺一不可,必須搭配使用。
- Postgres MCP 負責結構層: 處理 Schema、資料表、欄位與外鍵,告訴 LLM 資料庫裡「存在哪些東西」,防止 LLM 產生結構上的幻覺(Structural Hallucinations)。
- 客製化 MCP 負責邏輯層: 封裝了業務邏輯、篩選條件、合規規則與團隊內部的潛規則,告訴 LLM 這些資料「代表什麼意思」,杜絕語義上的幻覺(Semantic Hallucinations)。
實際運作時的互補效果:
- 面對欄位命名古怪的舊系統?由 Postgres MCP 即時探索結構。
- 面對複雜的狀態旗標或潛規則 JOIN?由客製化 MCP 內建規則。
為什麼不直接用其他方法?
要把資料庫的 Context 提供給 LLM,其實還有其他幾種做法。以下是它們與 MCP 的對比:
| 作法 | 資料即時性 | 擴充性 | 最終評語 |
| 直接把 Schema 貼進提示詞 | 瞬間過期 | 浪費 Token | 僅適合微型專案或快速測試 |
| 對文件進行 RAG | 向量資料易與真實 Schema 脫節 | 良好 | 適合處理語義,不適合精準的結構 |
| 微調模型 (Fine-tuning) | 部署的瞬間就開始過期 | 成本高昂 | 用來記 Schema 太大材小用 |
| MCP (即時連線) | 永遠保持最新狀態 | 模組化、易組合 | 處理結構資訊的最佳解 |
- 直接貼 Schema: 這種做法玩玩小專案還可以,一旦資料表結構發生變化就直接破功。
- RAG 模式: 適合用來對文件進行語義搜尋,但面對需要百分之百精準的「結構化元資料」時,表現並不理想。
- 微調模型: 等於是把 Schema 的硬編碼進模型裡,只要資料庫一版更,模型懂的知識就過期了。
- MCP 機制: 每次遇到提問,都是直接去查當下最即時的線上資料庫。
核心結論
- 精準的 Prompt 無法解決幻覺: LLM 會寫出錯誤的 SQL,核心原因在於它們缺乏資料庫的即時上下文,這一點光靠優化提示詞是無法根治的。
- MCP 是橋接的關鍵: MCP 是一個將 LLM 連接到外部資料的開放協定。
pg-airman-mcp正是利用了pg_catalog與information_schema,將 Postgres 原生的「自我認知」直接轉化為 LLM 的背景知識。 - 客製化 MCP 補足盲點: 透過 FastMCP 建立的客製化 MCP 伺服器,能進一步將業務邏輯與團隊內部的「潛規則」封裝起來,解決單靠結構探索無法得知的業務語義。
- 雙劍合璧才是正解: 運用 Postgres MCP 搞定結構,加上 客製化 MCP 搞定語義 —— 這種混合模式才是讓 LLM 真正落地、搞定企業級複雜資料庫的終極方案。
本文翻譯自:Building Real-Time, Data-Aware Intelligence with Postgres and the Model Context Protocol
參考資料:
想瞭解更多?歡迎聯絡我們,或是 加入歐立威 Line 好友!








