让进程随时能死,对话才能一直活着
对话是一份不断加页的文档,不是一个一直运行的程序。
最近在把 Codex、Claude Code 这类 agent 的能力搬上云端,让用户能在网页里直接用。做着做着,撞上一个特别拧巴的问题。
我们跑这些 agent,用的是它们的命令行本体——比如 codex exec。它的性格很干脆:给它一个任务,它跑完,然后就退出。一次性的,跑完即死。
可我要做的,是一个能一直聊的对话式 agent:用户来回问、我来回答,直到我们一起把一个任务完成。一个”跑完就死”的进程,怎么撑得起一段”一直在聊”的对话?
这个问题我卡了挺久。后来才发现一个有点反直觉的答案:要让对话一直活着,恰恰得让进程随时能死。
别把”一段对话”,想成”一个进程”
我们太容易把连续的东西,想成一个一直运行的东西。一段对话,感觉上就该有个”它”在那儿待命——你说一句,“它”听着;你想两分钟,“它”等着。
但这是个错觉。
对话真正的本体,是那份不断加页的记录(transcript)。它是数据,不是进程。你上一句说了什么、agent 调了哪些工具、改了哪些文件——这些是”状态”,可以老老实实躺在硬盘或数据库里。而”跑一个回合”所需要的算力,只在你按下发送、到 agent 吐完这一轮的那几秒(有时几分钟)里存在。这之后,它没有任何理由继续占着一个进程。
所以我把这件事拆成两半,一半是数据,一半是算力:
- 对话 = 状态。 一份持久化的 session,躺在存储里。
- 回合 = 算力。 每次用户发消息,起一个用完即退的进程去算这一轮。
codex exec 刚好递上了那块拼图:codex exec resume <session_id>。你把上一回合拿到的 session id 存下来,下一回合用它 resume,Codex 会自动把前面所有轮次的上下文、计划、审批记录都 load 回来,接着往下跑。跑完,照旧退出。
于是一段对话,在后台其实长这样:
- 用户发消息 → 后端把这条消息入队;
- 一个 worker 取到任务,从存储里 load 出这个对话的 session id 和它的工作目录;
- 起一个容器,
codex exec resume <sid>,带着全部历史接着聊; - agent 一边跑,事件一边通过 SSE 流式推回浏览器;
- 这一轮完了,把改动过的工作目录存回去,session id 更新;
- 容器销毁,进程退出——直到用户发下一句。
用户看到的,是一条从头到尾连续的聊天线程。后台跑的,是一次次针对同一份 session 的、短命的调用。
这个套路,其实一点都不新
说到这儿你可能已经笑了——这不就是 Web 干了二十年的事吗?
HTTP 本身是无状态的。服务器从来不会为每个登录用户挂一个进程在那儿等他点下一下。它靠的是一个 cookie,加上服务端一个 session store,把一个个孤立的请求,拼成”连续会话”的错觉。你刷了一整天某个网站,感觉它一直”记得”你,其实它每次都是从存储里把你捞出来、处理完、再放回去。
我们只是把这套老智慧,原封不动搬到了 agent 上。对话是 session,回合是请求,resume 就是那个 cookie。没有新东西,只有一个被验证了二十年的模式,用在了一个新地方。
(我越来越觉得,做系统设计时的很多”顿悟”,本质上都是想起了某个早就存在的老办法。)
为什么”能死”这么重要
如果反过来做——为了让对话”跟手”,干脆让容器一直活着,用户不发消息就让它空转等着——会怎样?
会死得很惨。用户想一句话,可能几秒,也可能是去泡了杯咖啡、开了个会,半小时后才回来。你要是为每段对话钉住一个容器,你就是在养成千上万个几乎全程空转、只在偶尔那几秒真正干活的容器。这才是真正扛不住规模的做法。
“用完即退”不是妥协,恰恰是它能规模化的原因:进程只在真正算的那几秒存在;用户发呆、思考、离开的所有时间里,你占用的只是一份躺着的 session,几乎零成本。而且它天然容错——某个容器挂了,对话在存储里好好的,换任意一个 worker 一 resume 就回来了。
很多人同时用,怎么办?
一旦你接受了”一个回合 = 一个短命进程”,并发就变成一个特别标准、特别舒服的问题。
一个用户的一个回合,就是一个容器里的一次 codex exec。一人一容器,互相看不见彼此的代码和文件。那很多人同时来呢?前面架一个任务队列,后面一个 worker 池:
- 请求进来,先入队,不直接开工;
- 一组 worker 从队列里取任务,取到一个就拉一个容器去跑,跑完取下一个;
- worker 池按队列深度自动扩缩——忙了加机器,闲了缩回去。
并发的天花板,说到底就两条:你能同时开多少容器(你的算力),以及模型 API 的 rate limit(你的额度)。队列的意义,就是当请求超过这两条线时,让它排队,而不是让系统崩掉。你甚至可以很坦诚地告诉用户”前面还有几位,预计 X 秒”——这比一个 500 体面得多。
还有个小细节值得早点想清楚:agent 要用的那些工具,别每个会话都在容器里单独拉一个进程。并发一高,那就是成百上千个子进程。把它做成一个共享的 HTTP 服务,让所有会话去连它,会干净很多。
写在最后
这套东西拆开看,没有任何一块是新的:无状态的执行、持久化的状态、队列、worker 池——每一样都在别的系统里躺了很多年。新的只是把它们组合起来,套在 agent 这个新物种上。
我一直相信,好的架构往往不是发明出来的,是”想起来”的——想起某个早被验证的、朴素的老办法,然后有勇气不去为难它。就像你不需要一堆花哨的框架才能做出一个可靠的 agent,你也不需要一个”一直活着”的进程,才能撑起一段”一直在聊”的对话。
让进程随时能死。对话,反而因此活得更久、更稳,也更便宜。