Harness Academy
Tài liệu gốc 06 12 phútTiếng Việt + English
docs/en/s03-session-store.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

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:

  1. Đọ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)
  2. 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)
  3. 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.

Mặc định SQLite so với Chế độ WAL

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".

Vòng đời duy trì phiên

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ớpTrách nhiệmKhông xử lý
vòng lặp s01tin nhắn -> API -> tool_calls -> lượt tiếp theoCá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 s03Viết vào SQLite, đọc lịch sử, an toàn đồng thời WALVò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.

Mục lục nguồn