iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
AI Engineering

AI Agent 系統開發 30 天系列 第 2

打造第一個對話 Agent:從單次問答到記住上下文

  • 分享至 

  • xImage
  •  

這篇我們使用 Python 與 Anthropic 官方 SDK,實作一個可以在終端機連續聊天的對話程式。我們先從最基本的一問一答開始,再處理讓程式記住前文的問題,讓模型能接續上一輪話題順暢回答。

完成後,程式會在終端機持續接收訊息,並保留前幾輪的對話內容。使用者提出後續問題時,不必重新說明前面的需求,模型也能根據對話歷史接續回答。

這系列範例使用 uv 管理 Python 專案與依賴。執行 uv sync 時,uv 會為專案建立獨立的虛擬環境,並依照鎖定的版本安裝套件。

💡 範例程式碼庫
本系列所有章節的完整範例程式碼皆收錄在 GitHub :evanchen76/ai-agent-sample。你可以先將專案 clone 到本地端進行操作。

這個對話程式可以使用 Anthropic 或 OpenAI 的模型。進入 chat-model-basics 範例目錄後執行 uv sync,專案會安裝兩家的官方 Python SDK。依照選用的服務設定其中一組 API Key,後續章節統一以 Anthropic Claude API 當作範例。

git clone https://github.com/evanchen76/ai-agent-sample.git

uv sync

# 使用 Anthropic
export ANTHROPIC_API_KEY="..."

# 使用 OpenAI
export OPENAI_API_KEY="..."

為了讓後續範例共用初始化邏輯,我們在 src/chat_model_basics/model.py 中封裝 Client 的建立與模型常數:

# src/chat_model_basics/model.py
import os

from anthropic import Anthropic

# 指定預設使用的 Claude 模型
MODEL_NAME = "claude-haiku-4-5"

def create_client() -> Anthropic:
    if not os.getenv("ANTHROPIC_API_KEY"):
        raise SystemExit("請先設定 ANTHROPIC_API_KEY")

    return Anthropic()


def get_model_name() -> str:
    return MODEL_NAME

這段初始化邏輯有兩個設計重點:

  • API 憑證:因為涉及私密金鑰,透過環境變數 ANTHROPIC_API_KEY 讀取,避免寫死在程式碼中。
  • 模型選擇:在程式碼中以 MODEL_NAME 常數明確指定。這裡預設選用 claude-haiku-4-5;若後續任務需要更強的推理能力,也可以直接換成其他模型。

建立單次問答

我們先用最簡單的方式呼叫模型:透過 input() 讀取終端機的一行文字,將訊息發送給 Claude API,取得文字回覆後印出。在 src/chat_model_basics/ 新增 single_turn.py。先用 input() 從終端機取得使用者輸入,再把問題交給 ask_once()

from chat_model_basics.model import create_client, get_model_name

def main() -> None:
    # 1. 初始化 Client 與取得模型名稱
    client = create_client()
    model = get_model_name()

    # 2. 讀取終端機輸入
    question = input("你:").strip()
    if not question:
        return

    # 3. 發送請求並印出 AI 回覆
    answer = ask_once(client, model, question)
    print(f"AI:{answer}")

取得問題後,ask_once() 會包裝成一則 user 訊息傳送給模型:

def ask_once(client, model: str, question: str) -> str:
    response = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=[{"role": "user", "content": question}],
    )
    return response_text(response)

這次請求使用三個參數:

  • model 指定要呼叫的 Claude 模型。
  • max_tokens 限制這次回覆最多產生的 Token 數量。
  • messages 是要交給模型的對話清單,目前只放入使用者剛輸入的一則 user 訊息。

client.messages.create() 回傳 response 後,response_text() 會從中取出文字:

def response_text(response) -> str:
    return "".join(block.text for block in response.content if block.type == "text")

執行程式並輸入問題:

uv run chat-single-turn
你:Python 要怎麼在 list 裡面新增東西?
AI:可以使用 append() 方法,例如:fruits.append("蘋果")。

這樣就完成一個能接收終端機輸入、呼叫 Claude 並顯示回答的單次問答程式。每次執行只會處理一個獨立問題;接著把它擴展成能持續對話並記住前文的程式。

讓程式支援多輪對話

Messages API 本身是無狀態(Stateless)的,每次呼叫 client.messages.create() 都是一次獨立的請求,模型不會自動取得先前的問答。要讓模型接續前面的話題,應用程式必須保存對話歷史,並在每次呼叫 API 時將完整紀錄一併傳入。

src/chat_model_basics/ 新增 with_history.py。程式在迴圈外宣告 messages 列表,並在每次對話時累積內容:

def main() -> None:
    client = create_client()
    model = get_model_name()

    # 在迴圈外建立對話歷史,後續每一輪都會沿用
    messages: list[MessageParam] = []

    print("輸入 /exit 結束對話。")

    # 持續接收問題直到使用者輸入 /exit
    while True:
        try:
            question = input("你:").strip()
        except EOFError:
            print()
            break

        if question == "/exit":
            break
        if not question:
            continue

        # 傳入既有歷史與新問題,取得加入本輪問答後的歷史
        messages = run_turn(client, model, messages, question)

        # 最後一筆訊息是模型在本輪產生的回答
        print(f"AI:{messages[-1]['content']}")

messages 建立在迴圈外,因此每次執行 run_turn() 時都會沿用同一份對話歷史。若使用者輸入 /exit,程式才會離開迴圈。

run_turn() 負責處理一輪對話。它先將使用者的新問題加入 messages,把完整的歷史交給模型,再將模型的回答也加入同一個列表:

def run_turn(
    client,
    model: str,
    messages: list[MessageParam],
    question: str,
) -> list[MessageParam]:
    # 將使用者問題加入歷史
    messages.append({"role": "user", "content": question})

    response = client.messages.create(
        model=model,
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=messages,
    )

    # 將 AI 回答也加入歷史供下一輪使用
    messages.append({"role": "assistant", "content": response_text(response)})
    return messages

每輪都要把使用者問題與 AI 回答一起放進 messages。如果只保存問題,下一輪模型就不知道自己先前回答了什麼;兩種訊息都保留下來,下一次呼叫 API 時才能傳入完整對話。

執行具有對話歷史的程式:

uv run chat-with-history

你:Python 要怎麼在 list 裡面新增東西?
AI:可以使用 append() 方法,例如:fruits.append("蘋果")。
你:那要怎麼把它刪除?
AI:可以使用 remove("蘋果") 刪除指定元素,或是用 pop() 依索引刪除。
你:/exit

用 System Prompt 設定行為規則與邊界

能接續話題後,下一步是為模型設定全域的行為規範。例如:要求使用繁體中文、僅依據已知事實回答,以及在資訊不足時主動向使用者確認,而非自行猜測。

這些系統層級的規則不應該與使用者的聊天內容混在同一個 messages 清單中。透過 API 獨立提供的 system 參數(System Prompt),程式可以在每一輪對話中,為模型提供穩定且持續生效的指導原則。

with_history.py 中加入 SYSTEM_PROMPT

SYSTEM_PROMPT = """你是一個使用繁體中文回答的聊天助理。
回答時使用對話中已經提供的資訊;缺少必要資訊時先提問,不要自行猜測。
"""

呼叫 API 時,透過 system 參數傳入這段固定規則:

response = client.messages.create(
    model=model,
    max_tokens=1024,
    system=SYSTEM_PROMPT,
    messages=messages,
)

SYSTEM_PROMPT 不會隨著對話輪次改變。每次呼叫模型時都會傳入相同內容,讓模型在整段對話中遵守一致的語言與回答規則。

模型能看見哪些內容

加入 System Prompt 之後,程式在每次呼叫模型時,都會同時送出兩份資料:固定的行為規則(system)與累積的對話紀錄(messages)。

這些在單次呼叫中提供給模型的完整輸入內容,在技術上統稱為 Context(上下文)

為了具體看清 Context 的內容,以剛才的對話為例,當使用者問到第二輪時,傳給模型的 messages 實際上包含了三個項目:

[
    {
        "role": "user",
        "content": "Python 要怎麼在 list 裡面新增東西?",
    },
    {
        "role": "assistant",
        "content": '可以使用 append() 方法,例如:fruits.append("蘋果")。',
    },
    {
        "role": "user",
        "content": "那要怎麼把它刪除?",
    },
]

此時模型收到的 Context 便由兩部分各司其職:

  • SYSTEM_PROMPT 是系統層級的行為準則與邊界:負責規範角色身分、要求僅能依據已知事實回答,並明定在資訊不足時禁止自行臆測、必須主動提問。
  • messages 是動態的事實脈絡與對話歷程:記錄了先前使用者與模型的互動事實,讓模型知道前面討論的是 Python 的 list 操作,進而能理解第二個問題中的「它」是在詢問如何刪除 list 中的元素。

Context 決定了模型在這一輪能使用哪些資訊。缺少先前的問答時,模型無法理解依賴前文的問題;加入對話歷史後,模型才能沿用已經提供的資訊繼續回答。因此,應用程式如何選擇與組裝 Context,會直接影響回答是否連貫且符合需求。

目前程式送給模型的 Context,僅由最基礎的 System Prompt 與對話歷史組成。在後續的系列文章中,我們還會逐步讓 Context 擴展到更多維度——包含 Tool 執行結果Graph State 任務進度RAG 檢索出的文件片段,以及長期記憶中的使用者偏好
https://ithelp.ithome.com.tw/upload/images/20260913/201118962iyDwlM0J4.png

但無論未來加入多少資料,核心本質都相同:只有在當次呼叫中實際送進模型的內容,才會成為它當輪能看見的 Context。 應用程式必須根據當前任務挑選必要資料,並確認資料的可信度、時效與存取權限。

將更多資料放進 Context 可以提供更完整的資訊,但每次呼叫模型時都要重新傳送這些內容,也會帶來兩項實際的工程限制:

  1. Token 累積成本與延遲:每次對話,先前所有的訊息都會重複傳送一次。對話輪次越多,每次呼叫所消耗的 Prompt Tokens 與網路傳輸時間就越高。
  2. Context Window 上限:每個模型都有單次能處理的 Token 數量上限(Context Window)。對話若無限制增長,最終會超出模型能接收的最大長度。

外部資料與 Tool 的必要性

有了 Context 與 System Prompt,程式已經能記住上下文並遵守角色規範。但它依然受限於語言模型的本質:無法取得即時資料,也無法操作外部系統。

例如,即使模型能透過對話歷史(Context)記住使用者人在台中、打算出門散步,但當使用者接著問:「那現在氣溫幾度、會不會下雨?」時,模型依然缺乏當下的即時氣象資料,不能也不應該憑空捏造溫度與降雨機率。

要解決這個問題,應用程式必須提供一個查詢即時資料的 Python 函式,並授權模型在需要時主動提出調用請求,也就是 Tool(工具調用 / Function Calling)

下一篇,我們將為程式接入第一個即時天氣查詢 Tool,讓對話程式具備取得外部即時資料的能力。


上一篇
認識 AI Agent 與學習地圖
系列文
AI Agent 系統開發 30 天2
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言