Claude Code 的 subagent 生態系現在很豐富——社群有 database-architect、debugger、error-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 |
| Model | sonnet |
| Tools | Read, Write, Edit, Bash, Glob, Grep |
| 目標 codebase | 10 年 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
- Batch Printing(generator + ob_clean + unset)
- Database Query Optimization(PDO prepared + LIMIT/OFFSET + 避免 N+1)
- 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'";
三個安全主題
- SQL Injection:明確列 Good vs Bad,不用問 AI 知道哪個是反例
- XSS Prevention:
htmlspecialchars($x, ENT_QUOTES, 'UTF-8')三個參數每次都要寫對 - 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-1、demo-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_id、org_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.php | Legacy 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 條輸出指引,重點:
- 「Follow existing AppSystem code structure and naming」 —— 跟現有 code 一致比理論最佳實踐重要
- 「Test with actual AppSystem database structure」 —— 這個系統有 schema 怪癖(10 年累積),通用測試不夠
- 「Consider multi-tenant implications in all queries」 —— 所有 query 都要想多租戶情境
- 「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 的
matchexpression → 加進 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。