昨天有提到,今天要來把整份程式做打包,提供給其他人使用,我本來以為打包成執行檔用下載的會比較簡單,後來發現直接在終端輸入指令安裝更方便,所以,我們今天讓程式可以透過 uv tool install 的方式下載。
在打包之前,最後再新增一個功能以及改幾個地方:
在 Day 27 時,做了當「模型回答時」按 ctrl + c 可以中斷模型的功能,
但目前如果在輸入時按下會發生什麼事呢?
沒錯,一行行的報錯,因為我們沒有做好這部分的捕捉,直譯器以為遇到了意外狀況,給出了報錯。
在使用者輸入時,加上 try ... except 來捕捉,這裡用了元組把 KeyboardInterrupt、EOFError 同時做捕捉,
前者是之前寫過的 ctrl + c;後者則是 ctrl + d 的 End-Of-File Error(檔案結尾、輸入串流結束錯誤)。
End-Of-File Error:
在命令列或終端機程式中,當程式正在等待使用者輸入(例如呼叫 Python 的input()或prompt_toolkit的prompt()),而標準輸入串流(stdin)卻突然被關閉且沒有讀到任何資料時,Python 就會拋出這個錯誤。
sys.exit(0),0 是結束代碼,代表「正常結束」。
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
try:
while ...:
try:
user_input, images = get_input(rprompt=model.get_context_status_text)
except (KeyboardInterrupt, EOFError):
cli.console.print(cli.render_end())
sys.exit(0)
if not user_input ...:
...
一直以來,我匯入專案內的其他模組(檔案)的方式都是直接用例如 from cli import get_input 這樣的方式匯入,而之前都沒有出錯,是因為我在終端開啟程式的位置是在 meowgent/src/meowgent 裡頭,直譯器一上門就發現了「cli 檔案夾在那,我要進去裡面找!」。
然而打包後就不同了,執行的起點位於最外層的 meowgent,直譯器一來,找不到 cli 在哪裡,就直接報錯了,這裡有兩種改法:
from meowgent.cli import get_input 這樣的匯入方式。main.py 中,把 meowgent/src/meowgent 加入「模組搜尋路徑清單」,讓直譯器知道可以在這裡面找模組。可以看出,前者是非常麻煩的,所以我選擇後者(當然,如果一開始就用這樣的方式匯入,也不至於這麼麻煩)。
所以現在 main.py 的匯入大致長這樣:
主要看
BASE_DIR開始的部分:先定義出
meowgent/src/meowgent的路徑,
從當前檔案meowgent/src/meowgent/cli/main.py的位置開始推,然後要先resolve()把前面的目錄展開,parent是cli目錄,再往上一層parent.parent才是我們所求的meowgent/src/meowgent。然後,檢查它是否有在「模組搜尋路徑清單」-
sys.path串列裡,如果沒有的話加入到串列的第一個元素。
(不放在第一個其實也沒差,主要是防止有其他的套件跟我們的檔案夾同名時,其他套件因為排序在前面而被優先使用,但在本專案內沒有這種狀況)
# src/meowgent/cli/main.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from providers import LLMResponse
# -----
from pathlib import Path
import sys
BASE_DIR = Path(__file__).resolve().parent.parent # 取得 src/meowgent 位置
if str(BASE_DIR) not in sys.path:
sys.path.insert(0, str(BASE_DIR))
# -----
from agent import Agent
from providers import OllamaProvider
from cli import get_input, get_tool_approval, CLIRenderer, start_ollama, clean_ollama, handle_cmd, select_directory
import os
from config import ConfigManager, repair
import ollama
import threading
from typing import Optional
from tool import set_subagent_callback
記住以下原則:
from __future__ import annotations 的部分一定要在「第一行」。BASE_DIR 加入到 sys.path 之後才能匯入(除非你用方法一)。外部的模組不一定要在路徑加到
sys.path才能匯入,也可以放在前面,而Path跟sys因為要先用到,一定要先寫出來。
先前在 main.py 時我們寫的入口是 if __name__ == "__main__":,
意思是「當呼叫者為程式本身才執行」,但打包後會有問題,因為呼叫者不是 main.py 本身了(變成了指令封裝腳本)。
把程式入口改成 def main():(宣告為程式進入點),然後下方我們也保留原先的 if __name__ == "__main__": 方式:
還要把
def on_subagent_status(...):移進來,否則會報錯(因為執行作用域問題)。
# src/meowgent/cli/main.py
...
def main():
def on_subagent_status(...):
...
cli = ...
...
if __name__ == "__main__":
main()
找找看你專案裡面有沒有 pyproject.toml 這份檔案,如果是使用 uv 來下載套件的話,那應該會有 [project] 裡面的內容了,而我們還需要新增上下兩部分的內容:
[tool.hatch.build.targets.wheel])與建構([build-system]):
tool.hatch.build.targets.wheel:指定打包成 .whl 時的原始碼路徑採用 src layout(就是有寫 src/meowgent 的意思),明確告知 Hatchling 核心程式碼位於 src/meowgent。build-system:宣告專案使用 Hatchling 作為建置後端。Hatchling 可以理解為是負責打包程式的工具。
[project]:
[project.scripts]):
meowgent 的執行檔,可透過輸入 meowgent 來啟動程式。meowgent 時,會自動調用 src/meowgent/cli/main.py 裡面的 main() 函式。[tool.hatch.build.targets.wheel]
packages = ["src/meowgent"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "meowgent"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"html2text>=2025.4.15",
"httpx>=0.28.1",
"live>=0.0.3",
"mcp>=2.1.1",
"ollama>=0.6.2",
"openai>=2.46.0",
"pathlib>=1.0.1",
"prompt-toolkit>=3.0.52",
"pydantic>=2.13.4",
"python-dotenv>=1.2.1",
"questionary>=2.1.1",
"readability-lxml>=0.8.4.1",
"regex>=2026.1.15",
"rich>=15.0.0",
]
[project.scripts]
meowgent = "meowgent.cli.main:main"
到這裡,接下來只要把你的專案放到 GitHub 上並「公開」,大家就可以透過 uv 來下載了!
舉例來說,我這份專案的下載方式為:
uv tool install git+https://github.com/LeeMatthewCat/meowgent.git
但要記得電腦裡面要有 ollama 軟體。
然後啟動:
meowgent
終於啊~30 天的文章,順利被我寫完了,接下來這邊,就來說說我寫這份專案及文章的心得吧!
過程中,我無數次地懷疑過自己,到底寫這東西做什麼,我寫的,也沒有人家 codex、claude code 好用啊!
後來,我也釋懷了,在學校,我開著電腦寫程式時,總會有人問我「你寫這做什麼?」,我都會跟他說「我寫來玩玩的」。
沒錯,真的就是寫來玩的,更認真點來說是寫來「練習」的,沒錯,就像我參賽介紹中寫的「身為剛升大一的新鮮人,我想趁著暑假扎實磨練 Python 能力,因此決定挑戰從零打造屬於自己的 AI Agent!」,我的目的就只是為了練習 Python 而已,並不是要寫出什麼舉世矚目的專案出來,我沒有那麼厲害。
這份專案其實我默默寫了很久了,第一個 commit 是在 7/25 就提交了,但印象中 7 月初甚至更早我就開始做了。
在 8 月底,我舅舅建議我可以把這份專案拿去參加今年的 iThome 鐵人賽(他去年也參加了,而且還拿到冠軍,超強的)。
隨後,我開始看著已一個月沒碰的 ollama_provider.py 等前期 code 寫起了文章,
寫文章的過程中,發現了好多當初沒寫好,或已經看不懂的部分,我又重新把它改正、理解,甚至重構。
而到了 9/13,也就是 Day 1 發布當天,我把「文章的雛形」已經寫到了 Day 18,
沒錯,蠻好笑的,我也不知道那時候為什麼不一次寫完,哈哈。
而那時,我提到未來想做的功能:
有把前面幾篇都看到最後的人,應該都有發現,我一直在說我後面幾天都是壓線上傳的,但不是在 Day 1 時就已經寫到 Day 18 去了嗎?
因為我只寫了個雛形,後面再看時發現很多地方好像不至於分這麼多天,把它合併了,甚至有一篇最後因為我後面加了新功能不需要了而被刪掉了。
還有,不知道有沒有人發現,我後期的文章是一篇比一篇還要長啊,寫得越來越詳細了。
(當然,還有因為我開學了,越來越忙了,暑假真的好開心,每天只要待在家寫程式就好)
所以,我可以如期地繳出這 30 天的內容,自己還是覺得非常慶幸的。
這裡要自首一下,後期有幾篇,其實我都是在大致寫完後就先趕緊上傳了,然後過了 12:00 再用編輯模式把修好的內容補上,非常抱歉啊。
整份專案包括文章,我都使用到了大量的 AI 來幫忙。
但是基本上,每行程式碼都是我打的。
如果沒有它,才學會 Python 半年的我,估計是沒辦法在這麼短的期間內,完成相對完整的大專案。
真的沒有想到我寫了這麼多。
我想新增什麼功能,就把想法告訴它;出了什麼 bug 就告訴它,叫它幫我解;就連文章寫完,我也叫它幫我檢查錯字(而我錯字真的好多,不知道有沒有沒被它抓到而被你們抓到的錯字呢?)。
我時常在想 AI 到底會不會取代寫程式這件事,從 2017 年 Transformer 出現至今,僅僅過去了 9 年,AI 已經可以做到絕大部分的 coding,只需要人從旁給 prompt。
而今年我才大一,大學四年碩士兩年,出來還要當個兵,我真正出社會已經又是 7 年後了,很難想像之後會發展成什麼樣。
這份專案加上這個文章,我很明顯感受到自己 Python 的能力提升了不少,
而我的時間規劃能力也是有待加強,剛開始很有信心地覺得自己一定來得及,寫一寫就開始在旁邊滑手機,後面發現來不及了才在那趕。
而我又看到了個有趣的專案 https://github.com/ARahim3/DigUp/tree/main ,接下來,我想模仿他來自己再寫一份專案,
預計是用 MLX 來做較貼合於 Mac 的運算,然後跟他一樣採用 Google 的 EmbeddingGemma 2 模型來做檔案轉換為空間向量的部分,
然後打算先採用已經熟悉的 CLI 介面做開發,後期再嘗試跟他一樣用 Swift 做前端。
至於會不會再把它給寫成文章,那就要再看看啦,
可以確定的是,我不會想要像這 30 天一樣每天更新,太累了而且感覺品質也不是很好。
非常感謝每位曾經看過我這 30 篇文章的人,我有時候都在想,我寫出來的東西我自己都不一定想看了,真的有人要看嗎?
所以真的非常謝謝看到這裡的各位!
「手刻 AI Agent!大一新生的 Python 實戰筆記」正式完結~
恭喜完賽!
從讀寫檔案、執行指令到打包安裝,能看到你一步步把程式做完整。之後如果還有維護 GitHub,可以放幾個能直接跑的小範例,也在 README 分享不同模型的結果和耗時,讓想試用的人更容易上手、選模型。
謝謝支持!