LLM 為什麼沒有記憶?Stateless 原理與 Prompt 層記憶模擬實戰(含 Token 成長曲線)
一、大模型沒有記憶
由於 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」向上彎曲(平方級)。
