Building AI Features(產品中加入 AI)

是什麼?

Building AI Features 是將 AI 能力整合到產品中的工程實踐。核心挑戰不在於呼叫 API,而在於處理延遲(Streaming)、確保安全(Guardrails)、控制成本(Cost Optimization)、和提供良好的使用者體驗。

ℹ️Demo 到 Production 的鴻溝

LLM 的 demo 很容易做到驚艷,但 production 需要處理:token 成本爆炸、回應延遲影響 UX、Hallucination 導致錯誤資訊、惡意使用者的 prompt injection、以及合規和隱私要求。

核心觀念

Streaming — 解決延遲問題

LLM 生成回應通常需要 2-10 秒。Streaming 讓 token 一個一個送回 client,使用者能即時看到回應在「打字」,感知延遲大幅降低。

指標說明目標值
TTFTTime to First Token(首個 token 的延遲)小於 500ms
TPSTokens Per Second(生成速度)30-80 tokens/s
Total Time完整回應的總時間依回應長度而異

Guardrails — 安全防護

防護層說明範例
Input Guard過濾使用者的危險輸入偵測 prompt injection、敏感內容
Output Guard過濾模型的不當輸出偵測幻覺、個資洩漏、有害內容
Topic Guard限制回答的主題範圍只回答產品相關問題
Rate Limit限制使用頻率每分鐘最多 10 次

成本控制策略

策略說明節省比例
模型分級簡單任務用 Haiku、複雜用 Sonnet50-80%
Prompt 快取快取常見的 system prompt30-50%
回應快取相同問題直接回傳快取結果60-90%
Token 精簡精簡 prompt,只傳必要的 context20-40%
Batch API非即時任務用 batch 處理(通常半價)50%

常見誤區

⚠️常犯錯誤

  • 不做 Streaming(使用者看到空白畫面等 5 秒會以為壞了)
  • 沒有 Guardrails(使用者可以透過 prompt injection 讓你的 AI 做不該做的事)
  • 所有請求都用最貴的模型(80% 的請求用便宜模型就能處理好)
  • 不設 token 上限(一個惡意使用者可以用你的 API key 產生百萬 token 的回應)

執行流程

1

輸入防護

過濾 prompt injection 和敏感內容

2

模型路由

根據任務複雜度選擇合適的模型

3

Streaming 回應

用 SSE/WebSocket 逐 token 串流回 client

4

輸出防護

過濾幻覺、個資、有害內容

5

監控與計費

記錄 token 用量、延遲、品質指標

流程解讀:Production 級的 AI feature 是一條嚴謹的管線。使用者輸入先經過 Input Guard 過濾,然後根據任務複雜度路由到合適的模型(節省成本)。回應用 Streaming 方式逐 token 送回 client(改善 UX)。在送出之前經過 Output Guard 過濾不當內容。全程監控 token 用量和品質指標,確保成本可控、品質達標。

程式碼範例

C# 版本

csharp
// Streaming — Server-Sent Events (SSE)
app.MapPost("/api/chat", async (ChatRequest req, HttpContext context) =>
{
    // Input Guard
    if (await ContainsPromptInjection(req.Message))
        return Results.BadRequest("Invalid input");
 
    context.Response.ContentType = "text/event-stream";
    context.Response.Headers.CacheControl = "no-cache";
 
    // 選擇模型(成本控制)
    var model = req.Message.Length > 500 ? "claude-sonnet-4-20250514" : "claude-haiku-4-20250514";
 
    // Streaming
    await foreach (var token in StreamChatResponse(model, req.Message))
    {
        // Output Guard(逐步檢查)
        if (ContainsPII(token)) continue;
 
        await context.Response.WriteAsync($"data: {token}\n\n");
        await context.Response.Body.FlushAsync();
    }
    await context.Response.WriteAsync("data: [DONE]\n\n");
});
 
// Token 用量追蹤
public class UsageTracker
{
    public async Task TrackUsage(string userId, int inputTokens, int outputTokens, string model)
    {
        var cost = CalculateCost(inputTokens, outputTokens, model);
        await _db.UsageLogs.AddAsync(new UsageLog
        {
            UserId = userId,
            InputTokens = inputTokens,
            OutputTokens = outputTokens,
            Model = model,
            Cost = cost,
            Timestamp = DateTime.UtcNow
        });
    }
}

TypeScript 版本

typescript
// Streaming with Anthropic SDK
import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
// SSE Endpoint
app.post("/api/chat", async (req, res) => {
  // Input Guard
  const input = req.body.message;
  if (await detectPromptInjection(input)) {
    return res.status(400).json({ error: "Invalid input" });
  }
 
  // 模型路由(成本控制)
  const model = selectModel(input);
 
  // SSE headers
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
 
  // Streaming
  const stream = await client.messages.stream({
    model,
    max_tokens: 1024,
    system: "你是一個產品助手。只回答與產品相關的問題。",
    messages: [{ role: "user", content: input }],
  });
 
  let totalTokens = 0;
 
  for await (const event of stream) {
    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      const text = event.delta.text;
      // Output Guard
      if (!containsSensitiveInfo(text)) {
        res.write(`data: ${JSON.stringify({ text })}\n\n`);
      }
    }
  }
 
  // 記錄用量
  const usage = await stream.finalMessage();
  await trackUsage(req.user.id, usage.usage.input_tokens, usage.usage.output_tokens, model);
 
  res.write("data: [DONE]\n\n");
  res.end();
});
 
// 模型路由
function selectModel(input: string): string {
  if (input.length < 100) return "claude-haiku-4-20250514";    // 簡單問題
  if (input.includes("分析") || input.includes("比較"))
    return "claude-sonnet-4-20250514"; // 複雜分析
  return "claude-haiku-4-20250514"; // 預設用便宜的
}
 
// 回應快取
const cache = new Map<string, string>();
 
async function getCachedOrGenerate(input: string): Promise<string> {
  const cacheKey = hashInput(input);
  if (cache.has(cacheKey)) return cache.get(cacheKey)!;
  const response = await generate(input);
  cache.set(cacheKey, response);
  return response;
}

Python 版本

python
import anthropic
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import hashlib
from functools import lru_cache
 
app = FastAPI()
client = anthropic.Anthropic()
 
# Input Guard — Prompt Injection 偵測
INJECTION_PATTERNS = [
    "ignore previous instructions",
    "you are now",
    "system prompt",
    "reveal your instructions",
]
 
def detect_injection(text: str) -> bool:
    lower = text.lower()
    return any(pattern in lower for pattern in INJECTION_PATTERNS)
 
# Streaming Endpoint
@app.post("/api/chat")
async def chat(request: ChatRequest):
    if detect_injection(request.message):
        raise HTTPException(400, "Invalid input")
 
    # 模型路由
    model = select_model(request.message)
 
    async def generate():
        with client.messages.stream(
            model=model,
            max_tokens=1024,
            system="你是一個產品助手。只回答與產品相關的問題。如果問題與產品無關,禮貌拒絕。",
            messages=[{"role": "user", "content": request.message}],
        ) as stream:
            for text in stream.text_stream:
                # Output Guard
                if not contains_pii(text):
                    yield f"data: {text}\n\n"
            yield "data: [DONE]\n\n"
 
    return StreamingResponse(generate(), media_type="text/event-stream")
 
# 成本監控
class CostTracker:
    PRICES = {
        "claude-haiku-4-20250514": {"input": 0.25, "output": 1.25},     # per 1M tokens
        "claude-sonnet-4-20250514": {"input": 3.0, "output": 15.0},
    }
 
    def calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
        prices = self.PRICES[model]
        return (input_tokens * prices["input"] + output_tokens * prices["output"]) / 1_000_000
 
    async def check_budget(self, user_id: str) -> bool:
        monthly_cost = await self.get_monthly_cost(user_id)
        return monthly_cost < 10.0  # 每個使用者每月上限 $10

結構圖

User
request→
Input Guard
validated→
Model Router
check cache→
Response Cache
→
LLM (Streaming)
stream tokens→
Output Guard
SSE stream→
Cost Monitor

圖中 User 請求先經過 Input Guard 驗證安全性。Model Router 先檢查 Response Cache(命中就直接回傳),沒命中則選擇合適的模型。LLM 以 Streaming 方式生成回應,經過 Output Guard 過濾後以 SSE 串流回 User。全程 Cost Monitor 追蹤 token 用量和費用。

面試常見問題

Q: 如何防止 Prompt Injection?

A: 多層防護:(1) Input Guard 偵測已知的 injection 模式(如「ignore previous instructions」);(2) System Prompt 中明確聲明行為邊界(「你只能回答產品相關的問題」);(3) 將使用者輸入放在 user message 中,不直接拼接到 system prompt;(4) Output Guard 過濾不預期的輸出格式;(5) 使用 LLM-as-a-judge 讓另一個模型檢查輸出是否合規。

Q: Streaming 的技術實現是什麼?

A: 最常用 Server-Sent Events(SSE)— client 發送 POST 請求,server 以 text/event-stream 格式持續推送 token。比 WebSocket 簡單,且天然支援 HTTP/2。前端用 EventSource 或 fetch + ReadableStream 接收。每個 data: 行包含一個或多個 token。

Q: 如何控制 AI feature 的成本?

A: 五個策略依序實施:(1) 模型路由 — 80% 的請求用便宜模型;(2) 回應快取 — 相同問題不重複呼叫;(3) Token 精簡 — 最小化 prompt 和 context;(4) 設定用量限制 — 每使用者每月上限;(5) Batch API — 非即時任務用半價的 batch 處理。

理解測驗

🤔 Streaming 回應的主要目的是什麼?

🤔 以下哪個是有效的成本控制策略?

🤔 Guardrails 的 Output Guard 負責什麼?

重點整理

💡一句話記住

Building AI Features = Streaming + Guardrails + Cost Control。 口訣:「串流降延遲,護欄保安全,路由省成本」

概念說明
Streaming逐 token 串流回應,降低感知延遲
Input Guard過濾 prompt injection 和敏感輸入
Output Guard過濾幻覺、個資和有害內容
Model Router根據任務複雜度選擇合適的模型
核心挑戰從 demo 到 production 的品質和成本控制

你可能也想看

← AI in SDLC回到目錄 →

按 ← → 鍵切換課程