> For the complete documentation index, see [llms.txt](https://levon.gitbook.io/agent-engineering/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://levon.gitbook.io/agent-engineering/04-hui-hua-chi-jiu-hua.md).

# 第 4 课：Session 持久化——Transcript、Checkpoint 与 Memory

第 3 课已经把排查过程写进了 `session.jsonl`。现在增加一个需求：关闭程序，明天还能接着查同一个问题。

文件还在，只解决了记录没有丢。程序还要知道打开哪份记录、已经进行到哪一步，以及本次应该把什么交给模型。上一课的临时目录用于观察结果，长期续跑还需要稳定的存储位置。存了聊天记录，不等于接上了昨天的话。

## 1. Session：会话身份与存储边界

把同一段连续交互归在一起，这个单位叫 **Session（会话）**。应用可以给它一个编号，例如 `session_id="order-debug-01"`。第三课从用户提问到最终回答是一个 Turn；围绕这次故障继续追问，可以形成同一个 Session 中的多个 Turn。

一种本地存储布局可以是：

```
sessions/
|-- order-debug-01/
|   |-- session.jsonl
|   +-- checkpoint.json
+-- order-debug-02/
    |-- session.jsonl
    +-- checkpoint.json
```

选择 `order-debug-01`，程序就去找这一组记录；开启新会话，则分配另一个编号。编号负责关联数据，不会让模型自动记住内容，也不代表访问授权。

这是布局示意，不是上一课 demo 已有的续跑入口。Session 是逻辑归属，可以落在目录、数据库记录里，也可以对接模型服务托管的对话状态。\[1] 文件扩展名不是它的定义。

## 2. Transcript 与 Checkpoint：历史记录和恢复状态

**Transcript** 记录这段会话按顺序发生过什么，包括用户消息、模型调用申请和工具结果。**Checkpoint** 保存某个位置的状态，帮助程序从那里继续。

先用三笔流水看区别：存入 10 元，支出 3 元，再存入 5 元。完整流水能算出余额 12 元；如果已经保存了“处理完前两笔，余额 7 元”，恢复时只需再处理第三笔。

```python
events = [10, -3, 5]

balance = 0
for amount in events:
    balance += amount
print(balance)

checkpoint = {"processed": 2, "balance": 7}
balance = checkpoint["balance"]
for amount in events[checkpoint["processed"]:]:
    balance += amount
print(balance)
```

两次输出都是 `12`。`processed=2` 表示前两笔已经计入，切片从第三笔开始。**快照中的状态必须和它标记的位置对应**，否则会重复计算或漏掉事件。

这里是把已记录的第三笔算进余额，不是再存一次钱。同样，回放 Transcript 是重建应用状态，不代表把历史工具调用重新执行一遍。

放回排查助手：

| 概念         | 要回答的问题        | 可以保存什么                   |
| ---------- | ------------- | ------------------------ |
| Transcript | 当时问了什么，读到了什么？ | 完整的消息、调用编号、工具结果与相关事件     |
| Checkpoint | 程序最后保存到了哪里？   | 截至某个位置的消息或摘要、运行进度、恢复所需字段 |

一份快照可以包含部分历史，也可以包含已经整理好的状态，但它不一定能还原所有原始事件。保存哪些字段，要由程序准备怎样恢复来决定；Checkpoint 不要求复制整个 Python 进程。

## 3. JSON 与 JSONL：数据格式和文件组织

**JSON** 表示一份完整的结构化数据，适合保存一份当前快照；**JSONL** 则是一行一个独立 JSON 值，本书用每行一个对象记录事件，便于顺序追加。

下面用两条手工构造的短消息演示快照存取。它们只用来展示文件内容，不是模型运行实录。

```python
import json
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as folder:
    session_dir = Path(folder) / "order-debug-01"
    session_dir.mkdir()
    state_file = session_dir / "checkpoint.json"
    state = {
        "session_id": "order-debug-01",
        "processed_entries": 2,
        "messages": [
            {"role": "user", "content": "排查订单接口的500"},
            {"role": "assistant", "content": "请提供日志位置"},
        ],
        "summary": "等待用户提供日志位置",
    }

    temporary = state_file.with_suffix(".tmp")
    temporary.write_text(
        json.dumps(state, ensure_ascii=False), encoding="utf-8"
    )
    temporary.replace(state_file)
    restored = json.loads(state_file.read_text(encoding="utf-8"))
    print(restored["session_id"])
    print(restored["processed_entries"])
```

输出为：

```
order-debug-01
2
```

这份快照包含会话编号、截至第几条记录、两条消息和当前摘要。`processed_entries` 对应前面流水例子的 `processed`，这里计数的是会话记录。先写完临时文件，再在同一文件系统内替换目标，避免在覆盖过程中读到半份 JSON。示例只演示单写者的文件存取；断电持久性、并发写入还需要额外保证。临时目录会被清理，真实应用应使用长期保留的位置。

相同的两条消息，放进 Transcript 时可以逐条追加到 JSONL：

```jsonl
{"type":"message","message":{"role":"user","content":"排查订单接口的500"}}
{"type":"message","message":{"role":"assistant","content":"请提供日志位置"}}
```

读取时逐行解析，每行都是完整 JSON；不能把整份多行文件当作一个 JSON 对象一次解析。完整 Transcript 也能存成 JSON 数组，这里选 JSONL 是为了方便逐条追加。本例只有两条消息事件；真实 Transcript 还可能记录其他事件，恢复位置必须按所用格式计算。第三课的 Tool Call 与 Tool Result 字段也必须完整保留。

Checkpoint 不一定要单独放文件。它也可以作为一种事件，写进同一个 JSONL：

```
第 1 行：用户消息
第 2 行：模型消息
第 3 行：Checkpoint，保存截至第 2 行的状态
第 4 行：新的用户消息
```

程序找到这份快照后，再处理对应的后续事件。第 3 行是一条状态记录，不是多发生了一次用户交互。因此，**Transcript 与 Checkpoint 描述用途，JSON 与 JSONL 描述数据怎样组织**；同一个文件可以承担两种用途。

## 4. Memory 与 Context：跨会话信息和当次输入

继续 `order-debug-01`，需要知道上次停在“等待日志位置”。开启一个全新的会话，却不应该无缘无故继承这项进度。

有些信息适用得更久，例如用户确认的“默认使用中文”，或项目约定“诊断要注明文件位置”。这些经过选择、允许跨会话使用的信息，可以保存为 **Memory（长期记忆）**。

| 信息         | 使用范围                |
| ---------- | ------------------- |
| 当前排查等待哪份材料 | 当前 Session          |
| 本项目的诊断报告约定 | 同一项目的多个 Session     |
| 用户默认使用哪种语言 | 适用该偏好的不同项目与 Session |

沿用上一节读回的 `restored`，下面只比较本次选中的材料，不调用模型：

```python
def context_materials(session_id):
    materials = ["默认使用中文", "诊断要注明文件位置"]
    if session_id == restored["session_id"]:
        materials.append(restored["summary"])
    return materials

print("旧会话：", context_materials("order-debug-01"))
print("新会话：", context_materials("new"))
```

输出为：

```
旧会话： ['默认使用中文', '诊断要注明文件位置', '等待用户提供日志位置']
新会话： ['默认使用中文', '诊断要注明文件位置']
```

同一份长期信息可以被再次选用，旧进度却只跟随对应会话。代码中的字符串列表只是材料清单；实际请求还要按模型协议组织指令、历史消息和工具说明，不能拿它替代完整的消息列表。

写到文件里，叫**持久化**；经过应用选择、实际放进这次模型请求的材料，才构成这次的 **Context（上下文）**。磁盘上保存了多少，不等于模型本次看到了多少。

Memory 不应照抄所有历史。临时日志、未确认的猜测和凭据，不应自动成为长期事实；已保存内容也要能更新或删除。用户本次要求英文，可以覆盖默认中文偏好；强制安全策略不能被新偏好覆盖，更不能靠记忆里一句“已经批准”获得权限。

## 5. 恢复边界与存储演进

快照只证明程序保存过某个状态，不证明外部世界仍停在那里。以后允许助手修改文件时，可能出现：

```
Checkpoint：尚未记录这次修改的结果
实际文件：修改已经发生
```

此时按旧快照重新执行，可能重复操作或覆盖用户后来的修改。第 6 课会用工具执行账本 **Ledger** 核对执行结果。只读工具也要分清：重新读取的是当前文件，不一定是当时的那份内容。

Checkpoint 可以在完整 Turn 结束后保存，也可以支持中途恢复，前提是保存了对应的现场。这里的文件读回还不是一次跨进程 Agent 续跑验证，不能把它当成完整故障恢复系统。

存储规模增长以后，再根据读写方式决定是否需要数据库。低频顺序追加与回放可以使用 JSONL；频繁查询、需要事务和唯一约束时，可以考虑 SQLite 等数据库。\[2] 不必为了“到了十万行”就迁移，也不必等到十万行才处理并发问题。相关实现留在第 6 课和存储专项练习。

会话现在有了归属，历史与恢复点也能保存。下一课处理另一个问题：记录越积越多，哪些内容应该进入有限的模型窗口？

## 资料与配套实验

1. [OpenAI：服务端对话状态](https://developers.openai.com/api/docs/guides/conversation-state)
2. [存储方式与固定源码核验](https://github.com/unix2dos/agent-engineering-book/tree/main/research/storage-backend-source-verification.md)
3. [已有 Session 与 Memory 教学实现](https://github.com/unix2dos/agent-engineering-book/tree/main/examples/lesson_04_session_memory.py)
4. [配套综合实践](https://github.com/unix2dos/agent-engineering-book/tree/main/exercises/phase-1-capstone/README.md)
5. [SQLite 专项练习](https://github.com/unix2dos/agent-engineering-book/tree/main/exercises/session-storage-sqlite/README.md)
6. [第 4～5 课相关资料核验](https://github.com/unix2dos/agent-engineering-book/tree/main/research/01-05-chapter-promotion-sources.md)
7. [正文代码检查](https://github.com/unix2dos/agent-engineering-book/tree/main/experiments/reading-pilot/check_lesson_04.py)：`python -B experiments/reading-pilot/check_lesson_04.py`，只验证正文数据与代码，不调用模型。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://levon.gitbook.io/agent-engineering/04-hui-hua-chi-jiu-hua.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
