iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
AI Engineering

從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理系列 第 10 篇

讓資料能被交換:JSON 與 Agent 的共通語言

  • 分享至 

  • xImage
  •  

昨天我們讓程式學會把資料寫進檔案,但用的是純文字檔,一行一個待辦事項。

問題來了:如果一筆待辦不只有「標題」,還有「優先度」、「是否完成」、「建立時間」呢?

用 txt 檔的話,我們可能會想到這種土法煉鋼的做法:

買牛奶,高,False,2026-09-24
寫鐵人賽文章,高,True,2026-09-24

然後讀的時候 line.split(",") 拆開。看起來可以,但只要遇到「買牛奶, 順便買麵包」這種標題裡有逗號的情況,整個就崩了。而且第三欄的 "False" 讀回來是字串不是布林值,還要自己轉。

這種問題有標準答案,就是今天的主角:JSON。


一、JSON 是什麼

JSON(JavaScript Object Notation)是一種文字格式的資料交換標準。雖然名字裡有 JavaScript,但現在幾乎所有語言、所有 API 都在用它。

它長這樣:

{
  "name": "Alice",
  "age": 25,
  "is_student": false,
  "hobbies": ["閱讀", "跑步"],
  "address": {
    "city": "台北",
    "district": "大安區"
  }
}

有沒有覺得很眼熟?它跟 Python 的 Dictionary 幾乎一模一樣。

這就是 JSON 好用的原因——它的結構天然對應到我們已經會的資料型態:

JSON Python
object {...} dict
array [...] list
string "..." str
number 25 / 3.14 int / float
true / false True / False
null None

只要記住三個差異:布林值是小寫的 true/false、空值是 null、字串一律用雙引號(JSON 不接受單引號)。


二、Python 的 json 模組

Python 內建 json 模組,不用安裝。核心只有四個函式,而且名字有規律:

函式 方向 對象
json.dumps() Python → JSON 字串 字串
json.loads() JSON 字串 → Python 字串
json.dump() Python → JSON 檔案 檔案
json.load() JSON 檔案 → Python 檔案

記法:s 代表 string。有 s 的處理字串,沒 s 的處理檔案。dump 是倒出去(寫),load 是載進來(讀)。

字串版本

import json

data = {
    "name": "Alice",
    "age": 25,
    "hobbies": ["閱讀", "跑步"],
}

# Python → JSON 字串
text = json.dumps(data, ensure_ascii=False, indent=2)
print(text)

輸出:

{
  "name": "Alice",
  "age": 25,
  "hobbies": [
    "閱讀",
    "跑步"
  ]
}

兩個很重要的參數:

  • ensure_ascii=False:不加的話,中文會變成 "阅讀" 這種跳脫字元。雖然電腦讀得懂,但人看不懂。處理中文一定要加。
  • indent=2:加上縮排讓人類好讀。不加的話會全部擠成一行(檔案比較小,適合傳輸;要存檔給人看就加)。

反過來:

text = '{"name": "Alice", "age": 25}'
data = json.loads(text)

print(type(data))      # <class 'dict'>
print(data["name"])    # Alice
print(data["age"] + 1) # 26 ← 是真的數字,不是字串

注意 data["age"] + 1 可以直接運算——JSON 幫我們把型態保留下來了。這是純文字檔做不到的。

檔案版本

import json

data = {"name": "Alice", "age": 25}

# 寫檔
with open("profile.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

# 讀檔
with open("profile.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded)   # {'name': 'Alice', 'age': 25}

跟昨天的 with open() 完全一樣,只是把 f.write() 換成 json.dump(data, f)。


三、重寫待辦清單:這次有結構了

把昨天的程式升級成 JSON 版:

import json
from datetime import datetime
from pathlib import Path

DATA_DIR = Path("data")
TODO_FILE = DATA_DIR / "todos.json"


def load_todos():
    """讀取待辦清單。檔案不存在或格式壞掉時回傳空清單。"""
    if not TODO_FILE.exists():
        return []
    try:
        with open(TODO_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError:
        print("⚠️ 待辦檔案格式有問題,當作空的處理")
        return []


def save_todos(todos):
    """把待辦清單寫回 JSON 檔。"""
    DATA_DIR.mkdir(exist_ok=True)
    with open(TODO_FILE, "w", encoding="utf-8") as f:
        json.dump(todos, f, ensure_ascii=False, indent=2)


def add_todo(title, priority="中"):
    if not title.strip():
        raise ValueError("標題不能是空的")
    if priority not in ("高", "中", "低"):
        raise ValueError(f"優先度只能是 高/中/低,收到的是「{priority}」")

    todos = load_todos()
    todos.append({
        "id": len(todos) + 1,
        "title": title.strip(),
        "priority": priority,
        "done": False,
        "created_at": datetime.now().isoformat(timespec="seconds"),
    })
    save_todos(todos)
    return todos[-1]


def complete_todo(todo_id):
    todos = load_todos()
    for todo in todos:
        if todo["id"] == todo_id:
            todo["done"] = True
            save_todos(todos)
            return todo
    raise ValueError(f"找不到 id={todo_id} 的待辦")


# 試跑
add_todo("買牛奶", "高")
add_todo("寫鐵人賽文章", "高")
add_todo("整理房間", "低")
complete_todo(2)

for todo in load_todos():
    mark = "✓" if todo["done"] else " "
    print(f"[{mark}] #{todo['id']} {todo['title']}({todo['priority']})")

輸出:

[ ] #1 買牛奶(高)
[✓] #2 寫鐵人賽文章(高)
[ ] #3 整理房間(低)

打開 data/todos.json 看看:

[
  {
    "id": 1,
    "title": "買牛奶",
    "priority": "高",
    "done": false,
    "created_at": "2026-09-24T21:30:15"
  },
  ...
]

**這個檔案人看得懂,程式也讀得回來,而且型態不會跑掉。**這就是結構化資料的價值。

注意我加了 except json.JSONDecodeError——如果有人手動去改這個檔案,改壞了格式,程式不會直接爆掉。這是 Day 8 的觀念延續。


四、JSON 存不了的東西

JSON 不是萬能的,有些 Python 物件它裝不下:

import json
from datetime import datetime

json.dumps({"now": datetime.now()})
# TypeError: Object of type datetime is not JSON serializable

datetime、set、自訂的 class,JSON 都不認識。常見解法是先轉成字串:

data = {"now": datetime.now().isoformat()}
json.dumps(data)   # {"now": "2026-09-24T21:30:15.123456"}

讀回來的時候再轉回去:

from datetime import datetime
dt = datetime.fromisoformat(data["now"])

上面的待辦清單我就是用 isoformat() 來存時間的。ISO 8601 格式(2026-09-24T21:30:15)是跨語言的標準,而且字串排序等於時間排序,很好用。

另外,字典的 key 在 JSON 裡一定是字串:

json.loads(json.dumps({1: "a"}))
# {'1': 'a'}  ← 數字 key 變成字串了

這個坑很隱晦,要注意。


五、為什麼 JSON 對 Agent 這麼重要

今天的內容看起來只是「另一種存檔方式」,但 JSON 其實是整個 Agent 架構的通用語言。接下來每一天幾乎都會用到它:

1. API 的回應格式(Day 13–15)

幾乎所有 Web API 回傳的都是 JSON。中央氣象署的天氣資料、Google 行事曆、Notion、Slack——全都是 JSON。我們要串任何服務,第一步就是 json.loads()。

2. AI 的結構化輸出(Day 20)

這點最關鍵。我們希望 AI 回答的不只是一段文字,而是程式可以直接用的資料。

比較一下:

使用者:幫我記一下,明天下午三點跟客戶開會

# 如果 AI 回傳文字:
「好的,我幫你記下明天下午三點跟客戶開會。」
→ 程式看得懂嗎?要用正規表示式去抓「明天」「下午三點」嗎?惡夢。

# 如果 AI 回傳 JSON:
{
  "action": "create_event",
  "title": "跟客戶開會",
  "date": "2026-09-25",
  "time": "15:00"
}
→ json.loads() 之後直接就能用。

**這個差別,就是「聊天機器人」和「Agent」的分界線。**一個只能講話,一個能做事。

3. 工具的定義與呼叫(Day 21–22)

當我們告訴 AI「你可以使用這些工具」,工具的定義本身就是 JSON Schema:

tool = {
    "name": "get_weather",
    "description": "查詢指定城市的今日天氣",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "城市名稱,例如「台北」"}
        },
        "required": ["city"],
    },
}

而 AI 決定要呼叫工具時,它回傳的參數也是 JSON。

4. 狀態的保存(Day 26)

Agent 的記憶、對話歷史、使用者偏好,全部都可以序列化成 JSON 存檔。


六、一個小練習:使用者偏好檔

幫生活助理做一個記憶檔,先建立雛形:

import json
from pathlib import Path

PREF_FILE = Path("data/preferences.json")

DEFAULT_PREFS = {
    "name": None,
    "city": "台北",
    "wake_up_time": "07:00",
    "language": "zh-TW",
    "dietary_restrictions": [],
    "reminder_enabled": True,
}


def load_preferences():
    """讀取偏好設定,缺少的欄位用預設值補上。"""
    if not PREF_FILE.exists():
        return DEFAULT_PREFS.copy()
    try:
        with open(PREF_FILE, "r", encoding="utf-8") as f:
            saved = json.load(f)
    except json.JSONDecodeError:
        return DEFAULT_PREFS.copy()

    # 用預設值當底,再把存檔的內容蓋上去
    prefs = DEFAULT_PREFS.copy()
    prefs.update(saved)
    return prefs


def save_preferences(prefs):
    PREF_FILE.parent.mkdir(exist_ok=True)
    with open(PREF_FILE, "w", encoding="utf-8") as f:
        json.dump(prefs, f, ensure_ascii=False, indent=2)


prefs = load_preferences()
prefs["name"] = "pzhiqi"
prefs["dietary_restrictions"] = ["不吃牛肉"]
save_preferences(prefs)

print(load_preferences())

prefs.update(saved) 這招很實用——先鋪一層預設值,再把使用者存過的蓋上去。這樣以後我新增一個設定項目,舊的存檔也不會因為缺欄位而出錯。


小結

  • JSON 是跨語言的資料交換格式,結構跟 Python 的 dict / list 幾乎一樣
  • dumps/loads 處理字串,dump/load 處理檔案,有 s 的是 string
  • 處理中文記得 ensure_ascii=False,要人看得懂記得 indent=2
  • datetime 之類的物件要先轉成字串(建議用 isoformat())
  • JSON 是後面所有內容的共通語言:API 回應、AI 的結構化輸出、工具定義、Agent 記憶

明天要講用 Class 描述一件事——當資料的結構越來越複雜,我們需要一個更好的方式來組織它。


上一篇
讓資料離開記憶體:檔案讀寫與 Agent 的長期記憶
下一篇
用 Class 描述一件事:物件導向與 Agent 的角色設計
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言