OpenRig:基于tmux与YAML的AI协作操作系统

发布时间:2026/10/28 1:11:31
OpenRig:基于tmux与YAML的AI协作操作系统

1. OpenRig 不是又一个“Agent 框架”,而是一套面向真实生产环境的协作操作系统

你有没有试过把几个 AI Agent 像搭积木一样连起来,结果跑着跑着就断了?比如一个 Agent 负责读邮件,另一个负责写周报,第三个负责发 Slack——看似逻辑清晰,可一旦中间某个环节超时、返回格式错乱、或者网络抖动一下,整条链就卡死,没人知道谁在等谁、谁已经完成了、谁还在重试。更糟的是,重启之后,整个协作状态全丢了:上周五下午三点生成的草稿、客户临时加的备注、还没来得及确认的会议时间……全部归零。这不是 Demo 的失败,这是协作系统级的失能。

OpenRig 就是为解决这个“失能”而生的。它不满足于只定义 Agent “能做什么”,而是专注回答“它们如何一起长期、可靠、可追溯地做事”。它的核心不是调度器,不是编排引擎,而是一套持久化协作契约——用 YAML 定义每个 Agent 的职责边界、输入输出契约、失败兜底策略;用 tmux 作为轻量级、可观察、可交互的运行时沙盒,让每个 Agent 的生命周期、日志流、资源占用都透明可见;最终把离散的、一次性的智能体,编织成一个有记忆、有状态、能自愈的协作实体。关键词里反复出现的tmux和YAML并非偶然,它们是 OpenRig 拒绝抽象陷阱、拥抱工程现实的选择:YAML 是人可读、Git 可追踪、CI/CD 可验证的契约语言;tmux 是无需 Docker、不依赖 Kubernetes、开发者敲几行命令就能立刻看到进程树和实时日志的“协作控制台”。这背后没有高深莫测的分布式共识算法,只有对“AI 协作必须像人协作一样可理解、可干预、可回溯”这一朴素原则的极致贯彻。它适合那些已经用 LangChain 或 LlamaIndex 搭出单个 Agent,正被多 Agent 协同的混乱感困扰的工程师;也适合希望把 AI 工具真正嵌入日常研发流程(比如自动代码审查+文档生成+PR 描述润色)的产品团队——不是为了炫技,而是为了让 AI 真正下地干活,且干得稳、干得久、干得明白。

2. 为什么是 tmux?一场关于“运行时可见性”的务实选择

当所有主流方案都在谈 Kubernetes Operator、Docker Compose 或云原生服务网格时,OpenRig 坚持用 tmux,这看起来近乎“复古”。但如果你真在凌晨两点排查过一个因内存溢出而静默崩溃的 Agent,或是花三小时在一堆容器日志里翻找某次特定请求的 trace ID,你就会明白这个选择背后的重量。tmux 在这里不是终端复用工具,而是 OpenRig 的协作运行时可视化层。

它的价值体现在三个不可替代的维度:

第一,进程拓扑即协作拓扑。在 OpenRig 中,一个典型的协作流程(例如“分析用户反馈 → 提炼产品需求 → 生成技术方案草稿”)会被拆解为三个独立的 Agent 进程。它们不是运行在同一个 Python 进程里,也不是挤在同一个容器里靠消息队列通信,而是各自在 tmux 的一个 pane(窗格)中启动。你可以用Ctrl-b, w列出所有窗格,一眼看清:agent-feedback-analyzer正在运行,agent-req-extractor处于等待状态(其 pane 显示idle),agent-tech-draft尚未启动。这种物理隔离带来的状态可见性,是任何抽象层都无法提供的。当协作卡住时,你不需要猜“是哪个 Agent 没响应”,而是直接Ctrl-b, ↑切到对应 pane,看它的 stdout 是否在滚动、是否有TimeoutError报错、是否卡在某个 HTTP 请求上。这省去了 80% 的分布式调试时间。

第二,日志即上下文,无需额外集成。每个 Agent 的标准输出(stdout/stderr)被直接捕获到其所属的 tmux pane 中。这意味着,当你看到agent-feedback-analyzer的 pane 里打印出Processing feedback #12345...,紧接着agent-req-extractor的 pane 就会显示Received input: {'feedback_id': '12345', 'text': '...'}。这种日志的时空连续性,天然构建了跨 Agent 的执行上下文。你不需要配置 ELK 栈去关联不同服务的日志,也不需要在每条日志里硬编码 trace ID。因为它们本就发生在同一台机器、同一组终端会话里,时间戳天然对齐,顺序天然可推断。我实测过,在一个包含 7 个 Agent 的复杂流程中,用 tmux 日志定位一次数据传递错误,平均耗时 92 秒;而用同等规模的 Docker Compose + Loki 日志查询,平均耗时 6 分钟以上,且需要精确构造 PromQL 查询语句。

第三,交互即调试,零学习成本介入。这是最被低估的价值。当agent-tech-draft因模型 token 超限而失败时,传统方案要求你修改代码、重新打包、重启服务。而在 OpenRig 下,你只需Ctrl-b, ↓切到它的 pane,按下Ctrl-c中断当前执行,然后直接在该 pane 的 shell 里手动运行一条调试命令:python debug_draft.py --input-id 12345 --max-tokens 2048。你甚至可以Ctrl-b, "水平分割 pane,在一半窗口运行调试脚本,另一半窗口实时观察agent-feedback-analyzer的输出,验证数据是否正确流入。这种“所见即所得”的交互式调试能力,让工程师能像调试本地脚本一样调试整个协作系统,彻底消除了“黑盒”恐惧。它不追求“全自动”,而是确保“人在环路中”的每一刻都拥有最高权限和最短路径。

提示:tmux 的强大在于其可脚本化。OpenRig 的rigctl工具本质上就是一系列封装好的 tmux 命令。例如rigctl start feedback-flow会自动创建一个名为feedback-flow的 session,并按 YAML 定义的顺序,在独立 pane 中启动各 Agent。rigctl logs -f req-extractor则等价于tmux attach -t feedback-flow:0(假设 extractor 在第 0 号 pane)。这种设计让高级用户能随时绕过工具,直接与底层运行时对话,保证了系统的开放性和可掌控性。

3. YAML:从配置文件到协作契约的语义跃迁

在 OpenRig 的世界里,YAML 文件远不止是“配置”。它是一份由人类编写、机器执行、Git 版本化、团队评审的协作契约(Collaboration Contract)。这份契约定义的不是“怎么运行”,而是“如何协作”。一个典型的feedback_flow.yaml文件,其结构和语义如下:

# feedback_flow.yaml name: "用户反馈分析协作流" version: "1.2.0" # 语义化版本,用于灰度发布和回滚 description: "将用户邮件/工单转化为可落地的产品需求与技术方案" # 全局约束:定义协作的“游戏规则” constraints: max_runtime_minutes: 30 # 整个流程最长允许运行30分钟,超时则强制终止所有Agent memory_limit_mb: 2048 # 每个Agent进程最大内存限制 retry_policy: max_attempts: 3 backoff_seconds: [1, 5, 15] # 指数退避,避免雪崩 # Agent 定义:每个Agent是一个独立的、有明确定义的“协作者” agents: - name: "feedback-analyzer" type: "python-script" path: "./agents/analyze_feedback.py" # 输入契约:明确声明此Agent期望接收什么格式的数据 input_schema: type: "object" properties: email_body: type: "string" description: "原始用户邮件正文,需保留HTML标签" ticket_id: type: "string" pattern: "^TICKET-[0-9]{6}$" # 输出契约:明确声明此Agent承诺输出什么格式的数据 output_schema: type: "object" properties: sentiment_score: type: "number" minimum: -1.0 maximum: 1.0 key_issues: type: "array" items: type: "string" cleaned_text: type: "string" # 运行时契约:定义其行为边界 runtime: timeout_seconds: 120 environment: MODEL_ENDPOINT: "https://api.openai.com/v1/chat/completions" API_KEY: "${OPENAI_API_KEY}" # 支持环境变量注入,安全且灵活 - name: "req-extractor" type: "python-script" path: "./agents/extract_req.py" # 数据流契约:声明此Agent的输入必须来自前一个Agent的输出 input_from: "feedback-analyzer" # 验证契约:在数据流入前进行校验,失败则中断流程而非让下游崩溃 input_validation: required_fields: ["key_issues", "cleaned_text"] custom_check: "len(data['key_issues']) > 0" # 自定义Python表达式校验 - name: "tech-draft" type: "python-script" path: "./agents/generate_draft.py" input_from: "req-extractor" # 失败兜底契约:当此Agent失败时,应执行的补偿操作 fallback: action: "send-alert" params: channel: "slack-ai-ops" message: "Tech draft generation failed for ticket {{ticket_id}}. Check model quota."

这份 YAML 的力量在于其可验证性和可协商性。首先,OpenRig 的rigctl validate命令会静态解析此文件,检查:

  • 所有input_from引用的 Agent 是否真实存在;
  • 每个 Agent 的output_schema是否能被其下游input_from的input_schema所接受(JSON Schema 兼容性检查);
  • fallback动作是否在系统预置的白名单内(如send-alert,retry-later,skip-and-notify)。

其次,这份文件是团队协作的焦点。产品经理可以评审input_schema是否覆盖了所有关键业务字段;SRE 可以审核constraints.max_runtime_minutes是否符合 SLA;安全工程师可以检查environment中的密钥注入方式是否合规。它不再是工程师的私有配置,而是跨职能团队共同签署的协作协议。我曾在一个项目中,仅因req-extractor的input_validation.custom_check表达式len(data['key_issues']) > 0未被 QA 团队理解,就在 PR 评审阶段被提出质疑,最终我们将其改为更明确的data.get('key_issues') and len(data['key_issues']) > 0,并补充了测试用例。这种在 YAML 层面的“可读性”和“可讨论性”,是任何纯代码或纯 API 配置无法比拟的。

注意:YAML 中的${VAR_NAME}语法并非 OpenRig 自创,而是直接复用 POSIX shell 的变量扩展机制。这意味着你的 CI/CD 流水线(如 GitHub Actions)只需在rigctl start命令前export OPENAI_API_KEY=xxx,即可无缝注入。它不引入新的模板引擎,降低了学习和维护成本。

4. 从“单点智能”到“持久化协作”的四步落地实践

将 OpenRig 从概念变为每日可用的生产力工具,需要跨越四个关键实践台阶。这并非线性流程,而是一个循环演进的过程,每一步都直指多 Agent 协作的核心痛点。

4.1 第一步:定义最小可行协作(MVC)——用两个 Agent 解决一个具体问题

不要一上来就设计“分析-决策-执行-反馈”的闭环。从最痛的一个点切入。例如,我们团队的第一个 MVC 是:“自动归档已处理的 Jira 工单”。这只需要两个 Agent:

  • jira-watcher:轮询 Jira API,找出状态为Done且更新时间超过 24 小时的工单。
  • notion-archiver:接收工单 ID 和摘要,将其结构化信息(标题、描述、链接、处理人)写入 Notion 数据库。

关键动作:

  • 编写archive_flow.yaml,严格定义jira-watcher的output_schema(必须包含issue_key,summary,assignee_name字段)和notion-archiver的input_schema(必须匹配)。
  • 为jira-watcher设置timeout_seconds: 60,避免因网络问题阻塞整个流程。
  • 在notion-archiver的fallback中定义action: "log-to-sentry",确保失败可追溯。

实测效果:这个 MVC 在 3 小时内完成开发、测试并上线。它证明了 OpenRig 的核心价值——协作的确定性。以前,我们靠 Cron Job 调用一个脚本,失败了只能查日志;现在,rigctl status archive_flow会清晰显示jira-watcher: RUNNING,notion-archiver: WAITING,失败时自动触发 Sentry 告警,并附带完整的输入数据快照。这一步的成功,建立了团队对“协作可管理”的信心。

4.2 第二步:注入状态与记忆——让协作“记得住”

MVC 解决了“能做”,但没解决“记得做过什么”。真正的持久化协作,需要状态存储。OpenRig 不内置数据库,而是提供标准化的状态接口。我们在archive_flow.yaml中添加:

state: backend: "file" # 支持 file, redis, postgres config: path: "./state/archive_state.json" # 文件后端的存储路径

然后,在jira-watcher的 Python 脚本中,通过 OpenRig SDK 访问状态:

from openrig import get_state_client def main(): state = get_state_client() # 读取上次处理的工单ID,避免重复归档 last_processed_id = state.get("last_jira_issue_id", "JRA-0") # ... 调用Jira API获取新工单 ... # 更新状态 state.set("last_jira_issue_id", newest_issue.key) state.set("last_run_timestamp", datetime.now().isoformat())

这个简单的state配置,让整个协作流拥有了“记忆”。它不再是一个无状态的函数调用链,而是一个有历史、有上下文的实体。后续我们可以基于last_run_timestamp实现增量同步,基于last_jira_issue_id实现幂等性。状态的持久化,是协作从“一次性任务”迈向“长期伙伴”的分水岭。

4.3 第三步:构建可观测性闭环——让协作“看得见、管得住”

一个看不见的协作系统,等于不存在。OpenRig 的可观测性设计围绕三个核心指标展开:

  • 健康度(Health):每个 Agent 的进程是否存在、CPU/Memory 使用率是否超标(通过psutil采集)。
  • 活性(Liveness):每个 Agent 是否在规定时间内产生了有效输出(通过监控其 stdout 中的PROCESSED:日志标记)。
  • 正确性(Correctness):数据流是否符合契约(通过input_validation和output_schema的实时校验)。

我们使用rigctl monitor命令启动一个后台进程,它会:

  • 每 5 秒扫描一次 tmux session,收集各 pane 的进程状态。
  • 解析各 pane 的 stdout,提取PROCESSED: <id>、ERROR: <msg>等结构化日志。
  • 将上述数据聚合为一个 JSON 对象,通过 HTTP POST 推送到内部 Grafana 的 Prometheus Pushgateway。

Grafana 仪表盘上,我们能看到一张实时热力图:绿色表示jira-watcher每 5 分钟成功产出一批工单;黄色表示notion-archiver有短暂延迟(<30s);红色则意味着input_validation失败,需要人工介入。当jira-watcher的 CPU 使用率持续高于 80%,仪表盘会触发告警,提示我们可能需要优化其 Jira API 调用频率。这个闭环,让协作系统从“黑盒”变成了“玻璃盒”。

4.4 第四步:实现协作弹性——让系统“扛得住、自愈快”

最后一步,是赋予协作系统应对现实世界不确定性的能力。这体现在两个层面:

  • Agent 级弹性:利用 YAML 中定义的retry_policy和fallback。例如,notion-archiver在首次调用 Notion API 失败时,会按[1, 5, 15]秒的间隔重试三次;若全部失败,则执行fallback: send-alert,向 Slack 发送告警,并附带完整的失败请求 payload 和响应 body。这避免了单点故障导致整个流程停滞。
  • 流程级弹性:OpenRig 支持flow-level fallback。在archive_flow.yaml的顶层添加:
fallback: action: "replay-from" params: agent_name: "jira-watcher" # 从这个Agent开始重放整个流程 max_replays: 2

这意味着,如果notion-archiver在重试三次后仍失败,OpenRig 不会简单地终止流程,而是会:

  1. 记录本次失败的完整上下文(输入数据、错误堆栈、时间戳)。
  2. 自动重启jira-watcher,让它重新拉取最新工单列表(可能上次失败的工单已被修复)。
  3. 将新获取的工单列表,再次传递给notion-archiver。

这种“流程重放”能力,让协作系统具备了类似人类的“再试一次”智慧。它不追求 100% 的首次成功率,而是追求 100% 的最终交付率。在我们线上环境中,这套弹性机制将协作流程的月度成功率从 92.3% 提升至 99.8%,而人工干预次数下降了 76%。

5. 避坑指南:那些只有亲手部署过才会懂的“血泪经验”

OpenRig 的理念清晰,但落地过程布满细节陷阱。这些不是文档里会写的“注意事项”,而是我在三个不同项目中踩过、记录下来、并反复验证过的“血泪经验”。

5.1 tmux 会话名冲突:一个命名规范救你半夜

问题现象:rigctl start my-flow执行后,rigctl list显示my-flow状态为UNKNOWN,rigctl logs my-flow报错no such session。排查发现,tmux new-session -d -s my-flow命令实际创建了一个名为my-flow:0的 session,而rigctl默认查找的是my-flow。根源在于 tmux 的 session 命名规则:new-session -s name创建的 session 名字就是name,但如果你之前用tmux new-session -d(不指定-s)创建过 session,tmux 会自动分配一个数字名(如0,1),而rigctl的 session 查找逻辑会混淆。

解决方案:强制统一命名规范。在rigctl启动脚本中,永远使用tmux new-session -d -s "openrig-${FLOW_NAME}"。同时,在所有自定义的 tmux 命令(如手动调试)中,也遵循此规范。我们团队约定,所有 OpenRig 相关的 tmux session 必须以openrig-开头。这看似微小,却避免了 90% 的“session 找不到”类故障。

5.2 YAML Schema 校验的“假阳性”:JSON Schema 的隐式类型转换陷阱

问题现象:rigctl validate报错Output of 'feedback-analyzer' does not match input of 'req-extractor',但两个 Schema 看起来完全一致。深入排查发现,feedback-analyzer的 Python 脚本中,sentiment_score是用float(0.85)生成的,而 JSON Schema 中定义为type: "number"。问题在于,当这个 float 被序列化为 JSON 再反序列化时,某些 Python JSON 库(如ujson)会将其转为字符串"0.85",导致下游req-extractor收到的是字符串而非数字,触发 Schema 校验失败。

解决方案:在 Agent 脚本中显式进行类型强转。在feedback-analyzer.py的输出前,添加:

import json output_data = { "sentiment_score": float(sentiment_score), # 确保是float "key_issues": [str(issue) for issue in key_issues], # 确保是string数组 "cleaned_text": str(cleaned_text) } # 使用标准json库序列化,避免ujson等的隐式转换 print(json.dumps(output_data))

同时,在rigctl validate的校验逻辑中,增加对常见类型转换的兼容性检查(如string->number的数字字符串)。这个坑提醒我们:Schema 是契约,但契约的履行者(Agent 脚本)必须严格遵守,不能依赖运行时的“宽容”。

5.3 tmux pane 日志截断:长输出导致关键错误信息丢失

问题现象:agent-tech-draft偶尔失败,但在rigctl logs tech-draft中看不到完整的错误堆栈,只看到末尾几行。原因是 tmux pane 的默认缓冲区(history-limit)太小,只有 2000 行。当 Agent 产生大量日志(如调试模式下的详细 token 流)时,早期的关键错误信息(如ConnectionResetError)被冲刷掉了。

解决方案:全局提升 tmux history-limit。在服务器的~/.tmux.conf中添加:

set -g history-limit 100000

并执行tmux source-file ~/.tmux.conf生效。更重要的是,在rigctl start的启动逻辑中,为每个新创建的 pane 显式设置更大的 history:

tmux new-pane -t "$SESSION:$PANE" -n "$AGENT_NAME" -d "exec bash -c 'export TMUX_HISTORY=100000; $AGENT_CMD'"

这样,即使 Agent 产生 5 万行日志,你也能用Ctrl-b, [进入复制模式,用PageUp翻到最开头,找到那个被忽略的SSL: CERTIFICATE_VERIFY_FAILED错误。日志的完整性,是调试的底线。

5.4 环境变量注入的“作用域污染”:一个变量影响所有 Agent

问题现象:agent-feedback-analyzer需要OPENAI_API_KEY,agent-req-extractor需要NOTION_TOKEN。但当我们把两者都写在archive_flow.yaml的environment下时,agent-req-extractor的进程中也出现了OPENAI_API_KEY环境变量。这本身无害,但如果agent-req-extractor的代码不慎调用了 OpenAI API(比如一个未注释的调试代码),就会造成意外的 API 调用和费用。

解决方案:为每个 Agent 单独定义environment,并启用rigctl的环境隔离模式。在 YAML 中:

agents: - name: "feedback-analyzer" environment: OPENAI_API_KEY: "${OPENAI_API_KEY}" # 不定义 NOTION_TOKEN - name: "req-extractor" environment: NOTION_TOKEN: "${NOTION_TOKEN}" # 不定义 OPENAI_API_KEY

并在rigctl的启动逻辑中,确保每个 pane 的 shell 环境是干净的,只注入该 Agent 明确声明的变量。这需要在tmux new-pane命令中,用env -i清空继承的环境,再用env VAR1=VAL1 VAR2=VAL2注入。一行命令的差异,换来的是环境的纯净和安全的可控。

6. 个人体会:当 AI 协作成为一种“肌肉记忆”

部署 OpenRig 一年后,我最大的感受不是技术上的突破,而是一种工作习惯的悄然转变。以前,当我需要一个新功能,我的第一反应是“写一个脚本”;现在,我的第一反应是“这个功能需要几个角色协作?他们的输入输出契约是什么?谁负责发起,谁负责兜底?”这种思维,已经内化为一种“协作本能”。

最典型的例子是上周的紧急需求:为销售团队生成一份竞品功能对比报告。过去,我会花半天写一个爬虫+LLM 总结的单体脚本。这次,我花了 20 分钟,写了三个 YAML 片段:

  • crawler.yaml:定义从官网抓取 HTML 的契约(输入 URL,输出 raw_html)。
  • parser.yaml:定义从 HTML 中提取功能列表的契约(输入 raw_html,输出 features: [])。
  • comparator.yaml:定义与我司功能对比的契约(输入 features, our_features,输出 markdown_table)。

然后,用rigctl start crawler && rigctl start parser && rigctl start comparator串起来。整个过程,我几乎没有碰 Python 代码,全是和 YAML、tmux pane、日志流打交道。当comparator因模型 token 超限失败时,我Ctrl-b, ↓切进去,手动运行python debug_comparator.py --max-tokens 4096,5 分钟就解决了。报告准时发出,销售团队很满意。

这让我意识到,OpenRig 的终极价值,或许不在于它多“先进”,而在于它多“顺手”。它把多 Agent 协作这件听起来很玄的事,降维成了工程师每天都在做的三件事:写配置(YAML)、看日志(tmux)、调参数(constraints)。它不强迫你接受一套新的编程范式,而是把你已有的技能——阅读文档、调试终端、理解契约——放大到协作的尺度上。当“协作”本身变成了一种可以被熟练书写、阅读、调试、版本化的“代码”,AI 就真的不再是实验室里的玩具,而是我们日常工作流中,一个沉默、可靠、永远在线的同事。