iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

之前在 Day 1 時不知道各位有沒有遇到一個問題,沒法調用模型,出現 httpx.ConnectError: [Errno 61] Connection refused 這樣的連線錯誤,
這是因為你沒有把 ollama 軟體打開,也就是「ollama 的進程未開啟」。

而若你開啟了,結束對話還會有個問題,剛剛本地模型回答所佔用的 RAM 沒有被釋放,還需要我們手動關閉程式來釋放,非常麻煩。

而今天這篇,就是要來解決這個問題,程序開啟前,主動發送 HTTP 請求給 ollama,結束後將其關閉。


開啟進程

檢查是否已開啟

這裡我們檢查的不僅只是進程存在,而且進程還要能給出回應,這種做法稱為「就緒探針」。

用一句話總結就緒探針的概念:「在把工作交給它之前,先輕輕敲門確認它是否『已經準備好接客了』」。

在這裡,我們使用 httpx.get() 來做探針,回傳的部分用元組包起代表進程狀態的布林值以及 Popen 物件(選):

等等會做「如果進程狀態為 False 結束程式」的判斷(因為沒有 ollama 進程就無法調用模型了)。

Popen 意思就是「Process Open」,蠻有趣的!

# src/meowgent/cli/ollama_manager.py

import subprocess
import httpx
import time
from typing import Tuple, Optional

def start_ollama() -> Tuple[bool, Optional[subprocess.Popen]]:

    try:
        httpx.get("http://localhost:11434", timeout=1) 
        # 檢查是否已經有連線,設定一秒逾時
        
        return (True, None)

探針探測會出現有和無兩種結果:

  1. 如果有探測到(發送 httpx 請求有得到回應),那就代表這個 ollama 進程已經存在了,所以我們不做任何動作,只回傳 None。
  2. 另一種就是請求沒有回應,那我們就要嘗試啟動進程了。

關於這個 url,localhost 是本地 IP,11434 是 Ollama 官方指定的預設連接埠(Port)。
而逾時(timeout)的部分,等的是「進程已經存在,但伺服器沒給出回應」。


嘗試連線

要開啟 ollama 的進程有兩種方法,一種就是直接開啟軟體,而想當然,這個做法在我們設計的 Agent 上是無法實現的。
另一種方法就是透過「Shell 指令」的方式,直接去啟動 ollama 的背景服務:

ollama serve

透過這個指令,就可以開啟進程,把它給放入 subprocess.Popen() 的 args 參數。

之前 Day 4 講過了,為了防止注入,正常是用「串列」來做指令輸入的。

而這裡,把 stdout 輸出、stderr 錯誤訊息設為 subprocess.DEVNULL 表示我們把這些資訊丟掉,不去管它。

因為:

  1. 成功時,會跑出一堆訊息提示,不需要看。
  2. 失敗時的提示,這裡我們也沒打算做個別處理。
# src/meowgent/cli/ollama_manager.py
...

def start_ollama() ...:

    try:
	    ...

	except httpx.ConnectError: # 未連線 -> 嘗試連線

        try:
            process = subprocess.Popen(
                args=["ollama", "serve"],
                stdout=subprocess.DEVNULL, # 捨棄終端輸出
                stderr=subprocess.DEVNULL # 捨棄錯誤輸出
            )
        except Exception: # 攔截沒裝 ollama、權限不足等
            return (False, None)

檢查開啟狀態

然而,輸入完指令不代表連線一定會存在,還是要用「探針」檢查連線狀況。
用 for _ in range(20): 重複 20 次的連線嘗試,若進程未啟動(except httpx.ConnectError:)則等待 0.5 秒,成功則回傳 (True, process)。

前面有說過 timeout 等的是「進程未回應」,所以要用 time.sleep() 才能做到「等待進程建立」的效果。

如果 20 次後還是失敗,則呼叫等等要實作的 clean_ollama() 來防止進程已開啟,但未回應的情況(要關閉進程),最後回傳 (False, None)。

# src/meowgent/cli/ollama_manager.py
...

def start_ollama() ...:
	...
	except ...:
		...
		except ...:
			...
			
		for _ in range(20):
            if process.poll() is not None: # 指令已崩潰
                return (False, None)

            try:
                httpx.get("http://localhost:11434", timeout=1)

                return (True, process) # 成功連線

            except httpx.ConnectError:
                time.sleep(0.5) # 還未連線成功,等待

        clean_ollama(process)
        return (False, None) # 嘗試 20 次還是沒有連線

清理進程

前面有說過進程如果沒關閉會持續佔用 RAM,所以我們需要將它給關上。
需要接收由 start_ollama() 產生的 Popen 物件來做處理:

處理前先判斷「物件是否存在」且「進程還在跑(.poll() 為 None 表示還在跑)」,使用 terminate() 「溫和」地終止進程,並用 wait() 等待終止完成。

什麼叫「溫和」的終止?
還有另一種比較激進的方式是 kill(),可以理解為它不會在結束前做「清理」,而這裡用的 terminate() 則會。

terminate() 後還需要 wait() 是因為這個清理的動作是「非阻塞式」的,也就是說 terminate() 執行如果沒有 wait() 清理的部分就瞬間被帶過了,繼續執行下面的東西,但實質上,還沒有完成清理,這樣會造成:

  1. 關閉後馬上再開啟,舊的進程還佔用在通訊埠上,新舊兩個就撞上了,無法正常啟動。
  2. 若退出過程發生意外,它會永遠佔用著 GPU 不放,俗稱「孤兒進程」。
# src/meowgent/cli/ollama_manager.py
...

def clean_ollama(process: Optional[subprocess.Popen]):
    if process and process.poll() is None: # 確保 ollama 程序還在跑
        process.terminate() # 清理進程

        process.wait() # 等待終止

進行到這裡,我們已經搞定了目前 CLI 所需的所有東西,
而下一篇,激動人心的時刻來了!終於可以來做主程式 - main.py,讓 Agent 跑起來啦!


上一篇
Day 12 - 漂亮的 CLI 介面 - 渲染 - 下
下一篇
Day 14 - 主程式
系列文
手刻 AI Agent!大一新生的 Python 實戰筆記 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言