← 全部极客知识库

大模型通识 / 大模型应用开发入门

14. 保存会话与计划——让应用再次打开仍能用

原文备份 · 第 16 / 19 篇 · 查看极客时间原知识库 ↗

本篇目录 · 9 节

来源:大模型应用开发入门

昨天,你已经在网页里生成了一张带引用的复习卡。今天重新启动助手,旧结果地址却打不开了。文件夹里明明还有JSON,页面为什么说不存在?

上一课把“模型请求的记录”和“网页正在使用的状态”分开了:JSON写在磁盘上,预览与结果索引放在Python内存中。退出程序,内存里的索引就没了。本篇把网页状态也保存下来,让你能够重启后继续查看卡片,并为它建立一份需要亲自确认的复习计划。

我们还要解决一个紧挨着的问题:如果昨天点击生成后电脑突然断电,今天重新打开,程序该继续显示结果、再次调用模型,还是先停下来核对?这不是换一个存储文件就能自动解决的,必须给每次操作一个身份,并记录它走到了哪一步。

下载完整代码 · 代码说明 · 上一篇:本地网页

核实日期:2026年10月4日。沿用Python 3.11+、openai 3.24.0、Flask 3.1.3;新增Python标准库sqlite3,无须额外安装数据库服务。作者在Python 3.11.8、SQLite 3.45.1上完成97项离线测试,并在浏览器验证保存计划和重启恢复。未使用真实API密钥或发起付费调用;默认卡片仍为作者预置演示。

一、先分清我们要保存的三种东西

这里的“会话”指浏览器连续使用这个应用的身份。同一个浏览器配置下,多个标签页通常属于同一会话;另一浏览器或独立的隐私窗口通常是另一会话。它不是第3篇模型请求中的多轮聊天消息列表。

在这个会话下面,我们保存两类工作记录:一次笔记问答操作,以及依附于这次操作的复习计划。

对象 保存什么 用来回答什么问题
浏览器会话 随机编号、创建时间 当前浏览器可以查看哪些工作记录?
问答操作 操作编号、所属会话、模式、预览快照、运行状态、结果 当时用了哪些资料?是否已经请求过?卡片在哪里?
复习计划 计划编号、所属会话、关联操作、日期、时长、草稿或已保存状态 复习哪张卡?用户是否确认?

卡片和完整的来源快照放在操作记录的JSON字段中,本课没有再为每个字段拆一张表。需要搜索大量卡片时,可以再把常用字段拆出来。现在先让身份、关系和状态足够清楚。

这也意味着:数据库里保留了过去十次问答,不等于下一次请求应该把十次问答全部发送给模型。新建问答仍使用当前问题和选中的片段。保存历史和选择模型上下文,是两个不同的决定。 旧命令行聊天入口仍沿用原来的行为,本篇不会自动恢复它的消息列表。

二、SQLite怎样接进现有项目

SQLite把结构化数据保存在本地数据库文件中,不需要另外启动一个数据库服务器;Python提供sqlite3模块访问它。Python sqlite3文档

可以把“表”先理解成有固定列的记录集合。每一行代表一个对象,主键是这一行的唯一编号;外键描述它关联的另一行。例如,一条问答操作属于一个会话,一条复习计划关联一次问答操作。

本篇保留此前的读取、检索、调用和校验代码,主要新增storage.py,让网页通过它存取数据。inspect_db.py提供独立的只读检查。启动后会逐步出现这样的目录:

lesson-14/
├── web_app.py
├── storage.py
├── inspect_db.py
├── templates/web.html
├── sample-notes/
└── data/
    ├── assistant.sqlite3       会话、操作和计划
    ├── session.key             本应用签署会话Cookie的随机密钥
    └── operations/
        └── 某个操作编号/
            └── records/       本次操作的请求、响应、来源和校验JSON

data由程序创建,不在下载包里。正常的一次演示生成仍产生五份JSON,只是网页把它们放进对应操作的子目录,方便中断后定位。旧CLI入口的records、tool-records、plans路径不变。

数据库负责“应用现在如何找回和使用记录”;这些JSON负责“核对一次调用究竟发生了什么”。两者有重叠,是这份教学项目为了便于追踪做出的选择。现阶段不把JSON目录扫描当成自动恢复逻辑,也不自动导入第13篇旧记录。

三、先跑通“生成—计划—重启”这一圈

下载并解压到新的lesson-14目录,保留前一课的文件。从终端进入新目录,再执行以下命令。不要把数据库文件当文本文档打开修改。

macOS/Linux:

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
cp config.example.json config.json
.venv/bin/python web_app.py

Windows PowerShell:

py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item config.example.json config.json
.\.venv\Scripts\python.exe web_app.py

已有自己的config.json时保留它,不重复覆盖。示例配置不包含密钥。停止仍在运行的上篇服务,避免占用8765端口;确实需要换端口,可加--port 8766并使用对应浏览器地址。同一时间先只运行这一份教学网页。

打开http://127.0.0.1:8765。顶部应显示“离线演示 · 不请求模型”。保持默认两份笔记、问题“笔记中的RAG流程是什么?”、关键词“检索 片段”和最多2个片段:

  1. 点击“预览检索证据”,检查选中F1-C2与F1-C3。
  2. 点击“生成演示卡片(离线)”,应看到操作状态done,卡片校验结果validated_needs_review。
  3. 核对答案和引用,然后在“为第一张卡片安排复习”中填写标题、日期和分钟数。例如“复习RAG流程”、2026-10-05、20。这是练习日期,你可换成自己的安排。
  4. 点击“预览复习计划”。此时页面标题是“确认这份复习计划”,还没有把它标记为已确认。
  5. 看清标题、日期、时长与来源卡片,再点“确认保存计划”。页面应变成“计划已保存”。

当前问答任务只生成一张卡,因此本篇计划关联第1张卡。保存的是本机安排,不会创建日历事件,也不会到点弹出提醒。演示卡片的计划明确标记为“演示卡片”,不能被当作真实模型验证结果。

现在先把计划页面留着。在终端按Ctrl+C停止服务,然后用相同命令重新启动同一目录。回到原浏览器刷新计划页:标题、日期和分钟数应还在。也可以回到首页,从“本浏览器会话的历史”进入卡片,再点击“查看已有计划”。首页显示最近50次操作,较早记录不会因此被删除,原地址仍可读取。

本例允许格式正确的过去日期,因为它也可用于记录历史安排;没有实现“到期未完成”或提醒规则。

四、不要只相信页面显示“已保存”

在另一个终端进入同一lesson-14目录,执行只读检查:

.venv/bin/python inspect_db.py

Windows使用:

.\.venv\Scripts\python.exe inspect_db.py

如果是全新目录、仅用一个浏览器做了一次问答和一份计划,输出中的关键值应类似:

{
  "integrity": "ok",
  "foreign_key_errors": 0,
  "counts": {
    "sessions": 1,
    "operations": 1,
    "plans": 1
  }
}

实际输出还会列出操作和计划的编号、状态及计划内容。编号每次随机,不需要与你看到的示例一致。integrity检查数据库结构是否完整,foreign_key_errors检查关联是否存在;它们不检查模型答案是否正确。

在计划确认之前执行这个命令,也能看到一行计划,只是状态为draft。因为“把提议保存下来,等用户确认”本身就需要持久化。确认后同一行变成saved,不会额外插入第二行。

你可以刷新结果页和计划页,再运行检查。operations和plans数量应该不变。再停止、重启服务,数量仍不变。这个验证同时排除了“页面只显示缓存”“刷新又创建了一份计划”两类问题。

检查工具使用SQLite只读连接,数据库不存在会报错,不会偷偷创建空文件。它供在本机拥有文件权限的本人检查全部会话;网页的会话隔离不会限制你直接读取自己的磁盘文件。

五、为什么重启后还能认出这个浏览器

当浏览器第一次访问,服务器创建一个随机会话编号。后续请求通过Cookie带回这个身份,服务器才知道该查哪组记录。Flask默认会话Cookie经过签名,防止内容被任意修改,但内容并没有被加密。Flask会话说明

所以,本例Cookie只包含随机身份、表单校验标记和有效期相关信息,不包含笔记、卡片或DeepSeek密钥。会话签名密钥保存在data/session.key,首次启动时随机生成,后续启动读取同一份。若每次启动都换一份签名密钥,旧Cookie就无法继续被认可,重启恢复也就断了。

它与API密钥用途不同:session.key负责本应用的浏览器会话;DeepSeek API Key仍只在自行启动真实模式后,由终端隐藏输入并保留在后端进程内。

本例把Cookie有效期设为30天,访问时续期,并启用HttpOnly和SameSite=Strict。由于只在本机HTTP演示,没有开启要求HTTPS的Secure属性;这个配置不能直接当作公网配置使用。Flask配置文档

更关键的是,数据库查询必须同时检查记录编号和当前会话。storage.py里取问答操作的代码是:

    def operation(self, owner, ticket):
        with self.connection() as db:
            return self.unpack(db.execute("SELECT * FROM operations WHERE id=? AND owner=?", (ticket, owner)).fetchone())

id=?查的是哪个操作;owner=?检查它属于谁。即使有人把另一个会话的结果地址复制过来,也不能只凭编号读取。问答预览、生成、卡片结果、计划预览和确认都做了所属会话检查。这里的问号是SQL参数占位符,数据作为参数传入,不把浏览器文字拼进SQL语句。sqlite3参数绑定说明

你可以做一个可逆的小实验:保持原浏览器正常窗口打开,另开一个独立的隐私窗口访问首页。那里应没有原会话历史;粘贴原结果或计划地址,应得到404。回到原窗口,数据依然可以看到。不要通过清理原窗口Cookie做这个练习,否则会丢掉原会话的访问凭据。

这仍不是账号系统:同一浏览器配置通常共用会话;丢失Cookie或30天不访问导致过期后,没有用户名密码可帮助找回;其他能读本机数据、会话密钥或浏览器凭据的程序也不在隔离范围内。笔记目录仍由本人管理、所有会话共用,隔离的是工作记录,不是每个浏览器各自的文件仓库。Cookie也不按端口隔离,所以不要把多个使用同一Cookie名称的教学副本并排当成互不影响的应用。

六、给一次生成操作一个不会随重启消失的身份

第13篇用内存里的pop防止同一个预览重复发送。本篇把这件事变成数据库状态变化:

预览保存:ready
       ↓ 认领并提交数据库事务
执行中:running
       ↓ 发出一次模型请求或运行离线演示
流程结束:done(再查看卡片的校验结果)

异常被捕获:uncertain
进程突然退出:可能停留在running

每份预览生成一个随机操作编号。生成按钮只提交编号;服务器取回已经冻结的问题、片段与来源快照,不重新接受浏览器指定的答案或文件路径。修改磁盘笔记后,旧预览仍然对应旧内容;需要新内容时再建立预览。

真正发送前,应用必须先完成ready → running。关键代码如下:

    def claim(self, owner, ticket, mode):
        with self.connection(write=True) as db:
            changed = db.execute(
                "UPDATE operations SET state='running' WHERE id=? AND owner=? AND state='ready' AND mode=?",
                (ticket, owner, mode)).rowcount
        return changed == 1  # The transaction has committed before the caller sends anything.

这条更新只接受当前会话、当前运行模式、还处于ready状态的操作。数据库实际改动一行才算认领成功。两个相同提交先后到达时,后一个已经找不到ready行,因此不会再次发送。

connection(write=True)使用短事务:先BEGIN IMMEDIATE,写入成功就提交,异常则回滚,最后关闭连接。事务可以把一组本地数据库变化作为整体完成;本例还启用了外键检查。SQLite的写入需要协调锁,不能把它当作任意数量写请求都同时执行的存储。SQLite事务说明

注意事务在模型请求之前已经结束。不能一边等待模型几十秒,一边占着数据库写事务。模型返回后再用另一个短事务保存结果;这也意味着两次事务之间存在中断窗口。

同一编号再次提交时,本例会跳转回原结果页,不重做。重新预览则会创建新编号,即使问题文字一样,也属于新操作,仍可能再次计费。我们没有根据问题相似度做去重,更没有保证所有内容相同的请求只收费一次。

done只表示本次调用流程已结束并已存入数据库,并不等于卡片正确。仍要查看内部的validated_needs_review、request_failed等结果。校验失败的操作没有可安排复习的有效卡片。

七、最容易误解的边界:数据库不能替远端保证“恰好一次”

想象三个时间点:

  • 刚提交running,还没发请求,程序退出了。
  • 请求发出,服务端可能已经处理,但本地没收到响应。
  • 本地已经保存响应JSON,却没来得及把结果提交到数据库。

重启后,这三种情况都可能留下running。单看数据库,无法证明模型到底处理了没有,更无法证明是否已经计费。因此页面显示“执行中或结果未确认”,保留原操作编号,不自动把状态改回ready。

如果普通异常被捕获,状态可记录为uncertain;突然终止则可能停在running。两种状态都没有“自动补发”按钮。即使这个选择有时让一次根本没发出的请求也停住,它仍比不加判断地重复发送更容易核对。

遇到这种情况,先停止重复点击,找到data/operations/操作编号/records:查看有没有请求、attempt、响应和result记录;只读检查数据库中的状态。没有JSON也不能仅凭这一点判断远端绝对没处理。真实调用还应结合服务端可获得的请求记录或账单核对;不要因为本地错误就把费用记成零。

本课不会自动把残留JSON“导回”数据库,也不建议手工把状态改成ready。检查后若确实决定重新尝试,需要从首页明确新建操作,并接受可能重复调用的事实。SQLite的提交只能保护数据库自身的变化,不能把网络请求和磁盘JSON一起变成一个原子动作。

离线和真实模式也被写进操作记录。离线预览不能在重启为真实模式后突然变成一个付费请求,反过来也不行;模式不符返回409。已经完成的结果可以跨模式查看,并显示原操作的demo或live标记。API配置仍以本次启动配置为准,本例冻结的是资料和任务,不是重建一个历史版本的模型服务。

八、计划为什么要有第二次确认

第10篇展示了“模型提出工具调用—程序校验—用户确认—执行”。本篇复用其中的确认原则,但为了把持久化行为看清楚,网页计划由你填写,不再多发一次模型请求。旧planner.py仍保留原来的命令行流程和固定N1—N3白名单,没有偷偷放宽它。

网页现在使用动态资料,不能把本次的F1-C2直接当作所有记录都通用的计划来源。下一次换文件顺序,F1-C2可能就指向别的内容。因此计划保存的是操作编号+卡片序号,从对应操作再找到卡片、检索片段和来源快照。

“预览复习计划”会校验标题、日期和时长,再保存不可变的draft。确认按钮只提交计划编号,后端重新读取这份草稿并检查会话,然后把状态变成saved。它不会信任确认表单里另外混入的日期、时长或标题。

本课约定每次生成操作只有一份计划,数据库给operation_id加上唯一约束;重复提交建计划表单返回已有计划,重复确认只返回同一份已保存记录。标题或日期不会被旧表单悄悄覆盖。即使第一次保存成功、浏览器却没收到响应,再提交同一个计划编号也不会产生第二份。

这是本地数据库内可以实现的重复操作处理。将来如果动作变成“创建外部日历事件”,还要考虑对方接口的操作标识和结果核对,不能把本篇结论直接外推。

为保持本课范围清楚,尚未提供修改、删除或一张卡多份计划。填错草稿先不要确认;若要把它做成常用工具,下一步可加入显式编辑和版本校验,而不是让重复请求兼任“修改”。

九、数据该怎样保留,遇到问题怎样判断

本课仍默认离线。只有你自行停止服务后运行web_app.py --live,并在终端输入自己的Key,网页生成才会请求DeepSeek。请求继续使用deepseek-flash、关闭思考、非流式JSON,默认预算1024、一次尝试;所选笔记片段与问题会发送给API。DeepSeek接口文档

持久化后,资料不再随服务退出而消失。data里含来源全文、路径、卡片、计划和会话签名密钥,默认没有加密,也没有自动清理任务。不要把运行后的data打包进示例或公开仓库;.gitignore只是减少误提交,并不是访问控制。

做一次简单备份:先正常停止这一份服务,再通过文件管理器复制整个data目录到你控制的位置。保留同一时间点的数据库、操作记录与会话密钥,不只复制其中一个文件。若数据库仍有程序正在写入,应采用SQLite的备份机制,不能把任意时刻复制文件当作可靠热备份。SQLite备份说明

恢复到原项目时也先停止服务,保留现状备份再操作。即使数据完整,换电脑或清掉Cookie后也不会自动找回原浏览器身份。本课没有跨设备登录、会话找回或数据库版本迁移;不要把复制文件和账户迁移混为一谈。

现象 优先检查
重启后首页没历史,旧地址404 是否启动了同一目录?是否换浏览器、清了Cookie、Cookie过期或换了session.key?先用inspect_db检查记录还在不在
database is locked或网页503 是否另一个进程正在写同一数据库?停止多余服务和数据库编辑器,保留文件后检查;不要靠重复生成解决
页面仍为running或uncertain 查对应操作目录;可能已经发出请求,不自动重发
409 是否把demo预览拿到live进程使用?回到匹配模式或明确建立新预览
403 旧表单的会话标记可能失效,从当前首页重新进入;不要关闭表单校验
计划没有变成saved 查看是否只做了“预览”,还没按“确认保存计划”
再填不同日期仍显示旧计划 本课每次生成只允许一份不可变计划,重复提交不会覆盖
数据库或session.key损坏、版本不支持 保留原文件和备份,检查错误原因;程序不会自动删除旧库重建
inspect_db说数据库不存在 确认在lesson-14运行过网页,检查脚本所在目录的data;检查命令不会创建数据库

本篇的97项离线测试包括74项原有核心回归与23项持久化、网页和故障边界检查,覆盖重复认领、不同会话访问、事务回滚、保存失败、模式切换与重启恢复。浏览器也实际完成了计划确认和重启后读取。它们验证的是应用行为,不代表真实模型质量、线上计费或Windows实机验证。

你可以用三个问题检验自己的理解:清除Cookie后找不到历史,是否意味着数据库已删除?不一定,访问凭据与数据是两回事。计划草稿已写入数据库,是否表示用户确认了执行?没有,要看draft还是saved。数据库显示running,是否应该自动再发一次请求?不应该,它没有提供足够证据证明上次没执行。

现在的助手已经能把一次工作保留下来:浏览器找到自己的记录,卡片找到对应证据,计划找到来源卡片,中断操作也有可追查的编号。下一篇会整理配置、启动与交付方式,让别人拿到代码后也能正确运行,并明确本机工具与公开服务之间还有哪些差别。