通宵跑代码却卡在半夜:判定智能体“卡住”要用的两个信号

我们做无人值守任务时,最常见的告警配置是超时:任务跑过 X 分钟就报。这个规则对付不了凌晨那种情况——进程还在,不报错,输出也不再动,值守的人第二天早上才发现它其实三小时前就停住了。有一款开源终端监管工具(项目名 Polter,用于管理无人值守的 AI 编码代理)给出的做法很少见:它不设一条万能超时,而是用两个阈值加一种读屏方式来判断状态。这篇只讲这件事——用什么信号判定”卡住”,以及为什么读终端缓冲区比截图识别更适合做判定。至于大规模编排的断点续跑与失败隔离,是另一篇的内容。
还要先声明一点:据该项目文档,某编程智能体即使加上自动权限模式的参数,仍会停在”是否继续(y/n)”这类确认提示以及依赖安装确认上,此时进程零 token 消耗、不报错、可以整夜挂着。我们把它当作该文档描述的一种现象来处理,不当作对任何具体产品的结论,也不给识别准确率——那份文档没有给,我们也没有实测数可以补。
一、”卡住”要先拆成四种状态
一条超时规则盖不住的原因是,表面上”没动静”的四种状态,信号特征完全不同:
- 在算。终端输出不一定在变(一次长推理可能十几秒不打印),但请求与 token 在发生。
- 在等外部。编译、安装依赖、跑测试、等接口回包,进程在自己的轨道上,只是不快。
- 在等人点头。有一个明确的待确认提示摆在那里,而它不会自己消失,这是最贵的一种,因为它看起来最像”在跑”。
- 真的挂了。没有请求、没有输出、没有子进程,只有进程号还在。
判定顺序也因此不能按”多久没动静”来排,要按”哪个信号还在动”来排:先看请求与 token 有没有发生,再看工具调用的时间间隔,最后看屏幕缓冲区里有没有一句等待回答的话。三种信号合起来才能把这四态分开。
二、四类信号各自的代价
| 信号 | 采法 | 能区分什么 | 代价与坑 |
|---|---|---|---|
| 输出是否在变 | 缓冲区文本的指纹比对 | 有没有新内容产生 | 长计算与流式停顿都会误判为”没变” |
| token 与请求是否发生 | 客户端侧计数或代理侧统计 | 在算还是停了 | 只在自己发起的调用里可见,子进程内部看不见 |
| 工具调用间隔 | 每次工具调用的时间戳序列 | 是在调工具还是在空跑 | 需要在调用点埋记,跨进程时容易漏 |
| 是否存在待确认提示 | 缓冲区文本按模式匹配 | 在等人还是在干活 | 提示文案变体多,规则要能维护 |
前两条是很多平台已经有的,第三条常被省略,而恰恰是第三条让判定可用:据该项目文档,两个默认阈值各管一件事——屏幕无变化持续 3 分钟触发一次巡检,工具调用间隔 15 分钟则用来区分”正在调工具”和”只在跑后台进程”。
三、为什么必须是两个阈值而不是一个
单看”屏幕 3 分钟没变”会大量误判。真实项目里,一次全量编译、一次数据库迁移、一轮集成测试都可以轻松超过 3 分钟不打印任何东西,把它们判成阻塞,就会在正常负载上刷出一堆假告警,最后大家把告警关掉——这是这类系统最典型的死法。
两条信号合起来才成立:屏幕无变化到点只是”该巡检一次”,巡检要看的是这 3 分钟里有没有发生新的工具调用、间隔是否超过 15 分钟、缓冲区末尾是不是停在一个提问句式上。三项都指空,才转成阻塞。这样长编译会被”有工具调用在跑”解释掉,而等待确认会被”末尾提问”精确抓住。
阈值定完之后要做的不是调数,是回归:把典型场景各录一遍(长编译、长测试、依赖安装、等待确认、真挂死),检查每条场景在这两个阈值下会被判成什么状态。这份记录我没有替任何项目跳过——它比阈值本身更能说明配置是否合适。
四、读原始缓冲区,而不是截图加文字识别
该项目的读屏方式是读取终端的原始虚拟缓冲区文本,而不是截图再做文字识别。它给的理由很实在:截图识别成本高,还会误读语法与颜色——同一个字符在不同配色与字重下识别结果不同,而终端里恰恰大量出现下划线、连字符、括号这类容易粘连的符号。
从工程角度看,这个选择的关键不在准确率,而在可匹配性。判定要回答的是”末尾有没有一句等待回答的话”,这是文本模式匹配问题,不是图像问题。拿到缓冲区原文之后,可以精确匹配提示串、可以取到光标位置、可以只比对屏幕变化区域而不必重读整屏。
代价也要说清楚,实现里有三块绕不过去:转义序列要剥掉(颜色、光标移动、清行控制码都会混进文本);窗口尺寸变化会让”缓冲区”的内容重排,判定逻辑要按当前尺寸算;虚拟缓冲区与回滚缓冲区是两回事,要的是当前可见区域。输入侧同样有约定:该项目提供管道送键和特殊键两种输入方式,前者送一段文本,后者送回车一类的控制键。把读写两侧都当成接口来设计,比把它当成”模拟人看屏幕”要稳得多。
五、状态机与留证
判出来之后要落成状态。该项目的任务状态是四态:排队、进行、阻塞、完成。我们喜欢把这个结构直接搬过来,因为它给前端和值班的是四个可以分别处理的值。
| 状态 | 进入条件 | 允许的处置 | 离开条件 |
|---|---|---|---|
| 排队 | 已提交未开始执行 | 取消、重排 | 开始执行转进行 |
| 进行 | 有输出变化,或有请求 token 发生,或工具调用间隔未到 | 只观察,不打断 | 触发巡检且三项信号均空转阻塞 |
| 阻塞 | 屏幕无变化到点,且无新工具调用,且末尾存在待确认提示 | 分层处置:自动应答或人工告警 | 得到应答转进行;超过上限转失败 |
| 完成 | 正常结束并有产物 | 归档 | 无 |
留证要跟状态一起落。该项目把数据放在标准状态目录下,包含日志、聊天历史与任务板三样。我们的要求更具体一点,因为事后一定会被问到”那天夜里三点为什么没跑”:每条待确认提示要留原文(不是截图,是可复制的文本)、当时的上下文窗口内容、由谁应答(自动规则还是人)、应答之后的结果。这四样齐了才叫可复盘,缺一样就变成”值班的人记不得了”。
六、自动确认与人工告警必须分层
这一层是我最想让人抄的部分。该项目明确批评了”超时 60 秒自动点 Yes”这类做法,理由是它会把删除目录一类的破坏性命令一并执行掉;它的处理是把”已知依赖的自动确认”和”删除、覆盖类风险动作”分开,后者改为人工告警。
| 待确认动作 | 默认处置 | 判定依据 | 追加要求 |
|---|---|---|---|
| 已知依赖的安装确认 | 自动应答 | 依赖清单在白名单内且来源可核 | 记录版本与来源,装不上时转阻塞而不是重试到底 |
| 首次出现的依赖或新来源 | 人工告警 | 不在白名单 | 走完人工确认再决定是否入白名单 |
| 删除、覆盖、清空目录 | 人工告警 | 动词命中风险清单 | 禁止任何形式的自动应答 |
| 修改权限、改配置 | 人工告警 | 动词命中风险清单 | 要留变更前后对照 |
| 对外发送(提交、发布、调用外部接口) | 人工告警 | 目标在设备或项目边界之外 | 记录目标标识与返回码 |
分层规则要能配置,并按项目默认收敛:新项目的默认值应该是”只自动应答白名单依赖”,而不是”尽量自动”。
同一段还要配上长跑任务自身的三重保护:墙钟上限、token 上限、无进展即停,三者取最严的一条生效。这三条不是判定卡住,而是卡住判定成立之后的止损边界。
最后是演练,三条即可:人为制造一次确认提示、一次静默挂起、一次长编译,看系统能否分别判成阻塞、失败前告警、进行,并量出从发生到告警的实际时间。洹拓在内部平台上线这类监管模块时,还会加一条组织要求——把”任务状态”当成产品字段而不是日志文本。一旦状态只写在日志里,前端只能显示”运行中”,值班只能靠人盯,判定做得再准也没人消费它。