用 DecentrAI 寫 Python:8 個真實案例

「免費 AI 寫程式」這幾個字很多人講,但實測過 ChatGPT Free / Claude Free / Gemini 之後會發現:每個都有限額、有審查、有 SaaS 鎖。

DecentrAI 是另一條路——背後跑的是 Qwen 2.5-14B 微調,14B 對日常 Python 任務(API、爬蟲、資料處理)綽綽有餘。這篇用 8 個真實工作場景示範它怎麼用,包含完整 prompt、AI 實際輸出、跟我踩到的坑。

所有範例 prompts 都可以複製到 supertools.tw 直接試。模型是 dxai-shadow-v1,每次推論大約 5-10 秒。

案例 1:FastAPI 端點 + Pydantic 驗證

場景:要做個用戶註冊 API。

Prompt:

寫一個 FastAPI POST /register 端點,接受 email + password + name。
要求:
- email 用 Pydantic EmailStr 驗證
- password 至少 8 字元、含數字和大寫
- name 1-50 字元
- 失敗回 422,成功回 201 + UUID
- 不要連資料庫,回傳模擬 UUID 即可

DecentrAI 輸出:

import re
import uuid
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, field_validator

app = FastAPI()


class RegisterRequest(BaseModel):
    email: EmailStr
    password: str
    name: str

    @field_validator("password")
    @classmethod
    def validate_password(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("密碼至少 8 字元")
        if not re.search(r"[A-Z]", v):
            raise ValueError("密碼必須含大寫字母")
        if not re.search(r"\d", v):
            raise ValueError("密碼必須含數字")
        return v

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        if not (1 <= len(v) <= 50):
            raise ValueError("name 1-50 字元")
        return v


@app.post("/register", status_code=201)
async def register(req: RegisterRequest):
    user_id = str(uuid.uuid4())
    return {"user_id": user_id, "email": req.email, "name": req.name}

評:直接能跑、Pydantic v2 風格(用 field_validator 不是舊的 validator),edge case 都顧到。沒踩坑。

案例 2:SQLite 查詢 + Window function

Prompt:

寫 SQLite 查詢:找出近 7 天每個 user 的 order 統計,
回傳 user_id、order_count、total_amount、avg_amount、最新 order 時間。
按 total_amount 倒排。表 schema 自己假設。

DecentrAI 輸出:

-- 假設 orders 表:id, user_id, amount, created_at
SELECT
    user_id,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount,
    AVG(amount) AS avg_amount,
    MAX(created_at) AS last_order_at
FROM orders
WHERE created_at >= datetime('now', '-7 days')
GROUP BY user_id
ORDER BY total_amount DESC;

評:對;簡潔;用 SQLite 原生的 datetime('now', '-7 days')(不是 PostgreSQL 的 INTERVAL)。沒踩坑。

進階追問「改用 window function 加上每個 user 的排名」也能正確給出 RANK() OVER (ORDER BY SUM(amount) DESC)。

案例 3:Scrapy 爬蟲

Prompt:

用 scrapy 寫個爬蟲,從 example.com 爬所有 /article/* 頁面,
擷取 title, author, publish_date, content。
存進 JSON Lines。要處理 robots.txt + 設 download_delay 1 秒。

DecentrAI 輸出:

import scrapy
import json


class ArticleSpider(scrapy.Spider):
    name = "articles"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com/sitemap.xml"]

    custom_settings = {
        "DOWNLOAD_DELAY": 1.0,
        "ROBOTSTXT_OBEY": True,
        "FEEDS": {
            "articles.jsonl": {
                "format": "jsonlines",
                "encoding": "utf-8",
            },
        },
    }

    def parse(self, response):
        # 從 sitemap 找出 /article/* URLs
        response.selector.register_namespace(
            "ns", "http://www.sitemaps.org/schemas/sitemap/0.9"
        )
        for url in response.xpath("//ns:url/ns:loc/text()").getall():
            if "/article/" in url:
                yield scrapy.Request(url, callback=self.parse_article)

    def parse_article(self, response):
        yield {
            "url": response.url,
            "title": response.css("h1::text").get(),
            "author": response.css(".author::text").get(),
            "publish_date": response.css("time::attr(datetime)").get(),
            "content": " ".join(response.css(".content p::text").getall()),
        }

評:很完整。注意它選擇從 sitemap.xml 入口(比通用爬要靈活),自動 ROBOTSTXT_OBEY + DOWNLOAD_DELAY。沒踩坑。

案例 4:pytest fixture + parametrize

Prompt:

寫 pytest 測試:測 def slugify(s: str) -> str
要測 ASCII、中文、emoji、空字串、超長字串。
用 fixture + parametrize。

DecentrAI 輸出:

import pytest

# Assume the function under test
def slugify(s: str) -> str:
    import re, unicodedata
    s = unicodedata.normalize("NFKD", s)
    s = re.sub(r"[^\w\s-]", "", s).strip().lower()
    return re.sub(r"[\s_-]+", "-", s) or "untitled"


@pytest.fixture
def sample_inputs():
    return {
        "ascii": "Hello World!",
        "chinese": "你好 世界",
        "emoji": "🎉 Party Time! 🎊",
        "empty": "",
        "long": "a" * 1000,
    }


@pytest.mark.parametrize("key,expected", [
    ("ascii",   "hello-world"),
    ("chinese", "你好-世界"),
    ("emoji",   "party-time"),
    ("empty",   "untitled"),
])
def test_slugify(sample_inputs, key, expected):
    assert slugify(sample_inputs[key]) == expected


def test_slugify_long_input(sample_inputs):
    result = slugify(sample_inputs["long"])
    assert result == "a" * 1000

評:fixture + parametrize 用法正確、覆蓋邊角 case、untitled fallback 設計合理。沒踩坑。

案例 5:pandas 資料清洗

Prompt:

有個 CSV:date,user,event,duration_seconds
要做:
1. 移除 duration_seconds < 0 的 row
2. 解析 date 成 datetime
3. 加欄位 is_weekend
4. 按 user 加欄位 'event_count_30d'(過去 30 天事件數,rolling)
5. 輸出新 CSV

DecentrAI 輸出:

import pandas as pd

def clean_and_enrich(input_csv: str, output_csv: str) -> pd.DataFrame:
    df = pd.read_csv(input_csv, parse_dates=["date"])

    # 1. drop negative duration
    df = df[df["duration_seconds"] >= 0].copy()

    # 3. is_weekend
    df["is_weekend"] = df["date"].dt.dayofweek >= 5

    # 4. rolling 30-day event count per user
    df = df.sort_values(["user", "date"]).reset_index(drop=True)
    df["event_count_30d"] = (
        df.groupby("user")
          .apply(lambda g: g.rolling("30D", on="date")["event"].count())
          .reset_index(level=0, drop=True)
    )

    df.to_csv(output_csv, index=False)
    return df

評:邏輯對;rolling window 用 '30D' 符合 pandas 語法。有個小坑——這個 groupby + apply 在 pandas 2.0+ 有 deprecation warning,新版改用 groupby(...).rolling(...) 更直接。我提示「pandas 2.0+ 寫法」後它正確改成:

df["event_count_30d"] = (
    df.set_index("date")
      .groupby("user")["event"]
      .rolling("30D")
      .count()
      .reset_index(level=0, drop=True)
      .values
)

案例 6:async + asyncio.gather 並發

Prompt:

用 httpx async 同時 fetch 50 個 URL,限制最多 10 個並發。
失敗的 URL 要回傳 (url, error),不要整個 crash。
回傳 list of (url, status_code or "error", body)

DecentrAI 輸出:

import asyncio
import httpx


async def fetch_one(client: httpx.AsyncClient, url: str, sem: asyncio.Semaphore):
    async with sem:
        try:
            resp = await client.get(url, timeout=10.0)
            return (url, resp.status_code, resp.text[:1000])
        except Exception as e:
            return (url, "error", str(e))


async def fetch_many(urls: list[str], concurrency: int = 10) -> list:
    sem = asyncio.Semaphore(concurrency)
    async with httpx.AsyncClient() as client:
        tasks = [fetch_one(client, url, sem) for url in urls]
        return await asyncio.gather(*tasks, return_exceptions=False)


# Usage
if __name__ == "__main__":
    urls = [f"https://httpbin.org/status/{200 if i % 5 else 500}" for i in range(50)]
    results = asyncio.run(fetch_many(urls, concurrency=10))
    for url, status, body in results[:5]:
        print(url, status)

評:semaphore 限流、try/except 防爆、回傳結構正確。沒踩坑。

案例 7:CLI 工具 with Click + 進度條

Prompt:

寫 click CLI:輸入 input dir 跟 output dir,
把所有 .png 用 Pillow 轉 .webp(quality 80),
顯示 tqdm 進度條,跳過已存在檔案。

DecentrAI 輸出:

import click
from pathlib import Path
from PIL import Image
from tqdm import tqdm


@click.command()
@click.option("--input-dir", required=True, type=click.Path(exists=True, file_okay=False))
@click.option("--output-dir", required=True, type=click.Path(file_okay=False))
@click.option("--quality", default=80, type=int)
@click.option("--overwrite", is_flag=True)
def convert(input_dir: str, output_dir: str, quality: int, overwrite: bool):
    """Convert all .png in INPUT_DIR to .webp in OUTPUT_DIR."""
    src = Path(input_dir)
    dst = Path(output_dir)
    dst.mkdir(parents=True, exist_ok=True)

    pngs = list(src.glob("*.png"))
    skipped = 0
    converted = 0

    for p in tqdm(pngs, desc="Converting"):
        target = dst / (p.stem + ".webp")
        if target.exists() and not overwrite:
            skipped += 1
            continue
        with Image.open(p) as im:
            im.save(target, "WebP", quality=quality)
        converted += 1

    click.echo(f"Done: {converted} converted, {skipped} skipped.")


if __name__ == "__main__":
    convert()

評:click + tqdm 用法正確、mkdir(parents=True, exist_ok=True)、im.save("WebP", quality=...) API 對。沒踩坑。

案例 8:CI/CD GitHub Actions

Prompt:

寫 .github/workflows/ci.yml:
- 觸發:push 到 main、PR
- Python 3.11 + 3.12 matrix
- 安裝 poetry,cache .venv
- 跑 ruff + pytest
- 上傳 coverage 到 Codecov
- 失敗時 Slack 通知

DecentrAI 輸出:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ['3.11', '3.12']

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}

      - name: Install poetry
        run: pipx install poetry

      - name: Cache .venv
        uses: actions/cache@v4
        with:
          path: .venv
          key: venv-${{ matrix.python }}-${{ hashFiles('poetry.lock') }}

      - name: Install deps
        run: poetry install --no-interaction

      - name: Lint
        run: poetry run ruff check .

      - name: Test
        run: poetry run pytest --cov=. --cov-report=xml

      - uses: codecov/codecov-action@v4
        with:
          files: coverage.xml

      - name: Notify Slack on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

評:actions/setup-python@v5、actions/cache@v4 都是 2026 最新版本。matrix 寫法正確。有小坑——poetry 在某些 GitHub runner 預設沒有,建議用 pipx install poetry 比 pip install poetry 穩。它已經這樣寫了。✅

評分總結

8 個真實場景:

  • 零踩坑:6 個(案例 1, 2, 3, 4, 6, 7, 8 — 確切是 7 個算了)
  • 小坑可修:1 個(案例 5 pandas API deprecation)
  • 完全錯:0 個

體感跟 ChatGPT GPT-4o-mini 約 95% 同等。差距主要在:

  • DecentrAI 偶爾 prompt 模糊時要追問
  • ChatGPT 對最新 API 變動跟得快一點
  • DecentrAI 中文 prompt 理解優於英文 prompt

但 ChatGPT Free 有 40 條 / 天上限,DecentrAI 無限。對日常工作流的累積差異很大。

為什麼 14B 跑日常工作夠用?

Python 80% 任務(CRUD、I/O、testing、CI)都是「模式重複」的代碼。14B 經過 instruction-tuning + 9k 對話訓練已經內化這些模式。需要 70B+ 才有意義的反而是:

  • 大型架構決策(system design)
  • 複雜推理(多步驟證明)
  • 極長 context 跨檔案理解

這些不在 80% 日常 Python 工作裡。

立刻試

打開 supertools.tw,貼任何上面的 prompt 直接試。沒註冊、沒限額、回應速度跟 ChatGPT Free 接近。


延伸閱讀