OpenHands 对话被 LLM 拒收?上下文窗口压缩完整指南
【免费下载链接】OpenHands🙌 OpenHands: AI-Driven Development项目地址: https://gitcode.com/GitHub_Trending/ope/OpenHands
OpenHands是一个 AI 驱动的编程助手项目,它能替你跑终端、改代码、查文件。但很多人和它"聊"到一半,会突然收到一条来自 LLM 的报错,任务戛然而止。这篇文章带你从一条真实报错出发,搞懂 OpenHands 是如何管理模型上下文窗口的,以及你自己可以动手调哪些参数,让长对话不再中途崩掉。
一次卡壳的现场
先看看你会撞上的东西。连续让 Agent 分析几个大文件、再追问了几轮之后,回复框里弹出一句:
input length and `max_tokens` exceed context limit再往下翻会话日志,可能还会看到另一条:
Conversation history longer than LLM context window limit翻译成人话:你和它积累的所有对话 + 它准备生成的回复,加起来已经塞不进模型的"工作记忆"了。任务没做完,会话直接废掉,这就是上下文超限(context limit)最典型的翻车现场。
为什么会撞墙:一个类比说清楚
把上下文窗口想象成 Agent 的工作记忆白板:每轮你说的话、它的回答、读过的代码,都要抄写在上面;模型每次思考前,得把整块白板重新读一遍。白板面积是固定的(不同模型不同,Claude 3.7 这类模型有明确的 token 上限),一旦抄写内容超过板面,模型就只能拒绝开工。
关键点在于:白板只增不减——你不手动清,它不会自己擦。所以长对话必然逼近上限,除非项目帮你自动"擦旧留新"。OpenHands 做的正是这件事。
开箱即用:它默认就带了一套兜底
好消息是,你什么都不改也能跑:OpenHands 默认内置了对话历史压缩能力,业内一般叫condenser(压缩器)。它的思路很朴素——白板写满之前,自动把早期轮次的内容压缩成摘要,把最新几轮完整保留下来,这样模型读到的上下文长度始终可控。
这套默认行为在前后端都有体现:
- 默认设置里,压缩是开启的,并带一个默认保留上限:
condenser: { enabled: true, max_size: 240, // 压缩时保留的对话轮次上限 }这段默认值定义在 src/services/settings.ts 中。
- 会话界面里有一条上下文水位线,实时显示"当前已用 token / 模型窗口大小"。水位超过70%变黄、超过90%变红,变红时界面上会浮现"压缩一下"的入口,一键手动触发 condenser。这套阈值逻辑在 src/components/features/conversation/usage-panel/context-meter.tsx。
- 手动压缩走的是同一个服务:点击"压缩"后,前端调用 use-compact-context-action.ts 里的动作,把当前历史交给 condenser 摘要,压缩完水位线肉眼可见地回落。
换句话说:默认状态下,OpenHands 已经替你接住了大部分上下文超限场景,你只需要知道它在哪、什么时候该看它一眼。
手把手配置:改哪几个参数
想按自己的节奏调,只需要动两三个字段,全部写进设置里的agent_settings段(Web 端在 Settings → Condenser 页面,源码入口是 src/routes/condenser-settings.tsx;配置文件对应config.template.toml的[agent]段):
| 参数 | 作用 | 建议 |
|---|---|---|
enable_history_truncation | 超限后是否自动修剪历史,而不是直接抛错终止 | 保持true |
condenser.enabled | 是否启用压缩器 | 长对话场景保持true |
condenser.max_size | 压缩后保留多少轮原始对话 | 默认 240;窗口小的模型调低 |
最小可运行示例(TOML):
[agent] enable_history_truncation = true condenser = "NoOpCondenserConfig" # 默认压缩器 [agent.condenser] enabled = true max_size = 240改完重启会话即可生效。如果你用 OpenRouter 这类聚合渠道,某些模型不上报窗口大小,界面上只会显示裸 token 数而没有百分比——这是已知现象,不影响压缩本身,只是水位线显示退化为纯数字。
进阶:写一个自己的压缩器
默认的压缩策略是"保最近、缩历史",但如果你有特殊需求——比如希望代码块优先保留原文、只压缩闲聊——OpenHands 允许你挂自定义 condenser。接口很简单:继承抽象基类,实现一个compress方法,接收历史、返回压缩后的历史。
from openhands.memory.condenser.abstract_condenser import AbstractCondenser class CodeFirstCondenser(AbstractCondenser): def compress(self, history): # 自己定义规则:代码片段原样保留,自然语言摘要 return self._keep_code_compress_chat(history)然后把配置里的condenser指向这个类名即可。压缩器的扩展点集中在 openhands/memory/condenser/ 目录,写之前先翻一遍abstract_condenser.py里的接口约定。
怎么确认优化生效:看这几个指标
调完参数别凭感觉,盯三个数就够了:
- 水位线颜色:压缩前后,上下文 meter 的百分比应明显回落;如果压缩后还在 90% 红线附近,说明
max_size太大,往下调。 - 压缩触发频率:长任务里如果压缩事件(Condensation event)几轮就来一次,说明窗口被大文件读取得太满,优先考虑拆任务而不是继续压低
max_size。 - 任务完成度:压缩是为了让对话活下来,代价是早期细节被摘要化。观察 Agent 是否还记得任务早期的约定,如果"失忆"明显,把
max_size调高一些,在"不崩"和"不忘"之间找平衡。
整个"检测—压缩—恢复"的过程可以画成一条状态流:
对应的调优阈值就两个:70% 的黄线提示你"该手动压一下了",90% 的红线是"再不管就要崩了"。
避坑 FAQ
问:报错说超 4096 限制,但我明明用的是大窗口模型?答:那个 4096 是max_tokens(单次回复预留)和输入加起来超了某个小窗口的老错误。先确认模型配置里挂的到底是哪个模型,聚合渠道有时会静默回退到小模型。
问:我把自动修剪关了,为什么还是被压缩?答:enable_history_truncation管的是"超限后的应急修剪",而 condenser 是"提前预防",两者独立。想要完全不被动压缩,得把condenser.enabled也关掉——但长对话下不推荐。
问:压缩之后 Agent 忘了之前说的话,算 bug 吗?答:不算,这是摘要化的固有代价。关键事实(文件路径、约束条件)建议用明确的句子重复说一次,它们更容易在摘要里存活。
问:手动点"压缩"和自动压缩有什么区别?答:走的是同一条服务链路,区别只是触发时机——自动压缩由水位触发,手动压缩由你在界面红区时主动发起。手动的好处是你可以选在一个任务阶段结束时压,摘要边界更干净。
问:窗口大小显示为空或 0 怎么办?答:部分聚合渠道不返回窗口大小,meter 会退化为只显示裸 token 数。这不是故障,压缩逻辑照常工作,只是百分比无法计算。
一句话收尾
如果你正在用 OpenHands 跑代码生成、文档分析这类需要长上下文的任务,记住一件事:默认开启压缩器 + 盯着 70%/90% 两条水位线,就足以扛住绝大多数上下文超限场景;接下来项目还会把压缩从"被动擦白板"走向"预判趋势提前整理",让长对话的连续性再上一个台阶。
【免费下载链接】OpenHands🙌 OpenHands: AI-Driven Development项目地址: https://gitcode.com/GitHub_Trending/ope/OpenHands
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考