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

最近我在看 [apfel](https://github.com/Arthur-Ficial/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 的概念插畫](https://stwillblogassets.blob.core.windows.net/files/images/2026/10/apfel-foundationmodels/apfel-banner.webp)

> 本文於 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](https://developer.apple.com/documentation/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\. 安裝並確認模型可用

依專案的[安裝文件](https://github.com/Arthur-Ficial/apfel/blob/main/docs/install.md)，執行環境需要 Apple Silicon Mac、macOS 26 或更新版本，並啟用 Apple Intelligence。只安裝 CLI，並不能取代系統模型的下載與啟用程序。Apple Intelligence 的語言、地區及硬體條件，請以 [Apple 官方說明](https://support.apple.com/zh-tw/121115)為準。

透過 Homebrew 安裝：

```bash
brew install apfel
```

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

```bash
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](https://docs.ollama.com/api/openai-compatibility) 也提供 `/v1/chat/completions`，所以可以用相同格式的 cURL 請求分別測試：

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

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

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

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

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

```bash
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`：

```bash
apfel --serve --host 127.0.0.1 --port 11435
```

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

```bash
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 的健康狀態：

```bash
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` 陣列解讀為模型完全沒有能力；這個系統版本沒有回報該項能力資訊。

再查詢模型列表：

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

本機回傳：

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

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

* * *

### 4\. 傳送第一個聊天請求

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

```bash
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}'
```

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

```json
{
  "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` 關閉輸出緩衝：

```bash
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 @-` 從標準輸入讀取。

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

```bash
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 相容性文件](https://github.com/Arthur-Ficial/apfel/blob/main/docs/openai-api-compatibility.md)說明，`response_format: json_schema` 會透過 FoundationModels 的結構化生成機制處理；跟單純在提示詞裡要求回傳 JSON，是不同的做法。

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

```bash
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'
```

我本機實測得到：

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

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

* * *

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

先確認 `11434` 真的是 Ollama，再列出已安裝的模型：

```bash
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 準備模型，才能執行下面的請求。

```bash
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 目前載入的模型，確認剛才的本機模型確實已載入：

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

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

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

```bash
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` 的基本文字輸入：

```bash
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 狀態碼：

```bash
# 這個 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](https://github.com/Arthur-Ficial/apfel/blob/main/docs/server-security.md)。

* * *

### 實際使用前要知道的限制

這次測試能確認 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 內建模型適合處理哪些工作。

* * *

### 相關連結

-   [apfel GitHub 專案](https://github.com/Arthur-Ficial/apfel)
-   [本文查閱的 apfel 原始碼版本](https://github.com/Arthur-Ficial/apfel/tree/4ef6d555b4ebe499028ae71833ad11805f6c4a29)
-   [apfel 安裝說明](https://github.com/Arthur-Ficial/apfel/blob/main/docs/install.md)
-   [apfel OpenAI API 相容性說明](https://github.com/Arthur-Ficial/apfel/blob/main/docs/openai-api-compatibility.md)
-   [apfel HTTP 服務安全設定](https://github.com/Arthur-Ficial/apfel/blob/main/docs/server-security.md)
-   [Apple FoundationModels 文件](https://developer.apple.com/documentation/foundationmodels)
-   [Apple Intelligence 系統需求](https://support.apple.com/zh-tw/121115)
-   [Ollama OpenAI 相容 API](https://docs.ollama.com/api/openai-compatibility)
-   [Ollama 執行中模型列表](https://docs.ollama.com/api/ps)
-   [Ollama FAQ：預設監聽位址與環境變數](https://docs.ollama.com/faq)
