用 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 接近。
延伸閱讀