多智能体协作这件事,我在过去一年里反复折腾过好几套方案。最开始用脚本串行调用几个模型接口,跑得挺欢,但一旦任务超过三步、需要中途等人确认、或者某个环节要跑上十几分钟,整个链路就开始散架——状态丢了、上下文断了、进程挂了没人管。后来我把目光投向 tmux 和 MCP 这套组合,用 OpenRig 的思路把一堆离散的 AI Agent 编织成一个能持久化、能互相通信、能扛住中断的协作系统。这篇文章就把我踩过的坑、验证过的架构、以及可以直接抄的配置全部摊开讲清楚。
如果你正在做 AI Agent 开发,尤其是遇到"怎么让多个 Agent 长期协作而不掉线""怎么扛并发""怎么让 Agent 真正下地干活"这类问题,那这篇内容应该能帮你省下不少试错时间。我会从最底层的进程持久化讲起,一路讲到 MCP 协议怎么把工具能力接进来,再到多智能体编排的调度逻辑,最后给出实测中遇到的意外情况和处理办法。
1. 为什么离散 Agent 一上生产就散架
1.1 从"能跑通"到"能持续跑"之间的鸿沟
大部分人搭 AI Agent 的起点都差不多:写个 Python 脚本,调几个大模型接口,串起来跑一个流程,本地测试通过,感觉成了。但真正把它放到需要持续运行、多任务并发的场景里,问题立刻暴露。
我最早做的一个内容处理 Agent,任务是"抓取素材 → 摘要 → 改写 → 配图建议 → 输出"。单次跑没问题,但我需要它每半小时处理一批,还要能在我手动介入修改后继续往下走。结果就是:脚本跑完就退出,状态全丢;中途我改了一版摘要,它不知道,继续用旧的往下走;两个任务同时进来,日志混在一起完全没法排查。
这里的核心矛盾在于:离散 Agent 的设计假设是"一次性执行",而生产场景要求的是"持久化协作"。这两者之间的差距,不是加个循环就能补上的,它涉及到进程生命周期管理、状态持久化、消息传递、并发隔离四个层面的系统性改造。
我后来总结了一张对照表,把"能跑通"和"能持续跑"的差异列清楚,每次设计新系统前都会过一遍:
| 维度 | 一次性脚本 | 持久化协作系统 |
|---|---|---|
| 进程生命周期 | 跑完即退出 | 常驻,可中断可恢复 |
| 状态管理 | 内存变量,丢失即无 | 落盘持久化,可回溯 |
| 通信方式 | 函数调用返回值 | 消息队列/共享会话 |
| 并发处理 | 串行或简单多线程 | 隔离会话,独立上下文 |
| 人工介入 | 改代码重跑 | 实时注入,无缝续接 |
| 故障恢复 | 从头再来 | 断点续跑 |
这张表是我踩了无数次坑之后才理清的。你会发现,真正难的不是"让 Agent 聪明",而是"让 Agent 活着、记得住、能协作"。
1.2 tmux 为什么成了持久化的关键拼图
说到持久化,很多人第一反应是上数据库、上消息队列。这些当然有用,但对于 Agent 的"运行态"来说,最直接的持久化载体其实是终端会话。
tmux 在这里的价值被严重低估了。它本质上是一个终端复用器,但它提供的"会话与窗口分离"能力,恰好解决了 Agent 持久化的核心问题:进程不再依附于某一次 SSH 连接或某一个终端窗口。你把 Agent 跑在 tmux 会话里,关掉终端、断开连接,Agent 照样活着;重新连上来,attach 回去,看到的就是它当前的真实状态。
我实测下来,tmux 方案相比"用 systemd 托管进程"有几个明显优势:
- 可交互性:systemd 托管的进程你很难实时"钻进去"看它在干嘛、给它发指令。tmux 会话你可以随时 attach,直接在里面敲命令、看输出、甚至手动改状态。
- 多窗口隔离:一个 tmux 会话可以开多个窗口,每个窗口跑一个 Agent,天然隔离,日志不串。
- 轻量无依赖:不需要额外的守护进程框架,一个 tmux 二进制搞定。
提示:tmux 会话默认在服务器重启后会丢失。如果你需要跨重启持久化,得配合
tmux-resurrect或tmux-continuum这类插件,或者把关键状态定期落盘到文件/数据库,重启后由启动脚本重新拉起会话并恢复状态。
我现在的标准做法是:每个 Agent 一个 tmux window,命名规范统一,比如agent-summarizer、agent-rewriter、agent-publisher。启动脚本负责创建会话、拉起各窗口、注入初始状态。这样整个系统的"运行态"就是可见、可管、可恢复的。
1.3 并发问题的本质是会话隔离
"AI Agent 怎么扛并发"是热词里高频出现的问题。我的答案很直接:并发的本质不是线程数,而是会话隔离。
一个 Agent 处理两个任务时,如果共用同一份上下文内存,那必然互相污染。正确的做法是:每个任务实例拥有独立的会话上下文,包括独立的对话历史、独立的工具调用状态、独立的输出缓冲。这些会话可以跑在同一个 tmux 会话的不同窗口里,也可以跑在不同会话里,关键是上下文不共享。
我见过有人用全局变量存对话历史,然后开多线程跑,结果两个任务的回复串在一起,排查了半天才发现是共享状态的问题。这种坑,只要一开始就把"一会话一上下文"作为铁律,就能完全避免。
具体实现上,我会给每个任务分配一个唯一的 session id,所有状态读写都带上这个 id 作为命名空间。落盘时按sessions/{session_id}/分目录,内存里用字典按 id 隔离。这样即使同一个 Agent 进程处理多个任务,也不会串。
2. MCP 协议:把工具能力标准化接进来
2.1 MCP 到底解决了什么问题
MCP 这个词最近热度很高,但很多人对它的理解还停留在"又一个协议"。我用一句话概括它的价值:MCP 让 Agent 调用外部工具这件事,从"每个工具写一套适配代码"变成了"统一接口即插即用"。
在没有 MCP 之前,我的 Agent 要调用文件系统、要查数据库、要发消息,每个能力都得单独写一套调用逻辑,参数格式、错误处理、返回解析各不相同。工具一多,代码里全是胶水层,维护成本极高。
MCP 的思路是定义一个标准协议,工具提供方实现 MCP Server,Agent 作为 MCP Client 通过统一协议调用。这样新增一个工具,只要它实现了 MCP,Agent 这边几乎不用改代码就能接上。
我实测下来,MCP 带来的最大改变是工具生态的复用性。以前我写的文件操作工具只能在我自己的项目里用,现在只要封装成 MCP Server,任何支持 MCP 的 Agent 都能直接调用。
2.2 MCP Server 的接入实操
接入一个 MCP Server 的流程,我整理成可复现的步骤。以文件系统工具为例:
第一步,确认 MCP Server 的启动方式。大多数 MCP Server 支持 stdio 和 SSE 两种传输方式。stdio 适合本地进程,SSE 适合远程服务。
第二步,在 Agent 的配置里声明这个 Server。配置通常是一个 JSON 结构:
{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/workspace"], "env": {} } } }第三步,Agent 启动时读取配置,拉起 MCP Server 进程,建立连接,然后就可以通过协议调用它暴露的工具了。
这里有个容易忽略的细节:MCP Server 的进程生命周期要和 Agent 绑定。如果 Agent 重启但 MCP Server 没重启,可能出现连接失效;如果 MCP Server 挂了但 Agent 不知道,调用就会超时。我的做法是在 Agent 里加一个健康检查,定期 ping 一下 MCP Server,发现异常就重启它。
注意:不同 MCP Client 对配置格式的支持有差异。有的用
command+args,有的用url直连 SSE。接入前先确认你的 Agent 框架支持哪种,别照搬配置导致连不上。
2.3 工具调用失败的排查链路
MCP 工具调用失败是高频问题,我总结了一套排查顺序,基本能覆盖九成情况:
- 先看 Server 进程在不在。
ps aux | grep mcp确认进程活着。进程不在,说明启动配置有问题。 - 再看连接是否建立。Agent 日志里应该有"connected to MCP server"之类的记录。没有的话,检查传输方式是否匹配。
- 然后看工具是否被正确暴露。调用
list_tools看返回的工具列表里有没有你要的那个。没有的话,是 Server 端的问题。 - 最后看参数格式。MCP 工具的参数是 JSON Schema 定义的,传错类型会直接报错。对着 Schema 检查一遍。
我遇到过最隐蔽的一个问题:MCP Server 启动成功、连接也建立了,但调用一直超时。排查半天发现是 Server 的工作目录权限不对,它想读的文件读不到,卡在那里。这种问题日志里不一定有明显报错,得靠strace或者手动在 Server 进程里试一次才知道。
3. 多智能体编排的调度逻辑
3.1 编排不是"排队执行",而是"状态机驱动"
很多人对多智能体编排的理解是:Agent A 跑完,把结果给 Agent B,B 跑完给 C。这是流水线,不是编排。
真正的编排,核心是状态机。每个 Agent 是状态机里的一个节点,节点之间有转移条件,转移条件可以基于 Agent 的输出、外部事件、人工确认。这样系统才能处理"如果摘要质量不达标就退回重做""如果人工介入修改了就跳过某个节点"这类复杂逻辑。
我现在的编排层用一个简单的状态机实现,状态定义大致是这样:
class Orchestrator: def __init__(self): self.states = { "idle": self.handle_idle, "summarizing": self.handle_summarizing, "reviewing": self.handle_reviewing, "rewriting": self.handle_rewriting, "done": self.handle_done, } self.current = "idle" def transition(self, event): next_state = self.transitions[self.current].get(event) if next_state: self.current = next_state self.states[next_state]()这个结构的好处是,每个状态的处理逻辑独立,状态之间的转移条件清晰。加一个新环节,就是加一个状态和几条转移规则,不影响已有逻辑。
3.2 Agent 之间怎么传递上下文
Agent 之间传递上下文,最容易犯的错是"全量传递"。A 的完整对话历史传给 B,B 再传给 C,上下文越滚越大,最后超出模型窗口,而且大量无关信息干扰判断。
我的做法是结构化传递:每个 Agent 只接收它需要的字段,而不是整个上下文。比如摘要 Agent 只需要原始素材,改写 Agent 只需要摘要结果和改写要求,配图 Agent 只需要改写后的正文。
具体实现上,我会定义一个任务对象,各 Agent 读写自己负责的字段:
task = { "id": "task-001", "raw_material": "...", "summary": None, "rewritten": None, "image_suggestions": None, "status": "summarizing", }每个 Agent 从 task 里读自己需要的,处理完写回自己的字段。这样上下文传递是可控的、可追溯的,出问题也能定位到是哪个环节写坏了。
3.3 人工介入点怎么设计才不打断流程
持久化协作系统的一个关键能力是"人工随时介入"。但介入点设计不好,要么打断流程,要么介入后状态不一致。
我的经验是:把人工介入设计成状态机里的一个正常状态,而不是异常处理。比如在"reviewing"状态,系统主动暂停,等待人工确认或修改。人工操作完成后,发一个事件触发状态转移,流程继续。
这样设计的好处是,人工介入和自动流程用的是同一套状态转移机制,不会出现"人工改完系统不知道"的情况。而且介入点可以配置,哪些环节需要人工确认,哪些全自动,改配置就行。
提示:人工介入时,一定要把当前状态完整落盘。我遇到过人工改到一半系统崩了,重启后状态丢失,只能从头再来。后来加了"每次状态转移前落盘",这个问题就再没出现过。
4. 实测中的意外情况与处理
4.1 Agent 卡死但进程还在
这是最恶心的一类问题:ps看进程活着,tmux 窗口也在,但 Agent 就是不往下走。原因通常是某个工具调用阻塞了,比如等一个永远不返回的接口。
我的处理办法是加心跳检测 + 超时熔断。每个 Agent 定期往一个心跳文件写时间戳,编排层定期检查,发现某个 Agent 超过阈值没更新心跳,就判定它卡死,触发重启。
import time, os def heartbeat(agent_id, interval=10): while True: with open(f"/tmp/heartbeat_{agent_id}", "w") as f: f.write(str(time.time())) time.sleep(interval)编排层检查时,读这个文件,和当前时间对比,超过 3 倍 interval 就认为异常。这个机制救了我好几次,尤其是调用外部接口不稳定的时候。
4.2 上下文超长导致的静默失败
模型上下文有窗口限制,超了之后有的接口直接报错,有的会静默截断。静默截断最坑,因为你不报错,但结果质量明显下降,排查半天才发现是上下文被截了。
我的应对是主动管理上下文长度。在传给模型之前,先估算 token 数,超了就做摘要压缩或者只保留最近 N 轮。估算可以用简单的字符数除以 4 粗略估计,也可以用 tiktoken 这类库精确计算。
另外,我会在日志里记录每次调用的实际 token 数,这样一旦发现质量下降,能快速定位是不是上下文的问题。
4.3 多 Agent 同时写同一份状态
并发写冲突是另一个高频坑。两个 Agent 同时更新 task 对象的同一个字段,后写的覆盖先写的,数据就丢了。
解决办法是加锁或者用消息队列串行化写操作。简单场景下,我用文件锁:
import fcntl def safe_update(task_file, update_fn): with open(task_file, "r+") as f: fcntl.flock(f, fcntl.LOCK_EX) task = json.load(f) update_fn(task) f.seek(0) f.truncate() json.dump(task, f) fcntl.flock(f, fcntl.LOCK_UN)复杂场景下,我会引入一个轻量的消息队列,所有状态更新都通过队列串行处理,彻底避免并发写。
4.4 重启后状态恢复的完整性
系统重启后恢复状态,最容易出问题的是"恢复了一半"。比如任务状态恢复了,但 MCP 连接没恢复;或者 Agent 进程起来了,但上下文没加载。
我的做法是恢复流程分阶段,每阶段可验证:
- 先恢复持久化状态(从文件/数据库读 task 对象)。
- 再拉起各 Agent 进程(tmux 会话重建)。
- 然后重建 MCP 连接(重新拉起 Server 并连接)。
- 最后校验:检查每个 Agent 的心跳、检查 MCP 工具列表、检查 task 状态一致性。
每个阶段都有明确的成功标志,任何一步失败就停在那里报错,不会带着半吊子状态往下跑。
5. 从零搭一套的最小可行配置
5.1 目录结构与启动脚本
我把整套系统的目录结构固定下来,方便管理和迁移:
openrig/ ├── agents/ │ ├── summarizer.py │ ├── rewriter.py │ └── publisher.py ├── orchestrator.py ├── mcp_config.json ├── sessions/ │ └── {session_id}/ │ ├── task.json │ └── heartbeat ├── logs/ └── start.sh启动脚本负责创建 tmux 会话、拉起各 Agent、启动编排层:
#!/bin/bash SESSION="openrig" tmux new-session -d -s $SESSION -n orchestrator tmux send-keys -t $SESSION:orchestrator "python orchestrator.py" C-m tmux new-window -t $SESSION -n summarizer tmux send-keys -t $SESSION:summarizer "python agents/summarizer.py" C-m tmux new-window -t $SESSION -n rewriter tmux send-keys -t $SESSION:rewriter "python agents/rewriter.py" C-m echo "OpenRig started. Attach with: tmux attach -t $SESSION"这个脚本跑完,整个系统就起来了。tmux attach -t openrig就能看到所有 Agent 的实时状态。
5.2 关键参数与调优建议
实测下来,几个参数对系统稳定性影响最大:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 心跳间隔 | 10s | 太短浪费资源,太长故障发现慢 |
| 心跳超时阈值 | 30s | 3 倍心跳间隔,容忍偶发延迟 |
| 上下文保留轮数 | 最近 10 轮 | 兼顾连贯性和窗口限制 |
| 工具调用超时 | 60s | 大部分工具够用,特殊工具单独配 |
| 状态落盘频率 | 每次转移前 | 保证崩溃可恢复 |
这些值不是拍脑袋定的,是我在不同负载下反复调整出来的。比如心跳间隔,我试过 5s,结果日志刷得太快;试过 30s,故障发现太慢。10s 是个平衡点。
5.3 监控与日志的实用做法
日志这块,我的原则是结构化 + 分级 + 可检索。每条日志带上时间戳、Agent id、session id、日志级别,输出成 JSON 行格式,方便后续用工具检索。
import json, time def log(agent_id, session_id, level, message): entry = { "ts": time.time(), "agent": agent_id, "session": session_id, "level": level, "msg": message, } print(json.dumps(entry), flush=True)监控方面,我做了个简单的状态面板,定期打印各 Agent 的心跳状态、当前 task 状态、MCP 连接状态。不用上复杂的监控系统,一个定时脚本就够。
6. 几个值得单独说的经验
6.1 别急着上复杂框架
我见过很多人一上来就上 LangGraph、AutoGen 这类框架,结果被框架的抽象层绕晕,出了问题不知道是框架的锅还是自己的锅。我的建议是:先用最朴素的 tmux + 状态机 + MCP 把核心链路跑通,理解清楚每个环节在干嘛,再考虑要不要上框架。
框架解决的是"规模化"和"标准化"的问题,但如果你连基本的持久化和状态管理都没搞明白,框架只会让问题更隐蔽。我自己就是先用裸方案跑了两个月,把坑都踩明白了,后来才在部分环节引入框架,这时候用起来心里有底。
6.2 状态设计要面向恢复
设计状态结构时,永远问自己一个问题:如果现在崩溃,重启后能不能从这个状态恢复。如果答案是不能,那这个状态设计就有问题。
具体来说,状态里不能有"只存在于内存、无法重建"的东西。比如一个正在进行的 HTTP 请求,崩溃后没法恢复,那就得把"请求已发出、等待响应"这个状态落盘,重启后重新发起请求。所有状态都应该是可序列化、可重建的。
6.3 人工介入的体验决定系统可用性
一个多智能体系统好不好用,很大程度上取决于人工介入的体验。如果每次介入都要改代码、重启服务,那没人愿意用。
我的做法是提供一个简单的介入接口:一个命令行工具,可以查看当前所有 task 状态、修改某个 task 的字段、触发状态转移。这样人工介入就是敲几条命令的事,不用碰代码。
# 查看所有任务 openrig list # 查看某个任务详情 openrig show task-001 # 修改字段 openrig update task-001 summary "新的摘要内容" # 触发状态转移 openrig transition task-001 approve这套命令我用了大半年,基本覆盖了所有人工介入场景。体验上去了,系统才真正有人愿意用。
6.4 关于并发规模的现实预期
最后说个现实问题:单机 tmux 方案能扛多少并发?我的实测是,在普通 4 核 8G 的机器上,跑 10 到 20 个 Agent 实例比较稳,再多就开始出现资源竞争和调度延迟。如果你的并发需求远超这个量级,那就得考虑分布式方案,把 Agent 分散到多台机器,用消息队列做跨机通信。
但大多数个人项目和小团队场景,单机方案完全够用。别为了"可能的规模"过度设计,先把单机跑稳,规模上来了再演进。
这套 OpenRig 的思路,我从最初的一个脚本,到现在管理着十几个 Agent 的协作系统,中间迭代了不知道多少版。核心其实就三件事:用 tmux 解决持久化,用 MCP 解决工具接入,用状态机解决编排。把这三件事做扎实,多智能体协作就没那么玄乎了。