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 的不同啊.