青雲的博客

Article

一次 Orca 卡死反馈背后:真正要修的不是某个超时

· 17 分钟阅读

前言

这次修复来自一个很直接的用户反馈:

卡死了。

如果你还不了解 Orca,可以先看官网:orcaagent.dev。这篇不是功能介绍,而是一次真实问题从日志、定位、修复到验证的工程复盘。

如果只看这个描述,很容易把它当成一个普通 bug:是不是某个请求超时了?是不是某个线程没退出?是不是 TUI 没刷新?

但我把那份 session 日志拉出来看了一遍以后,越来越觉得事情没有那么简单。

它不是一个“某行代码写错了”的问题。

更像是一个长会话 Agent 系统必然会撞到的问题:上下文越来越重,工具结果越来越长,历史 reasoning 还在被反复带回模型,压缩又来得太晚。最后用户看到的不是一个明确错误,而是系统像是还活着,但就是不往前走。

这种状态最伤信任。

因为它既没有坦白失败,也没有继续交付。

那个 session 到底说明了什么

这次我没有先猜。

我先把用户给的详细 session 日志缩成一个可复现的回归 fixture,保留最能说明问题的几个特征:

{"type":"session.usage","input_tokens":877257,"output_tokens":20902,"cache_tokens":824576}
{"type":"conversation.message","message":{"role":"assistant","reasoning_content":"<synthetic long reasoning omitted>","tool_calls":[{"id":"call_read","function_name":"read_file","arguments":"{\"path\":\"lib/meta.ts\",\"offset\":175,\"limit\":70}"}]}}
{"type":"conversation.message","message":{"role":"tool","tool_call_id":"call_read","content":"<synthetic full-file output omitted>"}}

这几行其实已经很有信息量。

输入 tokens 已经接近 88 万。

缓存命中很多,说明不是“每一轮都完全重算”的问题。

但对一个 Coding Agent 来说,缓存命中并不等于体验稳定。因为用户感受到的是这一轮能不能继续推进、工具结果能不能被消化、模型是不是还在有效上下文里工作。

真正危险的是下面这几个东西叠在一起:

  • read_file 会把大文件完整读回来,缺少 offset / limit 这种基本的切片能力。
  • grep / glob 这类搜索工具没有默认分页,结果一多就会把上下文冲大。
  • 历史 assistant 的 raw reasoning_content 还可能进入后续 provider replay。
  • proactive compaction 绑在 DeepSeek 1M 上下文的硬上限附近,触发得太晚。
  • 用户界面没有足够早地告诉你:系统正在做 context compaction。

所以这不是“DeepSeek 不行”。

也不是“上下文 1M 就万事大吉”。

真正的问题是:Agent harness 没有足够早地管理上下文压力。

Orca 长会话被过多上下文、历史 reasoning 和工具输出压住

我先去看了 Codex 和 Claude Code 怎么处理

这一步挺关键。

很多时候我们修自己的系统,很容易陷在已有实现里。你会下意识问:“我现在这套结构里怎么补一个判断?”

但这次问题不该从某个判断开始。

它应该先问:成熟一点的 coding agent,会不会允许工具无限把上下文撑大?

答案基本是否定的。

Claude Code 的文件读取有 offsetlimit。这不是一个花哨功能,而是长任务里的基础设施。大文件不能每次都完整塞回模型,模型应该学会读范围。

搜索也一样。grep 结果应该有 head_limitoffset,默认只给前面一段,后续让模型自己翻页。

Codex 在上下文管理上还有另一个启发:不要只看模型最大窗口,也要看当前工作窗口的压力。真正影响体验的,不是“理论上还能塞多少”,而是“再塞下去这一轮会不会开始变慢、变贵、不可控”。

我最后没有照搬任何一套协议。

但提炼出来的原则很明确:

长会话稳定性不是靠一次大压缩解决的,而是靠每一层都少制造一点上下文债。

Agent harness 通过范围读取、分页搜索、过滤旧 reasoning 和提前压缩来控制上下文增长

第一刀:让工具别再默认吐太多

最直接的问题是工具。

以前 read_file 更像一个简单读文件工具。给一个 path,它返回内容。短文件没问题,长文件就很容易变成上下文炸弹。

这次我给它加了 ranged read:

{
  "path": "notes.txt",
  "offset": 2,
  "limit": 2
}

输出会变成带行号的片段:

2: two
3: three

如果 offset 超出文件长度,也不会假装读到了东西,而是明确返回:

[file has 1 lines; requested offset 5 is past end]

这个细节我挺在意。

因为 Agent 最怕的是模糊反馈。工具没有读到,就应该说没有读到。不要给模型一个含混结果,让它继续在错误上下文里推。

搜索工具也做了类似处理。

grep 默认只返回前 250 条,glob 默认只返回前 500 个路径。超过以后会提示下一次从哪个 offset 继续:

[Showing first 250 results; use offset=250 to continue]

这不是为了省一点 token。

这是为了把“读很多东西”变成一个可控动作。

模型可以继续翻页,但它不能在自己没意识到的情况下,把几百上千条结果直接倒进上下文。

第二刀:旧 reasoning 不应该一直跟着跑

另一个很隐蔽的问题是 reasoning_content

Orca 会把 assistant 的 reasoning 存在 transcript 里。这件事本身没问题。历史记录应该尽量完整,方便审计、恢复和调试。

但 transcript 里保存,不等于每次 provider replay 都要把旧 reasoning 再塞回模型。

这两个边界必须分清。

这次修复里,我保留了 transcript 里的 reasoning_content,但正常上下文预算和 DeepSeek replay 都不再把旧 raw reasoning 当成模型可见上下文的一部分。

这背后其实是一个很重要的 harness 原则:

可审计状态和模型可见状态不是一回事。

有些东西应该被记录下来。

但不应该每一轮都重新喂给模型。

如果不分这两层,长会话会越来越重,而且你还很难从表面看出来,因为那些 token 不是用户刚刚输入的,也不是工具刚刚返回的,而是历史里悄悄带回来的。

第三刀:压缩不能等到快撞墙

DeepSeek 有 1M 上下文,这当然是优势。

但这次问题也说明了一件事:大上下文不是不做上下文管理的理由。

如果等到 80% 的 1M 窗口才开始压缩,很多用户体感上的“卡住”早就发生了。

所以这次我加了 soft context pressure。

它不再只问:

有没有超过模型硬上限?

而是先问:

有没有超过当前会话应该保持顺滑的软窗口?

超过软窗口,就走现有 summary compaction 路径,提前把历史压掉。

这里我没有另起一套复杂的新压缩算法。

我更想先把触发时机做对。

因为系统里已经有 summary compaction,真正缺的是“别太晚”。如果一上来就发明第二套压缩协议,很容易把问题变成另一个复杂系统。

这也是我现在做 Orca 越来越克制的一点:

能用已有链路解决的问题,不要为了看起来高级再造一条链路。

第四刀:工具结果先做 micro-compaction

summary compaction 是比较重的动作。

但很多长会话里,真正占上下文的是工具结果。

比如连续读文件、grep、glob、bash 输出、web search 结果、MCP 工具返回。它们一开始有用,但过了几轮以后,很多旧结果只需要保留头尾和摘要标记。

所以这次在 summary compaction 前面,又加了一层 tool output micro-compaction。

它不是总结整段对话。

它只是先把老工具结果压成类似这样的结构:

[tool output micro-compact]
original_bytes: ...
head:
...

tail:
...

这个动作很朴素,但很实用。

因为它保留了可追溯性:你知道这里原来有一大段工具输出,也知道它的大概头尾是什么。

同时它不再允许一个很早之前的工具结果,在后面几十轮里一直完整占着上下文。

第五刀:用户要知道系统正在压缩

卡死最糟糕的地方,不只是慢。

而是用户不知道它在干什么。

所以这次 TUI 也补了一个状态:当 soft compaction 开始时,界面会进入 Compacting context...

这不是一个大功能,但它对信任很重要。

Agent 系统里很多等待都可以被接受,前提是用户知道系统正在做一件合理的事。

如果什么都不显示,用户只会觉得它挂了。

这也是我最近反复写的观点:Harness 不只是权限、工具和沙箱,也包括状态表达。

一个用户看不到内部状态的系统,很难长期被信任。

顺手把 approval 也改顺了一点

修这轮问题时,还有一个很小但真实的交互改动。

approval 弹窗原来主要靠 y / a / A / n 这种快捷键。

老用户习惯了可以继续用,但对新用户不够直观。所以我把它改成了更明确的数字序列:

1 -> allow this once
2 -> always allow this exact call
3 -> always allow 当前工具
4 -> deny

旧快捷键仍然保留:y / A / a / n

这件事跟卡死不是同一个根因。

但它属于同一种产品判断:Agent 在等待人类决策时,交互要尽量低摩擦。尤其是长任务里,用户已经在等了,不应该再让他猜一个大小写敏感的快捷键。

最后真正让我满意的是测试

这次修复我最看重的不是改了多少代码。

而是它最后被压进了一组回归测试。

我把用户反馈里的关键形态抽成了 session_stuck_2026_07_08.min.jsonl,然后加了 reported_session_triggers_soft_compaction

这个测试不关心“当时那台机器为什么慢”。

它关心更稳定的事实:

  • 一个接近 88 万 input tokens 的长会话,应该触发 soft compaction。
  • read_file 要尊重 offset / limit
  • stale reasoning 不应该再进入 provider replay。
  • 工具输出增长应该被 micro-compaction 控住。

除此之外,我还修了几条原本容易挂住的 server / shell 测试。

其中有几条测试本身有一个很典型的问题:先等 streaming output,再写 stdin 或 terminate。但实际系统里 output delta 可能要到下一次读/写/终止时才 flush。测试就把自己挂住了。

这类测试很容易误导人。

你以为是实现卡了,其实是测试在等待一个不稳定的时序假设。

最后我把它们改成 marker 驱动:先让子进程明确写一个 marker 文件,证明自己已经进入目标状态,再继续后续动作。

这和修 Agent 本身是一回事:

不要靠猜时间,靠证据推进。

从卡死反馈到 session log、修复、测试和 build check 的证据闭环

验证过程也没有省

最后这次合并前,我跑的是完整验证:

cargo test --workspace -- --test-threads=1
cargo fmt --check
git diff --check
cargo check --workspace

中间完整 workspace test 不是一次就绿。

先是 workflow_types_contract 暴露了 BackgroundTaskSummary 新增字段后,测试 fixture 没同步。

然后是 shell_session_kill_stops_running_task_and_collects_partial_output 暴露了 sleep 150ms 这种时序假设不稳。

再然后是 session_server_contract 里几条 command/exec streaming 测试会卡住,因为测试在等一个不保证立即出现的 streaming delta。

这些都不是主线功能,但它们很有价值。

因为一个 harness 如果连自己的测试都靠运气推进,那它就很难证明自己能托住 Agent。

最后修完以后,完整 workspace test 通过,提交到了 main

2e7b5c3a fix: stabilize context handling and approval UX

这次我真正学到的

这次修复以后,我对 Orca 的 context engineering 有一个更明确的判断:

上下文稳定性不是一个 compaction 功能,而是一整条链路。

它从工具 schema 开始。

工具能不能分页,能不能范围读取,能不能明确告诉模型结果被截断了。

它经过 provider replay。

哪些历史应该保留给审计,哪些历史应该继续喂给模型。

它经过 runtime pressure。

什么时候该提前压缩,什么时候只是正常增长。

它也经过 TUI。

用户能不能看到系统正在压缩,而不是误以为卡死。

这几层缺一层,长会话都会慢慢积累债务。

模型越强、上下文越大,这个问题反而越容易被掩盖。因为系统会撑得更久,看起来更能扛,直到某个真实用户任务里突然表现成“卡死”。

所以这次我最后修的,其实不是一个超时。

我修的是 Orca 对长任务的自我约束能力。

这也是我越来越在意 harness 的原因。

Agent 真正走向可用,不是因为它永远不卡。

而是因为它知道自己什么时候快撑不住,知道该提前收束,知道该把状态告诉用户,也知道最后要把修复变成测试。

这件事不酷。

但很关键。

Keep Reading

相关文章

评论