📌 本文重點
- 小模型別當全能 Agent,要當被流程管控的小工
- 用顯式狀態機拆任務,大幅提升穩定性與可回滾性
- 每步輸出 JSON + schema 驗證,讓小模型也能穩定改碼
只靠 prompt 堆疊,13GB 本地模型在中大型改碼任務幾乎必翻車:上下文飄掉、一次回錯一堆檔、改到一半忘記需求。把模型包進顯式狀態機,把「一次大任務」拆成可恢復的子任務,可以在不改模型的前提下,大幅提升穩定性、可觀測性與可測試性——正是那篇 13.8GB 模型從 2/10 變成 10/10 的核心做法。
💡 關鍵: 只改調用方式與流程設計,就能把同一顆 13.8GB 小模型的表現從 2/10 拉到 10/10。
重點說明
1. 小模型為什麼在長對話裡特別容易翻車?
從工程視角,有三個根本原因:
-
token 預算太小 + 資訊密度太高
13GB 級(多是 7B〜13B 參數)在 4k–16k context 內要同時塞:需求、專案結構、幾個檔案內容、測試結果、對話歷史,關鍵訊息會被截斷或壓縮到模型抓不到。 -
上下文漂移(context drift)
多輪長對話時,你不可能每次都重貼完整需求與檔案。模型只能靠「語意回憶」之前說過什麼,多輪後任務邊界就開始模糊:忘記原本的 constraint、改到不該動的檔案、把舊 bug 當新需求。 -
一次性決策成本過高
傳統「一條大 prompt + chain-of-thought」會在單輪裡要求:理解需求 → 找檔 → 設計改動 → 寫碼 → 自我檢查。這在 token 限制與小模型推理能力下,極易在中間任一步 hallucinate,之後又沒有明確的 rollback 機制。
關鍵結論: 小模型不適合當「一次性全能 Agent」,更適合當「被嚴格流程控制的小工」,讓狀態機負責 long-term 記憶與決策邊界。
💡 關鍵: 把 long-term 記憶與流程決策交給狀態機,小模型只做局部推理,能顯著降低翻車率。
2. 用顯式狀態機拆解大任務:核心設計
把「改造一個中小型專案」拆成明確的 State + Transition:
常見狀態設計可以是:
DISCOVER_PROJECT:掃描 repo、建立檔案索引PLAN_CHANGE:根據需求與索引產生修改計畫(檔案清單、步驟)EDIT_FILE:逐檔案修改(step-by-step)RUN_TESTS:執行測試、收集結果ROLLBACK_OR_FIX:測試失敗→嘗試修復或回滾DONE/FAILED:終止狀態
每個狀態都只給模型 極簡上下文 + 明確輸入/輸出 schema,例如在 EDIT_FILE:
- 輸入:
- 需求摘要(短)
- 該檔案目前內容(或片段)
- 計畫中對此檔案的變更描述
- 輸出:
- 結構化 JSON:
{"status": "ok|skip|abort", "patch": "...diff..."}
轉移條件示例:
DISCOVER_PROJECT→PLAN_CHANGE:索引成功建立PLAN_CHANGE→EDIT_FILE:生成的計畫通過 schema 檢查EDIT_FILE→RUN_TESTS:所有目標檔案處理完RUN_TESTS→- 全綠 →
DONE - 有失敗 + 可定位 →
EDIT_FILE(targeted fix) - 多次失敗 →
ROLLBACK_OR_FIX
失敗重試策略與超時機制:
- 每個狀態設定
max_retries,例如 2–3 次,超過則標記為FAILED或轉ROLLBACK_OR_FIX。 - 每次 LLM 回應必經:
- JSON schema 驗證
- domain guard(例如禁止刪除大量無關 code)
- 超時機制:
- 單次呼叫 timeout(例如 60s),保障工作流不被卡死
- 整個工作流 wall-clock timeout(例如 30 分鐘),方便在 CI 或自動化工具中運行
💡 關鍵: 把重試、超時、回滾寫死在狀態機邏輯裡,比指望 prompt 提醒模型「要小心」可靠太多。
3. 實作範例:13GB 本地模型改造專案(Python)
以下是精簡版 pseudo-code,示範如何把本地模型包在狀態機裡,跑 step-by-step 編碼、測試與回滾。假設:
- 使用 vLLM / llama.cpp server 暴露出 OpenAI-compatible API
- GPU:3060 12GB,模型用 Q4 / Q5 量化
import enum
import json
import subprocess
from dataclasses import dataclass
from typing import Dict, Any, List
import requests
OPENAI_BASE = "http://localhost:8000/v1"
MODEL_NAME = "local-13b-q4"
class State(enum.Enum):
DISCOVER_PROJECT = "DISCOVER_PROJECT"
PLAN_CHANGE = "PLAN_CHANGE"
EDIT_FILE = "EDIT_FILE"
RUN_TESTS = "RUN_TESTS"
ROLLBACK_OR_FIX = "ROLLBACK_OR_FIX"
DONE = "DONE"
FAILED = "FAILED"
@dataclass
class Context:
repo_path: str
requirement: str
file_index: Dict[str, Any] = None
plan: List[Dict[str, Any]] = None
current_file_idx: int = 0
test_result: str = ""
def call_llm(system_prompt: str, user_prompt: str, max_tokens: int = 1024) -> str:
resp = requests.post(
f"{OPENAI_BASE}/chat/completions",
json={
"model": MODEL_NAME,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.2,
"max_tokens": max_tokens,
},
timeout=60,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def discover_project(ctx: Context) -> Context:
# 這裡可以用 ripgrep / fd 產生檔案清單,略
ctx.file_index = {"files": ["src/a.py", "src/b.py"], "tests": ["tests/test_a.py"]}
return ctx
def plan_change(ctx: Context) -> Context:
system = """你是資深工程師,輸出 JSON,字段: steps: [{file, description}]。"""
user = f"需求: {ctx.requirement}\n可修改檔案: {ctx.file_index['files']}\n請產生最多 10 個步驟。"
raw = call_llm(system, user)
try:
plan = json.loads(raw)
except Exception:
raise ValueError("PLAN_CHANGE: model output not JSON")
ctx.plan = plan["steps"]
ctx.current_file_idx = 0
return ctx
def apply_patch(repo_path: str, file: str, patch: str):
# 建議用 unified diff + `patch` 指令,這裡簡化處理
with open(f"{repo_path}/{file}", "w", encoding="utf-8") as f:
f.write(patch)
def edit_file(ctx: Context) -> Context:
step = ctx.plan[ctx.current_file_idx]
file_path = step["file"]
with open(f"{ctx.repo_path}/{file_path}", encoding="utf-8") as f:
content = f.read()
system = """你只負責修改單一檔案。輸出 JSON: {status, patch}。
- status: ok | skip | abort
- patch: 完整檔案內容,不要解釋文字。"""
user = f"需求: {ctx.requirement}\n此步驟: {step['description']}\n原始內容:\n{content[:4000]}"
raw = call_llm(system, user, max_tokens=2048)
try:
out = json.loads(raw)
except Exception:
raise ValueError("EDIT_FILE: invalid JSON")
if out["status"] == "ok":
apply_patch(ctx.repo_path, file_path, out["patch"])
elif out["status"] == "abort":
raise RuntimeError("Model aborted edit")
ctx.current_file_idx += 1
return ctx
def run_tests(ctx: Context) -> Context:
proc = subprocess.run(["pytest"], cwd=ctx.repo_path, capture_output=True, text=True)
ctx.test_result = proc.stdout + "\n" + proc.stderr
return ctx
def rollback_or_fix(ctx: Context) -> Context:
# 真實情況應該搭配 git: reset --hard HEAD~1 或建立 branch
# 這裡示意:交給模型看測試輸出,決定要修哪個檔案
system = "請從測試輸出中找出最可能需要修改的單一檔案,輸出 JSON: {file, reason}"
user = ctx.test_result[:4000]
raw = call_llm(system, user)
try:
out = json.loads(raw)
except Exception:
raise ValueError("ROLLBACK_OR_FIX: invalid JSON")
# 根據 out['file'] 重新插入 plan
ctx.plan.insert(ctx.current_file_idx, {"file": out["file"], "description": out["reason"]})
return ctx
def run_state_machine(ctx: Context):
state = State.DISCOVER_PROJECT
retries: Dict[State, int] = {s: 0 for s in State}
MAX_RETRIES = 2
while True:
try:
if state == State.DISCOVER_PROJECT:
ctx = discover_project(ctx)
state = State.PLAN_CHANGE
elif state == State.PLAN_CHANGE:
ctx = plan_change(ctx)
state = State.EDIT_FILE
elif state == State.EDIT_FILE:
if ctx.current_file_idx >= len(ctx.plan):
state = State.RUN_TESTS
else:
ctx = edit_file(ctx)
elif state == State.RUN_TESTS:
ctx = run_tests(ctx)
if "failed" in ctx.test_result:
state = State.ROLLBACK_OR_FIX
else:
state = State.DONE
elif state == State.ROLLBACK_OR_FIX:
ctx = rollback_or_fix(ctx)
state = State.EDIT_FILE
elif state in (State.DONE, State.FAILED):
return state, ctx
except Exception as e:
print(f"State {state} error: {e}")
retries[state] += 1
if retries[state] > MAX_RETRIES:
return State.FAILED, ctx
if __name__ == "__main__":
ctx = Context(repo_path="/path/to/repo", requirement="把 API v1 換成 v2 並修正測試")
final_state, final_ctx = run_state_machine(ctx)
print("Final state:", final_state)
重點:
- 模型只做 局部、可回滾的決策(例如一次只改一檔)。
- 工作流邏輯(狀態、重試、回滾)都在 可測試的 Python 函式 中,而不是藏在 prompt 裡。
若用 TypeScript + LangGraph / 自行寫狀態機,模式相同:每個 Node 是一個狀態,Edge 由測試結果與 JSON 輸出決定。
4. 與「prompt + chain-of-thought」相比的實際好處
- 穩定性
- CoT 依賴模型「自己監督自己」,小模型的推理錯誤會被往後 propagate,沒有硬性 checkpoint。
-
狀態機把流程切成多個 可檢查的邏輯節點,每步都能強制過 schema、判斷失敗與回滾。
-
成本與資源
- 單輪 prompt 巨大 → token 費用高,且在本地 GPU 上速度慢。
-
狀態機讓每輪上下文更短、更聚焦,在 3060 12GB + Q4 模型上可以穩定跑 多輪短對話,總延遲往往比一輪巨 prompt 更好控制。
-
觀測性(logging / trace)
-
把每個狀態轉移、LLM input/output、git diff 全記錄(例如存到 SQLite / OpenTelemetry trace),可以:
- 後覽失敗案例
- 做離線分析:哪個狀態最常出錯?哪種需求最難?
-
可測試性
- 傳統做法難以單元測試 Agent:prompt 無法 deterministic。
- 狀態機可以用 fake LLM 或 replay 真實輸出,對每個 state handler 寫 unit test,例如:當測試結果是某種錯誤訊息時,
ROLLBACK_OR_FIX應插入哪個 plan。
建議與注意事項
1. 避免狀態爆炸
- 限制狀態數量在 5–10 個,複雜度放在狀態內部的子函式,而不是新增一堆細碎狀態。
- 優先建立 通用狀態模板:
PLAN/EXECUTE/VERIFY/RECOVER四類,大部分工程任務都能套這個骨架。
2. 處理 hallucination 與非法狀態
- 所有 LLM 輸出一律要求 JSON + schema 驗證,非法就走重試邏輯。
- 在
EDIT_FILE等關鍵步驟設計 domain guard: - 檢查 patch 是否刪除超過 X% 行數;
- 檢查是否涉及黑名單檔案(例如 config、CI YAML)。
3. 設計「保守模式」避免改壞檔案
建議預設開啟:
- 所有改動先走分支 / 工作目錄拷貝
-
狀態機只在 temp branch/dir 上動手,最後才由人類 review + merge。
-
只允許白名單檔案被修改
-
在
PLAN_CHANGE事先產出可修改清單,EDIT_FILE收到不在清單內的檔案時直接拒絕。 -
必備 diff 檢查
- 每次改檔後,log 一份 git diff。
- 可以加一個
HUMAN_APPROVAL狀態,在 CI 或 IDE 裡讓人按「Approve」才繼續。
4. 3060 12GB 本地 GPU 的實務建議
- 模型:選
Q4_K_M/Q5量化的 7B–13B 開源模型(如 Llama 系家族、Qwen 等),在 Agentic coding 任務上實測延遲可接受。 - 推理引擎:
llama.cpp/ollama:部署簡單,適合單機開發。vLLM:若你需要高併發與更細緻的 batching,可考慮,但對記憶體稍敏感。- 參數建議:
max_tokens控制在 512–2048,依狀態不同調整。temperature低於 0.3,減少 hallucination。- 避免在單輪塞完整檔案,改成 片段 + 明確上下文(例如「你只看這個 function」)。
5. 映射到現有 Agent framework 的模式
這套思路可以直接映射到:
- LangChain / LangGraph:
- 每個狀態 = 一個
Node(通常是Tool+LLM)。 - 轉移條件透過 conditional edges 判斷 JSON output 中的
status/next_state。 -
用 LangGraph 的 checkpointing 把
Context存到外部 store,可做恢復與可視化。 -
LangMCP(可檢視狀態的 Agent framework)
- 把
Context中的file_index/plan/test_result全部納入 inspectable state。 -
除錯時可以直接在 UI 裡看到「Agent 在哪一步做錯決策」,而不是只看 tokens trace。
-
Claude Code / goal-workflow 類工具
- 把這裡的狀態機當作後端 orchestrator,前端 IDE 只負責:設定 goal → 顯示 plan → 顯示每步 diff / 測試 → 提供人類 approve。
總結工程模式:
LLM 做局部推理 + 生成,狀態機做長期決策 + 記憶 + 恢復。
把「智慧」從模型本體,搬到你可控、可測、可觀測的工作流程式碼中,13GB 小模型也能在工程任務裡穩定交付 10/10 的結果。
🚀 你現在可以做的事
- 在本地架一個
llama.cpp或vLLM的 OpenAI-compatible 服務,載入一顆 7B–13B Q4/Q5 模型試跑上文的狀態機範例- 把你現有的「一條大 prompt 改碼流程」改寫成 5–10 個明確狀態,並為每步定義 JSON schema 與
max_retries- 在 CI 或開發機中為這套狀態機加上 logging / trace(例如 SQLite 或 OpenTelemetry),實際分析哪個 state 最常出錯

