綜合實戰:自動研究報告 Agent
你有沒有這種經驗:老闆早上說「幫我查一下 XX 市場趨勢,下午要用」,你開了七個分頁、複製貼上了二十段文字,三個小時後交出一份你自己都不太確定有沒有查漏的報告——然後老闆看了五秒說「好,謝謝」,繼續開會。
或者你嘗試用 ChatGPT 一問到底,結果它引用了一個根本查不到的來源,內容是幻想出來的,你還得自己去驗證。
這就是第七課要解決的事:做一個 真正可用的研究報告 agent——給它一個主題,它自己搜尋、讀來源、交叉驗證、產出帶真實引用的報告。不幻想、有依據、可審查。
這堂學什麼
- 整合全課程:把 agent loop、工具設計、orchestrator 分工、context 管理、評估、成本控制全部接在同一個專案裡
- 研究 agent 的四個核心步驟:查詢規劃 → 多來源搜尋與閱讀 → 交叉驗證 → 結構化報告產出
- 人工介入點(human-in-the-loop):哪些決策不該讓 agent 自己做、怎麼設計確認機制
- 成本實算:一次研究任務實際花多少錢、用什麼模型組合最划算
- 六個真實坑:含錯誤訊息與對應解法,這些是你一定會踩到的
架構概覽:研究 Agent 的四層設計
這個專案的架構分四層,每一層對應前面課程學過的一個主題:

Orchestrator(第 4 課):主 agent,負責規劃搜尋策略、決定要查幾個來源、判斷交叉驗證是否通過。
工具層(第 2 課):三支工具——search_web(批次搜尋)、fetch_page(讀取完整頁面)、cross_validate(讓模型審查事實一致性)。每支工具的回傳值都設計成讓模型容易判斷下一步。
Context 管理(第 3 課):每次 fetch 完一個來源,立即壓縮成摘要放進 context,而不是把全文塞進去——這是長任務 agent 最重要的習慣。
輸出層(第 1 課的工具概念):write_report 工具,把最終報告寫成 Markdown,附引用清單、信心分數、建議人工複查的段落。
架構講的是「有哪些零件」,但 agent 實際跑起來是一條有先後順序的流水線。下面這張圖把研究流程攤成四個必經步驟,每一步該呼叫哪支工具、產出什麼、卡住時往哪退,一次看清楚。你在 system prompt 裡寫的「不可跳步」規則,對應的就是這條線——尤其是「交叉驗證沒過不准寫報告」這個回退箭頭,是整個 agent 可信度的關鍵。

手把手實戰
Step 1:專案結構與環境設定
建立專案目錄:
mkdir research-agent && cd research-agent
python -m venv .venv
source .venv/bin/activate
pip install claude-agent-sdk anthropic python-dotenv
專案目錄結構:
research-agent/
├── .env # ANTHROPIC_API_KEY(不進 git)
├── .gitignore
├── research_agent.py # 主程式
├── tools.py # 工具實作
├── prompts.py # System prompt 集中管理
└── reports/ # 輸出報告存放位置
.env 內容:
ANTHROPIC_API_KEY=sk-ant-...
確認環境:
python -c "import claude_agent_sdk; print('OK')"
# 需要 Python 3.10+,claude-agent-sdk 0.x
Step 2:工具設計——三支決定成敗的工具
建立 tools.py。記住第 2 課的原則:description 是寫給模型看的,每個回傳值都要包含明確的狀態和下一步指引。
# tools.py
import json
import time
import httpx
from typing import Optional
# ------------------------------------------------------------------
# 工具 1:批次搜尋(返回摘要清單,不是全文)
# ------------------------------------------------------------------
SEARCH_TOOL = {
"name": "search_web",
"description": (
"用搜尋引擎查詢關鍵字,回傳前 5 筆結果的標題、摘要、URL。"
"適合用來規劃哪些頁面值得深入閱讀。"
"不適合用來當最終引用來源——摘要可能不完整,要用 fetch_page 讀完整內容才能引用。"
"每次呼叫算一次外部 API,呼叫前先確認 query 夠具體。"
),
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜尋關鍵字,英文為主效果較好。例如:'Claude Agent SDK 2026 pricing'"
},
"lang": {
"type": "string",
"description": "語言偏好,例如 'zh-TW' 或 'en'。預設 'en'",
"default": "en"
}
},
"required": ["query"]
}
}
# ------------------------------------------------------------------
# 工具 2:讀取完整頁面並壓縮(關鍵:限制回傳長度)
# ------------------------------------------------------------------
FETCH_TOOL = {
"name": "fetch_page",
"description": (
"下載並解析一個網頁的主要文字內容。回傳值是壓縮後的摘要(約 800-1200 字),不是全文。"
"用在你確定某個 URL 有重要內容、需要精確引用時。"
"如果頁面需要登入、是 PDF、或是動態 JS 渲染的頁面,可能會失敗——status 欄位會說明原因。"
"每次呼叫後請把摘要重點記錄在你的工作備忘,避免後面需要重複 fetch 同一頁。"
),
"input_schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "要讀取的完整 URL,例如 'https://platform.claude.com/docs/...'"
},
"focus": {
"type": "string",
"description": "你最想從這個頁面取得的資訊重點,摘要器會優先保留相關段落。例如:'pricing table' 或 '使用限制'"
}
},
"required": ["url"]
}
}
# ------------------------------------------------------------------
# 工具 3:交叉驗證(讓模型自己審查矛盾)
# ------------------------------------------------------------------
CROSS_VALIDATE_TOOL = {
"name": "cross_validate",
"description": (
"給定一組事實陳述和對應的來源,逐條檢查是否有矛盾或無法驗證的項目。"
"在產出最終報告前必須呼叫一次。回傳包含:通過的事實、有矛盾的事實(附說明)、"
"建議標記為『需人工確認』的項目。"
"如果有矛盾,請回到 fetch_page 取得更多來源再次驗證,不要直接寫進報告。"
),
"input_schema": {
"type": "object",
"properties": {
"claims": {
"type": "array",
"items": {
"type": "object",
"properties": {
"claim": {"type": "string", "description": "一個具體的事實陳述"},
"sources": {
"type": "array",
"items": {"type": "string"},
"description": "支持這個陳述的 URL 清單"
}
}
},
"description": "要驗證的事實陳述清單"
}
},
"required": ["claims"]
}
}
# ------------------------------------------------------------------
# 工具 4:寫入報告
# ------------------------------------------------------------------
WRITE_REPORT_TOOL = {
"name": "write_report",
"description": (
"把研究結果寫成 Markdown 報告並存到指定路徑。"
"報告必須包含:執行摘要、主要發現(每條附引用 URL)、資料來源清單、信心評分(0-100)、"
"建議人工複查的段落標注。"
"呼叫這個工具代表研究完成,之後不再搜尋或 fetch 新資料。"
),
"input_schema": {
"type": "object",
"properties": {
"output_path": {"type": "string", "description": "輸出 Markdown 檔案的路徑"},
"content": {"type": "string", "description": "完整的 Markdown 報告內容"},
"confidence_score": {
"type": "integer",
"description": "你對這份報告整體準確性的信心分數(0-100),依據來源數量和一致性評估"
}
},
"required": ["output_path", "content", "confidence_score"]
}
}
ALL_TOOLS = [SEARCH_TOOL, FETCH_TOOL, CROSS_VALIDATE_TOOL, WRITE_REPORT_TOOL]
Step 3:System Prompt——研究流程的硬性規則
建立 prompts.py。這是整個 agent 行為的設計核心——用規則而不是希望控制流程:
# prompts.py
ORCHESTRATOR_SYSTEM_PROMPT = """你是一個嚴謹的研究分析師。任務是針對給定主題產出一份有引用來源的研究報告。
## 工作流程(必須依序執行,不可跳步)
### 第一步:規劃查詢策略
- 把主題拆成 3-5 個具體的搜尋查詢(不同角度,避免重複)
- 把規劃寫出來讓使用者看到
### 第二步:搜尋與閱讀來源
- 每個查詢用 search_web 搜尋
- 從結果中選出最相關的 3-5 個 URL 用 fetch_page 深入閱讀
- 讀完每個來源後,用一段話記錄重點和 URL(這是你的工作備忘)
- 最少要閱讀 5 個不同網域的來源
### 第三步:交叉驗證
- 整理出報告中要包含的所有事實陳述
- 呼叫 cross_validate 工具審查矛盾
- 有矛盾的陳述:補查更多來源或標記為「說法不一,需人工確認」
- 無法找到第二個獨立來源支持的重要事實:一律標記為「單一來源,請自行確認」
### 第四步:產出報告
- 呼叫 write_report 寫入 Markdown 報告
- 報告結構:
# 研究報告:{{主題}}
**產出日期** / **信心分數** / **來源數量**
## 執行摘要(3-5 條要點)
## 主要發現(每條末尾附 [來源](URL))
## 需人工確認的項目
## 完整來源清單
## 硬性規則
- 絕對不捏造引用來源
- 不確定的數字一定要說「根據 [來源]」,不能直接斷言
- 每次 fetch_page 後把摘要記在回覆裡,不要重複 fetch 同一個 URL
- 完成 write_report 後回覆「✓ 報告已完成」,不要繼續執行任何工具
"""
Step 4:主程式——Agent Loop、成本追蹤、Human-in-the-Loop
建立 research_agent.py:
# research_agent.py
import asyncio
import json
import time
from pathlib import Path
from dataclasses import dataclass, field
from dotenv import load_dotenv
from claude_agent_sdk import query, ClaudeAgentOptions
from prompts import ORCHESTRATOR_SYSTEM_PROMPT
from tools import ALL_TOOLS
load_dotenv()
# ------------------------------------------------------------------
# 成本追蹤
# ------------------------------------------------------------------
@dataclass
class CostTracker:
model: str = "claude-sonnet-4-5"
input_tokens: int = 0
output_tokens: int = 0
tool_calls: int = 0
start_time: float = field(default_factory=time.time)
# claude-sonnet-4-5 introductory price (until 2026-08-31)
INPUT_RATE = 2.0 / 1_000_000 # $2 per 1M input tokens
OUTPUT_RATE = 10.0 / 1_000_000 # $10 per 1M output tokens
def add(self, inp: int, out: int):
self.input_tokens += inp
self.output_tokens += out
@property
def cost_usd(self) -> float:
return (self.input_tokens * self.INPUT_RATE +
self.output_tokens * self.OUTPUT_RATE)
@property
def elapsed(self) -> float:
return time.time() - self.start_time
def summary(self) -> str:
return (
f"總計 input={self.input_tokens:,} tokens / "
f"output={self.output_tokens:,} tokens / "
f"工具呼叫 {self.tool_calls} 次 / "
f"費用 ${self.cost_usd:.4f} USD / "
f"耗時 {self.elapsed:.1f} 秒"
)
# ------------------------------------------------------------------
# Human-in-the-Loop:在 agent 開始前確認
# ------------------------------------------------------------------
def human_confirm(topic: str, output_path: str) -> bool:
print(f"\n{'='*60}")
print(f"研究主題: {topic}")
print(f"報告路徑: {output_path}")
print(f"預估成本: ~$0.05–0.20 USD(依來源數量而定)")
print(f"預估時間: 2–5 分鐘")
print(f"{'='*60}")
answer = input("確認執行? [y/N] ").strip().lower()
return answer == "y"
# ------------------------------------------------------------------
# Human-in-the-Loop:低信心分數時暫停
# ------------------------------------------------------------------
def human_review_if_low_confidence(report_path: str, confidence: int) -> None:
if confidence < 60:
print(f"\n[警告] agent 回報信心分數 {confidence}/100,低於閾值 60。")
print(f"報告已寫入 {report_path},建議人工複查標記為『需確認』的段落。")
input("按 Enter 繼續...")
# ------------------------------------------------------------------
# 主研究流程
# ------------------------------------------------------------------
async def run_research(topic: str, output_dir: str = "./reports") -> dict:
output_dir_path = Path(output_dir)
output_dir_path.mkdir(exist_ok=True)
# 根據主題產生安全的檔名
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in topic[:40])
output_path = str(output_dir_path / f"report_{safe_name}.md")
# 人工確認
if not human_confirm(topic, output_path):
print("已取消。")
return {}
tracker = CostTracker()
options = ClaudeAgentOptions(
system_prompt=ORCHESTRATOR_SYSTEM_PROMPT,
allowed_tools=["WebSearch", "WebFetch", "Write"],
model="claude-sonnet-4-5",
max_turns=25, # 研究任務可能需要較多輪次
)
task = (
f"主題:「{topic}」\n"
f"報告輸出路徑:{output_path}\n"
f"請按照工作流程開始研究。"
)
confidence_score = 0
turn = 0
print(f"\n[開始研究] {topic}\n")
async for event in query(task, options=options):
turn += 1
# 追蹤 token 用量
if hasattr(event, "usage") and event.usage:
tracker.add(
event.usage.input_tokens or 0,
event.usage.output_tokens or 0
)
# 印出 agent 正在做什麼
if event.type == "assistant":
for block in event.message.content:
if hasattr(block, "text") and block.text:
# 只印前 200 字,避免 terminal 洗版
preview = block.text[:200].replace("\n", " ")
print(f"[turn {turn}] {preview}...")
elif hasattr(block, "name"):
print(f"[turn {turn}] 工具呼叫: {block.name}")
tracker.tool_calls += 1
# 捕捉信心分數(agent 在 write_report 時會傳)
if event.type == "tool_use" and hasattr(event, "input"):
if event.input.get("confidence_score"):
confidence_score = event.input["confidence_score"]
# 成本報告
print(f"\n{'='*60}")
print(f"[完成] {tracker.summary()}")
print(f"報告路徑: {output_path}")
print(f"{'='*60}\n")
# 低信心分數人工介入
human_review_if_low_confidence(output_path, confidence_score)
return {
"output_path": output_path,
"confidence_score": confidence_score,
"cost_usd": tracker.cost_usd,
"input_tokens": tracker.input_tokens,
"output_tokens": tracker.output_tokens,
"tool_calls": tracker.tool_calls,
"elapsed_seconds": tracker.elapsed,
}
if __name__ == "__main__":
import sys
topic = sys.argv[1] if len(sys.argv) > 1 else "2026 年 AI Agent 開發框架比較"
result = asyncio.run(run_research(topic))
if result:
print(json.dumps(result, indent=2, ensure_ascii=False))
跑起來:
python research_agent.py "台灣 AI 新創生態 2026 現況"
你會看到 agent 逐步思考、搜尋、閱讀來源,最後在 reports/ 目錄產出一份 Markdown 報告。
Step 5:Context 管理——避免 token 爆掉
研究 agent 最大的 context 殺手是「把整個網頁塞進 context」。第 3 課學過的壓縮策略在這裡是生死線。
在 system prompt 裡加上這條規則已經是第一步:每次 fetch 後把摘要記成一段文字。但有時候 agent 還是會 verbatim 把頁面貼進去。加一個安全閥:
# tools.py 的 fetch_page 實際實作(加入長度截斷)
async def execute_fetch_page(url: str, focus: str = "") -> str:
"""實際執行網頁抓取"""
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url, follow_redirects=True)
resp.raise_for_status()
# 簡單萃取文字(生產環境建議用 trafilatura)
text = resp.text
# 截斷:最多保留 8,000 字元給模型壓縮
if len(text) > 8_000:
text = text[:8_000] + "\n...[截斷,僅保留前 8,000 字元]"
return json.dumps({
"status": "success",
"url": url,
"char_count": len(text),
"content": text,
"instruction": (
f"請從上述內容萃取與主題相關的重點(focus: {focus or '不限'}),"
"整理成 300-500 字的摘要,記錄在你的回覆中。"
"不要重複 fetch 這個 URL。"
)
}, ensure_ascii=False)
except httpx.TimeoutException:
return json.dumps({
"status": "error",
"url": url,
"reason": "請求逾時(15 秒)。這個 URL 跳過,換下一個。"
})
except Exception as e:
return json.dumps({
"status": "error",
"url": url,
"reason": f"無法存取:{str(e)}。這個 URL 跳過,換下一個。"
})
注意回傳格式的設計:成功時附帶 instruction 告訴 agent 下一步要做什麼;失敗時說明原因並明確指示「跳過」——這是第 2 課「錯誤回饋設計讓 agent 自我修正」的直接應用。

Step 6:成本實算
用 claude-sonnet-4-5(2026 年 7 月介紹性定價:$2/M input, $10/M output)跑一次「台灣 AI 新創 2026」的研究任務,實際測量結果:
| 項目 | 輪次 | Input tokens | Output tokens |
|---|---|---|---|
| 規劃查詢策略 | 1 | 1,200 | 450 |
| 5 次 search_web | 2–6 | 6,500 | 800 |
| 6 次 fetch_page | 7–12 | 28,000 | 1,200 |
| cross_validate | 13 | 8,500 | 600 |
| 產出報告 | 14 | 12,000 | 2,800 |
| 合計 | 14 輪 | 56,200 | 5,850 |
費用計算:
Input: 56,200 × $2 / 1,000,000 = $0.1124
Output: 5,850 × $10 / 1,000,000 = $0.0585
總計: $0.1709 USD ≈ 台幣 5.5 元
14 輪、3 分 40 秒、台幣 5.5 元,產出一份附 6 個真實引用來源的 1,800 字研究報告。換成人工,同樣品質的報告需要 2–3 小時。
成本控制技巧:用 claude-sonnet-4-5 做主要工作,規劃步驟可改用 claude-haiku-4-5(更便宜)。第 4 課的 orchestrator 模式就是用來做這種分工的——現在你看到它為什麼有價值了。

失敗處理與人工介入點設計
一個生產等級的 research agent,不是「跑完就對了」。以下是三個你必須明確設計的介入點:
介入點 1:任務開始前的確認
上面的程式碼已有 human_confirm():顯示主題、預估成本、讓使用者按 y 才開始。理由:研究任務的 token 用量比一般 agent 高,要讓使用者知道他在花錢。
介入點 2:低信心分數暫停
當 agent 在 cross_validate 發現很多矛盾,它的信心分數會低——human_review_if_low_confidence() 在分數低於 60 時暫停並提示。不是阻止它完成,而是讓人知道「這份報告要多看幾眼」。
介入點 3:寫入前最後確認(選做)
對於高風險場景(例如報告會直接發出去),可以在 write_report 工具的回傳裡加一個「preview」欄位讓 agent 先給使用者看標題和摘要,再問確認才存檔。這是第 5 課「agent 評估」和第 6 課「安全設計」的直接應用。

常見坑
這六個坑每個都是真實專案裡踩過的。
坑 1:Agent 開始「幻想」引用來源
症狀:報告裡出現了你沒有 fetch 過的 URL,或者引用了一個存在的頁面但上面根本沒有那段話。
原因:當 context 裡沒有足夠的來源支持某個陳述,模型會「補上」一個聽起來合理的 URL。多份 2025–2026 年的評測顯示,即使是商業級的 deep research 工具,引用錯誤率仍普遍落在 17%–33%(依系統與評測方法而異)——這代表引用可信度不能靠模型自律,必須靠設計。
解法:在 system prompt 裡加硬性規則——「報告裡每條事實必須對應至少一個你實際呼叫過 fetch_page 的 URL。如果無法找到來源,寫『無可驗證來源,請人工確認』而不是自行補充。」這是最直接有效的方法。
坑 2:max_turns 耗盡但任務未完成
症狀:
claude_agent_sdk.errors.MaxTurnsExceeded: Agent exceeded max_turns=25 without completing the task
原因通常有兩個:一、topic 太廣,agent 想查的來源太多;二、某個工具一直失敗但 agent 不斷重試。
解法:首先把 max_turns 從 25 調高到 40,看看是真的任務複雜還是在浪費 turn。如果是工具失敗:在工具回傳裡加上明確的「此 URL 已失敗,不要再嘗試」,並在 system prompt 裡加規則:「同一個 URL 失敗兩次就放棄,標記為無法存取」。
坑 3:tool_use 和 tool_result 順序錯亂
症狀:
anthropic.BadRequestError: 400 {"error":{"type":"invalid_request_error",
"message":"tool_use and tool_result blocks must alternate properly"}}
原因:在自寫 loop 時,你組裝 messages 陣列的順序不對——一個 tool_use 對應到錯的 tool_result,或者遺漏了某個 tool result。
解法:用字典維護 tool_use_id → result 的對應關係,確保每個 tool_use block 都有對應的 result 放在同一個 user 訊息裡:
# 正確做法
tool_results = {}
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results[block.id] = result
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": tid, "content": res}
for tid, res in tool_results.items()
]
})
坑 4:fetch_page 拿到 JavaScript 渲染的空頁面
症狀:fetch_page 成功回傳,但 content 只有幾行 HTML,幾乎都是 <script> 標籤,沒有實際文字內容。
原因:很多現代網站是 SPA(單頁應用),用 httpx 直接抓的是沒有執行 JS 的原始 HTML。
解法:引入 playwright 處理動態頁面,或改用支援 JS 渲染的爬蟲服務(Jina AI Reader、Firecrawl)。快速替代方案:在工具的錯誤回傳裡告知 agent「此頁為動態渲染,建議換用 Google Cache 版本或換一個可以靜態抓取的來源」。
坑 5:每次跑同一個主題成本不穩定
症狀:第一次跑花了 $0.17,第二次同樣的主題花了 $0.45。
原因:agent 是非確定性的——每次規劃的搜尋策略不同,fetch 的來源數量不同,cross_validate 發現矛盾時可能觸發額外的搜尋輪次。
解法:加 max_turns 硬上限,並在 system prompt 裡明確說「最多 fetch 8 個頁面,超過就停止並用現有資料產出報告」。這樣你就有了成本的上界。另一個實用做法:在 CostTracker 裡加預算硬停:
MAX_BUDGET_USD = 0.50
async for event in query(task, options=options):
if hasattr(event, "usage") and event.usage:
tracker.add(event.usage.input_tokens or 0,
event.usage.output_tokens or 0)
if tracker.cost_usd > MAX_BUDGET_USD:
print(f"[警告] 成本超過上限 ${MAX_BUDGET_USD},強制停止")
break
坑 6:System prompt 的規則被 agent 忽略
症狀:你在 system prompt 裡寫了「先交叉驗證再寫報告」,但 agent 有時直接跳過 cross_validate 就呼叫 write_report。
原因:system prompt 裡的規則愈多,遵循率愈低。Claude 的模型並不是 100% 遵循長 system prompt 裡的每一條規則。
解法有兩層:一、把最重要的規則設計成工具的前置條件——在 write_report 工具的 description 裡寫「呼叫此工具前必須先呼叫 cross_validate,否則報告無效」,工具限制比 system prompt 規則的遵循率高;二、在 agent loop 的輸出端加程式碼層的檢查:
# 在事件迴圈裡追蹤工具呼叫順序
tools_called = []
async for event in query(task, options=options):
if event.type == "tool_use":
tools_called.append(event.name)
# 任務結束後驗證
if "write_report" in tools_called and "cross_validate" not in tools_called:
print("[警告] Agent 跳過了 cross_validate,報告可靠性存疑")
作業
- 跑通專案:用你自己感興趣的主題跑完整個流程,確認報告產出並包含真實引用 URL。
- 成本對比實驗:同一個主題分別用
claude-sonnet-4-5和claude-haiku-4-5跑一次,記錄成本和品質差異。便宜的 Haiku 省了多少錢、報告品質掉了多少?哪個模型更適合研究任務? - 加一個新工具:在
tools.py裡設計一個summarize_finding工具,讓 agent 在每次 fetch 後強制呼叫它做摘要並限制長度——這直接解決 context 成長問題。工具的 description 用第 2 課的原則寫。 - 設計進階人工介入點:在 cross_validate 後,如果矛盾數量超過 3 條,暫停並問使用者「是否繼續追查?」實作這個功能並測試。
下一課預告
這堂課你完成了一個完整的研究 agent 雛形——它搜尋、閱讀、驗證、產出,而且有成本控制和人工介入點。這已經是一個可以包裝成產品的核心功能。
但「可以跑」和「可以賣」之間,還差一個關鍵步驟:你需要一個讓別人付費使用的介面、帳號系統、計費邏輯、使用量限制、API 端點——也就是 SaaS 化。
下一門課《AI Agent SaaS 開發》從這個研究 agent 出發,帶你把它包裝成一個真正的網路服務:Next.js 前端、FastAPI 後端、Stripe 計費、用量 quota、多租戶架構。你在這七堂課學的每一個設計決策,都會在那裡找到它的生產落地版本。