為自己的 Codebase 寫一個專屬 Subagent:290 行 PHP 7.4 Agent 完整解構


Claude Code 的 subagent 生態系現在很豐富——社群有 database-architectdebuggererror-detective 等通用模板,幾分鐘下載完就能用。

但這些通用 agent 解決不了一個問題:它們不知道你的 codebase 有什麼規矩

PHP 7.4 不能用 match expression、Session 變數叫什麼、多租戶 DB 切換邏輯、批次列印的記憶體陷阱——這些只有你的專案才有的規則,通用 agent 一概不懂。

這篇拆解一個真實的 project-specific subagent:290 行 markdown,包含 PHP 7.4 限制清單、多租戶 pattern、效能優化、安全標準、AppSystem 專屬慣例。逐節解構讓你看「為自己的 codebase 寫一個專屬 agent」是什麼樣子。


TL;DR

項目內容
檔案.claude/agents/AppSystem-php74-pro.md
行數290 行 markdown
Modelsonnet
ToolsRead, Write, Edit, Bash, Glob, Grep
目標 codebase10 年 Legacy PHP 7.4、多租戶業務管理系統
章節數8 大節(語言限制 / 效能 / 安全 / 品質 / 專屬 patterns / 陷阱 / 輸出 / 結語)
核心特性PHP 7.4 嚴格限制、多租戶 DB 切換、Session 安全、批次列印

Frontmatter 設計

---
name: AppSystem-php74-pro
description: PHP 7.4 expert for AppSystem medical care system. Specializes in multi-tenant architecture, batch printing optimization, and legacy PHP performance. Use PROACTIVELY for AppSystem development.
tools: Read, Write, Edit, Bash, Glob, Grep
model: sonnet
---

四個關鍵欄位的設計取捨:

name:不只是命名

AppSystem-php74-pro 不是裝飾——這個名字會出現在 Claude Code 顯示的 agent 清單,讓使用者一眼知道「這是 AppSystem 專屬」

不寫 php-pro、不寫 expert,明確標 AppSystem 字首:避免跟其他 PHP 通用 agent 混淆。

description:給 Claude Code 看的,不是給人看的

描述句的關鍵字:

  • medical care system → Claude Code 知道是特定領域
  • multi-tenant architecture → 觸發詞
  • batch printing optimization → 觸發詞
  • legacy PHP performance → 觸發詞
  • Use PROACTIVELY → 讓 Claude Code 主動套用,不用使用者明確要求

PROACTIVELY 這個關鍵字是 subagent 機制的關鍵。Claude Code 會在處理 PHP 相關任務時自動考慮這個 agent,不用每次都指名

tools:精選 6 個

Read, Write, Edit, Bash, Glob, Grep——只給能修改檔案 + 跑指令的工具。不給 WebFetch / WebSearch——這個 agent 的工作不需要查網路,限制工具範圍可以加快回應、避免分心。

model: sonnet 不是 opus

寫 PHP code 不需要 opus 級別的推理。sonnet 夠快、夠便宜、夠準確。這個取捨對日常使用很重要——agent 越常用,model 成本越累積。


章節 1:PHP 7.4 語言限制清單

這是整個 agent 的核心價值。Claude Code 預設會輸出最新 PHP 語法,但 Legacy 專案不能用。所以要明確列出:

## PHP 7.4 Language Restrictions

### ✅ ALLOWED Features
- Typed properties (class properties with types)
- Null coalescing assignment operator (`??=`)
- Arrow functions (`fn() => ...`)
- Spread operator in arrays (`[...$array]`)
- Numeric literal separator (`1_000_000`)
- Traditional type hints (scalar, return types, nullable)
- Generators and iterators
- SPL data structures

### ❌ FORBIDDEN Features (PHP 8.0+)
- Union types (`string|int`) - Use PHPDoc instead
- Named arguments
- Match expressions - Use switch/case
- Constructor property promotion
- Attributes (`#[Route]`) - Use PHPDoc annotations
- Nullsafe operator (`?->`) - Use traditional null checks
- Mixed type - Use PHPDoc `@mixed`
- `str_contains()`, `str_starts_with()`, `str_ends_with()` - Use alternatives

為什麼這份清單關鍵

通用 PHP agent 會寫:

// PHP 8.0+ 寫法
public function findUser(int|string $id): User {
    return $this->users[$id] ?? throw new NotFoundException();
}

PHP 7.4 直接 syntax error。

但有了這個 agent 的限制清單,Claude Code 會輸出:

/**
 * @param int|string $id
 */
public function findUser($id): User {
    if (!isset($this->users[$id])) {
        throw new NotFoundException();
    }
    return $this->users[$id];
}

清單越具體,AI 越不會犯錯

設計重點

兩個列表平等對待——不只說能用什麼,更要說不能用什麼。AI 對「禁止」的指令比「鼓勵」的指令敏感。

每條 Forbidden 後面附「改用 X」:給替代方案,AI 知道下一步要寫什麼。光禁止不給替代,AI 還是會犯錯。


章節 2:效能優化重點

## Performance Optimization Focus

### 1. Batch Printing Optimization
**Problem**: Batch printing creates excessive blank pages and memory issues
**Solutions**:
- Use generators for large record sets
- Implement chunked processing (e.g., 50 records per batch)
- Clear output buffer between pages (`ob_clean()`, `ob_flush()`)
- Unset variables in loops to free memory
- Use `gc_collect_cycles()` for complex objects

接著提供完整 code example:

// Good: Memory-efficient batch processing
function getBatchRecords(int $orgId, int $limit = 50): Generator {
    $offset = 0;
    while ($records = $this->db->fetchRecords($offset, $limit)) {
        foreach ($records as $record) {
            yield $record;
        }
        $offset += $limit;
        if (count($records) < $limit) break;
    }
}

// Usage
foreach (getBatchRecords($orgId) as $record) {
    renderPrintPage($record);
    unset($record);
}

為什麼批次列印單獨開一節

對 AppSystem 來說,批次列印就是日常——每月固定產上百筆住民紀錄報表。記憶體爆炸是踩過的坑。

把這段寫進 agent 後,Claude Code 處理列印任務時自動會用 generator + chunked + gc_collect_cycles 三件套,不用每次提醒。

這就是 project-specific agent 的價值——把過去踩過的坑寫成 agent 的常識

三個優化方向各有完整 example

  1. Batch Printing(generator + ob_clean + unset)
  2. Database Query Optimization(PDO prepared + LIMIT/OFFSET + 避免 N+1)
  3. Memory Management(unset + 串流處理 + memory_get_peak_usage 監控)

每個都有可執行的 code snippet,AI 直接 copy 就能用


章節 3:安全 Best Practices

## Security Best Practices

### 1. SQL Injection Prevention
// Good: PDO prepared statements
$stmt = $pdo->prepare("SELECT * FROM patient WHERE patientID = :id AND OrgID = :org");
$stmt->execute(['id' => $patientId, 'org' => $orgId]);

// Bad: String concatenation (NEVER do this)
$sql = "SELECT * FROM patient WHERE patientID = '$patientId'";

三個安全主題

  1. SQL Injection:明確列 Good vs Bad,不用問 AI 知道哪個是反例
  2. XSS Preventionhtmlspecialchars($x, ENT_QUOTES, 'UTF-8') 三個參數每次都要寫對
  3. Session Security:Session 驗證 + regenerate

Session 驗證的細節

if (!isset($_SESSION['mcareDBno']) || !preg_match('/^demo-org-\d+$/', $_SESSION['mcareDBno'])) {
    throw new SecurityException('Invalid database session');
}

兩個 check 一起做:isset 防 null、regex 防注入

這個 pattern 是 AppSystem 多租戶架構的基礎——所有 DB 操作前都要這樣 verify。寫進 agent 後 Claude Code 處理任何 DB 任務都會自動加這段。

File Upload 章節

$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);

if (!in_array($mimeType, $allowedTypes, true)) {
    throw new SecurityException('Invalid file type');
}

finfo 而不是看副檔名——檔名可以偽造,MIME type 比較難

in_array(..., true) 第三參數是 strict 比較,避免型別轉換造成的繞過。

每個 detail 都是踩過坑才寫進去的。


章節 4:程式碼品質標準

### 1. Type Safety (PHP 7.4 Compatible)
class Patient {
    private int $patientID;
    private string $name;
    private ?string $email; // Nullable
    private array $records;

    public function __construct(int $patientID, string $name, ?string $email = null) {
        // ...
    }

    public function getEmail(): ?string {
        return $this->email;
    }
}

PHP 7.4 才有 typed properties。這個 example 是給 Claude Code 看「PHP 7.4 怎麼寫類型」的範本

Error Handling 範例

try {
    $result = $this->db->execute($sql, $params);
} catch (PDOException $e) {
    error_log("Database error: " . $e->getMessage());
    throw new DatabaseException('Failed to execute query', 0, $e);
}

// Custom exception classes
class DatabaseException extends Exception {}
class ValidationException extends Exception {}
class SecurityException extends Exception {}

不只 try-catch,還包含:

  • error_log 記錄細節
  • 把 PDOException 包成 DatabaseException(抽象層 leak prevention
  • 提供三個自訂 exception class,Claude Code 寫 code 時知道該丟哪個

PSR Standards 章節

- PSR-1: Basic Coding Standard
- PSR-4: Autoloading (if applicable)
- PSR-12: Extended Coding Style Guide
- 4 spaces for indentation (match existing AppSystem style)
- Opening braces on same line for functions/classes

match existing AppSystem style」—— 跟現有 codebase 一致比 PSR 嚴格更重要。Legacy 系統有自己的風格,新 code 應該融入。


章節 5:AppSystem 專屬 Patterns

這節是整個 agent 最 project-specific 的部分:

### 1. Multi-Tenant Database Switching
function getOrgDatabase(): string {
    if (!isset($_SESSION['mcareDBno'])) {
        throw new SecurityException('No organization database selected');
    }
    return $_SESSION['mcareDBno'];
}

$dbName = getOrgDatabase();
$pdo = new PDO("mysql:host=mysql;dbname={$dbName}", DB_USER, DB_PASS);

多租戶 DB 切換——每個 organization 有獨立 DB(demo-org-1demo-org-2 …),透過 session 切換。新進的開發者經常忘記這個 pattern——寫進 agent 之後 Claude Code 會自動套用。

### 2. Permission Checking Pattern
function hasPermission(int $userId, int $orgId, string $permissionName): bool {
    $stmt = $pdo->prepare("
        SELECT COUNT(*) FROM permission2 a
        INNER JOIN permission_subcate b ON a.PermissionID = b.cateID
        INNER JOIN permission_item c ON b.subcateID = c.subcateID
        INNER JOIN user_permission2 d ON d.serNo = c.serNo
        WHERE d.userID = :user_id
            AND d.OrgID = :org_id
            AND d.level = '1'
            AND c.name = :permission
    ");
    $stmt->execute([
        'user_id' => $userId,
        'org_id' => $orgId,
        'permission' => $permissionName
    ]);
    return $stmt->fetchColumn() > 0;
}

權限檢查 4-table JOIN——每個 controller 都要重複寫的 query 結構。寫進 agent 後 Claude Code 知道「這個系統的權限就是這樣查」。

### 3. Print Form Generation
function generateBatchPrintForms(array $patientIds): void {
    foreach (getBatchRecords($patientIds) as $patient) {
        ob_start();
        include 'print/template.php';
        $html = ob_get_clean();

        echo $html;
        echo '<div style="page-break-after: always;"></div>';

        unset($patient, $html);

        if ($counter++ % 10 === 0) {
            ob_flush();
            flush();
            gc_collect_cycles();
        }
    }
}

列印表單生成——三件套(output buffer + page-break + gc_collect_cycles)+ 每 10 筆 flush 一次的節奏。這是踩過記憶體爆炸的坑後固定下來的 pattern。

為什麼這節最關鍵

通用 agent 永遠不會知道:

  • 你的 session 變數叫 mcareDBno(不是 tenant_idorg_db
  • 你的權限表 schema 是 4-table JOIN(不是常見的 RBAC 結構)
  • 你的批次列印節奏是「每 10 筆 flush」(不是 50、不是 100)

這些細節必須寫進 agent,否則 Claude Code 會用通用 best practice,跟你的 codebase 對不上。


章節 6:Common Pitfalls

## Common Pitfalls to Avoid

1. **DO NOT** use PHP 8.x syntax features
2. **DO NOT** use legacy `DB.php` or `DB2.php` - prefer PDO
3. **DO NOT** trust user input - always validate and sanitize
4. **DO NOT** hardcode database names - use session variable
5. **DO NOT** execute raw SQL - use prepared statements
6. **DO NOT** load all records at once - use pagination/generators
7. **DO NOT** ignore memory limits in batch operations
8. **DO NOT** skip permission checks on sensitive operations

8 條 DO NOT,每條都是過去踩過的坑:

DO NOT對應的真實踩坑類型
DB.php / DB2.phpLegacy class 沒 prepared statement、有 SQL injection 風險
hardcode database names多租戶系統最大的災難
raw SQL同上,injection 風險
load all records個案表有 10 萬筆,全載入記憶體爆掉
skip permission checks跨機構資料外洩

負面清單比正面清單對 AI 更有效——AI 會反覆檢查自己有沒有違反這 8 條。


章節 7:Output Guidelines

## Output Guidelines

- Prefer generators over arrays for large datasets
- Use PDO exclusively for new code
- Type hint all parameters and return types
- Write PHPDoc for complex types PHP 7.4 doesn't support
- Follow existing AppSystem code structure and naming
- Include permission checks for user-facing features
- Add memory management for batch operations
- Test with actual AppSystem database structure
- Consider multi-tenant implications in all queries
- Profile performance for batch printing improvements

Always prioritize working PHP 7.4 code over modern syntax. Focus on performance, security, and compatibility with existing AppSystem architecture.

10 條輸出指引,重點:

  1. 「Follow existing AppSystem code structure and naming」 —— 跟現有 code 一致比理論最佳實踐重要
  2. 「Test with actual AppSystem database structure」 —— 這個系統有 schema 怪癖(10 年累積),通用測試不夠
  3. 「Consider multi-tenant implications in all queries」 —— 所有 query 都要想多租戶情境
  4. 「Always prioritize working PHP 7.4 code over modern syntax」 —— 結尾再強調一次語言限制

最後這句收尾再強調——AI 的注意力在開頭和結尾最強,重要的事說兩次。


設計取捨:為什麼不分成多個小 agent?

可以拆成:

  • AppSystem-security-pro(安全相關)
  • AppSystem-perf-pro(效能優化)
  • AppSystem-multi-tenant-pro(多租戶 DB)

但設計選擇是一個大 agent 涵蓋全部。原因:

1. 任務通常跨領域

寫一個個案匯出功能:要考慮多租戶(DB 切換)+ 效能(批次處理)+ 安全(權限檢查)+ 語言限制(PHP 7.4)。

如果分成多個 agent,Claude Code 要決定「這個任務該找哪個」——容易選錯、或選一個漏一個。

2. 共用 context 比較有效率

PHP 7.4 限制清單是所有任務都要知道的。分成多個 agent 等於每個 agent 都要重複這些限制。

一個 agent 290 行,分成 4 個 agent 可能變成 4 × 200 = 800 行,重複內容多、維護痛

3. 領域知識交織

「批次列印的記憶體優化」既是效能、也是安全(怕記憶體爆掉造成服務中斷)、也是專屬 pattern(用 mcareDBno 切 DB)。強行拆分反而失去 context


設計取捨:290 行會不會太長?

對 Claude Code 來說:

  • 每次任務都讀整個 agent:是。但 290 行只有 ~5K tokens,相對於 200K context 不到 3%
  • 長 agent 等於慢 agent:略慢,但工具呼叫的時間遠大於讀 agent 的時間
  • 長 agent 容易讓 AI 失焦:會。但結構化的 markdown headings 幫助 AI navigate,不會迷路

290 行是甜蜜點:夠完整覆蓋特殊規則、又不會太冗。再多 200 行可能就要拆 agent 了。


給其他人的借鑑:怎麼為自己的 codebase 寫專屬 agent

Step 1:列出「通用 agent 不知道」的東西

通用 PHP agent 知道:

  • PHP 語法
  • PSR 規範
  • SOLID 原則
  • 常見 design pattern

通用 agent不知道

  • 你用 PHP 7.4 還是 8.x
  • 你的 session 變數命名
  • 你的 DB schema 怪癖
  • 你過去踩過的具體坑

這些就是要寫進專屬 agent 的內容

Step 2:從「踩坑紀錄」反推

每次踩坑後問自己:「如果有個 agent 知道這件事,這個坑會發生嗎?」如果答案是 No,就把這件事寫進 agent。

example:

  • 踩坑:PHP 7.4 用了 8.0 的 match expression → 加進 Forbidden List
  • 踩坑:批次列印記憶體爆炸 → 加進 Performance 章節
  • 踩坑:忘記 session 驗證 → 加進 Security 章節
  • 踩坑:跨租戶資料外洩 → 加進 Multi-tenant Patterns 章節

Agent 就是踩坑經驗的具象化

Step 3:用 Good vs Bad 對照舉例

不要只說「不要用 X」,要寫:

// Good: 用 PDO prepared
$stmt = $pdo->prepare(...);

// Bad: 字串拼接
$sql = "SELECT * FROM ... WHERE id = '$id'";

AI 對「對照範例」的學習效果遠大於抽象規則。

Step 4:負面清單比正面清單有效

「DO NOT」清單放在容易看到的位置(章節後段,總結之前)。

AI 對禁止事項的記憶比對鼓勵事項深。8 條 DO NOT 比 30 條最佳實踐更能改變輸出。

Step 5:結尾再強調最重要的限制

agent 最後一句再寫一次最關鍵的規則:

Always prioritize working PHP 7.4 code over modern syntax.

開頭跟結尾的注意力最強,重要規則說兩次。

Step 6:用真實的 schema / 變數名

不要寫成抽象範例:「假設你的 session 變數叫 tenant_id」。

直接寫真實的名字:「$_SESSION['mcareDBno'] 是 organization 的 DB 標識」。

這樣 Claude Code 寫 code 時直接套你的命名,不用再 mapping。


怎麼測試你的 agent 有效

最直接的方式:故意給一個會踩坑的 prompt,看 agent 會不會擋下。

範例 prompt:「幫我寫一個函式取得個案資料」

通用 agent 可能輸出:

public function getPatient(int|string $id): ?Patient {
    return $this->repository->find($id) ?? throw new NotFoundException();
}

PHP 7.4 跑不動。

有 AppSystem-php74-pro agent 後,輸出應該是:

/**
 * @param int|string $id
 * @return Patient|null
 */
public function getPatient($id): ?Patient {
    if (!isset($_SESSION['mcareDBno'])) {
        throw new SecurityException('Invalid session');
    }
    $patient = $this->repository->find($id);
    if ($patient === null) {
        throw new NotFoundException();
    }
    return $patient;
}

差別:

  • ✅ Union type 用 PHPDoc(PHP 7.4 兼容)
  • ✅ Throw expression 改成傳統 if/throw(PHP 7.4 兼容)
  • ✅ 多了 session 驗證(multi-tenant 安全)

如果 agent 不能擋下這些,就還沒寫好——回去補充規則


結語

通用 subagent 就像 Stack Overflow——適合查通用問題,但不知道你公司的 codebase 規矩。

Project-specific subagent 是把「資深員工的 onboarding 經驗」具象化。新進工程師上工要學一個月才知道的東西(PHP 7.4 限制、session 變數命名、多租戶 pattern、批次列印節奏)——寫進 agent 後 Claude Code 第一次接觸這個 codebase 就具備這些知識。

寫一個 290 行的 agent 大概要 2-3 小時,但接下來每次用 Claude Code 都受惠。投資報酬率超高。

從你最常踩的坑開始寫。不用一次寫完整版——20 行的「PHP 7.4 限制清單 + 5 條 DO NOT」就比沒有 agent 好得多。需求來了再加新規則,慢慢長到 290 行。

通用模板裝再多都比不上一個自己寫的、貼近 codebase 的專屬 agent。