# GPT api
## json格式介紹
前面的key一定要是用雙引號`" "`標起來的String
後面的值可以是
| 類型 | 範例 | 說明 |
| --- | --- | --- |
| **字串** | `"王總"` | 必須用雙引號包住 |
| **數字** | `42`、`3.14` | 不用引號,可以是整數或小數 |
| **布林值** | `true`、`false` | 小寫,不能寫成 True/False |
| **null** | `null` | 表示沒有值 |
| **陣列** | `[1, 2, 3]`、`["Java", "SQL"]` | 用中括號,裡面可以放各種值 |
| **物件** | `{ "school": "台大", "year": 2025 }` | 用大括號包住,裡面是 key-value 結構 |
例子
```json
{
"name": "王總",
"age": 30,
"isStudent": false,
"skills": ["Java", "Python", "SQL"],
"profile": {
"school": "台大",
"major": "資訊工程"
},
"messages": [
{
"role": "user",
"content": "你好"
},
{
"role": "assistant",
"content": "你好!有什麼我可以幫忙的嗎?"
}
]
}
```
說明
```json
{
"name": "王總", // 字串
"age": 30, // 數字
"isStudent": false, // 布林值
"skills": [...], // 陣列(字串)
"profile": {...}, // 巢狀物件
"messages": [ {...}, {...} ] // 陣列裡面是物件
}
```
陣列裡面也可以放不同類別的值 雖然不是很常見
```json
{
"data": [
"王總", // 字串
42, // 數字
true, // 布林值
null, // 空值
{ "a": 1 }, // 物件
[1, 2, 3] // 陣列
]
}
```
## send_message
```json
POST /v1/chat/completions
{
"model": "gpt-4o",
"stream": true,
"temperature": 0.2,
"top_p": 0.9,
"max_tokens": 512,
"seed": 1234,
"response_format": {"type":"json_object"},
"tools": [{
"type":"function",
"function":{
"name":"search_flights",
"description":"查詢航班",
"parameters":{
"type":"object",
"properties":{
"from":{"type":"string"},
"to":{"type":"string"},
"date":{"type":"string"}
},
"required":["from","to","date"]
}
}
}],
"tool_choice": "auto",
"messages": [
{"role":"system","content":"你是一位旅遊助理"},
{"role":"user","content":"幫我找 5/1 從 TPE 飛 KIX 的機票"}
],
"user": "uid_9876"
}
```
參數 | 型別/預設 | 重點
| - | - | -
model* | string | 指定模型名稱(如 gpt-4o、gpt-4o-mini)
messages* | array | 對話歷史,角色可為 system / user / assistant / tool
temperature | number 0–2,預設 1 | 越高回覆越發散;0 最穩定
top_p | number 0–1,預設 1 | nucleus sampling;與 temperature 二選一調整為主
max_tokens | int | 限制「輸出」token 上限;不設則以模型剩餘 context 為限
stop | string 或 string[](≤4) | 遇到指定字元/字串立即停止生成
presence_penalty | number -2~2,預設 0 | + 促進新主題,- 厭惡離題
frequency_penalty | number -2~2,預設 0 | 抑制重複用詞
logit_bias | {token:int} | 對單一 token 機率微調(-100~100)
seed | int | 固定隨機種子,讓同樣 prompt 可重現
tools | array | 宣告可呼叫的函式/外部工具
tool_choice | "auto" (預設) / "none" / {type:"function",function:{name:"foo"}} | 決定是否、或指定哪一支函式要被呼叫
response_format | {"type":"text"}(預設) / {"type":"json_object"} | 要求輸出純文字或有效 JSON
stream | bool,預設 false | true 時用 SSE 邊產生邊推送
stream_options | {"include_usage":true}(選填) | 串流同時夾帶 usage 統計
user | string | 你的內部使用者 ID,利於濫用追蹤
n | int,預設 1 | 生成幾個 choices;stream 時僅支援 1
logprobs | object | 要求回傳 top-k 機率(僅支援末一 token)
top_logprobs | int 1–20 | 與 logprobs 搭配,指定 k 值
### 語氣調控(temperature & top_p)
先固定 top_p=1,只調 temperature
```
0.0‒0.3 保守、幾乎不創新
0.4‒0.7 最自然、常用區間
0.8‒1.0 創意較多、字數可能變長
>1.0 天馬行空,注意邏輯錯誤機率上升
```
top-p
把「所有候選 token」依機率由高到底排序,僅保留「累積機率總和 ≥ p」的那一小撮,再在其中用 temperature 洗牌抽籤。
=>這麼做等於先「切掉最冷門、長尾的字」,讓模型寫作時不會忽然跳出怪句子;而 temperature 則負責決定剩下那些字要多隨機。
**越小越容易出現奇怪的字**
**EX:**

```
┌──────────────── ① presence_penalty ─────────────┐
│ 給「已出現過」的字詞 **一次性扣分**,鼓勵新主題 │
└───────────────────────────────────────────────┘
↓
┌──────────────── ② frequency_penalty ───────────┐
│ 按「已出現次數」乘權重扣分,抑制重複用語 │
└───────────────────────────────────────────────┘
↓
┌──────────────── ③ top-p (nucleus) ─────────────┐
│ 依機率排序,保留累積 ≥ p 的候選字(剪尾) │
└───────────────────────────────────────────────┘
↓
┌──────────────── ④ temperature ─────────────────┐
│ 對剩餘候選重新拉平/尖化機率,再隨機抽籤 │
└───────────────────────────────────────────────┘
↓
抽到下一個 token
```
| 參數 | 典型範圍 | 調高影響 | 建議用途 |
|------|----------|----------|----------|
| **presence_penalty** | `0 – 2` | ✔ 引入**全新**字詞、話題 | 腦力激盪、劇情走向 |
| **frequency_penalty** | `0 – 2` | ✔ 減少重覆句、口頭禪 | 摘要、客服、技術文 |
| **top_p** | `0.4 – 1` | ✔ 砍掉低機率的冷門字 | 設成1:不裁剪,等同「全池」<br>0.8~1.0 用於自由聊天<br>0.4~0.7 用於摘要、客服<br>控非法用語、縮短回答 |
| **temperature** | `0 – 1.5` | ✔ 提升/降低整體亂度<br>(控制剩下候選字的隨機度) | 設成1:不變,機率照原分佈<br>0.0~0.3 嚴謹、一致<br>0.4~0.8 常態人類語氣<br>>1 創意 |
### 角色
| `role` 值 | 說話者 | 說明 |
| --- | --- | --- |
| `"system"` | 系統/開場設定 | 用來設定助手的行為風格或背景,像是「你是一個日語老師」 |
| `"user"` | 使用者 | 代表你輸入給 GPT 的訊息 |
| `"assistant"` | ChatGPT 回覆的內容 | 代表 GPT 的回答 |
`role` 和 `content` 是用來構成「對話歷史」的基本單位,也就是一組組的訊息。每一則訊息都是一個物件。
**範例:一個完整的對話歷史**
```json
{
"model": "gpt-4",
"messages": [
{
"role": "system",
"content": "你是一個有禮貌的 AI 助手。"
},
{
"role": "user",
"content": "幫我寫一封請假信。"
},
{
"role": "assistant",
"content": "好的,以下是一封請假信的範例..."
}
]
}
```
- 系統先設定 GPT 的角色風格。
- 使用者提出請求。
- GPT 依照指令給出回覆。
### call function required
`required` 是 **告訴模型「這些欄位是一定要填的」**,不填就不能執行這個 function。
- GPT 會努力產生包含 `required` 欄位的 `arguments`
- 如果使用者說得不清楚,GPT 會「先問問題」來補全 `required` 的資訊再 call function
```json
{
"name": "create_calendar_event",
"parameters": {
"type": "object",
"properties": {
"title": { "type": "string" },
"start_time": { "type": "string" },
"end_time": { "type": "string" },
"location": { "type": "string" },
"description": { "type": "string" }
},
"required": ["title", "start_time", "end_time"]
}
}
```
### item與回傳array格式
`items` 是用來定義「**陣列(list)裡的每一項要長怎樣**」的。
```json
{
"name": "add_tags_to_event",
"description": "替事件加上多個標籤",
"parameters": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "事件的唯一 ID"
},
"tags": {
"type": "array",
"description": "要加入的標籤清單",
"items": {
"type": "string"
}
}
},
"required": ["event_id", "tags"]
}
}
```
回傳的樣子
```json
{
"role": "assistant",
"function_call": {
"name": "add_tags_to_event",
"arguments": {
"event_id\": "123",
"tags": ["會議", "重要", "線上"]
}
}
}
```
#### 二維陣列
```json
{
"name": "set_meeting_slots",
"description": "設定多個會議時段(每段都有開始與結束)",
"parameters": {
"type": "object",
"properties": {
"slots": {
"type": "array",
"description": "一組會議時段,每個時段是一個 [start, end] 陣列",
"items": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 2,
"maxItems": 2
}
}
},
"required": ["slots"]
}
}
```
回傳的樣子
```json
{
"role": "assistant",
"function_call": {
"name": "set_meeting_slots",
"arguments": "{
\"slots\": [
[\"2025-04-11T09:00:00\", \"2025-04-11T10:00:00\"],
[\"2025-04-11T13:00:00\", \"2025-04-11T14:00:00\"],
[\"2025-04-11T16:00:00\", \"2025-04-11T17:00:00\"]
]
}"
}
}
```
## gpt_response
### 一般(非串流)成功回傳
gpt原始回傳的格式大概長這樣
所以才要用 `reply = response['choices'][0]`
choices:陣列是為了支援 n>1 時返回多個候選答案。
finish_reason:判斷回答是否被截斷(length)或因違規被過濾(content_filter)。
usage:用來計算成本;同樣欄位在 Streaming 也會於最後一包送回。
```json
{
"id": "chatcmpl-abc1234567890",
"object": "chat.completion",
"created": 1713806400, // UNIX 時戳
"model": "gpt-4o-mina-2025-04-15",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "您好,這裡是回覆內容。",
"tool_calls": null // 使用 function/tool-calling 時才會出現
},
"finish_reason": "stop" // 可能是 "stop" | "length" | "tool_calls" | "content_filter"
}
],
"usage": { // ← Input / Output token 在這裡
"prompt_tokens": 45,
"completion_tokens": 92,
"total_tokens": 137
},
"system_fingerprint": "fp_a1b2c3..." // 選填:若開啟 system_fingerprint=true
}
```
### 串流(stream:true)模式(不會比較貴)
「stream : true」= 把完整回應切成一小塊一小塊(chunk)立即送出,而不是等全部字都生成完才一次回傳。
每行以 data: 開頭
delta 只包含新增的第一個文字;你需要把所有 chunk 依序串起來才是完整回答。
```json
event: message
data: {
"id": "chatcmpl-abc1234567890",
"object": "chat.completion.chunk",
"created": 1713806401,
"model": "gpt-4o-mina-2025-04-15",
"choices": [
{
"index": 0,
"delta": { "role": "assistant", "content": "您好" },
"finish_reason": null
}
]
}
event: message
data: {
"id": "chatcmpl-abc1234567890",
"object": "chat.completion.chunk",
...
"choices": [
{
"index": 0,
"delta": { "content": ",這裡是回覆內容。" },
"finish_reason": "stop"
}
]
}
event: done
data: { // 最終包帶 usage
"usage": { "prompt_tokens": 45, "completion_tokens": 92, "total_tokens": 137 }
}
```
### Function / Tool Calling 回傳
當你在 messages 或 tools 內宣告函式定義,模型若決定呼叫函式,message 會改為下列結構:
```json
{
"id": "chatcmpl-abc1234567890",
"object": "chat.completion",
"created": 1713806400, // UNIX 時戳
"model": "gpt-4o-mina-2025-04-15",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"tool_calls": [
{
"id": "call_01HE...",
"type": "function",
"function": {
"name": "search_flights",
"arguments": "{\"from\":\"TPE\",\"to\":\"KIX\",\"date\":\"2025-05-01\"}"
}
}
],
"content": null
},
"finish_reason": "stop" // 可能是 "stop" | "length" | "tool_calls" | "content_filter"
}
],
"usage": { // ← Input / Output token 在這裡
"prompt_tokens": 45,
"completion_tokens": 92,
"total_tokens": 137
},
"system_fingerprint": "fp_a1b2c3..." // 選填:若開啟 system_fingerprint=true
}
```
### 錯誤回傳格式
type | code | 說明
| - | - | - |
invalid_request_error | context_length_exceeded | token 超過模型上限
insufficient_quota | insufficient_quota | 額度用完
rate_limit_error | rate_limit_exceeded | 達到 RPM / TPM 上限
server_error | 500 | 伺服器內部錯誤,可重試
```
{
"error": {
"message": "You exceeded your quota...",
"type": "insufficient_quota",
"param": null,
"code": "insufficient_quota"
}
}
```
## swift api 交互
定義的數據結構
```swift
struct OpenAIRequest: Codable {
let model: String
let messages: [OpenAIMessage]
let temperature: Float
}
struct OpenAIResponse: Codable {
let id: String
let object: String
let created: Int
let model: String
let choices: [OpenAIResponseChoice]
}
```
```swift
class ChatViewModel: ObservableObject {
// 你的OpenAI API Key
private let apiKey = "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"
func sendMessageToGPT(messages: [ChatMessage]) async -> String? {
// 將ChatMessage轉換為OpenAI格式 並且可以記住對話(是所有)
let apiMessages = messages.map { message in
OpenAIMessage(
role: message.isMe ? "user" : "assistant",
content: message.text
)
}
let requestBody = OpenAIRequest(
model: "gpt-4.1-mini",
messages: apiMessages,
temperature: 0.7
)
// 編碼(將 Swift 對象轉換為 JSON):
// 使用 JSONEncoder 將 OpenAIRequest 對象編碼成 JSON 數據
// 這個 JSON 數據會被發送給 OpenAI API
guard let requestData = try? JSONEncoder().encode(requestBody) else {
print("Failed to encode request")
return nil
}
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
print("Invalid URL")
return nil
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = requestData
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
print("Error response: \(response)")
if let errorString = String(data: data, encoding: .utf8) {
print("Response body: \(errorString)")
}
return nil
}
// 解碼(將 API 返回的 JSON 轉換回 Swift 對象):
// 使用 JSONDecoder 將 API 返回的 JSON 數據解碼成 OpenAIResponse 對象
let openAIResponse = try JSONDecoder().decode(OpenAIResponse.self, from: data)
if let choice = openAIResponse.choices.first {
return choice.message.content
} else {
return nil
}
} catch {
print("Network error: \(error)")
return nil
}
}
}
```
這些結構體都遵循 Codable 協議,這使得它們可以自動進行 JSON 編碼和解碼。當數據在網絡上傳輸時:
發送請求時:Swift對象 → JSON數據
接收響應時:JSON數據 → Swift對象
**encode原理解釋**
apiMessages 會是一個 OpenAI 格式的消息數組,讓我用例子說明。
假設原始的 messages 數組包含以下對話:
```swift
[
ChatMessage(text: "你好,我是小明", isMe: true),
ChatMessage(text: "你好小明,很高興見到你", isMe: false),
ChatMessage(text: "今天天氣真好", isMe: true)
]
```
經過 map 轉換後,apiMessages 會變成這樣的格式:
```swift
[
OpenAIMessage(
role: "user", // isMe: true 所以是 user
content: "你好,我是小明"
),
OpenAIMessage(
role: "assistant", // isMe: false 所以是 assistant
content: "你好小明,很高興見到你"
),
OpenAIMessage(
role: "user", // isMe: true 所以是 user
content: "今天天氣真好"
)
]
```
當這個數據被編碼成 JSON 發送給 OpenAI API 時,會是這樣的格式:
```json
{
"model": "gpt-4.1-mini",
"messages": [
{
"role": "user",
"content": "你好,我是小明"
},
{
"role": "assistant",
"content": "你好小明,很高興見到你"
},
{
"role": "user",
"content": "今天天氣真好"
}
],
"temperature": 0.7
}
```
# 專題紀錄
## 費用
### 測試結果
gpt4o-mini
線性代數-對角化的應用2024/12/10~2024/12/30, * 1000次
tokens用量:25000
費用:0.01美元(0.3台幣)



### 省token的方法
| 手段 | 省 token 效果 | 備註 |
|------|---------------|------|
| **摘要舊訊息 / 只保留關鍵上下文** | ★★★★★ | 每少 1 K token ≈ 0.02 NTD(4o-mini input) |
| **改用 4o-mini 或動態模型升級** | ★★★★☆ | 價格差 6–10 倍;省下最明顯 |
| **降低溫度 / top-p,使回覆更短** | ★★★☆☆ | 若內容允許簡潔回應 |
| **JSON 最小化 / 刪空白** | ★★☆☆☆ | 規模大時才顯著 |
| **選擇標識縮寫(e.g. `"t"` 代 `"temperature"`)** | ★☆☆☆☆ | 只對巨量批次有感 |
* 先做「動態裁切 + 自動摘要」——投入一天,可立即砍掉 50 %以上 input token。
* 上 GPT-4o-mini 為預設,再寫條件判斷升級 GPT-4o。
* 研究 Embedding-RAG,把大綱或書本內容放向量庫,真正需要時才塞進 prompt。
* 若之後有「批次生成題庫」需求,再導入 Batch API 或快取。
## 資料結構
```swift
enum RepeatType: Hashable {
case none
case daily
case weekly([Int]) // 0=Sunday, 1=Monday, ...
case monthly([Int]) // 幾號
}
```
```swift
// Todo 任務資料結構
struct TodoTask: Identifiable {
let id = UUID() // 唯一識別符
var title: String // 任務標題
var note: String // 任務備註
var color: Color // 任務顏色
var focusTime: Int // 專注時間(分鐘)
var category: String // 任務類別
var isAllDay: Bool // 是否全天
var isCompleted: Bool // 是否完成
var repeatType: RepeatType // 重複類型
var startDate: Date // 任務開始日期(重複任務的起始日)
var endDate: Date // 任務結束日期(重複任務的結束日)
var createdAt: Date // 任務建立時間
}
```
## procloudflare proxy 自架 (無firebase安全驗證)
### 限制時間
| 名稱 | 公式 & 來源 | 影響計費? | 跟串流的關係 |
|------|-------------|-----------|--------------|
| **CPU time (Execution Duration)** | Worker 實際佔用 V8 執行緒的毫秒數 | **是**。免費方案每次最多 10 ms,Standard 預設 30 s,可加購到 300 s。| 只在你解析/轉送每個 chunk 時稍微累加;等待下個 chunk 的空檔不算 |
| **Request Duration (Wall-clock)** | 從請求進到節點開始,到最後一位元組送出為止 | 不計費;僅作為觀測指標。 | 串流會拉長這段時間,但不會拉高帳單 |
### 方案

#### 為何「串流」通常 更省 CPU time?
**一次回整段:**
Worker 先把 OpenAI 的整份回應讀完 → 暫存 → 再灌給前端。
單次 await apiRes.text() 會在 Worker 緩衝整段資料,CPU time 持續累加直到讀完。
**SSE 串流:**
Worker 把 ReadableStream 直接 pass-through;V8 只在每個 chunk 到來時醒一下,寫入 edge socket 後馬上 await。
多數時間處於 idle,不計 CPU。
實測同一段 1 500 字答案,串流版常見 CPU time < 5 ms,而「非串流 + resp.text()」可能 20-40 ms(仍在免費額度內,但數字高很多)。
### 教學
在專案資料夾內(例如 ~/Desktop/swift_app/cloudflare)
```bash
npm init -y
npm install wrangler # 瀏覽器跳出 OAuth
```

```bash
npx wrangler login #登入
npm create cloudflare@latest gpt-proxy-api
```
要選javascript
最後部署deploy

```
cd gpt-proxy-api
```
**編輯wrangler.jsonc**
OPENAI_API_KEY 屬於 Secret,不能寫在檔案裡,下一步用指令加入。
```jsonc
{
"name": "gpt-proxy-api",
"main": "src/index.js",
"compatibility_date": "2025-04-24", // 建議寫今天日期
"vars": { // 普通文字環境變數
"API_TOKEN": "my-secret-token"
}
}
```
**編輯src/index.js**
```javascript
export default {
async fetch(request, env) {
// 1. 簡易白名單:驗證自訂 token(避免任何人呼叫)
if (request.headers.get("x-api-token") !== env.API_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}
// 2. 直接把 body 轉交給 OpenAI Chat Completions
const openaiResp = await fetch(
"https://api.openai.com/v1/chat/completions",
{
method: "POST",
headers: {
"Authorization": `Bearer ${env.OPENAI_API_KEY}`,
"Content-Type": "application/json"
},
body: request.body
}
);
// 3. 將 OpenAI 的回應原樣丟回 App(status、headers、body 都保留)
return openaiResp;
}
};
```
加入秘密apikey
```bash
npx wrangler secret put OPENAI_API_KEY
```
大概長這樣type內是完整的`apikey`
```
[
{ "name": "OPENAI_API_KEY", "type": "sk-pro..." }
]
```
注意:這邊才輸入api的key

```bash
#查看已經輸入的secret key
wrangler secret list
```
```bash
#正式部署
npx wrangler deploy
或是指定入口
npx wrangler deploy src/index.ts
```
成功新增 http... 那行等等 proxyURL = URL 會用到

在網頁確認

swift修改
```swift
class ChatViewModel: ObservableObject {
// ❶ 刪掉存在 App 端的 OpenAI API Key
// private let apiKey = "sk-..." // ← 移除
// ❷ 新增 Worker URL 與驗證 Token
// ⬇︎ 修改
private let proxyURL = URL(string: "https://gpt-proxy-api.<帳號>.workers.dev")!
private let proxyToken = "my-secret-token"
func sendMessageToGPT(messages: [ChatMessage]) async -> String? {
...
// ⬇︎ 修改:改呼叫 proxyURL
var request = URLRequest(url: proxyURL)
request.httpMethod = "POST"
// ⬇︎ 修改:使用自訂 header,**不再帶 OpenAI Key**
request.addValue(proxyToken, forHTTPHeaderField: "x-api-token")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = requestData
...
}
}
```
### 安全問題
1. Cloudflare Worker 中 vars vs secret 差異:
**重點:無論 vars 還是 secret,瀏覽器端都看不到。**
Worker 是在 Cloudflare 邊緣執行的,程式碼(與環境變數)不會原封不動傳給用戶端。
類型 | 儲存方式 | 上線後能被誰看到? | 適用情境
-| - | - | -
vars | 以純文字隨 Worker 一起部署 | 只有 Worker 執行環境(你的程式)能讀,使用者透過 HTTP 拿不到 | 不算機密,但也不想硬寫死在程式裡的常數
secret | 加密存放,Cloudflare 後端在執行時才解密注入 | 同上,只是連 Dashboard 也只顯示名稱,不顯示內容 | 真正機密——API Key、DB 密碼…
2. 不夠安全
**App 內要送 x-api-token**
你必須把 my-secret-token 硬寫在 App,或是從伺服器拿一次後緩存。
不論哪種,只要有人抓封包或逆向 App,就能抄到這組 token。
**Header 沒加密**
Token 會隨每次請求出現在 HTTP header。雖然傳輸層有 HTTPS,但任何能在客戶端植入惡意程式/裝置 SSL Proxy 的人,都能擷取。
**無身分綁定**
任何拿到這組 token 的人,只要知道 Worker URL,就能重播請求。
類型 | 儲存方式 | 上線後能被誰看到? | 適用情境
-| - | - | -
vars | 以純文字隨 Worker 一起部署 | 只有 Worker 執行環境(你的程式)能讀,使用者透過 HTTP 拿不到 | 不算機密,但也不想硬寫死在程式裡的常數
secret | 加密存放,Cloudflare 後端在執行時才解密注入 | 同上,只是連 Dashboard 也只顯示名稱,不顯示內容 | 真正機密——API Key、DB 密碼…
### stream模式要用要用半雙工(但在cloudflare不用理會 因為是送到他們主機上游)
| 名稱 | 意義 | 與我們的串流聊天有何關聯 |
|------|------|--------------------------|
| **單工 (simplex)** | 先把整個 request 傳完,才能開始收 response;request 不能是 Stream | 傳統 `POST`;OpenAI 串流無法工作 |
| **半雙工 (half-duplex)** | 仍然**必須**把 request 全部送完,之後才能讀 response,但 request body 可以是 `ReadableStream` | ✅ 目前 fetch 唯一允許的模式;我們只要把小小的 JSON 送出去,就開始等 SSE |
|**全雙工 (full-duplex)** |request 還沒傳完就能邊收 response、甚至雙向持續推資料 | 規格裡還在討論;瀏覽器與 CF Workers 都尚未實作|
OpenAI SSE 來說,我們只需要「上傳一個 JSON ⇒ 持續下載串流文字」,半雙工就夠了。
| 方向 | 你的 Worker 目前在做什麼 | 規格何時 **必須** `duplex:"half"` | 結論 |
|------|------------------------|-----------------------------------|-------|
| **上傳(request → OpenAI)** | 把一小段 **JSON**(`Uint8Array` / `ArrayBuffer`)一次送出去 | **只有**當 *request body* 是 `ReadableStream` 時才強制要宣告。<br>(標準稱為「可串流上傳」)| 依然合法。 |
| **下載(OpenAI → 前端)** | `openaiResp.body` 本來就是 `ReadableStream`,<br>你直接把整個 `Response` return 回去 | **下載端** 不需要 `duplex`;Cloudflare 會自動把 stream pass-through 給瀏覽器 | 因此 SSE 可以順利一路流到 SwiftUI。 |
```javascript
var index_default = {
async fetch(request, env) {
if (request.headers.get("x-api-token") !== env.API_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}
const openaiResp = await fetch(
"https://api.openai.com/v1/chat/completions",
{
method: "POST",
headers: {
"Authorization": `Bearer ${env.OPENAI_API_KEY}`,
"Content-Type": "application/json"
},
body: request.body
}
);
return openaiResp;
}
};
export {
index_default as default
};
```
## 系統架構與安全
### App → 自家後端 → OpenAI
API Key 不落地客戶端。
後端可統一加上 RAG(向量搜尋)或商業邏輯。
屬於 「BFF—Backend For Frontend」 模式,符合 Apple 對遠端程式碼執行的規範。
### 向量資料庫
⚡ File Search(OpenAI 內建)雖然貴,但零維運;大規模時再換 Pinecone/Qdrant。
團隊小、用量可控的情況下,先用 File Search 省 DevOps 時間。

### 內容過濾
把 moderations API 或 Assistants 策略模版串進 pipeline。
Apple 審核看重「違禁內容舉報機制」;這裡直接把 OpenAI 風險等級轉為前端遮罩。
### 資料保留策略
在隱私權政策列「對話最長保存 X 天,逾期自動刪除」。
支援 GDPR/台灣個資法的 刪除請求 API,可用背景排程定期清理。
### 提省錢三招
Batch API:離峰時段批量丟入,官方直接再折 50%(計畫內含)。
Cache hit:對重複提示用 cache=true;輸入、輸出各再省 75%。
分層路由:先用 nano 做「草稿」,再把需要高精度的少數請求升級到 GPT-4.1。
## 串流與call function大致流程
tool_calls 內容會分片(delta)回來:name 通常在第一包,arguments 可能拆成好幾包。
當 finish_reason:"tool_calls" 出現,就代表「這次函式參數已完整」— 該去執行了。
執行完把結果以 role:"tool" 附加到 messages,再發下一次請求;模型會讀取結果並接著回答。
```
(1) user prompt
│
▼
(2) POST /chat/completions ← 夾帶 tools 定義, stream:true
│
▼
(3) SSE chunk n=0 ← delta.role="assistant"
delta.tool_calls[0].function.name="search_flights"
│
│ … (多個 chunk,arguments 逐字流回)
▼
(4) SSE chunk 最後一包 ← finish_reason="tool_calls"
│
▼
(5) 伺服器執行你的函式 ↙ 把結果寫成 role:"tool" 再 append
│
▼
(6) 再呼叫 /chat/completions 接續對話(可繼續串流)
```
### 1️⃣ 第 1 次請求(含工具定義,stream:true)
```json
POST /v1/chat/completions
{
"model": "gpt-4o",
"stream": true,
"tools": [
{
"type": "function",
"function": {
"name": "search_flights",
"description": "查機票",
"parameters": {
"type": "object",
"properties": {
"from": { "type": "string" },
"to": { "type": "string" },
"date": { "type": "string" }
},
"required": ["from","to","date"]
}
}
}
],
"messages": [
{ "role": "system", "content": "你是一位旅遊助理" },
{ "role": "user", "content": "幫我找 5/1 台北飛大阪的機票" }
]
}
```
### 2️⃣ 串流回傳(逐塊示意)
```json
data: { // 第一塊:宣告要呼叫函式
"choices":[{
"delta":{
"role":"assistant",
"tool_calls":[{
"id":"call_001",
"type":"function",
"function":{"name":"search_flights","arguments":"{"}
}]
}
}]}
---
data: { "choices":[{ "delta":{"tool_calls":[{ "id":"call_001", "function":{"arguments":"\"from\":\"TPE\",\""}}] } }] }
...
data: { "choices":[{ "delta":{"tool_calls":[{ "id":"call_001", "function":{"arguments":"\"to\":\"KIX\",\"date\":\"2025-05-01\"}"} }] } }] }
---
data: { "choices":[{ "finish_reason":"tool_calls" }] } ← 參數結束
你此時把 arguments 串成完整 JSON:
{"from":"TPE","to":"KIX","date":"2025-05-01"}
```
### 3️⃣ 後端執行函式
```json
const result = await searchFlights({from:"TPE",to:"KIX",date:"2025-05-01"});
/* 假設回傳 ↓ */
{
"flights":[
{"airline":"JX","price":4200,"dep":"07:40","arr":"11:25"},
{"airline":"CI","price":4500,"dep":"09:00","arr":"12:45"}
]
}
```
### 4️⃣ 第 2 次請求(把結果加回對話)
```json
POST /v1/chat/completions
{
"model": "gpt-4o",
"stream": true,
"messages": [
{ "role": "system", "content": "你是一位旅遊助理" },
{ "role": "user", "content": "幫我找 5/1 台北飛大阪的機票" },
{ // GPT 剛才的 tool_call
"role": "assistant",
"tool_calls": [{
"id": "call_001",
"type": "function",
"function": {
"name": "search_flights",
"arguments": "{\"from\":\"TPE\",\"to\":\"KIX\",\"date\":\"2025-05-01\"}"
}
}],
"content": null
},
{ // 你的程式塞進來的工具結果
"role": "tool",
"tool_call_id": "call_001",
"content": "{\"flights\":[{\"airline\":\"JX\",...}]}"
}
]
}
```
重要欄位
tool_call_id 必須對應前面 assistant 提供的 id。
content 放函式執行結果(JSON 或純文字皆可)。
所有先前訊息都要保留,讓模型維持上下文。
模型收到這組新對話,就會把工具結果讀進記憶並生成最終回覆(仍可串流):
```
「我找到了兩班直飛:JX 上午 7:40,票價 NT$4 200;CI 上午 9:00,票價 NT$4 500。需要我幫你預訂嗎?」
```
## 同一次stream中「先講話、後附帶 function call」
在串流過程中,模型可以先以 delta.content 實時吐出文字,等話說完後,再改吐 delta.function_call。這在社群討論中已被證實可行,常見輸出形態大概長這樣:
```
// 第一段
{ "delta": { "role": "assistant" } }
{ "delta": { "content": "好的,我來查詢……" } }
// 話說完後
{ "delta": { "content": null,
"function_call": { "name": "getWeather" } } }
// 參數會繼續分段流出
{ "delta": { "function_call": { "arguments": "{\"city\":\"Tokyo\"}" } } }
// 最後
{ "finish_reason": "function_call" }
```
需要自己偵測 delta.function_call,並在 finish_reason=="function_call" 時觸發後端函式。
示範流程可參考論壇範例碼:
https://community.openai.com/t/functions-calling-with-streaming/305742
注意:OpenAI 規格定義「最終合併後的訊息」若含 function_call,content 應為 null;所以很多函式庫在串流結束時只保留 function_call 而把文字丟掉。你得在 串流時 就把文字收集起來顯示,別等到組裝完成才讀取。
## function定義
add 還有其他細節
```swift
// 保存任务的方法
private func saveTask() {
// 创建新任务
let newTask = TodoTask(
title: title,
note: content,
color: selectedColor,
focusTime: 0, // 默认专注时间为0
category: category,
isAllDay: isAllDay,
isCompleted: false,
repeatType: repeat_option,
startDate: startDate,
endDate: endDate,
createdAt: Date() // 使用当前时间作为创建时间
)
// 将新任务添加到全域任务列表
allTasks.tasks.append(newTask)
// 新增後自動關閉新增視窗
isPresented = false
}
```
todoview
```swift
// 儲存任務
private func saveTask() {
// 已經在保存中,避免重複觸發
if viewModel.isLoading {
return
}
// 驗證表單
if !validateForm() {
return
}
// 確保離線操作時 UI 不會卡住
viewModel.isLoading = true
Task {
do {
// 將重複選項資料傳遞給 viewModel
viewModel.newTaskRepeatType = repeatOption
// 獲取當前使用者 ID
let userId = Auth.auth().currentUser?.uid ?? "default"
// 創建任務,確保包含用戶 ID
let task = TodoTask(
title: viewModel.newTaskTitle,
note: viewModel.newTaskNote,
color: viewModel.newTaskColor,
focusTime: viewModel.newTaskFocusTime,
category: viewModel.newTaskCategory,
isAllDay: viewModel.newTaskIsAllDay,
isCompleted: false,
repeatType: viewModel.newTaskRepeatType,
startDate: viewModel.newTaskStartDate,
endDate: viewModel.newTaskEndDate,
userId: userId
)
// 使用 addTask 而非 saveNewTask 以確保處理好 userId
await viewModel.addTask(task)
// 任務保存成功後關閉視圖
dismissWithAnimation()
} catch {
// 顯示錯誤訊息
viewModel.errorMessage = error.localizedDescription
viewModel.isLoading = false
print("Error saving task: \(error.localizedDescription)")
}
}
}
```
todoview model
```swift
func addTask(_ task: TodoTask) async {
isLoading = true
errorMessage = nil
// 確保已經登入
guard let currentUserId = Auth.auth().currentUser?.uid else {
errorMessage = "請先登入再新增任務"
isLoading = false
return
}
do {
// 確保任務有使用者 ID
var updatedTask = task
updatedTask.userId = currentUserId
try await firebaseService.saveTodoTask(updatedTask)
// 重新加載任務列表以獲取最新數據
try await loadTasks()
// 發送資料變更通知
postDataChangeNotification()
// 清空錯誤訊息
errorMessage = nil
} catch {
// 設置錯誤訊息
errorMessage = "儲存任務失敗: \(error.localizedDescription)"
print("Error adding task: \(error)")
}
isLoading = false
}
```
firebase service
```swift
func saveTodoTask(_ task: TodoTask) async throws {
currentSyncStatus = .syncing
// 使用 Task 添加超時機制
return try await withTimeout(seconds: 10) {
do {
let batch = self.db.batch()
let userId = self.getCurrentUserId()
// 創建一個包含當前使用者ID的任務副本
var updatedTask = task
updatedTask.userId = userId
// 將任務存儲在使用者ID下
let taskRef = self.db.collection(Collection.tasks.rawValue)
.document(userId)
.collection("userTasks")
.document(task.id)
// 儲存任務資料
batch.setData(updatedTask.toFirestore, forDocument: taskRef)
try await batch.commit()
self.lastSync = Date()
self.currentSyncStatus = .synced
} catch {
self.currentSyncStatus = .error(.networkError)
print("Firebase error: \(error.localizedDescription)")
throw error
}
}
}
```
# 其他
- [x] OpenAI thread 物件:建議至少保留與此 tool call 相關的對話,否則模型可能遺失上下文。若歷史太長,可裁剪、摘要或使用 OpenAI thread 物件(Assistants v2)來自動管理。
**需要thread物件的情況:**
=>
**要跑 Code Interpreter:** 讓 GPT 幫你算數、畫圖、產生檔案。
**要做 Retrieval / RAG:** 把 PDF、知識庫向量化後,讓 Assistant 自動引用。
**多人共用同一案件:** 多個使用者同時追加訊息,伺服器端必須鎖定同一 conversation id。
**想省掉手動裁切上下文、管理檔案:** Thread 幫你保存並「只抽必要段落」。
**價錢:** 貴

**(結論是目前只有聊天call function功能所以不用)**
- [x] Batch API:離峰時段批量丟入,官方直接再折 50%(計畫內含)。
| 需求 | Batch API 優勢 | 同步 API 劣勢 |
|------|----------------|---------------|
| **大量離線內容生成**(摘要文件、翻譯全文、資料標註) | 一檔丟上去就好,OpenAI 幫你排程;**單價 5 折** | 你得自己做併發、流控、重試;單價原價 |
| **不在乎秒級延遲**(可等數分鐘~數小時) | 背景作業,不佔用前端連線 | 同步模式要維持連線,易逾時 |
| **想省錢** | 同模型直接省 50 % | — |
「使用者按下送出要立即看到回答」或「需要串流漸進顯示」,Batch 就不適合。
- [x] Cache hit:對重複提示用 cache=true;輸入、輸出各再省 75%。
**解釋:** 當使用者或程式發出請求時,所要的資料已經存在於「快取層」(記憶體、瀏覽器、CDN 邊緣節點、資料庫查詢快取…)裡,系統便直接從這一層取出並回應,不必再去「上游」或「原始來源」(磁碟、主資料庫、遠端伺服器)重抓或重算。這種情況就稱作 命中快取;反之,若快取裡沒有,便是 Cache miss(快取未命中)。
**什麼時候「快取」才值得你加?**
你打算內建教材或題庫檢索 → 同一題目、同一篇文章摘要重複度高。
temperature 設 0、保證 determinism → 同 prompt 才能穩命中。
批次離線作業 → 晚上排程跑,Hit 就回舊答案,Miss 再算。
**如果以上都不是重點,就專心優化 prompt 長度和模型選擇,Cache Hit 可以先放一邊。**
- [x] Prompt-to-Response 快取
把「同一段 Prompt → GPT 給出的完整回應」對映成一顆 Key–Value的hash table,先查快取,命中就直接回傳;沒命中才真正呼叫 OpenAI。能把「重複輸入」直接省下來,尤其在 文件翻譯、摘要、FAQ 等大量批次且高重複度的工作最有效。
**自建 Prompt-to-Response 快取 —— 只在「固定問題」有價值**
聊天場景裡,使用者輸入千奇百怪,幾乎不會重複;就算你把 prompt 標準化,命中率仍低於 5%。
真正適合快取 的是:FAQ、固定範本翻譯、文件摘要……這些「同 prompt 重複率高、temperature = 0」的批次任務。
- [ ] 分層路由:先用 nano 做「草稿」,再把需要高精度的少數請求升級到 GPT-4.1。
- [x] openAI自己的向量資料庫(目前先不考慮)
- [x] gpt對話紀錄
### 前端 Streaming
- [ ] 使用 response_format: "json_object" 或者普通 text streaming;一旦偵測到 finish_reason=="function_call" 先停止渲染。
執行函式 → 把結果塞回 messages → 重新 call GPT(或走 「工具結果」後續補串流 的模式)。
### UX 提示
- [ ] 在 UI 上顯示「GPT 正在更新你的行事曆⋯⋯」spinner,等 EventKit 執行完才解除。完成後,由 GPT 再說一句「已經幫你排好 ×××」;這句就來自 function 回傳後 GPT 的下一輪回答,流程一致。
- [ ] token 控制:回傳 JSON 前,用 JSONSerialization 壓成 string,再送給 GPT,避免不必要空白與縮排。(可能要總結的話會比較好)
- [x] temperature & top_p
### 容易出現的安全問題排名
- [ ] OWASP Top 10
### api key 不能寫在app裡面(安全問題)
- [x] 邊緣運算
Cloudflare Worker
Supabase Edge Function
或是ai

### 登入問題
- [ ] apple goole 登入後的資料庫要怎麼知道
### firebase與sapabase比較
- [x] 結果應該還是用firebase


### 免費ai
- [x] grog是免費的(但好像有使用量的問題)
### 日曆排序
- [ ] 日歷的陣列用排序的 然後再用lowerbound去找 找大於startdate 然後如果有repaet就不判斷有沒有小於enddate
### cloudflare串流時間
- [x] 看一下gpt的串流是否會降低cloudflare的時間 以及cloudflare有最多10秒的限制(免費方案)30秒(付費方案)
## 程式原理筆記
chatroom:在聊天室泡泡給使用者看的
allMessage:給gpt看的 每次使用者發訊息會從chatroom中載入 過程中append的function結果 不會被一直保存也不會被加到chatroom中
## 待更新改動
**問題**
- [x] 像是取的savetask資訊時聊天室不會顯示正在安排任務
- [x] 對話規則改為
第一次對話使用 "none"
第二次對話使用 "required"
之後根據上一次的 tool_choice 和回覆類型來決定:
- 如果上一次是 "required",這次就用 "auto"
- 如果上一次是 "auto",則根據上一次回覆的類型決定:
- 文字回覆用 "required"
- 函數回覆用 "auto"
一直重複到gpt使用endfunction
- [x] 聊天室列表要是逆排序 從最後一筆聊天記錄開始
- [x] gpt的save task聊天要可以選顏色
- [ ] 修正gpt問完問題會自己安排task的毛病
- [ ] 加上刪除功能
- [ ] 加上修改功能
- [ ] 測試要不要[wating user]
- [ ] 收到回應,狀態碼:429 (太頻繁回應)
- [ ] 刪除不一定會真的刪除
- [ ] 刪除匡的顯示資訊有問題
- [x] 新增語調設定
- [x] 讓gpt可以讀取到使用者習慣(把習慣時常調長)
- [x] 修正 gpt 不同比對話不會多換一行
今日改動:
新增重新傳送的機制解決錯誤代碼429