一、大模型沒有記憶

由於 LLM 本質上是無狀態的(Stateless)。每一次 API 呼叫都是獨立的、全新的計算過程,模型內部不會保留任何「記住上一句話」的狀態。因此,兩次呼叫彼此獨立,第二次完全不知道第一次發生過的事情。於是乎就有了以下的做法。

Prompt 層模擬記憶

把「歷史聊天記錄」+「當前新問題」打包拼接,作為一個完整的 Prompt 一次性傳給模型,讓它在 單次推理中 看到全部上下文。

我們會把對話歷史塞進 context 中,如下:

messages = [
    {"role": "user",      "content": "我叫小明"},
    {"role": "assistant", "content": "你好小明!很高興認識你。"},
    {"role": "user",      "content": "我叫什麼名字?"},
]

這裏面的role代表如下:

role 作用 典型內容
system 設定助理的身份、規則、語氣(Anthropic 放在獨立的 system 參數) 「你是一位耐心的數學家教」
user 使用者說的話 「幫我解這題」
assistant 模型過去的回覆(由你把它加回歷史) 「好的,第一步…」

兩種計費 token

類型 說明 相對成本
input_tokens 輸入進去的(包含全部對話歷史) 便宜
output_tokens 模型生成輸出的 貴(通常 4~5×)

問題:每一輪你都把 全部歷史 當作 input 重新送一次。第 10 輪的 input 包含了前 9 輪的所有內容。

累計 input token ≈ 每輪歷史長度的總和
≈ 1 + 2 + 3 + … + n ≈ O(n²)

Anthropic 定價(每 MTok = 100 萬 token)

模型 Input Output
claude-opus-4-8 $15 $75
claude-sonnet-5 $3 $15
claude-haiku-4-5 $0.80 $4

Context Window 上限

每個模型有 上下文視窗(context window) 上限——單次呼叫能吃下的最大 token 數(input + output 合計)。將所有對話記錄都塞進去 context,歷史記錄一多就會超出限額。

模型世代 典型視窗
早期 GPT-3.5 4K / 16K
GPT-4 8K / 32K / 128K
Claude 3/4 200K
前沿長上下文 1M

撰寫一個簡單的 anthropic API 呼叫

import anthropic

client = anthropic.Anthropic()   # 讀取環境變數 ANTHROPIC_API_KEY

resp = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=512,
    system="你是一位友善的助理,用繁體中文回答。",
    messages=[
        {"role": "user", "content": "你好"},
    ],
)

print(resp.content[0].text)     # 模型的文字回覆
print(resp.usage.input_tokens, resp.usage.output_tokens)   # token 用量

幾個重點

  • system 是獨立參數,不放在 messages 裡(這是 Anthropic 與 OpenAI 的差異)。
  • messages 必須 user / assistant 交替,且以 user 開頭、user 結尾(送出時最後一則是使用者的新問題)。
  • 回覆是 content 陣列:純文字對話取 resp.content[0].text
  • resp.usage 直接告訴你這次用了多少 token——這是 Lab 3 畫曲線的資料來源,不需自己估算。

如果想知道精確 token 數量,可用:

count = client.messages.count_tokens(
    model="claude-haiku-4-5",
    system="你是一位友善的助理。",
    messages=messages,
)
print(count.input_tokens)

下面是全量拼接對話流程,並添加用於管理對話的 BufferMemory

class BufferMemory:
    def __init__(self, system=None):
        self.system = system
        self._messages = []      # [{"role": ..., "content": ...}, ...]

    def add_user(self, text):
        self._messages.append({"role": "user", "content": text})

    def add_assistant(self, text):
        self._messages.append({"role": "assistant", "content": text})

    def build_messages(self):
        """回傳要送給 API 的完整 messages(全量)。"""
        return list(self._messages)

    def __len__(self):
        return len(self._messages)
"""全量拼接的多輪 chatbot。輸入 exit 離開,clear 清空記憶。"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from memory.llm import chat, count_tokens
from memory.buffer import BufferMemory

SYSTEM = "你是一位友善、記性很好的助理,一律用繁體中文回答,回覆簡潔。"

def main():
    mem = BufferMemory(system=SYSTEM)
    print(" 全量拼接 chatbot(輸入 exit 離開,clear 清空)\n")

    while True:
        user = input("你 > ").strip()
        if not user:
            continue
        if user.lower() == "exit":
            break
        if user.lower() == "clear":
            mem = BufferMemory(system=SYSTEM)
            print("(記憶已清空)\n")
            continue

        mem.add_user(user)
        messages = mem.build_messages()

        # 送出前先看看這次帶了多少 input token
        tokens = count_tokens(messages, system=SYSTEM)
        reply, usage = chat(messages, system=SYSTEM)
        mem.add_assistant(reply)

        print(f"AI > {reply}")
        print(f"    [歷史 {len(mem)} 則 | 本輪 input≈{tokens} tok "
              f"| out {usage.output_tokens} tok]\n")

if __name__ == "__main__":
    main()

接著執行程式來測試看看對話記錄效果

python demos\w1_buffer_chat.py

 全量拼接 chatbot(輸入 exit 離開,clear 清空)

你 > 我在學做義大利麵,我叫小明
AI > 你好小明,很高興你在學做義大利麵。義大利麵有許多種類,像是Spaghetti、Penne、Fusilli等,做法也非常多變。需要我提供一些基本的義大利麵食譜或者特定菜式的作法嗎?
    [歷史 2 則 | 本輪 input≈62 tok | out 72 tok]

你 > 我剛剛說我叫什麼?
AI > 你剛剛說你叫小明。需要我幫忙的話,請隨時告訴我。
    [歷史 4 則 | 本輪 input≈176 tok | out 31 tok]

你 > 那我在學做什麼?
AI > 你在學做義大利麵。需要我提供一些義大利麵的烹飪技巧或是食譜嗎?
    [歷史 6 則 | 本輪 input≈223 tok | out 36 tok]

你 > 回到最上面,你是不是想提供什麽給我?
AI > 是的,小明,我可以提供一些義大利麵的基本食譜或者特定菜式的作法給你。如果你需要,我可以分享一些常見的義大利麵做法,讓 你嘗試。
    [歷史 8 則 | 本輪 input≈292 tok | out 55 tok]

Token 成長曲線

接著我們來測試一段固定劇本跑 N 輪,記錄每輪 input token,畫出成長曲線,並看到 O(n²)

新增 demos/w1_token_curve.py

"""量測全量拼接下 input token 隨對話輪數的成長,並畫圖。"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from memory.llm import chat, count_tokens
from memory.buffer import BufferMemory

SYSTEM = "你是一位友善的助理,用繁體中文簡短回答。"

# 一段固定劇本,讓每次量測可重現(也可換成你自己的長對話)
SCRIPT = [
    "我們來規劃一趟三天的京都旅行。",
    "第一天想去清水寺跟祇園。",
    "第二天想去嵐山跟金閣寺。",
    "第三天想留在市區逛街吃東西。",
    "幫我推薦第一天的午餐。",
    "預算大概每餐一千日圓。",
    "我對甲殼類過敏,請避開。",
    "順便推薦第二天嵐山的交通方式。",
    "我住在京都車站附近的旅館。",
    "最後幫我把三天行程總結成一張表。",
]

def main():
    mem = BufferMemory(system=SYSTEM)
    rounds, input_tokens, cumulative = [], [], []
    total = 0

    for i, user in enumerate(SCRIPT, start=1):
        mem.add_user(user)
        messages = mem.build_messages()
        tok = count_tokens(messages, system=SYSTEM)   # 本輪 input token
        reply, _ = chat(messages, system=SYSTEM, max_tokens=256)
        mem.add_assistant(reply)

        total += tok
        rounds.append(i)
        input_tokens.append(tok)
        cumulative.append(total)
        print(f"第 {i:2d} 輪:本輪 input={tok:5d} tok | 累計={total:6d} tok")

    _plot(rounds, input_tokens, cumulative)

def _plot(rounds, per_round, cumulative):
    import matplotlib
    matplotlib.rcParams["font.sans-serif"] = ["Microsoft JhengHei", "SimHei", "Arial"]
    matplotlib.rcParams["axes.unicode_minus"] = False
    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(1, 2, figsize=(11, 4))
    ax[0].plot(rounds, per_round, "o-", color="#2563eb")
    ax[0].set_title("每輪 input token(線性成長)")
    ax[0].set_xlabel("對話輪數"); ax[0].set_ylabel("input tokens")

    ax[1].plot(rounds, cumulative, "o-", color="#dc2626")
    ax[1].set_title("累計 input token(≈ O(n²))")
    ax[1].set_xlabel("對話輪數"); ax[1].set_ylabel("累計 input tokens")

    fig.tight_layout()
    out = os.path.join(os.path.dirname(__file__), "w1_token_curve.png")
    fig.savefig(out, dpi=120)
    print(f"\n 已存圖:{out}")

if __name__ == "__main__":
    main()

執行

python demos/w1_token_curve.py

最終我們獲得圖片如下,終端印出每輪 token,左圖「每輪 input」近似直線上升,右圖「累計 input」向上彎曲(平方級)。

83832-2zdh0ta5u4a.png

無標籤

關注作者:

新增評論