快轉到主要內容
  1. 教學文章/

本地 LLM 實戰:Ollama + Python 打造自己的小助手

·9 分鐘· loading · loading · ·
LLM Ollama Python AI Cli
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
科技觀點 - 本文屬於一個選集。
§ 1: 本文

featured

一. 前言:把 LLM 放進自己的工作流
#

雲端 LLM 很方便,但每次只是整理筆記、改一小段程式、產生 commit message,都把內容丟到雲端,多少有點小題大作。

Ollama 的可愛之處,就是它把「下載模型、啟動模型、提供 API」這幾件事包得很乾淨。你不需要先研究一堆推論框架,就能在自己的電腦上跑本地 LLM。

今天拍拍君不只介紹 Ollama,而是用 Python 做一個真的能用的小助手:可以在終端機聊天、串流回覆、切換 persona、保存簡單對話紀錄,還能把檔案內容交給模型分析。

它不會一開始就是完美 agent。這很好。先做出能跑的骨架,再慢慢加功能,才不會把小助手養成小災難。

這篇會用到:

  • Ollama:本地 LLM runtime
  • Python:串接 API 與寫工具邏輯
  • Typer:做命令列介面
  • Rich:讓輸出比較舒服
  • JSONL:保存簡單聊天紀錄

如果你已經看過拍拍君之前的 Ollama 入門,今天這篇就是下一步:把它從「可以聊天」變成「可以被你組裝進工作流」。

二. 安裝 Ollama 與建立 Python 專案
#

先安裝 Ollama。官方下載頁在這裡:

https://ollama.com/download

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.tomlconfig.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,通常只是比較快把自己嚇到。拍拍。

延伸閱讀
#

科技觀點 - 本文屬於一個選集。
§ 1: 本文

相關文章

在LLM的時代,為什麽我還要寫部落格?
·3 分鐘· loading · loading
LLM AI Blog
Python difflib 實戰:文字差異比對、相似度比較與 patch 輸出完全攻略
·10 分鐘· loading · loading
Python Difflib Text-Processing Developer-Tools Cli
Python prompt_toolkit 實戰:打造互動式 CLI、Auto-Completion 與 REPL 完全攻略
·10 分鐘· loading · loading
Python Prompt_toolkit Cli REPL Developer-Tools
Python Textual 實戰:終端機 TUI 應用開發完全攻略
·9 分鐘· loading · loading
Python Textual TUI Cli Terminal
Python tomllib 實戰:內建 TOML 解析、設定檔管理與 pyproject.toml 完全攻略
·7 分鐘· loading · loading
Python Tomllib TOML 設定檔 Pyproject.toml
Python hypothesis 實戰:Property-Based Testing 與自動化找 bug 完全攻略
·7 分鐘· loading · loading
Python Hypothesis Testing Pytest Developer-Tools