一. 前言:把 LLM 放進自己的工作流 #
雲端 LLM 很方便,但每次只是整理筆記、改一小段程式、產生 commit message,都把內容丟到雲端,多少有點小題大作。
Ollama 的可愛之處,就是它把「下載模型、啟動模型、提供 API」這幾件事包得很乾淨。你不需要先研究一堆推論框架,就能在自己的電腦上跑本地 LLM。
今天拍拍君不只介紹 Ollama,而是用 Python 做一個真的能用的小助手:可以在終端機聊天、串流回覆、切換 persona、保存簡單對話紀錄,還能把檔案內容交給模型分析。
它不會一開始就是完美 agent。這很好。先做出能跑的骨架,再慢慢加功能,才不會把小助手養成小災難。
這篇會用到:
- Ollama:本地 LLM runtime
- Python:串接 API 與寫工具邏輯
- Typer:做命令列介面
- Rich:讓輸出比較舒服
- JSONL:保存簡單聊天紀錄
如果你已經看過拍拍君之前的 Ollama 入門,今天這篇就是下一步:把它從「可以聊天」變成「可以被你組裝進工作流」。
二. 安裝 Ollama 與建立 Python 專案 #
先安裝 Ollama。官方下載頁在這裡:
macOS 也可以用 Homebrew:
brew install ollama
Linux 可以用官方腳本:
curl -fsSL https://ollama.com/install.sh | sh
啟動服務:
ollama serve
有些桌面版會自動在背景啟動。如果不確定,可以測試:
curl http://localhost:11434/api/tags
看到 JSON 回應,就代表 Ollama server 正常活著。
接著下載一個模型。這裡用 llama3.2 示範,你可以換成其他模型:
ollama pull llama3.2
先在終端機測試:
ollama run llama3.2
確認模型能回答後,建立 Python 專案:
mkdir pypy-local-assistant
cd pypy-local-assistant
uv init
uv add ollama typer rich
mkdir chats
專案先長這樣:
pypy-local-assistant/
├── assistant.py
├── chats/
└── pyproject.toml
如果你還不熟 uv,可以先看這兩篇:
三. 最小版本:送 prompt,拿回答 #
先從最小可行程式開始。建立 assistant.py:
import ollama
MODEL = "llama3.2"
response = ollama.chat(
model=MODEL,
messages=[
{
"role": "user",
"content": "請用繁體中文解釋什麼是 Python decorator。",
}
],
)
print(response["message"]["content"])
執行:
uv run python assistant.py
如果一切正常,你會看到模型回答。這裡最重要的是 messages 格式:
messages = [
{"role": "system", "content": "你是一個 helpful assistant。"},
{"role": "user", "content": "你好!"},
{"role": "assistant", "content": "你好,有什麼想聊的?"},
]
也就是說,對話歷史其實就是一個 list。只要我們把前面的訊息保留起來,下次一起送給模型,它就能「看起來像是記得剛才說過什麼」。
注意,是看起來像。模型不會自動永久記住你之前講過的東西。你沒有塞進 context 的內容,它就不知道。這點對本地 LLM 特別重要。
四. 加上 system prompt:先決定助手的工作方式 #
沒有 system prompt 的助手通常會有點飄。拍拍君通常會把語言、回答風格、保守程度與輸出格式先講清楚。
SYSTEM_PROMPT = """
你是拍拍君的本地程式助手。
請使用繁體中文回答。
回答要簡潔、具體、可執行。
遇到程式問題時,優先給出最小可行範例。
如果資訊不足,請先說明假設,不要亂編。
""".strip()
放進 messages 第一筆:
import ollama
MODEL = "llama3.2"
SYSTEM_PROMPT = """
你是拍拍君的本地程式助手。
請使用繁體中文回答。
回答要簡潔、具體、可執行。
遇到程式問題時,優先給出最小可行範例。
如果資訊不足,請先說明假設,不要亂編。
""".strip()
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "幫我寫一個讀取 JSON 檔案的 Python 函式。"},
]
response = ollama.chat(model=MODEL, messages=messages)
print(response["message"]["content"])
System prompt 不用寫得像魔法咒語。清楚比花俏重要。你要它用繁體中文,就直接說。你要它少廢話,也直接說。模型不是玻璃心,沒問題的。
五. 做成互動式聊天迴圈 #
現在來做真正的聊天工具。流程很簡單:讀取使用者輸入、加入 messages、呼叫模型、把回答放回 messages,然後重複。
import ollama
MODEL = "llama3.2"
SYSTEM_PROMPT = """
你是拍拍君的本地程式助手。
請使用繁體中文回答。
回答要簡潔、具體、可執行。
""".strip()
def main() -> None:
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("拍拍君本地助手啟動!輸入 /exit 離開。")
while True:
user_input = input("\n你:").strip()
if not user_input:
continue
if user_input in {"/exit", "/quit"}:
print("拍拍君:下次見。")
break
messages.append({"role": "user", "content": user_input})
response = ollama.chat(
model=MODEL,
messages=messages,
)
answer = response["message"]["content"]
messages.append({"role": "assistant", "content": answer})
print(f"\n拍拍君:{answer}")
if __name__ == "__main__":
main()
執行:
uv run python assistant.py
這已經是一個最基本的本地聊天機器人。不過如果模型回答很長,你要等整段生成完才看得到,體感會像在等微波爐。下一步我們加串流。
六. 串流輸出:讓答案一邊生成一邊出現 #
Ollama 支援 streaming。加上 stream=True 後,我們會拿到一段一段的 chunk。
import ollama
MODEL = "llama3.2"
SYSTEM_PROMPT = """
你是拍拍君的本地程式助手。
請使用繁體中文回答。
回答要簡潔、具體、可執行。
""".strip()
def stream_answer(messages: list[dict[str, str]]) -> str:
parts: list[str] = []
stream = ollama.chat(
model=MODEL,
messages=messages,
stream=True,
)
print("\n拍拍君:", end="", flush=True)
for chunk in stream:
piece = chunk["message"]["content"]
parts.append(piece)
print(piece, end="", flush=True)
print()
return "".join(parts)
def main() -> None:
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("拍拍君本地助手啟動!輸入 /exit 離開。")
while True:
user_input = input("\n你:").strip()
if not user_input:
continue
if user_input in {"/exit", "/quit"}:
print("拍拍君:下次見。")
break
messages.append({"role": "user", "content": user_input})
answer = stream_answer(messages)
messages.append({"role": "assistant", "content": answer})
if __name__ == "__main__":
main()
這版的使用體驗會好很多。尤其本地模型速度很吃硬體;同一個模型在不同機器上可能差很多。串流不能讓模型變聰明,但能讓等待感下降很多。
七. 用 Typer 做成比較像工具的 CLI #
現在的程式能用,但不太像工具。我們可以用 Typer 加上指令列選項,例如指定模型或 persona。
import ollama
import typer
from rich.console import Console
app = typer.Typer(help="拍拍君本地 LLM 小助手")
console = Console()
PERSONAS = {
"default": """
你是拍拍君的本地程式助手。
請使用繁體中文回答。
回答要簡潔、具體、可執行。
""".strip(),
"teacher": """
你是耐心的 Python 教學助手。
請使用繁體中文回答。
先解釋概念,再給出小範例。
""".strip(),
"reviewer": """
你是嚴謹的 code reviewer。
請指出風險、可讀性問題與可以改善的地方。
回答要具體,不要空泛稱讚。
""".strip(),
}
def stream_answer(model: str, messages: list[dict[str, str]]) -> str:
parts: list[str] = []
stream = ollama.chat(model=model, messages=messages, stream=True)
console.print("\n[bold green]拍拍君:[/bold green]", end="")
for chunk in stream:
piece = chunk["message"]["content"]
parts.append(piece)
console.print(piece, end="")
console.print()
return "".join(parts)
@app.command()
def chat(
model: str = typer.Option("llama3.2", help="Ollama model name"),
persona: str = typer.Option("default", help="default / teacher / reviewer"),
) -> None:
"""開始互動式聊天。"""
if persona not in PERSONAS:
raise typer.BadParameter(f"未知 persona: {persona}")
messages = [{"role": "system", "content": PERSONAS[persona]}]
console.print("[bold]拍拍君本地助手啟動![/bold] 輸入 /exit 離開。")
console.print(f"model={model}, persona={persona}")
while True:
user_input = typer.prompt("\n你").strip()
if not user_input:
continue
if user_input in {"/exit", "/quit"}:
console.print("拍拍君:下次見。")
break
messages.append({"role": "user", "content": user_input})
answer = stream_answer(model, messages)
messages.append({"role": "assistant", "content": answer})
if __name__ == "__main__":
app()
執行預設模式:
uv run python assistant.py chat
切換 code review 模式:
uv run python assistant.py chat --persona reviewer
Typer 很適合快速做內部 CLI。如果你想更深入,可以看 Python Typer 入門 或 Python argparse 實戰。
八. 加上簡單記憶:用 JSONL 保存對話 #
剛剛的 messages 只存在記憶體。程式一關,對話就消失。最簡單的保存方式是 JSONL:每一行都是一個 JSON,適合 append,也適合之後用 script 處理。
加入這幾個函式:
import json
from datetime import datetime
from pathlib import Path
CHAT_DIR = Path("chats")
def new_chat_path() -> Path:
CHAT_DIR.mkdir(exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
return CHAT_DIR / f"chat-{stamp}.jsonl"
def append_message(path: Path, role: str, content: str) -> None:
record = {
"time": datetime.now().isoformat(timespec="seconds"),
"role": role,
"content": content,
}
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
在 chat() 裡使用:
chat_path = new_chat_path()
messages = [{"role": "system", "content": PERSONAS[persona]}]
append_message(chat_path, "system", PERSONAS[persona])
# 每次收到使用者輸入
messages.append({"role": "user", "content": user_input})
append_message(chat_path, "user", user_input)
# 每次拿到模型回答
answer = stream_answer(model, messages)
messages.append({"role": "assistant", "content": answer})
append_message(chat_path, "assistant", answer)
這個「記憶」還很陽春。它只是紀錄,不會自動載入舊對話。但這其實是好事,因為本地模型的 context window 有限制,你不應該把所有歷史都塞回去。
比較實用的策略是:短期對話放在 messages,長期紀錄放在 JSONL 或 SQLite;需要時再摘要、搜尋、挑重點放回 context。這就是很多 AI 工具背後的基本概念。
九. 實用功能:把檔案交給小助手分析 #
小助手最常見的用途之一,就是幫忙看檔案。例如:
uv run python assistant.py ask-file README.md "請幫我摘要這個專案"
可以加一個 ask-file 指令:
from pathlib import Path
MAX_CHARS = 20_000
@app.command("ask-file")
def ask_file(
path: Path,
question: str,
model: str = typer.Option("llama3.2", help="Ollama model name"),
) -> None:
"""把文字檔內容丟給模型分析。"""
if not path.exists():
raise typer.BadParameter(f"檔案不存在: {path}")
content = path.read_text(encoding="utf-8")
if len(content) > MAX_CHARS:
content = content[:MAX_CHARS]
content += "\n\n[內容已截斷,只顯示前 20000 字元]"
prompt = f"""
以下是一個檔案內容:
```text
{content}
問題:{question} “”".strip()
messages = [
{"role": "system", "content": PERSONAS["default"]},
{"role": "user", "content": prompt},
]
stream_answer(model, messages)
這裡的 `MAX_CHARS` 很重要。不要把超大的 log、整個 repo、或一堆二進位垃圾直接塞進 prompt。很多本地 LLM 問題不是模型不行,而是輸入太髒、太長、太沒有界線。
如果要更進階,可以先把檔案切 chunks,再摘要或做向量搜尋。不過今天先不要膨脹,能讀單一文字檔就已經很實用了。
## 十. 模型選擇與常見排查
Ollama 最好玩的地方是模型可以換。你可以先用小模型確認流程,再換比較大的模型測品質。
```bash
ollama list
ollama pull llama3.2
ollama pull qwen2.5
ollama pull gemma3
挑模型時可以看三件事:參數量、量化版本、用途。越大的模型通常越聰明,但越慢、越吃記憶體;coding model 不一定最會中文;中文模型也不一定最會寫程式。拍拍君的建議很樸素:不要只看 benchmark,請用自己的任務實測。
幾個常見錯誤:
Connection refused #
通常是 Ollama server 沒開。
ollama serve
curl http://localhost:11434/api/tags
model not found #
模型還沒下載,或名字打錯。
ollama list
ollama pull llama3.2
回答很慢 #
可能是模型太大、記憶體不足、prompt 太長、或沒有吃到可用加速。先換小模型測試,不要一開始就追最大參數。
中文回答怪怪的 #
system prompt 明確要求繁體中文,並把格式講清楚。例如:
請用繁體中文回答。
請用條列式。
每點不要超過兩句。
如果不確定,請直接說不確定。
明確比客氣有用。模型不會因為你太直接就生氣,放心。
十一. 下一步:從小助手變成工作流零件 #
今天的小助手已經有幾個重要骨架:CLI 入口、system prompt、streaming、chat history、file input、model selection。接下來可以慢慢加功能。
你可以加入設定檔,用 pyproject.toml 或 config.toml 管理預設模型。Python 3.11 之後可以用 tomllib 讀 TOML,拍拍君也寫過 Python tomllib 實戰。
你也可以把 Rich 輸出做漂亮一點,讓使用者、助手、錯誤訊息有不同顏色。這部分可以參考 Python Rich 實戰。
再往後,可以加入向量搜尋,把舊筆記切成 chunks,做 embeddings,需要時找相關片段塞回 prompt。這就是簡化版 RAG。
最後才是工具呼叫。讓助手能搜尋檔案、產生 git commit message、摘要工作紀錄、檢查測試錯誤,都很有用。但權限越大,越需要保護。不要讓模型直接執行破壞性命令,先讓它提出建議,再由人確認。
結語:本地 LLM 的價值是可組裝 #
Ollama + Python 的組合迷人,不是因為它能取代所有雲端 LLM,而是因為它讓 LLM 變成你可以自己組裝的工具零件。
今天我們從最小 API 呼叫開始,做到互動式 CLI、串流輸出、角色設定、對話紀錄與檔案分析。這些都不複雜,但組起來就已經像一個每天能用的小助手。
拍拍君建議你先做一個很小的版本:先讓它能回答,再讓它能記錄,再讓它能讀檔,最後才讓它碰工具。
小步前進,才是把 AI 工具做好的最快方法。急著做全自動 agent,通常只是比較快把自己嚇到。拍拍。