工作中的很多事项,最初都只是一条聊天消息。
有人报告问题,有人补充截图,过一会儿又有人说正在处理。最后的处理结论,可能藏在很后面的某条回复里。想知道这件事现在是什么状态、还缺什么、下一步该做什么,往往得重新翻一遍聊天记录。
我做拾事(ShiShi),就是希望把这段工作交给 AI:从消息中发现事项,持续更新状态,保留判断依据,在需要时由用户发起调查和处置,再把有用的经验留给后续分析。
最早的框架已经有了增量分析、事项摘要、按需补查和持续处置。后面的迭代,主要在处理调用成本和上下文:减少没必要的分析,缩小每轮输入,尽量复用相同前缀,也让新会话能接着上一轮的结果工作。
下面从一条新消息进入系统说起。
技术栈
拾事是一款 Windows 桌面应用,核心使用 Rust,本地界面使用 Web 技术,无需另外部署常驻 Web 后端。
| 技术 | 职责 |
|---|---|
| Rust | 状态管理、文件读写、后台调度、子进程和 AI 协议接入 |
| tao、wry / WebView2 | 桌面窗口和本地页面渲染 |
| HTML、CSS、JavaScript | 界面交互,通过 IPC 与 Rust 交换结构化数据 |
| serde / serde_json、JSON / JSONL | 业务状态序列化与本地检索快照 |
| DLL、C ABI、JSON | 扩展消息来源插件 |
| Codex CLI、DSH / ACP、Chat Completions 兼容 API | 接入不同 AI 后端 |
应用负责组织上下文和校验结果,接入层负责各后端的协议差异。
消息过滤
如果每来一条新消息,都让能力较强的模型重新整理事项,调用次数和输入开销很快就会上去。聊天里有不少“收到”“好的”,也有暂时不影响事项状态的补充,并不需要立即分析。
只按较长的周期定时分析,重要消息又可能等太久。我在完整分析前加了一层轻量过滤:有待处理的新内容时,默认每半小时做一次完整分析;新消息到来后,先由低成本模型判断,要不要提前分析。
过滤模型只需要给出一个很小的决策:
- 等待:消息继续留在待分析队列,由常规调度兜底。
- 提前分析:提前唤起分析模型,更新事项。
- 不确定:交给完整分析判断;过滤调用失败时也走这条路径。
实现时会合并密集到达的消息,按批过滤,并设置最小调用间隔。过滤与完整分析可以分别配置模型:轻量模型负责高频判断,能力较强的模型负责整理事项。
过滤只决定什么时候分析,不会删除消息,也不会把消息标记为已分析。即使判断为等待,定时分析仍会处理积累下来的有效增量。
普通消息可以攒到一起处理,需要关注的变化则提前更新事项。这能否节省成本,还要看消息量、过滤调用的开销和提前触发的比例,所以过滤本身的用量也要单独记录。
增量处理
消息来源会被反复扫描,同一条消息可能被编辑、撤回,也可能只改了显示名称。增量处理要先分清两件事:这是不是同一条消息,以及它的内容有没有变化。
内容指纹
拾事用稳定 ID 区分不同消息,用内容指纹判断同一条消息有没有发生实质变化。当前参与变化判断的是正文和撤回状态。来源名称、群名、发送者显示名称或本地缓存路径发生变化,不应该让原本已经分析过的消息重新排队。
这里的“指纹”是内容的规范表示,用于比较版本,不要求一定使用哈希。要先确定的是比较哪些字段:哪些变化会影响 AI 判断,哪些只影响界面显示。
版本值与进度游标
旧消息被编辑时,原始发送时间可能没有变化,所以处理进度使用一个以毫秒时间为基准、严格递增的本地版本值。每次写入新增或内容发生变化的消息时,按下面的规则分配:
本次版本值 = max(当前毫秒时间, 上一次版本值 + 1)
假设三条消息依次写入时,系统时钟都还是 1000,分配的版本值可以是 1000、1001、1002。这里记录的是程序写入状态的顺序,不代表消息在外部系统中的真实到达先后;消息原始发送时间仍单独保留。
已有消息只有显示字段变化时,版本值保持不变。
过滤和完整分析分别维护自己的进度。游标记录已经确认到的版本位置,另外用一张按消息 ID 保存的待处理例外表,记录游标之前仍未处理的消息及其内容指纹。
例如,A、B、C 的版本依次是 101、102、103。A 暂时不在本轮回看范围内,B、C 已经分析完成,游标可以推进到 103,同时将 A 记为待处理例外。
后续扩大回看范围时,系统仍会把 A 找出来,不会只因为它的版本小于游标就判定已经处理。这种被游标越过、但仍未完成的记录,就是这里所说的“空洞”。
分析期间如果又有消息编辑,返回结果只能确认本次真正读取的旧版本。新版本仍然需要处理。模型调用失败、结果校验失败或保存失败,也不能消耗对应的分析进度。
前缀缓存
拾事每轮消息分析都创建新会话,再把事项、记忆和检索入口组织成输入。每轮带多少背景,由当前任务决定,避免对话越积越长。
KV Cache 原理
保持前缀一致,是为了利用大模型推理中的 KV Cache。采用因果注意力的自回归模型,在计算某个位置时只关注当前位置及之前的 token,不会读取后面的 token。因此,后续输入变化不会改变前面相同 token 已经计算出的 Key、Value;这些中间结果可以缓存复用。参考:Hugging Face 的 KV Cache 原理说明
如果推理服务支持跨请求的前缀缓存,同一模型下的新请求就有机会复用已缓存的相同前缀,减少输入预填充(prefill)的重复计算,缩短开始输出前的等待。新增输入和输出仍然需要计算,所以它的效果需要通过实际缓存用量和时延观察。
输入结构
为了提高推理服务复用前缀计算的机会,我把输入组织成固定的八段:
系统提示词
已归档记忆
目的与格式
标签
事项摘要
临时记忆
增量消息
运行上下文
变化少的内容放前面,变化频繁的内容放后面。当前时间、工作状态、触发方式和数据截止时间统一放在最后,避免这些每轮都变的字段过早打断公共前缀。
输入结构本身也需要稳定:
- 某一段没有内容时,保留固定占位。
- JSON 对象的键递归排序,数组保留原有顺序。
- 比较同类请求时,同时关注输出 schema 和工具定义。
归档记忆通常比临时记忆稳定,所以放得更靠前。记忆更新后,公共前缀也会变化;该更新的内容仍然要更新,不能为了命中缓存而保留已经失效的信息。
稳定前缀能够创造缓存复用的条件,实际命中仍由后端的缓存策略决定。例如 DeepSeek 的上下文缓存有自己的前缀持久化与匹配规则,也采用尽力命中的机制。因此,前缀一致性和实际缓存命中需要分别观察。参考:DeepSeek 上下文缓存说明
摘要与压缩
一轮增量分析除了新消息,还需要知道已有事项,才能判断应该创建新事项,还是更新某个正在跟进的事项。但把所有事项的完整描述和证据都放进首轮输入,开销会随着历史不断增加。
事项摘要
拾事先给模型一份紧凑的事项列表,包括标题、摘要、状态、截止时间、阻塞项和下一步等字段。模型据此识别可能相关的事项,需要更多信息时再按编号查询完整内容和消息依据。
事项同时保存内容更新时间和摘要生成时间。相关内容变了,旧摘要就会失效,程序暂时从当前内容中截取一段作为替代,直到生成新摘要。这样,后续分析仍有可用的内容索引。
长消息和长字段在首轮输入中会被截取,同时附上原始长度、截断标记和稳定编号。模型能知道自己只读到了部分内容,需要时继续查全文。原始内容仍然保留。
结构压缩
在语义摘要之外,还有一层结构压缩:
- 本次请求中的长 ID 映射为短编号。
- 消息使用固定列数组,会话元数据集中存储。
- 模型修改事项时,只返回发生变化的字段。
结果返回后,程序先恢复真实编号,再检查证据和业务动作。
这些处理可以省去重复字段和首轮暂时用不到的细节。短编号也需要额外说明,所以只有转换后的公开载荷字符数确实更少,才会采用。应用侧用字符数控制预算、比较输入大小,实际 token 数仍以供应商返回的用量为准。
快照检索
首轮输入省下的细节,模型需要时必须能查回来。
拾事从权威状态生成三类只读 JSONL 快照:
| 快照 | 内容 |
|---|---|
| 消息 | 消息原文与图片索引 |
| 未归档事项 | 完整事项信息和消息依据 |
| 事项更新流水 | 业务字段的完整前后变化 |
快照带有 schema 版本、生成时间、数据截止、来源修订信息和校验信息,可以从源状态重建。更新流水中的长备注、描述和证据也能够分页补查。
分析会话可以通过只读工具,按 ID、关键词、记录类型、来源、会话和时间范围查询,也能分页读取长文本。模型先看摘要,再决定哪些判断需要原文支持,查到依据后继续分析。
这是一种由 Agent 驱动的检索增强。当前检索以 ID、关键词和结构化范围为基础,模型负责决定需要什么,程序负责查询权限、返回规模和数据一致性。
检索也有自己的预算。单页响应受到限制,一次请求累计交付的字符数也受到限制,重复页和已读区域不会重复交付。预算耗尽时,程序明确说明覆盖不完整,让后续校验能够判断哪些工作尚未完成。
MCP 上下文接口
当用户决定调查或处理一个事项时,首条输入只需要交代事项标题和调查要求,详细背景可以通过上下文接口读取。
拾事为处置提供受事项范围限制的 MCP 工具,允许 AI 查询关联历史消息和图片。查询需要对应的事项凭证,服务端继续检查来源、会话、时间范围、授权有效期和返回限制。
用户把处置交给外部客户端后,原会话仍能查询事项背景,不必反复复制完整聊天记录。消息采集、事项整理和执行会话可以各自工作,需要时查阅同一批依据。
消息内容始终作为资料处理。是否发起处置,以及执行过程中的权限审批,由用户决定。
分层记忆
每轮分析都重新开始,有些信息却需要接着用:上一轮留下了什么线索,用户对某类事项有什么要求,之前的判断哪里需要修正。拾事用临时记忆和归档记忆保存这些信息,需要核对原始事实时再查历史。
| 记忆 | 维护主体 | 保存内容 | 输入位置 |
|---|---|---|---|
| 临时记忆 | 消息分析 | 当前线索、注意事项、跨轮交接信息 | 较靠后 |
| 归档记忆 | 归档流程 | 从用户归档备注中整理出的长期背景和处理经验 | 较靠前 |
临时记忆像一份跨轮使用的工作便条。模型可以留下当前值得注意的线索、尚未确认的问题和下一轮需要接着看的内容。它有长度限制,具体保留什么,由消息分析根据后续需要决定。
归档记忆保存通常更稳定的背景和经验。它怎样形成,和用户平时怎样修正、处理事项有关。
用户反馈与记忆更新
用户修改标题、合并重复事项、撤销一次误操作,或者把事项标为“无需处理”,都会留下带有修改前后值的事项变更记录。在归档窗口填写的备注,也会随这次操作保存下来。
归档时,程序从上次成功处理的游标继续收集未处理的变更记录,连同相关事项摘要、已有归档记忆和临时记忆交给模型分析。收集范围按处理进度计算,也可能包含之前留下的记录。
操作记录能说明用户做过什么,却未必能解释原因。例如,一条提醒被标为“无需处理”,可能是已经在线下办完,也可能是用户认为这类内容本来就不该生成事项。仅凭这一次操作,很难确定以后的处理方式。
所以,当前实现把归档记忆的写入依据限制为用户填写的归档备注。其他变更记录和事项摘要帮助模型理解经过,备注则表达用户希望保留的经验。没有可用备注时,归档仍可生成汇总,但不能修改归档记忆。
比如,用户在备注里写“这类通知以后只需汇总,不必逐条生成事项”,就给出了明确的适用范围,可以作为长期经验的来源。如果说明“这次临时不用处理”,则只随对应事项留存,不推广到其他事项。
归档会把可复用的说明整理成简短记忆,保留来源和版本;用户后来明确修正或撤销旧要求时,也可以更新或移除对应内容。这样既不用每轮重读全部操作记录,也能让后续分析参考用户已经解释过的职责、关注范围和判断偏好。
这里的“学习”发生在上下文中:把用户明确表达的经验带进之后的请求,让新会话也有这些背景。模型参数不变,归档记忆也不会自动改写系统提示词、启用行为规则或修改标签目录。行为规则建议需要用户预览后手动采用。
消息分析维护临时记忆,归档维护归档记忆,用户可以查看和编辑两者。归档只读临时记忆,不会在归档后将它清空。
记忆写入校验
以临时记忆为例,模型如果要提交一份替换后的全文,必须先完整获得旧版本,保存时还会进行并发校验。
旧记忆已经完整进入本次最终请求时,程序记录它已被交付,不要求再调用工具重复读取。
如果旧记忆被省略或截断,模型要更新它,就需要主动调用只读的 read_context 工具,按记忆编号查询,并在需要时分页读取全文。
Codex 接入中,这个工具通过 MCP 提供;兼容 API 接入通过工具调用提供同样的查询能力。模型决定是否查询或更新,程序检查完整旧版本是否确实交付。没有读全就提交覆盖,会被拒绝,旧记忆继续保留。
请求预算与结果校验
压缩之后,还得确保测量输入大小时用的内容,就是最终发给模型的内容。
拾事用一个 prepared 请求对象,固定本次调用的配置、标签目录、记忆、证据视图、运行时间、编码后的载荷、输出 schema 和工具定义。预算检查、诊断和实际发送都用同一个对象,避免测量后又追加信息或重新取时间,导致测量与实际输入不一致。
预算计算包含提示词、输出格式和工具定义在内的完整请求。确实超限时,才缩减可补查的背景或分批处理。归档也按完整请求的实际字符预算装批,一轮归档开始后就固定材料范围,新到的材料留到后续处理。
模型返回结果后,程序检查结构、字段、证据编号和事项版本。结果只能引用首轮输入或工具查询中实际交付过的证据;保存前再核对当前状态,防止旧结果覆盖调用期间发生的新修改。
业务结果和进度一起保存。归档按批校验和提交,后面的批次失败,也会保留前面已提交的结果和剩余材料。执行请求可能已经发出、结果却不确定时,不会直接自动重发,以免重复执行。
诊断与验证
为了判断这些调整有没有效果,我在调用记录里加入了同类请求比较:公共前缀有多长,哪些部分变了,输入有多大,供应商是否返回了缓存用量。
缓存用量区分三种状态:
- 未报告:供应商没有返回对应计数。
- 零命中:供应商明确返回缓存用量为零。
- 实际命中:供应商返回了命中的缓存用量。
应用侧的长度和指纹能帮助定位前缀变化,供应商返回的用量才反映实际缓存情况。消息过滤、完整分析、报表和归档的调用也分别统计,便于观察每个环节的成本。
验证则主要使用合成数据和模拟协议,覆盖正文编辑、元数据变化、超时、并发修改、记忆截断和跨批失败等情况。流式进度可以及时展示,但正式业务结果仍要等完整校验后才提交。
接下来值得持续记录的指标,是过滤的提前触发率、完整分析的输入规模、缓存命中用量,以及消息到事项更新的实际时延。有了这些数据,才能继续调整模型分工和调度参数。
项目源码:拾事 · ShiShi。
评论
欢迎交流。评论通过 GitHub 账号登录,保存在本博客的 Discussions 中。