2026年5月31日 星期日

2026 年第 21 周記事

不知不覺已快到年中了, 回顧年初到現在, 學習速度雖然慢, 但一步一腳印, 堅持寫下學習紀錄, 有時回看也覺得有點佩服自己, 居然能有這毅力. 本周仍是專注於 Ollama 的學習, 之前有測試過 OpenAI API 的經驗, Ollama 的 API 其實更簡單, 很容易上手. 

上週去高雄家附近的家樂福超市買貓罐罐, 得知月底租約到期要關了, 衝到貓飼料架發現家樂福自有品牌的大小包貓糧都沒貨了, 店員說賣完為止不再進貨. 週四去澄清湖受訓, 下課後直衝大順店搶購, 買到兩包大的, 九包小的, 週五下班又去搬回四包大的, 應該夠貓咪吃半年了. 

Ollama 學習筆記 : 本地模型的函式呼叫

本篇旨在測試 ollama 套件的函式呼叫功能. 函式呼叫機制是透過請求訊息中的 tools 參數告訴模型, 如果需要存取外部資料時 (call-out) 時可呼叫那些函式. Ollama 官網中的模型如果帶有 tools 標籤, 表示該模型支援函式呼叫, 例如 gemma4 與 qwen3 :




本篇旨在測試本地模型的函式呼叫. 

本系列全部測試文章索引參考 :


關於雲端模型函式呼叫可參考 OpenAI API 測試筆記 :


從之前的 OpenAI API 測試紀錄可知, 函式呼叫時需傳入一個 tools 參數給 API, 其值為一個描述函式呼叫介面 (函式名稱與參數結構) 的字典串列 (稱為 JSON Schema), Ollama 的 ollama 套件也是如此, 但早期它在 ollama._utils 模組中提供了一個便利的 convert_function_to_tool(func) 函式可自動去解析函式的名稱, 型別標註以及 Docstring (函式說明文件), 然後將其轉換為 Ollama 核心看得懂的 JSON Schema. 

不過自從 ollama 0.4.0 版本之後已進一步優化, 不需要手動呼叫 convert_function_to_tool(func) 函式了, 只要用標準的 Python 語法加上型別標註與 Google 風格的 Docstring 來撰寫函式, 然後將函式放進 tools 串列, ollama 套件會在幕後自動執行 convert_function_to_tool(), 毋須顯式呼叫它. 

先用 pip show 檢視目前安裝的 ollama 套件版本 :

PS C:\Users\USER> pip show ollama  
Name: ollama
Version: 0.6.2
Summary: The official Python client for Ollama.
Home-page: https://ollama.com
Author:
Author-email: hello@ollama.com
License-Expression: MIT
Location: C:\Users\USER\AppData\Local\Programs\Python\Python312\Lib\site-packages
Requires: httpx, pydantic
Required-by:

版本式最新的 0.6.2, 所以支援自動呼叫 convert_function_to_tool() 功能. 


1. 單一函式呼叫 : 

下面是透過 tools 參數指定以函式呼叫來計算房屋總價的範例 :

先匯入 ollama 套件 : 

>>> import ollama 

定義一個加上型別標註的房屋總價計算函式 calculate_house_price(), 兩個傳入參數型別都是 float, 傳回值型別也是 float, 開頭為 Docstring 函式描述 (說明傳入參數意義) : 

>>> # 加上型別與說明文字的標準 Python 函式
def calculate_house_price(ping: float, price_per_ping: float) -> float:
    """
    計算房屋總價(坪數 * 每坪單價)
    Args:
        ping: 房屋的坪數大小(單位:坪)
        price_per_ping: 每坪的單價(單位:萬元)
    """
    return round(ping * price_per_ping, 2)

注意, Docstring 在函式呼叫中的角色很重要, 可讓模型看懂函式的參數意義, 確保能精準的對齊語意. 然後在呼叫 ollama.chat 時將函式名稱 calculate_house_price 放進串列傳給 tools 參數即可, ollama 會在背後隱式地呼叫 convert_function_to_tool() 來建立函式呼叫的 JSON schema :

>>> reply=ollama.chat(
    model='gemma4:e4b',
    messages=[{'role': 'user', 'content': '幫我算 30 坪, 每坪 60 萬的房子總價'}],
    tools=[calculate_house_price] # ollama 會呼叫 convert_function_to_tool()
    )
>>> pprint(reply)  
ChatResponse(model='gemma4:e4b', created_at='2026-05-31T08:56:18.1535995Z', done=True, done_reason='stop', total_duration=8857022300, load_duration=5518332400, prompt_eval_count=138, prompt_eval_duration=70437200, eval_count=223, eval_duration=2999849600, message=Message(role='assistant', content='', thinking='The user wants to calculate the total price of a house given its size (30 ping) and the price per ping (60 million).\nThe available tool is `calculate_house_price`.\nThis tool requires two arguments: `ping` (the area in ping) and `price_per_ping` (the price per ping).\n\n1.  **Identify `ping`**: The user stated "30 坪", so `ping` = 30.\n2.  **Identify `price_per_ping`**: The user stated "每坪 60 萬". The tool description for `price_per_ping` specifies the unit is "萬元" (ten thousand NT dollars). Since 60萬 = 60 (in units of 萬元), `price_per_ping` = 60.\n\nI should call the `calculate_house_price` tool with these values.', images=None, tool_name=None, tool_calls=[ToolCall(function=Function(name='calculate_house_price', arguments={'ping': 30, 'price_per_ping': 60}))]), logprobs=None)

觀察 API 傳回的 ChatResponse 物件, 關鍵訊息放在 message 屬性中, 回應內容 content 為空字串,  這是因為模型認為現在的第一要務是去叫 Python 函式做計算而非聊天, 所以它沒有吐出任何回應. thinking 參數則紀錄了 gemma4 模型的思考鏈推理過程, 它理解使用者想計算房屋總價且手邊有 calculate_house_price 工具可用, 然後核對 Docstring 的兩個參數 (坪數與單價), 得到思考結論 :  應該用這組參數去呼叫工具. 然後它把函式打包成 ToolCall 物件, 而且參數都依照函式參數的型別標示轉成數值 (避免了型別錯誤).  

所以, 函式呼叫的第一階段目標就是取得回應訊息中的 message 鍵的 tool_calls 鍵 : 

>>> print(reply.message.tool_calls)  
[ToolCall(function=Function(name='calculate_house_price', arguments={'ping': 30, 'price_per_ping': 60}))]

第二階段應用程式要根據 tool_calls 鍵之值來研判是否要呼叫函式, 要的話就從裡面取出要執行的函式名稱與參數後呼叫它 : 

>>> # 判斷是否有工具呼叫需求
if reply.message.tool_calls:
    for tool in reply.message.tool_calls:
        func_name=tool.function.name       # 直接用 . 欄位名稱取得函式名稱
        func_args=tool.function.arguments  # 取得參數字典 {'ping': 30, 'price_per_ping': 60}        
        print(f"命令確定!即將執行地端函式: {func_name}")        
        if func_name == "calculate_house_price":
            # 直接用 ** 語法解包傳入參數執行函式呼叫
            final_ans=calculate_house_price(**func_args)
            print(f"實體計算答案: {final_ans} 萬元")

執行結果如下 :
            
命令確定!即將執行地端函式: calculate_house_price
實體計算答案: 1800 萬元

完整程式碼如下 :

# ollama_function_call_1.py
import ollama

# 加上型別與說明文字的標準 Python 函式
def calculate_house_price(ping: float, price_per_ping: float) -> float:
    """
    計算房屋總價(坪數 * 每坪單價)
    Args:
        ping: 房屋的坪數大小(單位:坪)
        price_per_ping: 每坪的單價(單位:萬元)
    """
    return round(ping * price_per_ping, 2)

# 呼叫 ollama.chat() 傳入 tools 參數 
reply=ollama.chat(
    model='gemma4:e4b',
    messages=[{'role': 'user', 'content': '幫我算 30 坪, 每坪 60 萬的房子總價'}],
    tools=[calculate_house_price] # ollama 會呼叫 convert_function_to_tool()
    )
# 輸出工具呼叫需求
print(reply.message.tool_calls)
# 判斷是否有工具呼叫需求
if reply.message.tool_calls:
    for tool in reply.message.tool_calls:
        func_name=tool.function.name       # 直接用 . 欄位名稱取得函式名稱
        func_args=tool.function.arguments  # 取得參數字典 {'ping': 30, 'price_per_ping': 60}        
        print(f"命令確定!即將執行地端函式: {func_name}")        
        if func_name == "calculate_house_price":
            # 直接用 ** 語法解包傳入參數執行函式呼叫
            final_ans=calculate_house_price(**func_args)
            print(f"實體計算答案: {final_ans} 萬元")

此範例因為只有單一個函式呼叫, 所以只需要用一個最簡單的條件判斷式即可, 不需要建立函式對照表字典. 


2. 多函式呼叫 : 

下面是多函式呼叫的範例 :

# ollama_function_call_2.py
import ollama

def add_two(a: int, b: int) -> int:
    """將兩數相加"""
    return int(a) + int(b)

def multiply_two(a: int, b: int) -> int:
    """將兩數相乘"""
    return int(a) * int(b)

available_funcs={"add_two": add_two, "multiply_two": multiply_two}
messages=[{"role": "user", "content": "請計算 123 加 456 是多少? 算完之後再將結果乘以 5."}]
print("正在將任務送給 Ollama...")

# 使用 while 迴圈以應付 AI 的多步思考
while True:
    reply=ollama.chat(
        model='gemma4:e4b',
        messages=messages,
        tools=[add_two, multiply_two] 
        )
    
    # 每次都必須把 AI 的回應(不管是想叫工具還是想說話)塞進歷史紀錄
    messages.append(reply['message'])
    
    # 檢查 AI 這輪是不是又想呼叫工具
    if reply['message'].get('tool_calls'):
        print("\n偵測到 AI 決定調用工具!")
        
        for tool in reply['message']['tool_calls']:
            func_name=tool['function']['name']
            func_args=tool['function']['arguments']
            print(f"-> AI 選擇了函式: {func_name} | 參數: {func_args}")
            
            if func_name in available_funcs:
                real_result=available_funcs[func_name](**func_args)
                print(f"[Python 執行結果]: {real_result}")
                
                # 回傳工具呼叫結果,將結果存入記憶送回大腦,讓 AI 決定下一步
                messages.append({
                    'role': 'tool',
                    'name': func_name,
                    'content': str(real_result)
                    })
        # 執行完本次工具後進入下一次迴圈,讓 AI 看看這個結果滿不滿意
        continue 
        
    else:
        # AI 不再需要呼叫任何工具,回應最終答案
        print("\n最終答案整合完畢!")
        print(f"AI 最終回覆: {reply['message']['content']}")
        break # 跳出迴圈

這個程式是一個典型的本地端 AI 代理透過一個 while 迴圈扮演 AI 大腦與地端 Python 函式之間的傳話筒與執行官的範例, 讓不擅長複雜數學的模型也能透過借用工具精準完成連鎖計算任務, 實現了多輪自動化工具呼叫機制. 

此處的提示詞 "請計算 123 加 456 是多少? 算完之後再將結果乘以 5" 是一個需要多步驟進行連鎖運算的複雜指令, 此程式能讓 AI 自動拆解步驟並連續調用本地的 Python 函式來解決問題. 此程式先定義了兩個基礎數學函式 add_two() 與 multiply_two(), 並加上了型別標註與 Docstring 說明, 讓 ollama 套件能自動將其轉換為 AI 看得懂的工具規格. 然後利用 available_funcs 字典作為路由對照表, 將 AI 決定的函式名稱字串動態映射到真正的 Python 函式. 

while 迴圈為此程式的核心-多輪自動化控制, 因為使用者的問題包含兩個步驟 (先加後乘), AI 無法一次給出最終答案, 所以必須靠迴圈來控制 :
  • 第一輪迴圈 :
    AI 辨識出需要加法, 吐出 add_two 請求, 函式執行完得到 579, 以 role: 'tool' 角色將結果存入歷史紀錄並進到下一輪. 
  • 第二輪迴圈 :
    AI 拿到 579 後發現還有乘法任務, 再度吐出 multiply_two 請求, 函式執行完得到 2895 並將結果存入歷史紀錄並進到下一輪. 
  • 第三輪迴圈 :
    AI 發現所有運算皆已完成不再觸發 tool_calls, 於是轉入 else 區塊生成最後回應結束程式. 
程式在迴圈中不斷執行 messages.append() 將 AI 的思考決定與本地 Python 函式執行結果存入對話紀錄, 這種滾動式記憶是確保 AI 大腦不會失憶, 能順利執行下一個正確步驟的關鍵. 

執行結果如下 :

>>> %Run ollama_function_call_2.py  
正在將任務送給 Ollama...

偵測到 AI 決定調用工具!
-> AI 選擇了函式: add_two | 參數: {'a': 123, 'b': 456}
[Python 執行結果]: 579

偵測到 AI 決定調用工具!
-> AI 選擇了函式: multiply_two | 參數: {'a': 579, 'b': 5}
[Python 執行結果]: 2895

最終答案整合完畢!
AI 最終回覆: 123 加 456 的結果是 579。
再將 579 乘以 5,結果是 2895。

與 OpenAI API 的函式呼叫比起來, 不需要手刻函式呼叫的 JSON Schema 真是省事很多, 程式碼也變得非常簡潔. 

Ollama 學習筆記 : 使用 OpenAI API 存取本地模型

在前一篇測試中, 我們使用 ollama 套件來呼叫 Ollama REST API 的 /api/chat 與 /api/generate 端點, 其參數格式與 OpenAI 的有些出入, 為了讓使用 OpenAI API 撰寫的 AI 應用程式可以不需要修改而能移植到存取本地模型的應用程式中, Ollama 內建了一個 OpenAI 相容轉接層, 提供 /v1/chat/completions 端點來處理此類請求. 本篇旨在測試此相容端點之用法. 

本系列全部測試文章索引參考 :  


關於 OpenAI API 用法參考 :


ollama 套件參數結構與 OpenAI API 參數結構之主要差異摘要如下 :
  • 核心參數 :
    極度相似但命名有別, model, messages, stream, role, content 這些核心參數是最基礎的對話欄位, Ollama 在設計時顯然參考了 OpenAI 的規範, 所以命名完全一樣. 
  • 進階參數 :
    OpenAI 格式直接把這些模型參數 (控制隨機性與生成長度等) 扁平地放在最外層; 而 Ollama 則將其打包在一個名為 "options" 的字典子欄位裡.
具體之差異對照如下表 :


 參數功能  OpenAI 格式  Ollama 格式
 參數擺放位置  直接平鋪在 JSON 的最外層  內層的 "options": { ... } 子字典中
 隨機性 (溫度)  "temperature": 0.7  "options": { "temperature": 0.7 }
 限制輸出字數  "max_tokens": 100  "options": { "num_predict": 100 }
 上下文記憶長度  "max_completion_tokens": 4096  "options": { "num_ctx": 4096 }
 核心採樣 (Top P)  "top_p": 0.9  "options": { "top_p": 0.9 }
 頻率懲罰 (避免重複)  "frequency_penalty": 1.1  "options": { "repeat_penalty": 1.1 }
 固定隨機種子  "seed": 42  "options": { "seed": 42 }


1. 使用 requests 套件 :

首先使用 requests 套件存取 Ollama 相容轉接層的 /v1/chat/completions 端點 :

定義端點網址 : 

>>> url="http://localhost:11434/v1/chat/completions"

定義 HTTP 請求標頭, 這裡需在 Authorization 欄位用 Bearer 指定 OpenAI API 金鑰, 但這只是形式, 不需要去申請金鑰, 隨便填例如 ollama 即可 : 

>>> headers={
    "Content-Type": "application/json",
    "Authorization": "Bearer ollama"  # api_key 填任意值 (例如 ollama)
    }

定義酬載, 依照 OpenAI API 格式, temperature 參數直接放在外層 : 

>>> payload={
    "model": "gemma4:e4b",
    "messages": [
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請用 100 個字簡介量子糾纏"}
        ],
    "temperature": 0.3,
    "stream": False
    }

呼叫 requests.post() 傳入 headers 與 json 酬載提出請求, 傳回值為一個 Response 物件, 呼叫其 json() 方法將 JSON 字串轉成字典後即可在 messges 鍵的 content 鍵取得模型回應 : 

>>> reply=requests.post(url, headers=headers, json=payload)    
>>> result=reply.json()    
>>> print(result['choices'][0]['message']['content'])   
量子糾纏是一種奇特的量子現象。當兩個或多個粒子發生糾纏時,它們會建立起一種超越空間的特殊連結。

這意味著,即使將這些粒子分開到極遠的地方,只要你測量其中一個粒子的某個屬性(例如自旋),會瞬間、同步地決定另一個粒子的狀態。這種「幽靈般的超距作用」,讓它們的行為高度相關,彷彿心有靈通。

糾纏現象是量子計算、量子通訊等尖端科技的基礎,是現代物理學最令人著迷的領域之一。


2. 使用 openai 套件 :

使用 OpenAI API 需先安裝 openai 套件 (但不需要申請金鑰, 任意填即可) :

pip install openai    

然後匯入 openai.OpenAI 類別, 傳入 base_url 與 api_key 參數建立 OpenAI 物件 : 

>>> from openai import OpenAI
>>> client=OpenAI(
    base_url='http://localhost:11434/v1/',
    api_key='ollama'  # 填任意值
    )

注意, 這裡 base_url 只要給端點的根目錄位置 http://localhost:11434/v1/ 即可, 具體要去哪個端點是由呼叫的函式決定的. 呼叫 OpenAI 物件的 chat.completions.create() 方法與指定之本地模型對話 :

>>> reply=client.chat.completions.create(
    model="gemma4:e4b",
    messages=[
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請用 100 個字簡介量子糾纏"}
        ],
    temperature=0.3,  # OpenAI 格式參數直接放最外層
    stream=False
    )

回應字串放在傳回物件 ChatCompletion 的 choices[0].message.content 鍵裡 : 

>>> print(reply.choices[0].message.content)   
量子糾纏是一種奇特的量子現象。當兩個或多個粒子發生糾纏後,它們會形成一種深層的量子連結。

無論這些粒子相隔多遠,它們的物理狀態都是相互關聯的。當你測量其中一個粒子的屬性(例如自旋)時,另一個粒子的屬性會瞬間、同步地決定,彷彿它們心有靈犀一般。

這種非定域的關聯性,超乎傳統物理學的預期。它不僅是理論概念,更是未來發展量子計算、量子通訊等尖端科技的關鍵資源。


3. 使用 ollama 套件 :

上面測試的 Ollama 版本如下 :

>>> import ollama 
>>> reply=ollama.chat(
    model='gemma4:e4b',
    messages=[
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請用 100 個字簡介量子糾纏"}
        ],
    stream=False,
    options={"temperature": 0.3}
    )
>>> print(reply['message']['content'])  
量子糾纏是一種奇特的量子現象,描述兩個或多個粒子之間存在著超越空間距離的特殊連結。

當這些粒子發生糾纏後,它們的量子狀態會互相綁定。無論將它們分開到多遠,只要你測量其中一個粒子的屬性(例如自旋),你就能**瞬間**知道另一個粒子的狀態,即使它們相隔數光年。

這就像一對心靈相通的「量子雙胞胎」,一個改變,另一個會同步改變。這現象是量子計算和量子通訊的基礎,曾被愛因斯坦戲稱為「鬼魅般的超距作用」。

2026年5月30日 星期六

Ollama 學習筆記 : REST API 用法 (三)

在前一篇測試中使用 requests 套件來呼叫 Ollama REST API 的端點, 我們須自行處理繁瑣的 HTTP 和資料格式解析, 本篇則是要使用 Ollama 官方的 ollama 套件來簡化處理程序, 擺脫 JSON 的序列化與反序列化麻煩. 在使用 requests 時, 我們必須自己用 json.loads() 將 JSON 字串字典, 如果開啟 stream=True 要寫迴圈, 做 decode('utf-8'), 與解析每行的 JSON 碎片, 並從巢狀結構中把字串挖出來, 非常麻煩. 使用 ollama 套件則有如下優點 :
  • 非串流模式 :
    直接回傳一個已經解析好的 Python 字典或物件.
  • 串流模式 :
    支援 Python 的生成器 (Generato), 直接用 for chunk in response 就能輕鬆讀取.
本系列全部測試文章索引參考 :
1. 安裝 ollama 套件 : 

ollama 為第三方套件, 需先用 pip 安裝 :

PS C:\Users\USER> pip install ollama   
Collecting ollama
  Downloading ollama-0.6.2-py3-none-any.whl.metadata (5.8 kB)
Requirement already satisfied: httpx>=0.27 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from ollama) (0.27.0)
Requirement already satisfied: pydantic>=2.9 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from ollama) (2.9.1)
Requirement already satisfied: anyio in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from httpx>=0.27->ollama) (4.3.0)
Requirement already satisfied: certifi in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from httpx>=0.27->ollama) (2024.2.2)
Requirement already satisfied: httpcore==1.* in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from httpx>=0.27->ollama) (1.0.5)
Requirement already satisfied: idna in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from httpx>=0.27->ollama) (3.7)
Requirement already satisfied: sniffio in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from httpx>=0.27->ollama) (1.3.1)
Requirement already satisfied: h11<0.15,>=0.13 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from httpcore==1.*->httpx>=0.27->ollama) (0.14.0)
Requirement already satisfied: annotated-types>=0.6.0 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from pydantic>=2.9->ollama) (0.7.0)
Requirement already satisfied: pydantic-core==2.23.3 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from pydantic>=2.9->ollama) (2.23.3)
Requirement already satisfied: typing-extensions>=4.6.1 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from pydantic>=2.9->ollama) (4.12.2)
Downloading ollama-0.6.2-py3-none-any.whl (15 kB)
Installing collected packages: ollama
Successfully installed ollama-0.6.2


2. 常用函式 : 

用 dir() 檢視 ollama 套件內容 : 

>>> import ollama 
>>> dir(ollama)   
['AsyncClient', 'ChatResponse', 'Client', 'EmbedResponse', 'EmbeddingsResponse', 'GenerateResponse', 'Image', 'ListResponse', 'Message', 'Options', 'ProcessResponse', 'ProgressResponse', 'RequestError', 'ResponseError', 'ShowResponse', 'StatusResponse', 'Tool', 'WebFetchResponse', 'WebSearchResponse', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_client', '_types', '_utils', 'chat', 'copy', 'create', 'delete', 'embed', 'embeddings', 'generate', 'list', 'ps', 'pull', 'push', 'show', 'web_fetch', 'web_search']

ollama 套件常用函式說明如下表 :


 ollama 函式  說明
 chat()  對應對話端點 /api/chat,傳入參數為 messages 串列與 stream (預設 True)。
 generate()  對應文字生成端點 /api/generate,傳入參數為 prompt 字串。
 list()  列出本機已下載模型清單,對應 /api/tags 端點。
 show()  檢視模型詳情,對應 /api/show 端點,傳入參數為模型名稱。
 ps()  查看載入記憶體運行中模型,對應 /api/ps 端點。
 pull()  下載模型,對應 /api/pull 端點,傳入參數為模型名稱。若模型已存在則檢查是否為最新版。
 embed()  計算文字之嵌入向量,對應 /api/embed 端點 (支援單一或多個文字輸入)。
 delete()  刪除模型釋放磁碟空間,對應 /api/delete 端點,傳入參數為模型名稱。
 create()  建立自訂模型,對應 /api/create 端點。


先用 list() 取得已下載之模型清單 : 

>>> response=ollama.list()  
>>> type(response)     
<class 'ollama._types.ListResponse'>  
>>> print(response)  
models=[Model(model='gemma4:e4b', modified_at=datetime.datetime(2026, 5, 28, 22, 36, 46, 350248, tzinfo=TzInfo(+08:00)), digest='c6eb396dbd5992bbe3f5cdb947e8bbc0ee413d7c17e2beaae69f5d569cf982eb', size=9608350718, details=ModelDetails(parent_model='', format='gguf', family='gemma4', families=['gemma4'], parameter_size='8.0B', quantization_level='Q4_K_M')), Model(model='llama3.2-vision:11b', modified_at=datetime.datetime(2026, 5, 25, 11, 30, 36, 32261, tzinfo=TzInfo(+08:00)), digest='6f2f9757ae97e8a3f8ea33d6adb2b11d93d9a35bef277cd2c0b1b5af8e8d0b1e', size=7816589186, details=ModelDetails(parent_model='', format='gguf', family='mllama', families=['mllama'], parameter_size='10.7B', quantization_level='Q4_K_M')), Model(model='phi4:14b', modified_at=datetime.datetime(2026, 5, 24, 17, 23, 24, 190702, tzinfo=TzInfo(+08:00)), digest='ac896e5b8b34a1f4efa7b14d7520725140d5512484457fab45d2a4ea14c69dba', size=9053116391, details=ModelDetails(parent_model='', format='gguf', family='phi3', families=['phi3'], parameter_size='14.7B', quantization_level='Q4_K_M')), Model(model='dagbs/deepseek-coder-v2-lite-instruct:q4_k_m', modified_at=datetime.datetime(2026, 5, 24, 0, 17, 21, 590078, tzinfo=TzInfo(+08:00)), digest='a6f5c73087ad25fc8666929492449eb0dc694326e4ca5b2313fef75b66645583', size=10364417401, details=ModelDetails(parent_model='', format='gguf', family='deepseek2', families=['deepseek2'], parameter_size='15.7B', quantization_level='Q4_K_M')), Model(model='mannix/deepseek-coder-v2-lite-instruct:q4_k_m', modified_at=datetime.datetime(2026, 5, 23, 21, 15, 32, 534056, tzinfo=TzInfo(+08:00)), digest='6171206208d0529a47806ebcf8ed37a88fe322859e269396dd16fdd98a56a102', size=10364432240, details=ModelDetails(parent_model='', format='gguf', family='deepseek2', families=['deepseek2'], parameter_size='15.7B', quantization_level='Q4_K_M')), Model(model='deepseek-r1:14b', modified_at=datetime.datetime(2026, 5, 23, 16, 43, 35, 552799, tzinfo=TzInfo(+08:00)), digest='c333b7232bdb521236694ffbb5f5a6b11cc45d98e9142c73123b670fca400b09', size=8988112209, details=ModelDetails(parent_model='', format='gguf', family='qwen2', families=['qwen2'], parameter_size='14.8B', quantization_level='Q4_K_M')), Model(model='qwen3:14b', modified_at=datetime.datetime(2026, 5, 22, 0, 30, 36, 339599, tzinfo=TzInfo(+08:00)), digest='bdbd181c33f2ed1b31c972991882db3cf4d192569092138a7d29e973cd9debe8', size=9276198565, details=ModelDetails(parent_model='', format='gguf', family='qwen3', families=['qwen3'], parameter_size='14.8B', quantization_level='Q4_K_M')), Model(model='gemma4:latest', modified_at=datetime.datetime(2026, 5, 20, 11, 45, 8, 247104, tzinfo=TzInfo(+08:00)), digest='c6eb396dbd5992bbe3f5cdb947e8bbc0ee413d7c17e2beaae69f5d569cf982eb', size=9608350718, details=ModelDetails(parent_model='', format='gguf', family='gemma4', families=['gemma4'], parameter_size='8.0B', quantization_level='Q4_K_M'))]

可見 list() 傳回的是一個 ListResponse 物件, 可用迴圈取出關鍵資訊存入串列後轉成 DataFrame :

>>> import pandas as pd   
>>> data=[]   
>>> for m in response.models:  
    data.append({
        "Model Name": m.model,
        "Family": m.details.family,
        "Params": m.details.parameter_size,
        "Quant": m.details.quantization_level,
        "Size (GB)": round(m.size / (1024 ** 3), 2)
    })

轉成 DatFrame : 

>>> df=pd.DataFrame(data)   
>>> print(df.to_string(index=False))   
                                   Model Name    Family Params  Quant  Size (GB)
                                   gemma4:e4b    gemma4   8.0B Q4_K_M       8.95
                          llama3.2-vision:11b    mllama  10.7B Q4_K_M       7.28
                                     phi4:14b      phi3  14.7B Q4_K_M       8.43
 dagbs/deepseek-coder-v2-lite-instruct:q4_k_m deepseek2  15.7B Q4_K_M       9.65
mannix/deepseek-coder-v2-lite-instruct:q4_k_m deepseek2  15.7B Q4_K_M       9.65
                              deepseek-r1:14b     qwen2  14.8B Q4_K_M       8.37
                                    qwen3:14b     qwen3  14.8B Q4_K_M       8.64
                                gemma4:latest    gemma4   8.0B Q4_K_M       8.95

ollama.show() 傳回指定模型的資訊 : 

>>> ollama.show("gemma4:e4b")   
ShowResponse(modified_at=datetime.datetime(2026, 5, 28, 22, 36, 46, 350248, tzinfo=TzInfo(+08:00)), template='{{ .Prompt }}', modelfile='# Modelfile generated by "ollama show"\n# To build a new Modelfile based on this, replace FROM with:\n# FROM gemma4:e4b\n\nFROM D:\\OllamaModels\\blobs\\sha256-4c27e0f5b5adf02ac956c7322bd2ee7636fe3f45a8512c9aba5385242cb6e09a\nTEMPLATE {{ .Prompt }}\nRENDERER gemma4\nPARSER gemma4\nPARAMETER top_k 64\nPARAMETER top_p 0.95\nPARAMETER temperature 1\nLICENSE """                                Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      "License" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      "Licensor" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      "Legal Entity" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      "control" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      "You" (or "Your") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      "Source" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      "Object" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      "Work" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      "Derivative Works" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      "Contribution" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, "submitted"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as "Not a Contribution."\n\n      "Contributor" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Con…

最常用的是 generate() 與 chat() 函式. 

generate() 的功能為單次文字接龍, 給它一段提示詞就會直接回應答案. generate() 不適合用來做需要記住來回歷史紀錄的聊天機器人, 但非常適合用來做自動化任務, 文章寫作, 程式碼補全, 或資料格式化等單次搞定的工作. 必要的傳入參數為用來指定模型的 model 與提示詞 prompt, 提示詞若傳入空字串時會觸發檢查模型是否載入, 若尚未載入就會強迫載入運行, 有 ollama run 的效果 :

當久未呼叫 API (預設 5 分鐘), Ollama 會自動將模型從記憶體中卸載, 這時呼叫 ollama.ps() 會傳回一個 model 屬性值為空串列的物件 :

>>> ollama.ps()  
ProcessResponse(models=[])

然後呼叫 ollma.generate() 並傳入模型名稱與空提示詞 :

>>> reply=ollama.generate(model='gemma4:e4b', prompt='')  

這時再次呼叫 ollama.ps() 就會顯示目前記憶體中已載入指定之模型 :

>>> ollama.ps()   
ProcessResponse(models=[Model(model='gemma4:e4b', name='gemma4:e4b', digest='c6eb396dbd5992bbe3f5cdb947e8bbc0ee413d7c17e2beaae69f5d569cf982eb', expires_at=datetime.datetime(2026, 5, 30, 22, 47, 1, 112747, tzinfo=TzInfo(+08:00)), size=10579079040, size_vram=10579079040, details=ModelDetails(parent_model='', format='gguf', family='gemma4', families=['gemma4'], parameter_size='8.0B', quantization_level='Q4_K_M'), context_length=4096)])

檢視回應物件會發現 done_reason 是 'load' (載入模型) 而非 'stop' (回應結束), 這是因為提示詞空字串, 模型無法做出回應, 只是將模型載入記憶體而已 :

>>> reply  
GenerateResponse(model='gemma4:e4b', created_at='2026-05-30T14:42:01.1127475Z', done=True, done_reason='load', total_duration=None, load_duration=None, prompt_eval_count=None, prompt_eval_duration=None, eval_count=None, eval_duration=None, response='', thinking=None, context=None, logprobs=None, image=None, completed=None, total=None)

當然, 如果提示詞不為空, 則會先載入模型, 然後做出回應. 

>>> reply=ollama.generate(
    model='gemma4:e4b',
    prompt='你是誰?'
    )  
>>> reply  
GenerateResponse(model='gemma4:e4b', created_at='2026-05-30T14:53:45.7277926Z', done=True, done_reason='stop', total_duration=5928727000, load_duration=252832000, prompt_eval_count=19, prompt_eval_duration=260057900, eval_count=386, eval_duration=5196020600, response='我是 **Gemma 4**。\n\n我是一個由 Google DeepMind 開發的大型語言模型 (Large Language Model)。\n\n我的設計目的是理解和生成人類語言,我可以回答問題、撰寫文本、總結資訊,並協助您進行各種知識性的任務。\n\n簡而言之,我是一個用來與您互動、提供資訊和協助您完成任務的 AI 模型。', thinking=None, context=[2, 105, 9731, 107, 98, 107, 106, 107, 105, 2364, 107, 95841, 240560, 236881, 106, 107, 105, 4368, 107, 100, 45518, 107, 120474, 12364, 236787, 108, 236770, 236761, 138, 1018, 137938, 506, 2430, 236789, 236751, 5192, 532, 7609, 53121, 669, 2430, 19565, 623, 95841, 240560, 7462, 568, 236797, 240622, 127880, 704, 155268, 78546, 837, 55544, 531, 623, 15938, 659, 611, 7462, 107, 236778, 236761, 138, 1018, 102752, 506, 19080, 236786, 36809, 53121, 564, 1921, 8932, 3894, 531, 1041, 6697, 9834, 6366, 236761, 107, 140, 236829, 139, 1567, 236787, 147224, 236743, 236812, 236761, 107, 140, 236829, 139, 96089, 236787, 108388, 684, 6475, 22267, 65153, 236761, 107, 140, 236829, 139, 46797, 236787, 25093, 22160, 9483, 568, 2182, 236792, 769, 107, 140, 236829, 139, 2328, 236787, 7607, 18710, 2028, 236761, 107, 236800, 236761, 138, 1018, 40414, 7157, 9834, 3298, 1131, 8555, 568, 63190, 653, 164557, 236764, 10264, 496, 12643, 236764, 54651, 15737, 1473, 1018, 107, 140, 236829, 139, 236775, 236777, 1006, 147224, 236743, 236812, 1781, 3921, 26911, 237026, 147224, 236743, 236812, 236924, 107, 140, 236829, 139, 236775, 236777, 1006, 496, 2455, 5192, 2028, 1781, 3921, 26911, 90432, 56762, 146569, 26609, 568, 236824, 241747, 127880, 166012, 236759, 237022, 110652, 236781, 122720, 185960, 236762, 3238, 35843, 236781, 122720, 45511, 107, 140, 236829, 139, 236775, 165684, 684, 6475, 22267, 65153, 1781, 3921, 26911, 237852, 6475, 22267, 65153, 65706, 238623, 568, 236824, 241747, 570, 236916, 236756, 6475, 22267, 65153, 54181, 584, 237448, 45511, 107, 140, 236829, 139, 236769, 43983, 840, 1535, 5428, 236787, 127297, 506, 1932, 236772, 38357, 4135, 2907, 3921, 26911, 90432, 237857, 238076, 240551, 101462, 26609, 568, 236777, 1006, 614, 1932, 236772, 6078, 2028, 769, 107, 236812, 236761, 138, 1018, 64477, 506, 1626, 3890, 53121, 70535, 1239, 3298, 1131, 496, 40137, 236764, 54651, 236764, 532, 13611, 3072, 528, 8555, 236761, 108, 16907, 25864, 236772, 135778, 236786, 7166, 29357, 1473, 41152, 506, 15737, 563, 40564, 532, 1982, 9603, 108, 236810, 236761, 138, 1018, 17667, 8555, 14503, 32955, 99382, 101, 44889, 5213, 236823, 12367, 236743, 236812, 1018, 236924, 108, 237169, 90432, 237852, 6475, 22267, 65153, 65706, 238623, 29854, 237731, 146569, 26609, 568, 31534, 22160, 9483, 45511, 108, 21480, 34611, 159605, 29666, 237206, 25352, 126592, 146569, 236900, 183868, 49695, 18053, 236951, 242761, 240564, 57489, 236951, 240018, 238331, 89998, 236900, 238953, 181143, 238602, 43682, 77722, 77880, 39146, 152807, 236924, 108, 239309, 63229, 237437, 236900, 237169, 90432, 237105, 237967, 238693, 238602, 206182, 236951, 12680, 89998, 237206, 181143, 238602, 19843, 152807, 236918, 12498, 228546, 236924], logprobs=None, image=None, completed=None, total=None)

可見回應是放在 response 屬性裡 :

>>> print(reply.response)    # 或 reply['response'] 亦可
我是 **Gemma 4**。

我是一個由 Google DeepMind 開發的大型語言模型 (Large Language Model)。

我的設計目的是理解和生成人類語言,我可以回答問題、撰寫文本、總結資訊,並協助您進行各種知識性的任務。

簡而言之,我是一個用來與您互動、提供資訊和協助您完成任務的 AI 模型。

串流用法是傳入 stream=True, 這會讓回應像打字機那樣一個字一個字出現, 例如 :

>>> stream=ollama.generate(
    model='gemma4:e4b',
    prompt='請用一句話形容春天?',
    stream=True
    )
>>> type(stream) 
<class 'generator'>

可見串流的回應是一個生成器, 可用迴圈將內容一個字一個字取出來 : 

>>> for chunk in stream:
    print(chunk['response'], end='', flush=True)
    
由於「形容」是一個主觀的過程,我提供幾個不同風格的選項,讓您可以選擇最符合您心情的句子。

---

### 🌷 意境詩歌型(強調氛圍、柔美)

> **春天是萬物甦醒時,最輕盈的暖意。**
> (或:春天是帶著希望,甦醒在萬物深處的溫柔。)

### ✨ 生動描寫型(強調活力、色彩)

> **春天是色彩灑滿大地,重新燃起生命躍動的季節。**

### 🕊️ 意象比喻型(強調轉變、希望)

> **春天,是大地從沉睡中醒來,換上了一身嫩綠夢衣的時刻。**

### 💖 簡潔有力型(最直白、直接)

> **春天,是充滿希望和生機的季節。**

***

**💡 建議:** 如果想要最能概括春天的「氣質」,我會推薦:「**春天是萬物甦醒時,最輕盈的暖意。**」

在多輪連續對話應用 (例如聊天機器人) 中會使用 ollama.chat() 而非 ollama.generate(), 它包含對話歷史紀錄的訊息串列, 只需要在 Python 中維護一個串列來記錄歷史對話, 並將每次的新對話附加進去, 模型就能擁有上下文記憶了. 


>>> reply=ollama.chat(
    model='gemma4:e4b',
    messages=[
        {"role": "system", "content": "你是美食專家, 請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請列出台灣最知名的十大美食"}
        ]
    )

回應內容放在 message 鍵的 content 鍵裡 : 

>>> print(reply['message']['content'])  
👋 嗨,各位美食愛好者!您可算是問對人了。身為一個從小吃到大、跟台灣巷弄小吃一路走來的美食專家,我必須跟您說,台灣這片土地,簡直就是「吃」的藝術品!

不過,要列出「十大」實在是天勾地鉤,因為台灣的美食幾乎沒有盡頭!但如果硬要選十個**最具代表性、最能代表台灣味**,又讓外地朋友一聽就能聯想到台灣的招牌,那這十個絕對沒錯!

我會把這些美食用最在地、最興奮的台灣口吻,為您好好介紹一下,請準備好您的胃,跟你的味蕾一起接受一次「爆炸性的旅行」吧!

---

## 🍜✨ 台灣傳說!必吃十大招牌美食 🥢🇹🇼

(這裡我會混合包含主食、點心、飲料,讓您體驗台灣餐飲的廣度!)

### 【主食與心靈慰藉系】

**🥇 1. 滷肉飯 (Braised Pork Rice)**
*   **為什麼經典?**:它就是台灣人的「精神食糧」。上層那香氣四溢、滷得軟爛、化不開的五花肉,搭配台灣獨有的醬香,配上一碗熱騰騰的米飯,幸福感直接拉滿。它簡潔到不可思議,卻有辦法讓您感到最踏實的滿足。
*   **專家心得:** 找一家肉燥油底不會太鹹的店,才是真高手!

**🥈 2. 台灣牛肉麵 (Taiwanese Beef Noodle Soup)**
*   **為什麼經典?**:台灣牛肉麵那燉湯的底子,厲害到一個層次。從湯頭的醇厚、牛肉的Q彈到麵體的吸汁能力,它已經超越了「碗麵」,更像是一道具有文化底蘊的「療癒藝術品」。
*   **專家心得:** 記得一定要點乾辣椒和醋,讓那層豐富的層次感被釋放出來!

**🥉 3. 小籠包 (Xiao Long Bao)**
*   **為什麼經典?**:這已經不是一個「點心」了,它是一道國際化的「品牌代表」。那薄如紙片、咬下去一口湯汁爆開的瞬間,搭配肉香與薑絲的點睛之筆,是每個外地遊客都無法免疫的誘惑。
*   **專家心得:** 重點是!吃之前一定要用熱油燙一下,讓那表皮酥脆度提升一個檔次!

---

### 【街邊小吃與罪惡誘惑系】

**🏅 4. 蚵仔煎 (Oyster Omelet)**
*   **為什麼經典?**:台灣街頭小吃的靈魂!軟嫩的蛋皮、Q彈到爆的蚵仔、甜鹹交織的醬汁,幾張食材完美地結合在一起。它的味道是那種「簡單,但又極其完美」的口感。
*   **專家心得:** 建議搭配一份清爽的酸梅汁,解膩又開胃,CP值超級高!

**🎖️ 5. 臭豆腐 (Stinky Tofu)**
*   **為什麼經典?**:它就是一個「挑戰味蕾」的極品!雖然氣味超強,但一旦放進嘴裡,那外酥內嫩、吸飽了肉汁的口感,配上一床酸甜的泡菜,會讓你產生一種「咦?其實超好吃耶!」的驚喜感。
*   **專家心得:** 這道美食永遠不會讓您覺得無聊,它總能帶來一場味覺的騷動!

**⭐ 6. 雞排 (Taiwanese Chicken Cutlet)**
*   **為什麼經典?**:體積巨大、外皮酥炸到金黃酥脆,內裡肉質卻保持著夠汁的彈性。它完美詮釋了台灣小吃那種「讓人吃完超飽,但又一點也不覺得罪惡」的魔法。
*   **專家心得:** 選擇帶有蒜香粉或泰式粉料的店家,更能提升風味層次。

---

### 【甜點、飲料與特殊風味系】

**💎 7. 珍珠奶茶 (Bubble Tea)**
*   **為什麼經典?**:雖然它不是傳統的台式小吃,但它已經是台灣在全球最「出名」的文化輸出之一。那Q彈黏牙的波霸珍珠,搭配奶茶特有的香氣,給人一種隨時都能享受的甜蜜能量。
*   **專家心得:** 建議搭配幾塊小零食一起享用,讓甜度和鹹味互相平衡。

**🧁 8. 鳳梨酥 (Pineapple Cake)**
*   **為什麼經典?**:這不只是一種糕點,更是一種台灣的伴手禮代表。那酥皮的鬆化感、搭配一顆晶瑩剔透、酸甜適中的鳳梨果肉,完美代表了「甜而不膩,有層次感」的台灣風味。
*   **專家心得:** 購買時,可以多留意一下果肉和奶油的比例,有時會發現風味大升級!

**🍎 9. 芒果冰 (Mango Dessert/Mango Shaved Ice)**
*   **為什麼經典?**:尤其在芒果季節,這幾乎是台灣夏天的代名詞。酸甜濃郁的芒果香氣,搭配冰涼的刨冰口感,是給人視覺和味覺雙重降溫的藝術品。
*   **專家心得:** 盡量選擇當季、果肉新鮮度爆高的店家,那香氣才能夠「炸」到您心房!

**✨ 10. 飯糰/早點心 (Rice Balls / Traditional Breakfast)**
*   **為什麼經典?**:這代表了台灣人那種「對生活最樸實、最熱情」的早晨。不論是配上鹹肉鬆的飯糰,還是店家現烤的麵包,它總是以最踏實的碳水化合物,讓您的一天從最在地的地方開始啟動。
*   **專家心得:** 台灣的早點心,是承載了時間與人文的味道,請一定要配上一杯熱咖啡或豆漿!

---

### 💡 總結給您的專家提醒:

各位,這十樣美食只是冰山一角!真正代表台灣的,是那**「巷弄深處,攤販呼喚聲」**裡散發出來的熱情和獨特性。下次您來台灣,除了吃這十樣,記得也要隨時留意那些不起眼、卻散發著濃郁誘惑力的街邊小攤,那才是最最真實的「台灣胃」味!🍽️❤️

Ollama 學習筆記 : REST API 用法 (二)

在前一篇測試中, 我們在 PS 視窗裡使用 curl.exe 程式向 Ollama 建立的 REST API 伺服器提出請求, 本篇則是要改用 Python 程式來存取 REST API 端點 (使用 requests 套件). 

本系列全部測試文章索引參考 :


茲將常用之 Ollama REST API 端點鈔錄如下 :


 POST 操作端點  說明
 /api/generate  文字生成 (用於單次提示詞的輸入與基本文字接龍)。
 /api/chat  多輪對話 (用於需要記錄上下文, 用 user/assistant/system 區分角色)。
 /api/embeddings  使用 Embedding 模型將文字轉成可用於 RAG 語意搜尋的嵌入向量。
 /api/pull  從 Ollama 官方模型庫下載指定的 LLM 模型到本機。
 /api/push  將模型推入 Ollama 官方模型庫 (需登入 & 輸入金鑰)。
 /api/show  顯示模型的詳細資訊。
 /api/create  透過傳入 Modelfile 的內容從本機檔案建立或自訂一個全新的模型。
 /api/copy  在本地將一個現有的模型複製並重新命名為另一個名稱。

 GET 操作端點  說明
 /api/tags  列出本地所有模型。
 /api/ps  查看目前有哪些模型正載入在記憶體 (DRAM/VRAM)。
 /api/version  取得 Ollama 的版本資訊。

 DELETE 操作端點  說明
 /api/delete  刪除本地指定之模型 (使用 name 鍵指定)。


2. 使用 requests  提出請求 :

requests 套件提供 get(), post(), 與 delete() 等相對於 GET, POST, 與 DELETE 方法的函式, 首先來測試 GET 方法, 例如呼叫 /a[i/tags 端點來取得函式清單. 由於 get() 無法指定模型, 因此需先手動載入模型, 開啟一個 CMD 或 PS 視窗, 輸入下列指令載入執行 gemma4:e4b 模型 : 

PS C:\Users\USER> ollama run gemma4:e4b  

然後進入 Python 執行環境匯入 requests 套件, 呼叫 requests.get() 函式向 /api/tags 端點提出 GET 請求, requests 套件會將 Ollama 回應的 HTTP 封包打包成一個 Response 物件傳回 : 

>>> import requests  
>>> url='http://localhost:11434/api/tags'  
>>> reply=requests.get(url)  
>>> reply    
<Response [200]>
>>> type(reply) 
<class 'requests.models.Response'>  

可呼叫 Response 物件的 json() 方法轉成字典, 為了整齊顯示字典結構, 可用 pprint.print() 來輸出 : 

>>> from pprint import pprint   
>>> result=reply.json()    
>>> pprint(result)   
{'models': [{'details': {'families': ['gemma4'],
                         'family': 'gemma4',
                         'format': 'gguf',
                         'parameter_size': '8.0B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'c6eb396dbd5992bbe3f5cdb947e8bbc0ee413d7c17e2beaae69f5d569cf982eb',
             'model': 'gemma4:e4b',
             'modified_at': '2026-05-28T22:36:46.3502486+08:00',
             'name': 'gemma4:e4b',
             'size': 9608350718},
            {'details': {'families': ['mllama'],
                         'family': 'mllama',
                         'format': 'gguf',
                         'parameter_size': '10.7B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': '6f2f9757ae97e8a3f8ea33d6adb2b11d93d9a35bef277cd2c0b1b5af8e8d0b1e',
             'model': 'llama3.2-vision:11b',
             'modified_at': '2026-05-25T11:30:36.0322613+08:00',
             'name': 'llama3.2-vision:11b',
             'size': 7816589186},
            {'details': {'families': ['phi3'],
                         'family': 'phi3',
                         'format': 'gguf',
                         'parameter_size': '14.7B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'ac896e5b8b34a1f4efa7b14d7520725140d5512484457fab45d2a4ea14c69dba',
             'model': 'phi4:14b',
             'modified_at': '2026-05-24T17:23:24.1907029+08:00',
             'name': 'phi4:14b',
             'size': 9053116391},
            {'details': {'families': ['deepseek2'],
                         'family': 'deepseek2',
                         'format': 'gguf',
                         'parameter_size': '15.7B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'a6f5c73087ad25fc8666929492449eb0dc694326e4ca5b2313fef75b66645583',
             'model': 'dagbs/deepseek-coder-v2-lite-instruct:q4_k_m',
             'modified_at': '2026-05-24T00:17:21.5900783+08:00',
             'name': 'dagbs/deepseek-coder-v2-lite-instruct:q4_k_m',
             'size': 10364417401},
            {'details': {'families': ['deepseek2'],
                         'family': 'deepseek2',
                         'format': 'gguf',
                         'parameter_size': '15.7B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': '6171206208d0529a47806ebcf8ed37a88fe322859e269396dd16fdd98a56a102',
             'model': 'mannix/deepseek-coder-v2-lite-instruct:q4_k_m',
             'modified_at': '2026-05-23T21:15:32.5340567+08:00',
             'name': 'mannix/deepseek-coder-v2-lite-instruct:q4_k_m',
             'size': 10364432240},
            {'details': {'families': ['qwen2'],
                         'family': 'qwen2',
                         'format': 'gguf',
                         'parameter_size': '14.8B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'c333b7232bdb521236694ffbb5f5a6b11cc45d98e9142c73123b670fca400b09',
             'model': 'deepseek-r1:14b',
             'modified_at': '2026-05-23T16:43:35.5527992+08:00',
             'name': 'deepseek-r1:14b',
             'size': 8988112209},
            {'details': {'families': ['qwen3'],
                         'family': 'qwen3',
                         'format': 'gguf',
                         'parameter_size': '14.8B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'bdbd181c33f2ed1b31c972991882db3cf4d192569092138a7d29e973cd9debe8',
             'model': 'qwen3:14b',
             'modified_at': '2026-05-22T00:30:36.3395998+08:00',
             'name': 'qwen3:14b',
             'size': 9276198565},
            {'details': {'families': ['gemma4'],
                         'family': 'gemma4',
                         'format': 'gguf',
                         'parameter_size': '8.0B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'c6eb396dbd5992bbe3f5cdb947e8bbc0ee413d7c17e2beaae69f5d569cf982eb',
             'model': 'gemma4:latest',
             'modified_at': '2026-05-20T11:45:08.2471048+08:00',
             'name': 'gemma4:latest',
             'size': 9608350718}]}

呼叫 /api/ps 端點會傳回目前載入記憶體之模型資訊 (相當於下 ollama ps 指令) : 

>>> url='http://localhost:11434/api/ps'    
>>> reply=requests.get(url)   
>>> result=reply.json()   
>>> pprint(result)  
{'models': [{'context_length': 4096,
             'details': {'families': ['gemma4'],
                         'family': 'gemma4',
                         'format': 'gguf',
                         'parameter_size': '8.0B',
                         'parent_model': '',
                         'quantization_level': 'Q4_K_M'},
             'digest': 'c6eb396dbd5992bbe3f5cdb947e8bbc0ee413d7c17e2beaae69f5d569cf982eb',
             'expires_at': '2026-05-30T08:51:38.2129288+08:00',
             'model': 'gemma4:e4b',
             'name': 'gemma4:e4b',
             'size': 10579079040,
             'size_vram': 10579079040}]}  


可見呼叫 /api/tags 端點會列出目前 Ollama 已下載的全部模型. 

但若呼叫距離前一次存取模型超過逾時時間 (預設 5 分鐘), Ollama 會自動將模型會從記憶體中清除, 這時呼叫 /api/ps 端點會得到 models 鍵為空串列的回應 : 

>>> pprint(result)  
{'models': []}

這時只要用 POST 方法呼叫 /api/generate 或 /api/chat, Ollama 便會觸發 Ollama 自動將指定之模型載入記憶體 (毋須手動在命令列用 ollama run 指令載入). 

注意, 呼叫 get() 無法指定模型, 必須呼叫 post() 才能指定模型, 因為呼叫 post() 時可傳入一個 json 參數, 用字典來打包要傳給 Ollama 端點之酬載資訊 (payload), 其中的 model 鍵便是用來指定要載入之模型, 例如下面是呼叫文字接龍端點 /api/generate 的酬載資訊 :

payload={
    "model": "gemma4:e4b",
    "prompt": "請簡介量子糾纏",
    "stream": False
    }

例如 :

>>> url='http://localhost:11434/api/generate'   
>>> payload={
    "model": "gemma4:e4b",
    "prompt": "你是誰",
    "stream": False
    }
>>> reply=requests.post(url, json=payload)   
>>> result=reply.json()  
>>> pprint(result)  
{'context': [2,
             105,
             9731,
             107,
... (略) ...
             237169,
             240975,
             239967,
             236918,
             238463,
             237536],
 'created_at': '2026-05-30T01:28:46.9601674Z',
 'done': True,
 'done_reason': 'stop',
 'eval_count': 443,
 'eval_duration': 5984802600,
 'load_duration': 229697200,
 'model': 'gemma4:e4b',
 'prompt_eval_count': 18,
 'prompt_eval_duration': 235963400,
 'response': '我叫 **Gemma 4**。\n'
             '\n'
             '我是一個大型語言模型(Large Language Model, LLM),由 Google DeepMind 開發。\n'
             '\n'
             '我的設計目的是用來理解和生成人類語言,我可以協助您回答問題、撰寫不同類型的文本、總結資訊,或進行創意性的對話。\n'
             '\n'
             '您有什麼問題需要我幫忙的呢?',
 'total_duration': 6698511600}

可見對於單一問答 (接龍) 呼叫, Response 物件中包含一長串的 context 訊息, 而模型實際的回應則是放在 resposne 鍵裡 :

>>> print(result['response'])   
我叫 **Gemma 4**。

我是一個大型語言模型(Large Language Model, LLM),由 Google DeepMind 開發。

我的設計目的是用來理解和生成人類語言,我可以協助您回答問題、撰寫不同類型的文本、總結資訊,或進行創意性的對話。

您有什麼問題需要我幫忙的呢?

如果是呼叫對話端點 /api/chat, 則可在 messages 鍵中放入包含 role 與 content 鍵之對話字典, role 可以是 system/user/assistant, 格式與 OpenAI API 類似 : 

payload={
    "model": "gemma4:e4b",
    "messages": [
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請用 100 個字簡介量子糾纏"}
        ],
    "stream": False
    }

例如 :

>>> url='http://localhost:11434/api/chat'    
>>> payload={
    "model": "gemma4:e4b",
    "messages": [
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請用 100 個字簡介量子糾纏"}
        ],
    "stream": False
    }
>>> reply=requests.post(url, json=payload)  
>>> result=reply.json()   
>>> pprint(result)   
{'created_at': '2026-05-30T01:39:36.5316751Z',
 'done': True,
 'done_reason': 'stop',
 'eval_count': 595,
 'eval_duration': 8100674200,
 'load_duration': 4374553000,
 'message': {'content': '量子糾纏是一種奇特的量子現象,描述兩個或多個粒子之間存在的緊密關聯。無論這些粒子相距多遠,牠們的量子狀態都會形成一個不可分割的整體。\n'
                        '\n'
                        '當我們測量其中一個粒子的某個性質(例如自旋)時,另一個粒子的狀態會瞬間、同步地確定,彷彿有無形的超距聯繫一般。愛因斯坦曾戲稱為「幽靈般的超距作用」。\n'
                        '\n'
                        '這種現象是量子計算和量子密碼學的基礎技術,是未來尖端科技不可或缺的核心概念。它挑戰了我們對物理世界空間和資訊傳輸的傳統認知。',
             'role': 'assistant',
             'thinking': "Here's a thinking process to ensure the response "
                         'meets the criteria (Taiwanese Mandarin, Traditional '
                         'Chinese, approximately 100 characters, and '
                         'scientifically accurate description of quantum '
                         'entanglement):\n'
                         '\n'
                         '1.  **Analyze the Request:**\n'
                         '    *   Topic: Quantum Entanglement (量子糾纏).\n'
                         '    *   Length: Approximately 100 characters.\n'
                         '    *   Style: Use Taiwanese terminology and '
                         'Traditional Chinese (台灣用語與繁體中文).\n'
                         '\n'
                         '2.  **Identify Key Concepts (Quantum '
                         'Entanglement):**\n'
                         '    *   Definition: A unique connection between two '
                         'or more quantum particles.\n'
                         '    *   Feature: Their fates are linked, regardless '
                         'of the distance separating them.\n'
                         '    *   Measurement Effect: Measuring a property '
                         '(like spin) of one instantly tells you the property '
                         'of the other.\n'
                         '    *   Paradox/Implication: Einstein called it '
                         '"spooky action at a distance" (幽靈般的超距作用).\n'
                         '    *   Application: Crucial for quantum computing '
                         'and quantum cryptography.\n'
                         '\n'
                         '3.  **Drafting the Content (Focusing on the '
                         '"Taiwanese Tone" and Flow):**\n'
                         '    *   *Goal:* Make it accessible yet precise.\n'
                         '    *   *Initial Draft Check (Mental Outline):* '
                         'Start with the definition, explain the link, mention '
                         'the distance independence, and end with the '
                         'application/impact.\n'
                         '\n'
                         '4.  **Refining and Polishing (Character Count and '
                         'Tone):**\n'
                         '    *   *Self-Correction/Refinement:* Ensure the '
                         'language feels natural for a Taiwanese audience '
                         '(using common Taiwanese scientific phrasing, but '
                         'remaining formal).\n'
                         '    *   *(Structuring the 100 characters):*\n'
                         '        *   Introduction (What it is).\n'
                         '        *   Mechanism (The spooky link).\n'
                         '        *   Conclusion (Why it matters).\n'
                         '\n'
                         '5.  **Final Polish (Review against constraints):**\n'
                         '    *   Taiwanese/Traditional Chinese: Yes.\n'
                         '    *   Length: Close to 100 characters (The target '
                         'is an *approximation*).\n'
                         '    *   Accuracy: Yes, captures the core idea.\n'
                         '\n'
                         '6.  **Final Output Generation.** (This leads to the '
                         'provided good answer.)'},
 'model': 'gemma4:e4b',
 'prompt_eval_count': 40,
 'prompt_eval_duration': 35168300,
 'total_duration': 12994831300}

模型的實際回應是放在 message 鍵的 content 鍵裡 : 

>>> print(result['message']['content'])   
量子糾纏是一種奇特的量子現象,描述兩個或多個粒子之間存在的緊密關聯。無論這些粒子相距多遠,牠們的量子狀態都會形成一個不可分割的整體。

當我們測量其中一個粒子的某個性質(例如自旋)時,另一個粒子的狀態會瞬間、同步地確定,彷彿有無形的超距聯繫一般。愛因斯坦曾戲稱為「幽靈般的超距作用」。

這種現象是量子計算和量子密碼學的基礎技術,是未來尖端科技不可或缺的核心概念。它挑戰了我們對物理世界空間和資訊傳輸的傳統認知。

或者連續呼叫字典的 get() 方法亦可 : 

>>> print(result.get('message').get('content'))   
量子糾纏是一種奇特的量子現象,描述兩個或多個粒子之間存在的緊密關聯。無論這些粒子相距多遠,牠們的量子狀態都會形成一個不可分割的整體。

當我們測量其中一個粒子的某個性質(例如自旋)時,另一個粒子的狀態會瞬間、同步地確定,彷彿有無形的超距聯繫一般。愛因斯坦曾戲稱為「幽靈般的超距作用」。

這種現象是量子計算和量子密碼學的基礎技術,是未來尖端科技不可或缺的核心概念。它挑戰了我們對物理世界空間和資訊傳輸的傳統認知。


3. 核心參數 (外層) 與進階參數 (內層) :  

Ollama REST API 對話端點 /api/chat 與文字接龍端點 /api/generate 的參數結構分為外層的核心參數與內層的進階參數 (options) 兩種, Ollama 原生端點規定最外層只能放控制 API 行為的核心參數, 其餘微調參數必須全部打包塞進一個叫 "options" 的子字典裡; OpenAI 則是把所有控制 AI 腦袋的參數 (如隨機性與記憶力等) 全部平鋪在最外層. 

核心參數說明如下表 :


 核心參數名稱  說明
 model  必要。指定要執行的模型名稱(字串),例如 "gemma4:e4b"。
 messages  必要。對話紀錄串列,內含多個指定 role (system/user/assistant) 與 content 鍵之字典。
 stream  選用。是否以串流回應 (預設 True)。
 format  選用。指定回傳格式 (目前僅支援 "json" : 強迫模型輸出結構化的 JSON 字串, 預設 null)。
 keep_alive  選用。設定模型在留在記憶體中的時間 (預設為 "5m",設為 0 表示立刻釋放)。
 options  選用。進階模型微調參數字典 (例如 Temperature 或 num_ctx 等)。


內層的 options 的微調參數 (鍵) 說明如下表 :


 內層參數名稱  說明
 temperature  隨機性 : 值越高回答越有創意但易離題; 值越低回答越精準與固定 (預設 0.8)
 num_ctx  含輸入與輸出之上下文視窗大小 (預設為 2048 個 token)。
 num_predict  單次回答的最大 Token 數 (預設 -1 表示無限制)
 top_p  核心採樣機率, 限制模型只能從累積機率前 P% 的高機率詞彙中挑選字 (預設 0.9)。
 top_k  最高機率詞彙數。限制模型選字時只考慮機率最高的前 K 個詞 (預設 40)。
 repeat_penalty  重複字詞懲罰係數。避開已經講過的詞來防止重覆同一句話 (預設 1.1)。
 seed  隨機數種子 (整數)。強迫模型吐出一模一樣的答案 (預設 -1, 完全隨機)。


format 參數預設為 null, 模型會以自由文本 (Free Text) 模式回應. 如果 Python 程式需要串接資料庫, 或是進行網頁 RAG 數據結構化時, 可以利用 "format": "json" 參數設定, 要求模型以符合 JSON 格式的字串回應 (可直接轉成 Python 字典). 注意, 啟用 JSON 輸出必須同時在 prompt 或 messages 提示詞中同時明確告訴模型要輸出 JSON, 否則可能會讓此設定破功, 例如 : 

payload={
    "model": "gemma4:e4b",
    "messages": [
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請列出全球人均 GDP 前十大國家, 包含 Country 與 GDP 兩欄位, 且以 JSON 格式回覆"}
        ],
    "format": "json",
    "stream": False
    }

>>> url='http://localhost:11434/api/chat'  
>>> payload={
    "model": "gemma4:e4b",
    "messages": [
        {"role": "system", "content": "請一律用台灣用語與繁體中文回答"},
        {"role": "user", "content": "請列出全球人均 GDP 前十大國家, 包含 Country 與 GDP 兩欄位, 且以 JSON 格式回覆"}
        ],
    "format": "json",
    "stream": False
    }
>>> reply=requests.post(url, json=payload)   
>>> result=reply.json()   
>>> type(result)   
<class 'dict'>   
>>> pprint(result)   
{'created_at': '2026-05-30T07:48:28.8572311Z',
 'done': True,
 'done_reason': 'stop',
 'eval_count': 512,
 'eval_duration': 6949736800,
 'load_duration': 4340405700,
 'message': {'content': '{\n'
                        '  "note": "由於「人均 '
                        'GDP」是高度依賴數據來源、計算時間點(例如是預估值還是實際值)以及使用哪個國際組織(如 IMF, '
                        '世界銀行)數據而有巨大變動的數據。此外,名單的具體數字和排名也可能因年度資料更新而改變。此處提供的數據為一個範例的參考格式,您應使用最新的權威資料庫(如世界銀行或 '
                        'IMF)來獲取最精準且最新的全球排名。由於我無法即時存取即時的國際金融數據,且無法確定您需要的「哪個年份」的數據,我會提供一個結構化的範例,並用一個說明性的年份數據作為範例。",\n'
                        '  "top_10_countries": [\n'
                        '    {\n'
                        '      "Country": "路易斯利亞 (Luxembourg)",\n'
                        '      "GDP (PPP, USD)": 120000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "新加坡 (Singapore)",\n'
                        '      "GDP (PPP, USD)": 105000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "挪威 (Norway)",\n'
                        '      "GDP (PPP, USD)": 95000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "瑞士 (Switzerland)",\n'
                        '      "GDP (PPP, USD)": 90000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "阿聯酋 (United Arab Emirates)",\n'
                        '      "GDP (PPP, USD)": 85000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "葡萄牙 (Portugal)",\n'
                        '      "GDP (PPP, USD)": 78000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "冰島 (Iceland)",\n'
                        '      "GDP (PPP, USD)": 75000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "愛爾蘭 (Ireland)",\n'
                        '      "GDP (PPP, USD)": 72000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "韓國 (South Korea)",\n'
                        '      "GDP (PPP, USD)": 65000\n'
                        '    },\n'
                        '    {\n'
                        '      "Country": "美國 (United States)",\n'
                        '      "GDP (PPP, USD)": 60000\n'
                        '    }\n'
                        '  ]\n'
                        '}',
             'role': 'assistant',
             'thinking': "Here's a thinking process to construct the desired "
                         'JSON response:\n'
                         '\n'
                         '1.  **Understand the Goal:** The user wants a list '
                         'of the top 10 countries by *per capita GDP* (人均 '
                         'GDP).\n'
                         '2.  **Understand the Format:** The output *must* be '
                         'in JSON format, with two specific fields: `Country` '
                         'and `GDP`.\n'
                         '3.  **Identify Constraints/Caveats (Crucial '
                         'Step):**\n'
                         '    *   The request requires real-time, global '
                         'economic data (top 10 per capita GDP).\n'
                         '    *   My knowledge cutoff is January 2025, and I '
                         'do not have access to a live, definitive, '
                         'up-to-the-minute economic database (like World Bank '
                         'or IMF).\n'
                         '    *   Therefore, the data I provide must be '
                         '*representative* of recent trends but might not be '
                         'the absolute current ranking. I must use '
                         'placeholder/estimated values and acknowledge the '
                         'nature of the data.\n'
                         '4.  **Determine the Data Structure (JSON):** The '
                         'best structure will be an array of objects.\n'
                         '    ```json\n'
                         '    [\n'
                         '      {"Country": "...", "GDP": "..." },\n'
                         '      {"Country": "...", "GDP": "..." },\n'
                         '      // ... up to 10\n'
                         '    ]\n'
                         '    ```\n'
                         '5.  **Gather Representative Data (Top 10 Per Capita '
                         'GDP):** *Self-Correction/Knowledge Retrieval:* High '
                         'per capita GDP countries usually include Luxembourg, '
                         'Singapore, Qatar, Ireland, Norway, etc. I will use '
                         'estimated/representative figures for the JSON '
                         'structure. (Note: I must ensure the GDP figures are '
                         'plausible approximations, usually given in USD.)\n'
                         '6.  **Final JSON Construction (Iterative '
                         'Drafting):** (Populate the structure with the '
                         'data.)\n'
                         '7.  **Review and Refine (Adherence to '
                         'Tone/Format):**\n'
                         '    *   *Language:* Use Taiwanese Mandarin (台灣用語, '
                         '繁體中文).\n'
                         '    *   *Tone:* Formal, professional.\n'
                         '    *   *Format:* Strict JSON.\n'
                         '    *   *Disclaimer:* It is essential to include a '
                         'disclaimer that economic figures change constantly '
                         'and the data provided is based on the knowledge '
                         'cutoff date.\n'
                         '\n'
                         '8.  **Final Output Generation.** (This leads to the '
                         'provided JSON and explanatory text.)'},
 'model': 'gemma4:e4b',
 'prompt_eval_count': 61,
 'prompt_eval_duration': 27626100,
 'total_duration': 18852258300}

可見模型成功傳回含有 JSON 格式的資料, 但它擅自將 GDP 欄位名稱改為 GDP (PPP, USD), 如果要避免此情形, 提示詞需要加以限制, 例如改為 :

請列出全球人均 GDP 前十大國家, 包含 Country 與 GDP 兩欄位, 且以 JSON 格式回覆. 注意, 欄位名稱請忠實使用 Country 與 GDP, 不可自行更改

上面的回應結果顯示, GDP 資料放在 result["messages"]["content"], 其值是包含 JSON 回應的字串, 先取出此回應字串 :

>>> json_string=result['message']['content']   

然後匯入內建的 json 模組, 呼叫其 loads() 函式將此 JSON 字串轉成 Python 字典 :

>>> import json   
>>> clean_data=json.loads(json_string)     
>>> clean_data  
{'note': '由於「人均 GDP」是高度依賴數據來源、計算時間點(例如是預估值還是實際值)以及使用哪個國際組織(如 IMF, 世界銀行)數據而有巨大變動的數據。此外,名單的具體數字和排名也可能因年度資料更新而改變。此處提供的數據為一個範例的參考格式,您應使用最新的權威資料庫(如世界銀行或 IMF)來獲取最精準且最新的全球排名。由於我無法即時存取即時的國際金融數據,且無法確定您需要的「哪個年份」的數據,我會提供一個結構化的範例,並用一個說明性的年份數據作為範例。', 'top_10_countries': [{'Country': '路易斯利亞 (Luxembourg)', 'GDP (PPP, USD)': 120000}, {'Country': '新加坡 (Singapore)', 'GDP (PPP, USD)': 105000}, {'Country': '挪威 (Norway)', 'GDP (PPP, USD)': 95000}, {'Country': '瑞士 (Switzerland)', 'GDP (PPP, USD)': 90000}, {'Country': '阿聯酋 (United Arab Emirates)', 'GDP (PPP, USD)': 85000}, {'Country': '葡萄牙 (Portugal)', 'GDP (PPP, USD)': 78000}, {'Country': '冰島 (Iceland)', 'GDP (PPP, USD)': 75000}, {'Country': '愛爾蘭 (Ireland)', 'GDP (PPP, USD)': 72000}, {'Country': '韓國 (South Korea)', 'GDP (PPP, USD)': 65000}, {'Country': '美國 (United States)', 'GDP (PPP, USD)': 60000}]}

人均 GDP 前十大國家資料放在 top_10_countries 鍵裡 : 

>>> countries_list=clean_data['top_10_countries']   
>>> countries_list   
[{'Country': '路易斯利亞 (Luxembourg)', 'GDP (PPP, USD)': 120000}, {'Country': '新加坡 (Singapore)', 'GDP (PPP, USD)': 105000}, {'Country': '挪威 (Norway)', 'GDP (PPP, USD)': 95000}, {'Country': '瑞士 (Switzerland)', 'GDP (PPP, USD)': 90000}, {'Country': '阿聯酋 (United Arab Emirates)', 'GDP (PPP, USD)': 85000}, {'Country': '葡萄牙 (Portugal)', 'GDP (PPP, USD)': 78000}, {'Country': '冰島 (Iceland)', 'GDP (PPP, USD)': 75000}, {'Country': '愛爾蘭 (Ireland)', 'GDP (PPP, USD)': 72000}, {'Country': '韓國 (South Korea)', 'GDP (PPP, USD)': 65000}, {'Country': '美國 (United States)', 'GDP (PPP, USD)': 60000}]

可以用 Pandas 將其轉成容易處理的 DataFrame :

>>> import pandas as pd   
>>> df=pd.DataFrame(countries_list)   
>>> df   
                      Country  GDP (PPP, USD)
0          路易斯利亞 (Luxembourg)          120000
1             新加坡 (Singapore)          105000
2                 挪威 (Norway)           95000
3            瑞士 (Switzerland)           90000
4  阿聯酋 (United Arab Emirates)           85000
5              葡萄牙 (Portugal)           78000
6                冰島 (Iceland)           75000
7               愛爾蘭 (Ireland)           72000
8            韓國 (South Korea)           65000
9          美國 (United States)           60000

注意, 由於未聯網搜尋, 資料的正確性可能受限. 

對於文字接龍的單次生成請求呼叫 /api/generate 端點時也可以用 json 參數限制輸出 JSON 格式資料, 下面我們改用 /api/generate 端點, 並加強提示詞關於欄位的限制重新測試 :

>>> url='http://localhost:11434/api/generate'  
>>> payload={ 
    "model": "gemma4:e4b",
    "prompt": "請列出全球人均 GDP 前十大國家, 包含 Country 與 GDP 兩欄位, 且以 JSON 格式回覆. 注意, 欄位名稱請忠實使用 Country 與 GDP, 不可自行更改",
    "format": "json",
    "stream": False
    }
>>> reply=requests.post(url, json=payload)  
>>> result=reply.json()   
>>> pprint(result)   
{'context': [2,
             105,
             9731,
             107,
             98,
             107,
             106,
             107,
             105,
             2364,
             107,
... (略) ...
             236783,
             107,
             138,
             236842,
             107,
             236783],
 'created_at': '2026-05-30T08:30:01.0055623Z',
 'done': True,
 'done_reason': 'stop',
 'eval_count': 329,
 'eval_duration': 4434360500,
 'load_duration': 4382299000,
 'model': 'gemma4:e4b',
 'prompt_eval_count': 64,
 'prompt_eval_duration': 40775600,
 'response': '{\n'
             '  "global_per_capita_gdp_top_10": [\n'
             '    {\n'
             '      "Country": "Singapore",\n'
             '      "GDP": "約 80,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Luxembourg",\n'
             '      "GDP": "約 140,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Qatar",\n'
             '      "GDP": "約 85,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Ireland",\n'
             '      "GDP": "約 90,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Norway",\n'
             '      "GDP": "約 80,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Switzerland",\n'
             '      "GDP": "約 95,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "United States",\n'
             '      "GDP": "約 75,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Australia",\n'
             '      "GDP": "約 65,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Iceland",\n'
             '      "GDP": "約 60,000 USD"\n'
             '    },\n'
             '    {\n'
             '      "Country": "Denmark",\n'
             '      "GDP": "約 55,000 USD"\n'
             '    }\n'
             '  ]\n'
             '}',
 'total_duration': 9343526600}

可見這回有遵照約束, 使用指定的 Country 與 GDP 做欄位名稱了. 其次, 與 /api/chat 不同的是, /api/generate 將回應放在字典的 response 鍵裡, 取出後用 json.loads() 將其轉成 Python 字典 :

>>> json_string=result['response']  
>>> clean_data=json.loads(json_string)  
>>> clean_data   
{'global_per_capita_gdp_top_10': [{'Country': 'Singapore', 'GDP': '約 80,000 USD'}, {'Country': 'Luxembourg', 'GDP': '約 140,000 USD'}, {'Country': 'Qatar', 'GDP': '約 85,000 USD'}, {'Country': 'Ireland', 'GDP': '約 90,000 USD'}, {'Country': 'Norway', 'GDP': '約 80,000 USD'}, {'Country': 'Switzerland', 'GDP': '約 95,000 USD'}, {'Country': 'United States', 'GDP': '約 75,000 USD'}, {'Country': 'Australia', 'GDP': '約 65,000 USD'}, {'Country': 'Iceland', 'GDP': '約 60,000 USD'}, {'Country': 'Denmark', 'GDP': '約 55,000 USD'}]}

人均 GDP 前十大國家資料放在 global_per_capita_gdp_top_10 鍵裡 : 

>>> countries_list=clean_data['global_per_capita_gdp_top_10']   
>>> countries_list  
[{'Country': 'Singapore', 'GDP': '約 80,000 USD'}, {'Country': 'Luxembourg', 'GDP': '約 140,000 USD'}, {'Country': 'Qatar', 'GDP': '約 85,000 USD'}, {'Country': 'Ireland', 'GDP': '約 90,000 USD'}, {'Country': 'Norway', 'GDP': '約 80,000 USD'}, {'Country': 'Switzerland', 'GDP': '約 95,000 USD'}, {'Country': 'United States', 'GDP': '約 75,000 USD'}, {'Country': 'Australia', 'GDP': '約 65,000 USD'}, {'Country': 'Iceland', 'GDP': '約 60,000 USD'}, {'Country': 'Denmark', 'GDP': '約 55,000 USD'}]

轉成 DataFrame :

>>> df=pd.DataFrame(countries_list)  
>>> df 
         Country            GDP
0      Singapore   約 80,000 USD
1     Luxembourg  約 140,000 USD
2          Qatar   約 85,000 USD
3        Ireland   約 90,000 USD
4         Norway   約 80,000 USD
5    Switzerland   約 95,000 USD
6  United States   約 75,000 USD
7      Australia   約 65,000 USD
8        Iceland   約 60,000 USD
9        Denmark   約 55,000 USD

怎麼排名跟上面呼叫 /api/chat 的不同啊.