给AI Agent造一个专属终端:从Claude Code到多Agent并行调度的架构实践
发布时间:2026/10/27 9:07:29 锦皓数字建站

1. 从AI 只会聊天到AI 能动手干活的转折点我最早用 Claude Code 的时候最大的别扭感不是它不够聪明而是它被关在盒子里。你让它改个文件它得先问你路径你让它跑个测试它得让你把输出贴回去你想让它同时处理前端和后端两个仓库它只能一个窗口一个窗口地等你切过去。这种体验就像请了个很厉害的工程师但把他锁在会议室里所有信息都得靠你人工传递。后来我把这个痛点拆开看发现本质上是三个问题叠在一起第一AI Agent 缺少一个稳定的、可编程的终端执行层它没法像人一样打开终端、敲命令、看回显、判断结果第二单个 Agent 的上下文窗口和注意力是有限的一个 Agent 同时管五件事必然顾此失彼第三多个 Agent 之间没有共享的工作台各自为战互相不知道对方在干什么。我做的这个项目就是给 AI Agent 写一个专属终端。它不是给人类用的终端模拟器而是给 Agent 用的操作台——Claude Code 可以通过它直接执行终端命令、读取结构化回显、感知命令状态同时我还能在这个终端里并行调度多个 Agent让它们各管一摊、互不打架。关键词里的AI Agent、Claude Code、终端、MCP、Agent这五个词基本就是这个项目的全部骨架。这篇文章适合三类人看一是已经在用 Claude Code 但觉得手脚被绑住的开发者二是正在搭 AI Agent 工作流、卡在怎么让 Agent 真正操作环境这一步的人三是对 MCP 协议感兴趣、想搞清楚它到底解决什么问题的人。我会把架构选型、终端复用逻辑、多 Agent 调度、MCP 接入细节、以及我踩过的坑全部摊开讲代码和配置能给的都给尽量让你看完能直接复现。先说结论这个终端不是又一个 Tabby 或者 iTerm2它的定位是Agent 的执行后端。人类终端追求的是交互体验Agent 终端追求的是可解析、可并发、可回放、可隔离。这四个词决定了后面所有的设计取舍。2. 为什么现成的终端工具喂不饱 Agent2.1 人类终端和 Agent 终端的根本差异我一开始的想法很朴素既然 Claude Code 能执行命令那我直接让它调用系统 shell 不就行了实测下来问题一大堆。人类终端的设计前提是有个活人在看屏幕所以它输出的是带 ANSI 转义序列的彩色文本、有光标闪烁、有交互式提示符等待输入。这些对人类友好对 Agent 就是灾难——Agent 拿到一堆\x1b[32m之类的控制字符根本没法稳定解析。我拿 Tabby 终端工具做过对比测试它的渲染层做得非常漂亮但它的输出是面向显示优化的不是面向解析优化的。Agent 需要的是结构化的命令结果这条命令退出码是多少、标准输出是什么、标准错误是什么、执行耗时多久、当前工作目录在哪、有没有产生子进程。这些信息在人类终端里要么被吞掉要么藏在渲染层里拿不到。所以第一个核心设计决策就出来了Agent 终端必须把执行和呈现彻底分离。执行层负责跑命令、抓状态、管生命周期呈现层才是给人看的。Claude Code 只跟执行层打交道拿到的永远是干净的结构化数据。2.2 交互式命令是最大的拦路虎真正让我头疼的是交互式命令。比如npm init会问你一堆问题ssh会等你输密码vim会进入全屏模式。人类终端天然支持这些因为人能实时响应。但 Agent 执行命令是发出去等结果的模式一旦命令卡在等待输入整个流程就死锁了。我的处理方案是给命令执行加超时和输入注入机制。每条命令执行时终端会维护一个状态机运行中、等待输入、已完成、超时、被中断。如果检测到命令在等待输入通过分析输出末尾是否有提示符特征Agent 可以选择注入预设输入或者直接判定为需要人工介入并抛出事件。这个机制听起来简单但实现时踩了不少坑后面第 5 节会详细讲。2.3 多 Agent 场景下终端必须支持会话隔离单个 Agent 用一个终端还好说多个 Agent 同时跑就麻烦了。如果它们共享一个 shell 会话环境变量会互相污染工作目录会互相干扰一个 Agent 跑了cd /tmp另一个 Agent 的下一条命令就在错误目录里执行了。我试过用多个独立进程来隔离但进程启动开销大而且 Agent 之间想共享一些状态比如我已经装好了依赖又变得很麻烦。最后我采用的是会话Session 工作区Workspace两级隔离每个 Agent 绑定一个会话会话内部有独立的环境变量、工作目录、命令历史多个会话可以挂在同一个工作区下共享文件系统和部分缓存。这样既隔离又协作实测下来是最平衡的方案。提示会话隔离的粒度要按任务划分不要按Agent 数量划分。一个 Agent 处理多个任务时也应该为每个任务开独立会话否则上下文会串。3. 终端核心架构从命令下发到结果回传的完整链路3.1 整体分层设计这个终端的架构我改了三版最终定型为四层层级职责关键技术点接入层接收 Agent 请求暴露 MCP 接口MCP 协议、JSON-RPC调度层会话管理、命令排队、并发控制会话池、优先级队列执行层真正跑命令、抓状态PTY、进程组管理状态层命令历史、结果缓存、回放结构化日志、快照接入层用 MCP 协议对外暴露能力这是关键。MCP 是什么简单说它是一套让 AI 模型和外部工具对话的标准协议模型通过它调用工具、拿结果。Claude Code 原生支持 MCP所以我把终端能力包装成 MCP 工具后Claude Code 不需要任何改造就能直接调用。这也是为什么关键词里 MCP 和 Claude Code 会绑在一起——MCP 是 Agent 和终端之间的插座标准。调度层是我花时间最多的地方。它要解决的核心问题是多个 Agent 同时发命令时怎么保证不互相踩踏又怎么保证高优先级任务先执行。我用的是会话池 每会话一个命令队列的设计会话之间完全并行会话内部串行。这样既避免了同一会话内的竞态又保证了跨会话的吞吐。3.2 为什么用 PTY 而不是普通管道执行层有个关键选择用普通管道pipe还是伪终端PTY。我一开始图省事用了 pipe结果发现很多命令的行为会变。比如ls在 pipe 模式下不输出颜色这倒无所谓但有些命令会检测到输出不是终端而切换到非交互模式甚至拒绝执行。更麻烦的是pipe 模式下拿不到命令的实时输出只能等命令结束一次性读取这对长时间运行的命令比如构建、测试非常不友好。换成 PTY 之后命令以为自己在一个真终端里跑行为完全正常而且输出是流式的可以实时抓取。代价是 PTY 会带 ANSI 转义序列需要在状态层做一次清洗。我的做法是保留原始输出用于回放同时生成一份清洗后的纯文本给 Agent 用。这样两边都不耽误。3.3 命令状态机的设计命令从下发到结束我定义了六个状态Pending已入队等待执行Running正在执行AwaitingInput检测到等待输入Completed正常结束Failed非零退出码Timeout超时被终止状态之间的转换不是靠猜而是靠组合信号判断进程是否存活、退出码、输出末尾特征、执行时长。其中AwaitingInput的判定最 tricky我用的是输出在最近 N 毫秒内停止增长 末尾匹配提示符正则的双条件。单靠任何一个都会误判比如一个命令在思考CPU 密集但没输出会被误判为等待输入加上时间窗口后就稳多了。# 命令状态判定的核心逻辑简化版 def detect_state(proc, last_output_time, output_buffer): if not proc.poll() is None: return Completed if proc.returncode 0 else Failed idle_ms now() - last_output_time if idle_ms IDLE_THRESHOLD and matches_prompt(output_buffer): return AwaitingInput if now() - proc.start_time TIMEOUT: return Timeout return Running这段逻辑看着简单但IDLE_THRESHOLD和matches_prompt的调参花了我好几天。阈值太短会误判太长会让 Agent 干等。最后我把它做成了可配置项不同场景用不同值。4. 多 Agent 并行调度让 Claude Code 指挥一支小队4.1 多 Agent 不是简单开多个窗口很多人理解的多 Agent 就是多开几个 Claude Code 窗口这其实是最低效的做法。真正的多 Agent 调度要解决三个问题任务怎么分、状态怎么同步、冲突怎么解。我的设计里有一个主 Agent负责拆解任务和分派多个工作 Agent负责执行。主 Agent 通过终端暴露的调度接口把任务下发给不同的工作 Agent每个工作 Agent 绑定一个独立会话。主 Agent 不需要知道工作 Agent 具体怎么执行只需要拿到最终结果。这其实就是经典的 orchestrator-worker 模式关键词里的agent 主流架构说的就是这类。任务分派我用的不是简单的轮询而是基于能力标签的匹配。每个工作 Agent 注册时会声明自己擅长什么比如前端构建数据库迁移日志分析主 Agent 分派任务时按标签匹配。这样避免了让一个只会跑测试的 Agent 去改配置文件这种错配。4.2 会话间的状态同步机制多 Agent 并行最大的坑是状态不同步。举个例子Agent A 装了一个依赖Agent B 不知道又装了一遍浪费时间和磁盘或者 Agent A 改了某个配置文件Agent B 还在用旧配置跑测试结果全是假失败。我的解法是在工作区层面维护一个共享状态板记录关键事件依赖安装、文件变更、服务启动、端口占用。每个 Agent 执行命令前可以查询状态板执行后可以更新状态板。状态板不是强一致的而是最终一致的——允许短暂的不一致但通过事件通知机制尽快收敛。这个取舍很重要强一致会拖慢并发最终一致在大多数开发场景下够用。{ workspace_id: proj-alpha, shared_state: { dependencies_installed: [react, vite, vitest], ports_in_use: [3000, 5173], files_modified: [src/App.tsx, vite.config.ts], services_running: [dev-server] } }Agent 拿到这个状态板就知道依赖已经装好了我不用再装3000 端口被占了我换个端口。这一下就省掉了大量重复劳动和冲突。4.3 冲突检测与自动退避即便有状态板冲突还是会发生。最常见的是两个 Agent 同时想改同一个文件或者同时想占用同一个端口。我的处理是乐观执行 冲突检测 自动退避允许 Agent 先干干的时候记录它动了哪些资源如果检测到资源冲突后到的 Agent 会被要求退避重试。退避策略我用的是指数退避加随机抖动避免多个 Agent 同时重试又撞在一起。实测下来三到五个 Agent 并行时冲突率能控制在很低水平而且大部分冲突都能自动化解不需要人工干预。注意自动退避要有上限重试超过 N 次还没成功就应该上报给主 Agent 或人类而不是无限重试。我设的上限是 5 次。5. MCP 接入实战把终端能力插进 Claude Code5.1 MCP 工具的定义与注册MCP 的核心是工具Tool这个概念。我要把终端能力暴露给 Claude Code就得把每个能力定义成一个 MCP 工具。我定义了这么几个核心工具terminal_execute执行一条命令返回结构化结果terminal_session_create创建一个新会话terminal_session_list列出所有活跃会话terminal_input_send向等待输入的命令发送输入terminal_state_query查询工作区共享状态每个工具都有明确的输入 schema 和输出 schema。这里有个经验schema 要尽量窄不要设计成万能工具。我一开始想做一个terminal_do_everything工具参数一大堆结果 Claude Code 经常传错参数。拆成多个专用工具后调用准确率明显提升。{ name: terminal_execute, description: 在指定会话中执行一条 shell 命令, inputSchema: { type: object, properties: { session_id: {type: string}, command: {type: string}, timeout_ms: {type: integer, default: 30000}, cwd: {type: string} }, required: [session_id, command] } }5.2 Claude Code 侧的配置Claude Code 接入 MCP 服务需要在配置里声明服务地址和启动方式。我这边终端是以本地服务形式跑的Claude Code 通过标准输入输出或者本地端口跟它通信。配置好之后Claude Code 启动时会自动拉取工具列表然后在需要的时候调用。这里有个容易忽略的点工具描述description的质量直接决定 Claude Code 会不会正确使用它。描述要写清楚这个工具干什么、什么时候用、参数什么意思、返回什么。我一开始描述写得太简略Claude Code 经常该用的时候不用或者用错工具。把描述写详细之后命中率大幅提升。5.3 流式输出的处理长时间运行的命令输出是流式的。MCP 本身支持流式返回但 Claude Code 对流的消费方式有讲究。我的做法是命令执行过程中终端持续把输出块推给 Claude CodeClaude Code 可以选择实时展示也可以攒着等命令结束再处理。对于构建、测试这类命令实时展示很有用用户能看到进度对于快速命令攒着处理更高效。流式输出还有个好处是可以提前终止。如果 Claude Code 从输出里发现命令已经出错了比如看到 Error 关键字它可以主动中断命令不用等它跑完。这在跑长测试时特别省时间。6. 那些文档里不会写的踩坑记录6.1 环境变量污染导致的幽灵 bug有一次我调试了整整一个下午发现 Agent B 跑测试总是失败但同样的命令我在 Agent A 的会话里手动跑就成功。排查到最后发现是环境变量污染Agent A 在某个命令里export了一个变量而这个变量通过某种方式泄漏到了 Agent B 的会话。根因是我早期版本的会话隔离做得不彻底子进程继承了父进程的环境。修复方案是每个会话启动时用干净的环境变量快照会话内的修改只在本会话生效。这个坑的教训是隔离要隔离到底不能有大部分隔离了这种中间状态。6.2 命令超时设置的两难超时设太短正常的长命令会被误杀设太长卡死的命令会拖垮整个调度。我最后的方案是分级超时 动态调整快速命令默认 30 秒构建类命令 5 分钟测试类命令 10 分钟同时允许 Agent 显式指定超时。另外加了一个心跳检测如果命令在持续输出即使超过基础超时也允许继续如果完全没输出又超时就果断杀掉。6.3 输出编码的坑有些命令的输出不是 UTF-8比如某些老工具输出 GBK或者二进制文件被误当文本读。直接按 UTF-8 解码会抛异常或者产生乱码。我的处理是先按字节流抓取再做编码探测探测失败就用替换字符兜底同时把原始字节保留下来供需要时回查。这个细节很小但不处理的话Agent 会拿到一堆乱码然后基于乱码做出错误判断。6.4 会话泄漏早期版本我忘了在 Agent 任务结束后清理会话结果跑了一晚上系统里堆了几百个僵尸会话内存直接爆了。后来加了会话生命周期管理会话有 TTL超时自动回收Agent 显式结束时立即回收系统定期扫描孤儿会话。这个坑很典型任何资源池都要有回收机制不能只借不还。7. 实测效果与几个真实使用场景7.1 场景一全栈项目的并行开发我拿一个前后端分离的项目做测试。主 Agent 把任务拆成前端组件开发后端接口开发数据库迁移三块分派给三个工作 Agent。三个 Agent 各自在独立会话里干活通过共享状态板同步接口契约和数据库 schema。实测下来原本串行要两小时的工作并行后四十分钟左右完成而且因为状态同步到位联调时几乎没有接口对不上的问题。7.2 场景二大规模测试的并行执行一个项目有上千个测试用例串行跑要很久。我让主 Agent 把测试按文件分组分派给多个工作 Agent 并行跑每个 Agent 负责一组。终端负责收集所有 Agent 的结果并汇总。这里的关键是结果汇总要能区分真失败和环境导致的失败否则并行跑出来的失败列表会误导人。我在状态层加了失败分类逻辑把环境问题单独标出来。7.3 场景三长时间任务的监控与干预有些任务要跑很久比如数据迁移、大规模构建。以前我只能干等现在 Claude Code 可以通过终端实时监控输出发现异常就中断或者注入输入继续。有一次一个迁移脚本卡在确认提示上Claude Code 检测到AwaitingInput状态自动注入了确认输入任务顺利继续。这种AI 自己看着办的体验是单靠人类终端做不到的。8. 关于 Agent 安全边界的一点个人看法能力越大越要划边界。这个终端让 Agent 能执行任意命令这本身就是个风险点。我在设计时加了几道防线命令白名单/黑名单危险命令如rm -rf /直接拦截、资源配额单会话 CPU、内存、磁盘写入上限、操作审计所有命令和结果留痕可回溯、敏感路径保护系统目录、密钥文件禁止访问。这些不是可选项是必选项。我见过太多人为了图方便把限制全关了结果 Agent 一个误操作把重要文件删了。Agent 安全这个事宁可麻烦一点也不要事后后悔。关键词里agent 安全能成为热词说明大家都意识到这个问题了。9. 后续可以继续深挖的方向这个终端目前跑得挺稳但还有几个我想继续做的方向。一是命令的语义理解现在 Agent 拿到的是原始输出如果能自动把输出解析成结构化数据比如把测试输出解析成通过/失败/跳过的统计Agent 用起来会更省心。二是跨机器的会话调度现在会话都在本机如果能调度到多台机器上并行能力还能再上一个台阶。三是和更多 Agent 框架的对接MCP 是个好标准但不同框架的接入细节还有差异做成通用的适配层会更有价值。如果你也在做类似的东西我的建议是先从单 Agent 单会话跑通再逐步加并发和隔离不要一上来就搞复杂架构。我第一版就是贪大求全结果调试了两周才跑通最基本的功能。先把一条命令的执行链路做扎实再谈多 Agent 调度这个顺序不能反。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。