docs/en/s03-session-store.mdXem bản gốc Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.
s03: Cửa hàng phiên
s00 > s01 > s02 > [ s03 ] > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18 > s19 > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27
Thoát khỏi chương trình và cuộc trò chuyện không còn nữa. Hai nền tảng gửi tin nhắn cùng lúc ở chế độ Cổng và khóa tệp xung đột.
Chương này giải quyết cả hai vấn đề với SQLite.
Chương này giải quyết vấn đề gì
Bởi s02 agent đã có hệ thống đăng ký và gửi công cụ hoàn chỉnh.
Nhưng có một lỗ hổng chết người: messages chỉ sống trong ký ức. Khi quá trình thoát ra, mọi thứ sẽ đặt lại về 0.
Điều này là không thể chấp nhận được ngay cả đối với CLI một người dùng - thoát và cuộc trò chuyện không còn nữa; --continue để tiếp tục phiên cuối cùng là không thể. Ở chế độ Cổng, vấn đề còn nghiêm trọng hơn -- tin nhắn từ nhiều nền tảng đến đồng thời và những cuộc hội thoại đồng thời đó cần phải:
- Đọc lịch sử hội thoại trước đó (nếu không mọi lượt sẽ bắt đầu lại từ đầu)
- Ghi đồng thời vào cùng một cơ sở dữ liệu mà không có xung đột khóa ghi (các phiên khác nhau không chia sẻ dữ liệu nhưng chúng ghi vào cùng một tệp cơ sở dữ liệu và SQLite có một khóa ghi duy nhất cho toàn bộ cơ sở dữ liệu)
- Hỗ trợ tìm kiếm các phiên lịch sử sau khi thực tế
Cách tiếp cận hệ thống tệp (một tệp JSON mỗi phiên) giải quyết điểm 1; điểm 2 không phải là vấn đề vì các tệp được cách ly một cách tự nhiên, mặc dù bạn mất đi sự đảm bảo về giao dịch; điểm 3 yêu cầu quét mọi tập tin.
Đó là lý do tại sao Hermes Agent chọn SQLite. Không phải vì "SQLite đẹp hơn các tập tin" mà vì nó giải quyết được cả ba vấn đề cùng một lúc.
Điều khoản chính
Phiên là gì
A session là một cuộc trò chuyện hoàn chỉnh
Nó có thời gian bắt đầu, thời gian kết thúc và ID duy nhất. Tất cả tin nhắn trong cuộc trò chuyện đều thuộc về phiên này.
Ở chế độ CLI, từ thời điểm bạn khởi chạy đến thời điểm bạn thoát là một phiên.
Ở chế độ Cổng, cuộc trò chuyện liên tục trong một cửa sổ trò chuyện là một phiên.

Chế độ WAL là gì
WAL là chế độ ghi nhật ký SQLite (Ghi nhật ký ghi trước). Kích hoạt nó mất một dòng:
conn.execute("PRAGMA journal_mode=WAL")
Đầu tiên, vấn đề: tại sao lại cần WAL?
Trong chế độ mặc định của SQLite, khi ai đó đang ghi vào cơ sở dữ liệu, thậm chí không ai khác có thể đọc được:
Telegram adapter is writing a message -> entire database is locked
|
Discord adapter wants to read history -> X blocked -- can't even read
Tốt cho CLI một người dùng, nhưng ở chế độ Cổng có nhiều nền tảng gửi và nhận đồng thời, điều này gây ra tình trạng ngừng hoạt động.
Cách WAL giải quyết vấn đề này:
Khi bật WAL, trước tiên hãy ghi vào tệp nhật ký tạm thời mà không cần chạm vào cơ sở dữ liệu chính. Các lần đọc tiếp tục nhấn vào tệp cơ sở dữ liệu chính, không bị ảnh hưởng:
Telegram adapter is writing a message -> writes to the WAL journal (main database untouched)
|
Discord adapter wants to read history -> OK reads from the main database
Nhật ký sẽ được tự động sáp nhập trở lại cơ sở dữ liệu chính vào đúng thời điểm.
Nhưng lưu ý: ghi vẫn xếp hàng sau nhau. Nếu hai bộ điều hợp cố gắng ghi cùng lúc, một bộ điều hợp phải đợi bộ điều hợp kia hoàn tất.
Default mode:
write --blocks--> read X
write --blocks--> write X
WAL mode:
write --does not block--> read OK <- the key improvement
write --still blocks--> write X <- unchanged
FTS5 là gì
FTS5 là tiện ích mở rộng tìm kiếm toàn văn bản của SQLite (Tìm kiếm toàn văn bản 5).
Nó cho phép bạn nhanh chóng tìm kiếm từ khóa trên khối lượng lớn thư lịch sử -- không cần phải quét từng hàng bằng LIKE '%keyword%'.
Parent_session_id là gì
Khi cuộc trò chuyện đủ dài để kích hoạt tính năng nén ngữ cảnh (s05), hệ thống sẽ tạo một phiên mới. Phiên mới quay lại phiên cũ thông qua parent_session_id.
Điều này tạo thành một chuỗi:
session_001 (full history, 500 messages)
^
| parent_session_id
session_002 (compressed summary + new messages)
^
| parent_session_id
session_003 (compressed again)
Lịch sử cũ không bị xóa -- chỉ được lưu trữ.
Nguồn là gì
Mỗi phiên có một source trường cho biết thông báo đến từ điểm nhập nào: cli, telegram, discord, slack, weixin, v.v.
Điều này cho phép bạn lọc các phiên theo nền tảng: "chỉ hiển thị các cuộc hội thoại Telegram".

Mô hình tinh thần tối thiểu
agent starts up
|
v
New session? --yes--> create a session record
|
no (continuing an existing session)
|
v
Read historical messages from SQLite
|
v
Pass them to AIAgent.run_conversation()
|
v
After each conversation turn, write new messages to SQLite
|
v
agent exits
|
v
On next launch, read them back from SQLite -- the conversation is still there
Cấu trúc dữ liệu chính
1. Bản ghi phiên
Phiên bản giảng dạy tối thiểu:
session = {
"id": "...",
"source": "cli",
"started_at": 1710000000.0,
}
Toàn bộ hệ thống cũng lưu trữ: mô hình, system_prompt, parent_session_id, thống kê token và ước tính chi phí. Nhưng phiên bản tối thiểu chỉ cần ba trường trên để hoạt động.
2. Bản ghi tin nhắn
Phiên bản giảng dạy tối thiểu:
message = {
"session_id": "...",
"role": "user" | "assistant" | "tool",
"content": "...",
"timestamp": 1710000000.0,
}
Toàn bộ hệ thống cũng lưu trữ: tool_calls (được tuần tự hóa JSON), tool_call_id, tool_name và token_count.
3. Chỉ số FTS
Không phải cấu trúc dữ liệu bạn thao tác trực tiếp. Nó là một chỉ mục tìm kiếm được SQLite duy trì tự động. Mỗi khi một tin nhắn được chèn vào, trình kích hoạt SQLite sẽ cập nhật chỉ mục FTS.
Thực hiện tối thiểu
Bước 1: Tạo bảng
import sqlite3
conn = sqlite3.connect("state.db")
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
started_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
role TEXT NOT NULL,
content TEXT,
timestamp REAL NOT NULL
);
""")
Đó là phiên bản tối thiểu. Hai bảng, chế độ WAL, sẵn sàng hoạt động.
Bước 2: Tạo phiên
import uuid, time
def create_session(conn, source="cli"):
session_id = str(uuid.uuid4())
conn.execute(
"INSERT INTO sessions (id, source, started_at) VALUES (?, ?, ?)",
(session_id, source, time.time()),
)
conn.commit()
return session_id
Bước 3: Viết tin nhắn
def add_messages(conn, session_id, messages):
for msg in messages:
conn.execute(
"INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)",
(session_id, msg["role"], msg.get("content", ""), time.time()),
)
conn.commit()
Bước 4: Đọc lịch sử
def get_session_messages(conn, session_id):
rows = conn.execute(
"SELECT role, content FROM messages WHERE session_id = ? ORDER BY id",
(session_id,),
).fetchall()
return [{"role": r[0], "content": r[1]} for r in rows]
Bước 5: Cắm nó vào vòng lặp
# At startup
session_id = create_session(conn, source="cli")
# After each conversation turn
new_messages = result["messages"][len(old_messages):] # only store the new ones
add_messages(conn, session_id, new_messages)
# On next launch
history = get_session_messages(conn, session_id)
result = agent.run_conversation(user_message, conversation_history=history)
Những thiết kế độc đáo ở agent Hermes
Xử lý xung đột khóa ghi
Trong chế độ SQLite WAL, việc ghi vẫn cần được tuần tự hóa. Trình xử lý bận mặc định sử dụng thời gian chờ xác định, trong điều kiện có tính đồng thời cao sẽ gây ra hiệu ứng xếp hàng (mọi người đều đợi cùng một khoảng thời gian).
Cách tiếp cận của agent Hermes: đặt thời gian chờ SQLite ngắn (1 giây), sau đó thực hiện các lần thử quay lại ngẫu nhiên ở lớp ứng dụng. Các khoảng thời gian ngẫu nhiên lan rộng ra các nhà văn cạnh tranh một cách tự nhiên.
bộ nhớ đệm system_prompt
Gateway tạo một phiên bản AIAgent mới cho mỗi tin nhắn. Nếu lời nhắc hệ thống được tập hợp lại mỗi lần, một thay đổi đối với MEMORY.md tạm thời có thể thay đổi lời nhắc, làm mất hiệu lực bộ nhớ đệm nhắc nhở của Anthropic.
Vì vậy, agent Hermes lưu trữ lời nhắc hệ thống được lắp ráp ban đầu trong bảng phiên. Các phiên bản tiếp theo được đọc trực tiếp từ bộ đệm, đảm bảo lời nhắc không thay đổi.
Quản lý phiên bản lược đồ
Lược đồ cơ sở dữ liệu phát triển trên các phiên bản. agent Hermes duy trì một schema_version table và kiểm tra số phiên bản khi khởi động. Nếu cần di chuyển, nó sẽ tự động chạy các câu lệnh ALTER TABLE thích hợp.
Những sai lầm phổ biến nhất của người mới bắt đầu
1. Không kích hoạt chế độ WAL
Ở chế độ mặc định, một thao tác ghi sẽ chặn tất cả các lần đọc. Trong trường hợp Cổng, khi tổng đài viên đang viết tin nhắn, yêu cầu đọc từ nền tảng khác sẽ bị treo.
2. Lưu trữ toàn bộ danh sách tin nhắn thay vì Delta
Viết đầy đủ danh sách tin nhắn sau mỗi lượt, thay vì chỉ bổ sung mới. Cuộc trò chuyện càng dài thì tốc độ viết càng chậm.
3. Session_id mã hóa cứng
Sử dụng cùng một session_id mỗi lần khởi chạy, trộn tất cả các cuộc hội thoại lại với nhau. Session_id mới sẽ được tạo cho mỗi cuộc trò chuyện mới.
4. Không xử lý tuần tự tool_calls
tool_calls là cấu trúc lồng nhau không thể được lưu trữ trực tiếp trong cột TEXT. Nó cần tuần tự hóa JSON.
5. Không phân tách bằng chat_id trong Gateway
Các cửa sổ trò chuyện khác nhau trên các nền tảng khác nhau sẽ có các phiên khác nhau. Nếu tất cả tin nhắn được đưa vào một phiên, tổng đài viên sẽ coi cuộc hội thoại của Người dùng A là bối cảnh cho Người dùng B.
Vòng lặp hoàn chỉnh cho đến nay
Kết hợp vòng lặp s01 + hệ thống công cụ s02 + tính bền bỉ của phiên s03, toàn bộ quy trình trông như thế này:
Program starts
|
+-- New conversation? -- yes --> create_session() -> get session_id
| messages = []
|
+-- Continue old conversation? -- yes --> read historical messages from SQLite
messages = get_session_messages(session_id)
|
v
User types a message
|
v
messages.append({"role": "user", "content": user_input})
|
v
+------------------ Loop starts (up to 90 iterations) -----------------+
| |
| Assemble api_messages = [system_prompt] + messages |
| | |
| v |
| Call model API |
| | |
| v |
| messages.append(assistant reply) |
| | |
| Has tool_calls? |
| / \ |
| no yes |
| | | |
| v v |
| Loop ends registry.dispatch(tool_name, args) |
| | |
| v |
| messages.append(tool result) |
| | |
| v |
| Next iteration |
| |
+-----------------------------------------------------------------------+
|
v
Write this turn's new messages to SQLite
|
v
Wait for the next user input (loop back up)
|
...
|
v
Program exits -> next launch can read from SQLite and resume
Trong mã:
# -- Startup --
conn = sqlite3.connect("state.db")
conn.execute("PRAGMA journal_mode=WAL")
if continue_session:
messages = get_session_messages(conn, session_id)
else:
session_id = create_session(conn, source="cli")
messages = []
# -- Main conversation loop --
while True:
user_input = input("> ")
if user_input == "exit":
break
messages.append({"role": "user", "content": user_input})
old_len = len(messages)
# s01 core loop
for i in range(90):
api_messages = [{"role": "system", "content": system_prompt}] + messages
response = client.chat.completions.create(
model="anthropic/claude-sonnet-4",
messages=api_messages,
tools=registry.get_definitions(), # s02: get tool schemas from the registry
)
assistant_msg = response.choices[0].message
messages.append({
"role": "assistant",
"content": assistant_msg.content,
"tool_calls": [...] or None,
})
if not assistant_msg.tool_calls:
break # Model is done; exit inner loop
# s02: registry dispatches execution (handles sync/async automatically)
for tc in assistant_msg.tool_calls:
result = registry.dispatch(tc.function.name, tc.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result,
})
# s03: write only the new messages to SQLite
add_messages(conn, session_id, messages[old_len:])
# Print the last assistant reply
print(messages[-1]["content"])
Mỗi lớp xử lý mối quan tâm riêng của mình:
| Lớp | Trách nhiệm | Không xử lý |
|---|---|---|
| vòng lặp s01 | tin nhắn -> API -> tool_calls -> lượt tiếp theo | Cách tìm thấy các công cụ; nơi lưu trữ tin nhắn |
| s02 công cụ | Tra cứu theo tên, công văn, cầu nối đồng bộ/không đồng bộ | Logic vòng lặp; kiên trì |
| cửa hàng s03 | Viết vào SQLite, đọc lịch sử, an toàn đồng thời WAL | Vòng lặp chạy như thế nào; cách các công cụ thực thi |
Ranh giới giảng dạy
Chương này bao gồm những gì:
Di chuyển tin nhắn từ bộ nhớ sang SQLite để các cuộc hội thoại vẫn tồn tại khi khởi động lại và hoạt động đồng thời trên nhiều nền tảng được an toàn.
Đây chưa phải là nén ngữ cảnh (s05, sử dụng parent_session_id), hệ thống bộ nhớ (s07, thông tin được quản lý kéo dài các phiên) hoặc định tuyến phiên Cổng (s12, chỉ định phiên bằng chat_id).
Cố tình bỏ đi:
- Cú pháp tìm kiếm FTS5 nâng cao
- Thống kê token và các trường ước tính chi phí
- Logic ALTER TABLE cụ thể để di chuyển lược đồ
- Cách Gateway ánh xạ chat_id tới đúng phiên ->
s12
Nếu bạn có thể thực hiện "nhân viên thoát ra và khởi động lại mà cuộc trò chuyện vẫn còn nguyên", thì bạn đã hoàn thành chương này.
Một câu để nhớ
Hermes Agent sử dụng SQLite + WAL thay vì hệ thống tệp vì nó được thiết kế để hoạt động đồng thời trên nhiều nền tảng ngay từ đầu -- đây không phải là một sự tối ưu hóa; đó là yêu cầu cơ bản của chế độ Cổng.