系列:從現場踩坑到 AI 工具 — IT Diagnostic Agent 開發實錄
如果你要做一個接 LLM 的網頁應用,幾乎所有教學文都會告訴你同一件事:
你需要一個後端。
前端把使用者的問題送到你的伺服器,伺服器帶著 API Key 去呼叫 Anthropic,拿到回應再轉回前端。
理由很明確:API Key 不能放在前端。 只要放進瀏覽器,任何人打開開發者工具就看得到,你的帳單就會被別人拿去用。
這個道理完全正確。但它有一個隱含前提,而那個前提在我的專案裡不成立。
上面那套邏輯的前提是——Key 是你(服務提供者)的。
在一個 SaaS 服務裡,使用者不需要自備 Key,他們用的是你的額度、你的帳單。所以你必須把 Key 藏在後端,否則就是把信用卡放在網路上。
但 IT Diagnostic Agent 不是 SaaS。它的模型是:
使用者自己去申請 API Key,填進自己瀏覽器裡的工具。
這個差別改變了整個安全計算:
| SaaS 模式 | IT Diagnostic Agent | |
|---|---|---|
| Key 擁有者 | 服務提供者 | 使用者自己 |
| Key 洩漏的受害者 | 服務提供者 | 使用者自己 |
| 誰有動機保護 Key | 服務提供者 | 使用者自己 |
| 需要後端嗎 | 需要 | 不需要 |
當 Key 的擁有者、保管者、受害者是同一個人時,「把 Key 放在他自己的瀏覽器裡」不是安全漏洞,那就是他自己的鑰匙放在自己的抽屜裡。
Anthropic API 預設會擋掉瀏覽器直接發出的請求(CORS 政策),但它提供了一個明確的開關:
async function callClaude(messages, systemPrompt){
const p = providers.claude;
if(!p.key) throw new Error('Claude API Key not set');
const res = await fetch('https://api.anthropic.com/v1/messages', {
method:'POST',
headers:{
'Content-Type':'application/json',
'x-api-key':p.key,
'anthropic-version':'2023-06-01',
'anthropic-dangerous-direct-browser-access':'true'
},
body:JSON.stringify({model:p.model, max_tokens:1024, system:systemPrompt, messages})
});
const data = await res.json();
if(data.error) throw new Error(data.error.message);
return data.content[0].text;
}
注意那個 header 的名字:
anthropic-dangerous-direct-browser-access
Anthropic 直接把 "dangerous" 寫進 header 名稱裡。
這個命名我覺得非常誠實。它不是偷偷提供一個方便的後門,而是每次你寫下這行程式碼,都在提醒你自己:你知道你在做什麼嗎?
我知道。而且我認為在這個專案的情境下,這是對的選擇。
使用者填進來的 Key 存在 localStorage。實際的實作是一個 provider 設定物件,每個供應商各自帶 key、model、endpoint:
const providers = {
claude: {
key: localStorage.getItem('it_key_claude') || '',
model: localStorage.getItem('it_model_claude') || 'claude-sonnet-4-20250514',
endpoint: 'https://api.anthropic.com'
},
gemini: {
key: localStorage.getItem('it_key_gemini') || '',
model: localStorage.getItem('it_model_gemini') || 'gemini-2.5-flash',
endpoint: ''
},
openai: {
key: localStorage.getItem('it_key_openai') || '',
model: localStorage.getItem('it_model_openai') || 'gpt-4o-mini',
endpoint: localStorage.getItem('it_ep_openai') || 'https://api.openai.com/v1'
},
ollama: {
key: '', // ← 注意:不需要 Key
model: localStorage.getItem('it_model_ollama') || 'gemma3:12b',
endpoint: localStorage.getItem('it_ep_ollama') || 'http://localhost:11434'
},
};
有兩個細節值得說。
第一,Ollama 那筆的 key 是空字串。 它不需要 Key,因為模型跑在本機。這一行就是整個工具唯一一條「零成本、零註冊」的路徑,我在後面會回來講它。
第二,這段程式碼還留著一段向下相容的遷移邏輯:
// Backward compat: migrate old single apiKey
if(!providers.claude.key && localStorage.getItem('it_key')) {
providers.claude.key = localStorage.getItem('it_key');
...
}
早期版本只支援 Claude,Key 存在一個叫 it_key 的欄位裡。後來改成多供應商架構之後,這些早期使用者的 Key 不能就這樣消失,所以留了一段搬遷程式碼。
這五行是這個工具已經有真實使用者的證據。 如果沒人用,我大可以直接改掉 key 名稱,誰在乎。
這樣的設計代表:
最後一點是缺點,但也是特性——沒有帳號系統,就沒有跨裝置同步,也就沒有任何我需要保管的東西。
純前端呼叫 API 不是沒有代價的。有兩個是真的痛。
任何人第一次打開這個工具,AI 功能都是不能用的。他必須:
這是五個步驟的門檻。相較於「打開就能用」的 SaaS,轉換率一定差很多。
我沒有解法,只能接受。這是「零後端」這個選擇的直接後果——我不能替使用者承擔他們的 API 成本,因為我沒有任何機制去承擔。
也正是這個代價,讓後來的 Ollama 本地模型支援變得特別重要。那是唯一一條「不需要 Key、不需要付費」的路徑。這件事我會在 Day 19–20 講。
瀏覽器擴充套件理論上讀得到 localStorage。共用電腦上,前一個人的 Key 可能留在那裡。
我在 README 裡加了警告,但這不是能靠程式碼解決的事,只能靠告知。
可控的
不可控的
我能做的是把可控的部分做對,並且為不可控的部分留退路。這也是為什麼 Adapter Pattern 從第一版就存在——那是 Day 16 的主題。
「API Key 不能放在前端」是一條正確的規則。
但每一條正確的規則都有它的適用前提,而工程師最容易犯的錯誤,是把規則背下來卻沒有背下前提。
當你的專案不符合那個前提時,照搬規則反而會讓你做出過度複雜的架構——為了保護一把根本不屬於你的鑰匙,架一台你不需要的伺服器。
明天預告: API 接好了,工具可以呼叫 Claude 了。但接下來我花最多時間的地方不是程式碼,是那段 systemPrompt。而關於那段 Prompt 該怎麼寫,我必須先誠實交代一件事:核心的五條規則,不是我想出來的。
作者:Rich Chang | IT 基礎建設工程師 | 越南・柬埔寨・台灣