介紹好用工具:apfel (透過 Apple FoundationModels 提供 LLM 服務)

最近我在看 apfel 這個工具。對我來說,它最有意思的地方,是可以把 Mac 裡面的 Apple Intelligence 模型變成一個本機 HTTP API。原本需要在 Swift 程式裡呼叫的 FoundationModels,現在用 cURL 就能送出提示詞,拿回模型的回答,不需要另外申請雲端模型的 API Key。

不過,我在本機測試時先碰到一個很實際的問題:apfel 的 HTTP 服務預設使用 11434,剛好跟本機 Ollama 一樣!如果平常已經用 Ollama 執行本機模型,再把 apfel 用預設值啟動,就會發生連接埠衝突。

這篇文章就來整理 apfel 的用途、安裝方式,以及我在本機用 cURL 測試聊天、串流、結構化輸出的過程。所有 apfel 的 API 範例都統一使用 11435,讓 Ollama 保留原本的 11434,兩套服務可以一起用。

apfel 將 Mac 內建的 Apple FoundationModels 連接到命令列與本機 HTTP API 的概念插畫

本文於 2026-10-18 在 Apple M5 Max、macOS 26.6.2、apfel 1.16.0、Ollama 0.40.6 實測。Apple Intelligence 已啟用,apfel 回報模型可用,上下文為 4,096 tokens。文中的提示詞與檔案內容皆為測試資料。macOS 27 的能力差異僅依專案文件整理,未在本機實測。封面圖片為 AI 生成的概念插畫。


先搞懂 apfel 提供的是哪一種 LLM 服務

Apple FoundationModels 是 Apple 提供給開發者使用的框架;apfel 則是將其中的裝置端模型包裝成命令列工具及 HTTP 服務的開源專案,採用 MIT 授權。我們使用 apfel 時,不必自己寫 Swift 程式,就能把這個模型接到原有的工具或指令稿。

這裡有幾個元件的分工要先釐清:

元件 負責的事情
Apple Intelligence/裝置端模型 提供由系統管理的本機模型,需要相容硬體與可用的系統設定
FoundationModels 讓程式透過 Apple 的框架呼叫模型
apfel 將模型包成 CLI、互動聊天及 OpenAI 相容 HTTP API
cURL 傳送 HTTP 請求;本文用它測試 API
Ollama 另一套模型服務;本文用它載入已下載的模型,在同一台 Mac 上執行推論

apfel 的裝置端推論在 Mac 上執行;API 採用 OpenAI 相容格式,不代表背後使用 OpenAI 的模型。 本文實測使用的模型識別名稱是 apple-foundationmodel,不能把 model 改成其他廠牌的名稱,就期待 apfel 幫你切換模型。

專案也有命令列單次問答、互動聊天與 MCP 整合功能。本文集中介紹 HTTP 服務,模型呼叫全部使用 cURL。若另外掛上會連網的 MCP 工具,工具本身的網路行為要另外看,不能因為模型在本機,就認定整條工作流程都不會連網。


1. 安裝並確認模型可用

依專案的安裝文件,執行環境需要 Apple Silicon Mac、macOS 26 或更新版本,並啟用 Apple Intelligence。只安裝 CLI,並不能取代系統模型的下載與啟用程序。Apple Intelligence 的語言、地區及硬體條件,請以 Apple 官方說明為準。

透過 Homebrew 安裝:

brew install apfel

接著確認版本與模型狀態:

apfel --version
apfel --model-info

我本機已安裝 apfel,這次直接使用既有的 1.16.0 版本測試,沒有重裝。--model-info 的幾個重要欄位如下,這裡只摘錄與環境確認有關的內容:

model: apple-foundationmodel
on-device: true (always)
available: yes
context: 4096 tokens

其中 available: yes 才是能否開始推論的關鍵。若模型不可用,要先回到系統設定確認 Apple Intelligence 狀態,不是換一個 HTTP Port 就能解決。


2. 讓 apfel 與本機 Ollama 使用不同的連接埠

apfel 與 Ollama 可以在同一台 Mac 上執行,但不能同時監聽相同的位址與連接埠。 兩個服務預設都使用 11434,因此我保留 Ollama 的預設設定,將 apfel 改成 11435。

兩者都能進行本機推論,但模型的來源與管理方式不同:apfel 使用系統管理的 Apple 裝置端模型,Ollama 則載入你另外下載的模型。Ollama 的 OpenAI 相容 API 也提供 /v1/chat/completions,所以可以用相同格式的 cURL 請求分別測試:

服務呼叫路徑本文的 Base URL
apfelcURL → apfel → Apple 裝置端模型http://127.0.0.1:11435/v1
本機 OllamacURL → Ollama → 已下載的本機模型http://127.0.0.1:11434/v1

先看看這兩個連接埠有沒有被使用:

lsof -nP -iTCP:11434 -sTCP:LISTEN
lsof -nP -iTCP:11435 -sTCP:LISTEN

沒有符合的監聽處理序時,lsof 不會列出結果;這種情況也可能回傳非零結束碼,並不代表網路故障。

我測試開始時,Ollama 並未執行,因此先在一個終端機啟動:

ollama serve

如果你的 Ollama App 或背景服務原本就有啟動,不需要再執行一次。在 Ollama 已監聽 11434 的情況下,我刻意用預設設定啟動 apfel,確實得到以下錯誤:

error: Port 11434 is already in use.
Another process (e.g. Ollama) may be listening on this port.
Fix: apfel --serve --port <other-port> or stop the other process.

解法是在另一個終端機,明確指定 apfel 使用 11435:

apfel --serve --host 127.0.0.1 --port 11435

這次就能啟動,服務位址是 http://127.0.0.1:11435。我也測過用環境變數設定連接埠,效果相同;下面是替代寫法,不是要另外再開一個服務:

APFEL_PORT=11435 apfel --serve --host 127.0.0.1

這裡我刻意先用前景服務,方便觀察實際監聽的位址,也避免把測試設定直接變成登入後自動啟動的服務。測試結束後,在該終端機按 Ctrl-C 就能停止。

改完伺服器的連接埠,呼叫端的 URL 也要一起改。 如果 cURL 還是送到 11434,你拿到的就是 Ollama 的回應,不是 apfel。設定其他工具時,apfel 的 OpenAI 相容 Base URL 應填 http://127.0.0.1:11435/v1。


3. 用 cURL 確認服務與模型

先確認 apfel 的健康狀態:

curl -fsS http://127.0.0.1:11435/health | jq .

我實際拿到 HTTP 200,其中有 status: "ok"、model_available: true、context_window: 4096 與 version: "1.16.0"。macOS 26 上的 capabilities_reported 是 false,不能將空的 capabilities 陣列解讀為模型完全沒有能力;這個系統版本沒有回報該項能力資訊。

再查詢模型列表:

curl -fsS http://127.0.0.1:11435/v1/models \
  | jq '.data[] | {id, context_window}'

本機回傳:

{
  "id": "apple-foundationmodel",
  "context_window": 4096
}

本文使用 jq 整理 JSON。如果本機沒有這個工具,需要先安裝 jq。它負責組合或解析 JSON,HTTP 請求仍然全部由 cURL 送出。


4. 傳送第一個聊天請求

POST /v1/chat/completions 是最容易串接的入口。指定模型、訊息與輸出長度,就能取得回答:

curl -fsS http://127.0.0.1:11435/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "apple-foundationmodel",
    "messages": [
      {"role": "system", "content": "請用台灣正體中文回答。"},
      {"role": "user", "content": "用一句話解釋什麼是 HTTP。"}
    ],
    "max_tokens": 160,
    "temperature": 0
  }' | jq '{content: .choices[0].message.content, finish_reason: .choices[0].finish_reason, usage}'

我本機測試的回應如下。這是模型的原始輸出,並非人工改寫的定義;即使指定正體中文,內容是否精確仍要另外判斷:

{
  "content": "HTTP(Hypertext Transfer Protocol)是一種協議,用於傳輸網頁內容,由兩端互動來完成。",
  "finish_reason": "stop",
  "usage": {
    "completion_tokens": 34,
    "prompt_tokens": 40,
    "total_tokens": 74
  }
}

max_tokens 限制的是這次輸出的 token 數量,不是字數。也要留意 finish_reason:stop 表示這次正常結束,length 則表示輸出碰到長度限制,HTTP 200 不保證內容已完整回答。


5. 開啟串流輸出

如果希望模型一邊產生、一邊回傳內容,加入 "stream": true,並讓 cURL 使用 -N 關閉輸出緩衝:

curl -N -fsS http://127.0.0.1:11435/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "apple-foundationmodel",
    "messages": [
      {"role": "system", "content": "請用台灣正體中文回答。"},
      {"role": "user", "content": "用一句話解釋什麼是 HTTP。"}
    ],
    "max_tokens": 160,
    "temperature": 0,
    "stream": true
  }'

本機測試會收到一連串 data: 事件,文字放在 choices[0].delta.content,最後以 data: [DONE] 結束。這是 Server-Sent Events(SSE)格式,不能直接把整段串流當成一個 JSON 物件交給 jq .。


6. 把文字檔內容交給模型摘要

若要摘要文字檔,不要直接將檔案內容塞進手寫 JSON 字串,否則遇到雙引號或換行就容易出錯。我會先用 jq --rawfile 讀檔並產生正確的 JSON,再透過 cURL 的 --data-binary @- 從標準輸入讀取。

下面先準備一份小型測試資料,所有檔案都放在這次產生的暫存資料夾:

apfel_demo_dir="$(mktemp -d -t apfel-demo)"
cat > "$apfel_demo_dir/notes.txt" <<'EOF'
今天完成 API 登入功能。
明天處理單元測試。
目前的阻礙是測試環境尚未提供。
EOF

jq -n --rawfile notes "$apfel_demo_dir/notes.txt" '{
  model: "apple-foundationmodel",
  messages: [
    {role: "system", content: "請用台灣正體中文回答,只摘要提供的內容。"},
    {role: "user", content: ("用一句話摘要以下工作紀錄:\n" + $notes)}
  ],
  max_tokens: 160
}' | curl -fsS http://127.0.0.1:11435/v1/chat/completions \
  -H 'Content-Type: application/json' \
  --data-binary @- \
  | jq -r '.choices[0].message.content'

這個範例在本機可取得摘要。它做的是把文字內容放進 messages,不是上傳附件,也沒有讓 HTTP 伺服器讀取你的本機檔案路徑。長文件仍然會受到上下文限制,不能將整個專案或大型 PDF 當成一般文字無限塞入。


7. 用 JSON Schema 取得結構化輸出

自動化流程通常不只要一段說明文字,而是需要固定欄位。apfel 的 API 相容性文件說明,response_format: json_schema 會透過 FoundationModels 的結構化生成機制處理;跟單純在提示詞裡要求回傳 JSON,是不同的做法。

以下範例從文字擷取姓名與年齡:

curl -fsS http://127.0.0.1:11435/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "apple-foundationmodel",
    "messages": [
      {"role": "user", "content": "Extract: Alice is 30 years old."}
    ],
    "max_tokens": 128,
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "person",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer"}
          },
          "required": ["name", "age"],
          "additionalProperties": false
        }
      }
    }
  }' | jq '.choices[0].message.content | fromjson'

我本機實測得到:

{
  "name": "Alice",
  "age": 30
}

注意外層 API 回應仍然是 Chat Completions 格式,而 message.content 是一個包含 JSON 的字串,所以範例使用 fromjson 再解析一次。符合 Schema 只能約束資料結構,不能保證模型擷取的事實正確。 複雜 Schema 也需要確認 apfel 支援的範圍。


8. 實際確認本機 Ollama 與 apfel 都能回答

先確認 11434 真的是 Ollama,再列出已安裝的模型:

curl -fsS http://127.0.0.1:11434/api/version | jq .
curl -fsS http://127.0.0.1:11434/api/tags \
  | jq -r '.models[].name'

我本機的版本是 0.40.6,這次使用已下載的 gemma4:latest 測試,不需要另外下載模型。你的模型名稱可能不同,請依照自己的清單替換 model。若清單沒有可用的本機模型,須先透過 Ollama 準備模型,才能執行下面的請求。

curl -fsS http://127.0.0.1:11434/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gemma4:latest",
    "messages": [
      {"role": "user", "content": "Reply with exactly OK."}
    ],
    "max_tokens": 256,
    "stream": false
  }' | jq '{model, content: .choices[0].message.content, finish_reason: .choices[0].finish_reason}'

接著查詢 Ollama 目前載入的模型,確認剛才的本機模型確實已載入:

curl -fsS http://127.0.0.1:11434/api/ps \
  | jq '.models[] | {name, size_vram}'

/api/ps 是 Ollama 的執行中模型列表。本次回傳包含 gemma4:latest 及非零的 size_vram,可作為模型已載入本機記憶體的驗證。這裡使用的是已下載的本機模型,沒有選用雲端模型。

接著在兩個服務仍同時執行的情況下,再送一次請求給 apfel:

curl -fsS http://127.0.0.1:11435/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "apple-foundationmodel",
    "messages": [
      {"role": "user", "content": "Reply with exactly OK."}
    ],
    "max_tokens": 32
  }' | jq '{model, content: .choices[0].message.content, finish_reason: .choices[0].finish_reason}'

我本機實測兩個端點皆回傳 HTTP 200,都有文字回答,且 finish_reason 為 stop。這樣就能確認本機 Ollama 與 apfel 已並存。這次測試確認的是兩個服務同時啟動後能各自完成請求,沒有進行同時推論的壓力測試。

使用 OpenAI 相容介面切換服務時,要同時切換 Base URL 與模型名稱。 這次 Ollama 使用 http://127.0.0.1:11434/v1 搭配 gemma4:latest,apfel 使用 http://127.0.0.1:11435/v1 搭配 apple-foundationmodel。只改其中一個,仍可能送錯服務或找不到模型。

實際使用時,可以將同一份短文字分別交給兩個本機模型,觀察回答品質,再決定哪一個適合自己的工作。兩者仍共用同一台 Mac 的運算資源與記憶體;連接埠分開能解決服務衝突,不代表同時推論不會互相影響。


9. 相容 API 的支援範圍也要驗證

我也測過 /v1/responses 的基本文字輸入:

curl -fsS http://127.0.0.1:11435/v1/responses \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "apple-foundationmodel",
    "input": "Reply with the word hello.",
    "max_output_tokens": 64
  }' | jq '{status, text: .output[0].content[0].text}'

這個範例回傳 HTTP 200、status: "completed",文字是 Hello!。不過,支援這個路徑不代表完整支援所有 Responses API 功能。將既有工具改接 apfel 前,仍要確認工具實際送出的欄位與需求。

我另外用 cURL 測了幾種應該被拒絕的請求。以下刻意不加 -f,讓 cURL 顯示錯誤回應的 JSON,並用 -w 印出 HTTP 狀態碼:

# 這個 Origin 不在預設允許範圍,應回傳 403。
curl -sS -w '\nHTTP %{http_code}\n' \
  http://127.0.0.1:11435/v1/models \
  -H 'Origin: https://example.com'

# apfel 不提供 embeddings,應回傳 501。
curl -sS -w '\nHTTP %{http_code}\n' \
  http://127.0.0.1:11435/v1/embeddings \
  -H 'Content-Type: application/json' \
  -d '{"model":"apple-foundationmodel","input":"Hello"}'

# max_tokens 必須為正整數,應回傳 400。
curl -sS -w '\nHTTP %{http_code}\n' \
  http://127.0.0.1:11435/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "apple-foundationmodel",
    "messages": [{"role":"user","content":"Hello"}],
    "max_tokens": 0
  }'

這三項在本機分別得到 403、501、400。Origin 檢查是瀏覽器來源限制,並不是所有 HTTP 用戶端的身分認證;一般 cURL 不帶 Origin 時仍可呼叫。若需要限制誰能使用服務,要另外設定 Token,相關設定見 Server Security。


實際使用前要知道的限制

這次測試能確認 API 可用,但不能因此推論它能取代所有雲端模型。我會先留意以下幾點:

  • 上下文是輸入與輸出共用的預算。 本機回報 4,096 tokens,訊息歷史、提示詞及回答都需要空間。聊天愈長或文件愈大,愈需要控制內容長度。
  • 不要將 4,096 寫成所有 Mac 的固定規格。 專案目前文件指出,macOS 27 搭配 M3 或更新晶片及至少 12 GB 記憶體,可使用 8,192 tokens 的裝置端模型;本文沒有在該環境驗證,請以自己的 --model-info 與 /health 結果為準。
  • 圖片能力與系統版本有關。 專案文件指出 macOS 27 支援特定的圖片輸入格式;本文的 macOS 26 環境未實測圖片 API,不把 CLI 的 OCR 或文字擷取能力當成 HTTP 視覺模型的證據。
  • 模型仍可能答錯或拒絕請求。 短文字能成功生成,不代表長篇推理、程式碼修改或事實查詢的品質也符合需求。本文不提供沒有量測依據的速度或品質保證。
  • 沒有雲端 API Key,不代表模型會自動取得即時資料。 日期、工作內容與其他必要背景應由呼叫端提供,不能依賴模型自行得知。

這也是我認為 apfel 與本機 Ollama 適合並存的原因:一個使用 Mac 內建的模型,另一個讓我選擇另外下載的模型。先把連接埠與呼叫路徑分清楚,之後才有辦法針對同一份工作測試回答品質與資源使用情況。


總結

apfel 把 Apple 裝置端模型整理成可直接呼叫的 HTTP 服務,不必自己寫 Swift,就能用 cURL 做聊天、串流與結構化輸出。這次我在本機驗證了這些流程,也測過基本 Responses API 與幾個錯誤情境。

如果你原本就在使用本機 Ollama,最直接的並存方式是:Ollama 保留 11434,apfel 改用 11435,呼叫端再依模型需求選擇正確的 URL。 這樣既有的 Ollama 呼叫路徑可以繼續使用,也能另外測試 Mac 內建模型適合處理哪些工作。


相關連結

留言評論