iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0

實際問題

昨天我說我們要用 AI Agent 審查規格書。但「用 AI」有不同的方式:付費 API(Claude、GPT)或本地模型(Ollama)。

我選了本地。原因很簡單:

  1. 成本。免費。一份 1000 行的規格書不會花你錢。
  2. 隱私。規格書不離開你的機器。
  3. 可控。想改什麼就改什麼,不受 API 版本限制。

缺點是速度。本地模型跑 7 億參數需要時間。在我的 Mac 上,第一次分析大概 2 分鐘。但那是一次性的學習成本,之後就快了。

今天我們要做的是:30 秒內讓這套系統跑起來,拿著你寫的規格書,看本地 LLM 能挑出什麼問題。

快速開始:三個命令

三行代碼搞定:

# 1. 裝 Ollama(Mac/Windows/Linux,2 分鐘)
# 去 <https://ollama.ai> 下載,一鍵安裝

# 2. 裝模型(Mistral 7B,推薦用這個,快又能用)
ollama pull mistral

# 3. 啟動本地 LLM 服務
ollama serve

看到 Listening on 127.0.0.1:11434 就算成功。這個窗口保持開著。

建立項目

在另一個終端,建立項目目錄:

mkdir srs-review-agent
cd srs-review-agent

# 建立項目結構
mkdir -p src tests/fixtures
touch src/__init__.py src/main.py src/models.py
touch tests/fixtures/srs_small.md requirements.txt .env

預期結構:

srs-review-agent/
├── src/
│   ├── __init__.py
│   ├── main.py              # 核心邏輯(今天寫)
│   └── models.py            # 數據模型
├── tests/fixtures/
│   └── srs_small.md         # 測試檔案
├── requirements.txt
└── .env

裝依賴

requirements.txt

langchain==0.2.0
langgraph==0.0.60
ollama==0.6.2
requests==2.31.0
python-dotenv==1.0.0
pydantic==2.5.2
typer==0.9.0
pytest==7.4.3

安裝:

pip install -r requirements.txt

寫測試規格書

故意設計有問題的規格書。tests/fixtures/srs_small.md

# 線上筆記系統

## 功能需求
- REQ-2.1.1:系統支持多用戶並行存取
- REQ-2.1.2:每個用戶可建立無限筆記
- REQ-2.2.1:用戶可分享筆記
- REQ-2.2.2:支持三種權限:檢視、編輯、管理

## 性能需求
- REQ-3.1.1:響應時間 < 500ms
- REQ-3.1.2:支持 1000 個並行用戶

## 安全需求
- REQ-4.1.1:所有數據加密存儲
- REQ-4.1.2:系統採用單用戶模式,每次只有一個用戶活動

矛盾在這裡:REQ-2.1.1 說多用戶,REQ-4.1.2 說單用戶。Agent 應該能檢出來。

配置環境變數

.env

OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=mistral

寫 Agent

src/models.py 很簡單,先只有一個結果類:

from dataclasses import dataclass
from typing import Optional

@dataclass
class AnalysisResult:
    srs_filename: str
    analysis_text: str
    model_used: str
    timestamp: Optional[str] = None

    def __str__(self) -> str:
        return f"分析結果 ({self.model_used}): {self.srs_filename}"

src/main.py 是核心。這裡做三件事:

  1. 檢查 Ollama 能不能連上,模型有沒有裝
  2. 讀你的規格書
  3. 丟給 LLM,拿回分析
import os
from pathlib import Path
from dotenv import load_dotenv
import requests
import typer

load_dotenv()
app = typer.Typer()

class OllamaAgent:
    """用本地 LLM 分析規格書"""

    def __init__(self, model: str = "mistral", ollama_url: str = "<http://localhost:11434>"):
        self.model = model
        self.ollama_url = ollama_url
        self.api_endpoint = f"{ollama_url}/api/generate"
        self._verify_connection()

    def _verify_connection(self):
        """檢查 Ollama 有沒有在跑、模型有沒有裝"""
        try:
            response = requests.get(f"{self.ollama_url}/api/tags", timeout=5)
            if response.status_code != 200:
                raise ConnectionError("Ollama 服務未響應")

            data = response.json()
            models = [m['name'].split(':')[0] for m in data.get('models', [])]

            if self.model not in models:
                raise ConnectionError(
                    f"模型 '{self.model}' 未下載。\n"
                    f"請執行:ollama pull {self.model}\n"
                    f"已下載:{', '.join(models) if models else '無'}"
                )

            print(f"✓ Ollama 連接成功,模型:{self.model}")

        except requests.exceptions.ConnectionError:
            raise ConnectionError(
                "無法連接 Ollama。\n"
                "檢查清單:\n"
                "1. 安裝 Ollama (<https://ollama.ai>)\n"
                "2. 執行 ollama serve\n"
                "3. 在另一個終端執行本命令"
            )

    def read_srs(self, filepath: str) -> str:
        """讀規格書"""
        path = Path(filepath)
        if not path.exists():
            raise FileNotFoundError(f"檔案不存在: {filepath}")

        with open(path, 'r', encoding='utf-8') as f:
            return f.read()

    def analyze_srs(self, srs_text: str) -> str:
        """呼叫 LLM 分析"""
        system_prompt = """你是 SRS 審查專家。分析規格書找出:
1. 邏輯矛盾(需求相衝突)
2. 模糊不清的地方
3. 跨需求依賴問題
4. 改進建議

要具體,引用需求編號,說明問題,提建議。"""

        prompt = f"{system_prompt}\n\n分析這份規格書:\n\n{srs_text}"

        try:
            response = requests.post(
                self.api_endpoint,
                json={
                    "model": self.model,
                    "prompt": prompt,
                    "stream": False,
                    "temperature": 0.7,
                },
                timeout=300
            )

            if response.status_code != 200:
                raise RuntimeError(f"LLM 呼叫失敗: {response.text}")

            return response.json().get('response', '無法獲取結果')

        except requests.exceptions.Timeout:
            raise RuntimeError(
                f"分析超時。{self.model} 可能比預期慢。"
            )
        except requests.exceptions.ConnectionError:
            raise RuntimeError("無法連接 Ollama。執行:ollama serve")

@app.command()
def analyze(
    filepath: str = typer.Argument(
        ...,
        help="規格書路徑"
    ),
    model: str = typer.Option(
        "mistral",
        "--model",
        "-m",
        help="使用的模型"
    ),
    verbose: bool = typer.Option(
        False,
        "--verbose",
        "-v",
        help="顯示詳細日誌"
    )
):
    """分析規格書(使用本地 LLM)

    用法:
        python -m src.main analyze tests/fixtures/srs_small.md
        python -m src.main analyze tests/fixtures/srs_small.md -m llama2 -v
    """

    try:
        if verbose:
            print(f"模型: {model}, Ollama: <http://localhost:11434>")

        agent = OllamaAgent(model=model)

        filepath_obj = Path(filepath)
        if not filepath_obj.exists():
            print(f"❌ 檔案不存在: {filepath}")
            raise typer.Exit(1)

        print(f"📖 讀取: {filepath}")
        srs_text = agent.read_srs(filepath)
        print(f"✓ {len(srs_text)} 字符, {len(srs_text.split(chr(10)))} 行")

        if verbose:
            print(f"\n預覽(前 300 字):\n{srs_text[:300]}...\n")

        print(f"\n🔍 用 {model} 分析中...")
        print("⏳ 等等(30-120 秒,取決於硬件和規格書大小)")

        analysis_result = agent.analyze_srs(srs_text)

        print("\n" + "=" * 70)
        print("✅ 分析結果")
        print("=" * 70)
        print(analysis_result)
        print("=" * 70)

    except ConnectionError as e:
        print(f"❌ Ollama 連接錯誤: {e}")
        raise typer.Exit(1)
    except FileNotFoundError as e:
        print(f"❌ 檔案錯誤: {e}")
        raise typer.Exit(1)
    except RuntimeError as e:
        print(f"❌ 執行錯誤: {e}")
        raise typer.Exit(1)
    except Exception as e:
        print(f"❌ 未知錯誤: {e}")
        if verbose:
            import traceback
            print(traceback.format_exc())
        raise typer.Exit(1)

if __name__ == "__main__":
    app()

運行

打開第一個終端,保持 ollama serve 執行。

打開第二個終端,進入專案目錄,執行:

python -m src.main analyze tests/fixtures/srs_small.md

等 30-120 秒。應該看到類似:

✓ Ollama 連接成功,模型:mistral
📖 讀取: tests/fixtures/srs_small.md
✓ 687 字符, 20 行

🔍 用 mistral 分析中...
⏳ 等等(30-120 秒,取決於硬件和規格書大小)

======================================================================
✅ 分析結果
======================================================================
### 發現的矛盾:

1. **多用戶 vs 單用戶衝突**(REQ-2.1.1 vs REQ-4.1.2)
   - REQ-2.1.1:「系統支持多用戶並行存取」
   - REQ-4.1.2:「系統採用單用戶模式」
   - 這兩個需求直接相衝

2. **性能假設問題**
   - 單用戶系統為什麼要支持 1000 個並行用戶?

### 建議:
澄清系統目標:多用戶協作或單用戶應用?
======================================================================

成功! 🎉 你的本地 LLM Agent 跑起來了。

會遇到的問題

「Ollama 服務未響應」

➜ 新開終端,執行 ollama serve,看到 Listening on 127.0.0.1:11434。不要關這個窗口。

「模型 mistral 未下載」

➜ 執行 ollama pull mistral。第一次要幾分鐘,取決於網速。

「分析超時」或「很慢」

➜ 正常。Mistral 7B 在 CPU 上需要時間。

  • 有 GPU?安裝 CUDA,快 5-10 倍
  • 或用更小的模型:ollama pull neural-chat(也能用,就是回答品質會差一點)
  • 第一次用某個模型會特別慢(model warmup),之後快一些

「ModuleNotFoundError」

➜ 執行 pip install -r requirements.txt,確保依賴都裝了。

「No module named 'src'」

➜ 檢查你在 srs-review-agent/ 目錄裡執行命令,而且 src/__init__.py 存在(即使是空檔案)。

「我的硬件够嗎?」

Mistral 7B 需要:

  • 最少 4GB RAM(會很卡)
  • 推薦 8GB+(舒服)
  • 有 GPU 會快很多,但不是必需

接下來

今天我們有了一個「會跑」的系統。明天我們把它改成狀態機(用 LangGraph),這樣就能做多步工作流:讀→分塊→提取約束→檢測衝突→生成報告。

現在的 src/main.py 只是一個簡單的函數。明天它會變成有狀態、可追蹤、能改進的 Agent。

代碼已經在你的機器上跑了。下一步就是疊功能。


進度

第 1 週:透明的 SRS 分析

  • Day 1:為什麼需要 Agent ✓
  • Day 2:本地 LLM 跑起來 ← 今天
  • Day 3-4:狀態機與多步工作流
  • Day 5-7:MCP 工具與約束提取

上一篇
Day 1:為什麼需要 SRS 審查 Agent?
系列文
解決需求規格書矛盾:用 Claude Code × MCP 實作自律型文檔審查 Agent2
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言