结论:核心架构值得保留,但确实存在需要修复的边界 BUG,以及两项成本较低的性能优化。 最优先的是 handoff 的断点恢复,其次是摘要调用的预算与用量统计。

本次只分析,没有修改代码。检查了 contextruntime,并沿调用链阅读了 transcript、checkpoint、session、hooks、tools、provider 接口等相关实现。

验证结果:

一、确认存在的问题

1. [P1] 中断后恢复,可能丢失已经触发的 handoff

涉及 [tool_calls.py (line 352)](/Users/neo/repo/lovia/lovia/runtime/tool_calls.py:352)、[checkpoint.py (line 147)](/Users/neo/repo/lovia/lovia/runtime/checkpoint.py:147)、[loop.py (line 1127)](/Users/neo/repo/lovia/lovia/runtime/loop.py:1127)。

当前转交分为两个时刻:工具先写入 pending_handoff 和“Transferred to b”结果,随后 loop 才切换 active agent。但 checkpoint 只记录当前 agent,没有记录 pending handoff。

我复现的顺序是:

同一轮请求:transfer_to_b、另一个工具
→ 转交工具成功,结果进入 transcript
→ 后续工具执行前收到取消
→ checkpoint:agent=a,结果却写着 Transferred to b
→ resume:转交工具已有结果,不会重跑
→ 最终仍由 a 回答,b 的 provider 调用次数为 0

这不是普通的“工具可能重复执行”,而是持久化的对话和实际控制状态不一致。取消、预算中断,以及两次状态保存之间的进程退出都值得覆盖。

最小修复方向:持久化“待应用的转交目标/阶段”,恢复时继续完成转交,并避免重复执行 on_handoff。不需要引入通用工作流引擎。

2. [P2] 摘要分块没有按完整摘要请求计算预算

涉及 [stages.py (line 416)](/Users/neo/repo/lovia/lovia/context/stages.py:416)、[summarizer.py (line 106)](/Users/neo/repo/lovia/lovia/context/summarizer.py:106)。

目前把新增历史块限制到 usable window 的一半,但实际请求还包括:

这些内容没有从 chunk budget 中扣除。max_summary_chars=16_000 是字符上限,也不能保证适合较小窗口。

最小探针中,摘要模型窗口为 4,096,旧摘要为 15,000 字符,加入一个合法大小的新增块后,实际拼出的请求估算达到 5,133 tokens,摘要失败。

此外,单条超大历史或不可拆分的工具配对会直接作为 oversized chunk 发出,所以“分块保证摘要调用不会 overflow”的注释并不成立。

建议:按最终摘要请求扣除固定内容、旧摘要及输出预留,再决定新增块大小;对单条超大内容提供明确的降级路径。无需换掉现有 Stage 接口。

3. [P2] 摘要调用的 token usage 完全没有进入运行总账

涉及 [summarizer.py (line 143)](/Users/neo/repo/lovia/lovia/context/summarizer.py:143)、[loop.py (line 1244)](/Users/neo/repo/lovia/lovia/runtime/loop.py:1244)。

LLMSummarizer 直接调用 provider.stream(),只消费文本和结束原因,忽略 UsageDelta。runtime 则只累计主模型轮次的 usage。

实测:

2 次摘要调用 + 1 次回答调用
provider 共报告:3,000 tokens
RunResult.usage:1,000 tokens

影响不只是统计展示:累计 token budget、checkpoint usage,以及向父运行汇总的支出都遗漏摘要成本。长会话中,多块折叠和章节纠正重试会放大偏差。

建议:给摘要支出一个轻量的上报通道,并入总 usage,同时在连续折叠之间检查预算。不要把摘要的 input tokens 写入主对话的 last_input_tokens,否则会污染上下文估算校准。

4. [P2] 初始化失败时,流缺少承诺的终止事件

涉及 [loop.py (line 300)](/Users/neo/repo/lovia/lovia/runtime/loop.py:300)、[result.py (line 142)](/Users/neo/repo/lovia/lovia/runtime/result.py:142)。

RunHandle 文档承诺每个流以 RunCompletedRunFailed 结束,但 bootstrap 在 loop 的主要异常处理范围之外。初始化抛错后,handle 捕获并保存异常,却没有生成 RunFailed

用一个 setup() 抛出 ValueError 的插件复现:

async for 收到的事件:[]
await handle.result():抛出 ValueError

只依赖终止事件更新状态的 UI、日志收集器和调用方可能漏掉失败。调用 result() 的代码能知道错误,但这与流协议承诺不一致。

建议:在统一出口补齐尚未发送的失败终止事件,确保不重复发送;不需要改变普通运行失败的语义。

5. [P2,历史被裁剪/改写时] 结构指纹可能错误复用旧摘要

涉及 [state.py (line 278)](/Users/neo/repo/lovia/lovia/context/state.py:278)、[compaction.py (line 183)](/Users/neo/repo/lovia/lovia/context/compaction.py:183)。

所谓 fingerprint 主要记录类型、角色、内容长度和 call ID,没有校验文本内容。因此,它只能检测一部分历史变化。

实测把相同长度的:

use red / yes red

替换为:

use tan / yes tan

仍然通过校验。新 view 保留“User chose red”的旧摘要,并隐藏新的原文。

正常 append-only 使用不会主动触发这一问题;但代码和文档明确声称支持检测历史裁剪、改写,而裁剪后的前缀也可能恰好具有相同结构。

建议使用缓存的内容摘要或可信的历史版本标识。不建议简单地每轮重新哈希全部大文本,否则会把正确性修复变成性能问题。

二、值得做的性能优化

1. 优先修复 TokenCounter 的缓存容量断崖

位置:[tokens.py (line 232)](/Users/neo/repo/lovia/lovia/context/tokens.py:232)。

缓存最多 8,192 项,满后按插入顺序淘汰;而每次 count 从头扫描整个 transcript。历史略超过容量时,前部的 miss 会淘汰稍后就要访问的条目,形成连续失效。

本机微基准,每条文本 4,000 字符,预热后重复计数:

历史条目5 次扫描重新估算次数每次平均耗时
8,00000.71 ms
8,20041,00017.79 ms

仅增加 2.5% 条目,估算耗时约增加 25 倍。这是局部计数测试,不是端到端运行提速预测;真实 tokenizer 较昂贵时影响可能更大。

建议先修缓存准入/淘汰策略,避免顺序扫描污染整份缓存,再考虑按条目生命周期缓存。单纯扩大上限只会推迟断崖,普通 LRU 也不能自动解决循环扫描问题。

另外,注释中的“每轮 O(new entries)”不准确:缓存命中只减少文本估算,count() 本身仍然遍历全部条目。

2. checkpoint 直接切新增尾部,避免先复制整段运行历史

位置:[checkpoint.py (line 145)](/Users/neo/repo/lovia/lovia/runtime/checkpoint.py:145)。

当前相当于:

run_entries = transcript[run_start:]
delta = run_entries[persisted:]

每次保存先复制整段 run,再取得很小的增量。工具结果逐个保存时,会反复复制不断增长的列表。

可以直接从 run_start + persisted 切尾部,再单独计算长度。改动小、语义清楚,对长工具运行有益。这里复制的是引用,不应夸大成反复复制完整工具输出。

我暂时不建议引入增量 transcript 树、统一缓存层或复杂的预算调度器。这两项局部优化更符合 lovia 的定位。

三、DX 欠佳的地方

四、对核心设计的判断

做得好的部分包括:compaction 只改变 provider view、canonical transcript 与 Message 分离、串行 preflight 配合并行工具执行、工具配对保护,以及只在实际缩小上下文后进行 overflow 重试。这些设计相互配合,建议继续保留。

目前最大的薄弱点是:对话条目的持久化比控制状态更完整;主模型调用的管理比摘要调用更完整。 优先补齐这两个边界,比继续拆分模块或增加抽象更有收益。

建议实施顺序为:handoff 恢复 → 摘要预算与 usage → 流终止事件 → 缓存断崖 → 指纹与文档/DX 修正