AI Agent 工作流编排实战:从单 Agent 到多 Agent,手搭一套能跑通的协作系统

上个月我搭了个单 Agent 跑自动化测试,效果还行。然后我天真地想着,"再加两个 Agent 并行干活,效率不就三倍了吗?"

结果是三个 Agent 抢着改同一个文件,一个说要升级依赖版本,另一个说把那个版本锁死------项目直接崩了。

这就是单 Agent 到多 Agent 的"死亡率最高"的一步。不是模型不够聪明,是没有编排的大脑

我花了两周时间,把市面上能用的编排方案翻了个底朝天,最后用 OpenClaw + MCP 搭了一套能跑通的全流程方案。这篇就是完整的手把手教程------从架构设计到代码实现,从单 Agent 到三 Agent 协作,步步有代码。

先搞清楚:多 Agent 到底需要什么

单 Agent 的核心问题是上下文窗口:一个 prompt 装不下所有事。

多 Agent 的核心问题是协调:多个"大脑"各自决策,需要一个仲裁者。

我总结出三个逃不掉的组件:

组件 职责 类比
Orchestrator(编排器) 拆解任务、分派给子 Agent、汇总结果 项目经理
Memory Store(记忆存储) 跨会话持久化上下文、决策历史和中间结果 项目文档
Tool Registry(工具注册表) 统一管理 Agent 能调用的工具和服务 IT 资产清单

你说的那三个 Agent 为什么打架?因为每个 Agent 都有自己"觉得对的"判断,但没有一个统一的记忆层告诉它们------"十分钟前隔壁兄弟刚改了这个文件"。

Step 1:搭记忆层------用 MCP Server 做统一存储

所有编排的起点是记忆。先搭一个轻量级 MCP 记忆服务器,让所有 Agent 共享同一块"黑板"。

python 复制代码
# mcp_memory_server.py --- 基于 SQLite + 向量的轻量记忆服务器
import json
import sqlite3
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler

DB_PATH = "agent_memory.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS memories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            key TEXT UNIQUE,
            value TEXT,
            tags TEXT,
            created_at TEXT,
            updated_at TEXT
        )
    """)
    conn.execute("""
        CREATE INDEX IF NOT EXISTS idx_key ON memories(key)
    """)
    conn.commit()
    conn.close()

class MemoryHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = json.loads(self.rfile.read(length))
        
        if self.path == '/store':
            conn = sqlite3.connect(DB_PATH)
            now = datetime.now().isoformat()
            conn.execute(
                "INSERT OR REPLACE INTO memories (key, value, tags, created_at, updated_at) "
                "VALUES (?, ?, ?, COALESCE((SELECT created_at FROM memories WHERE key=?), ?), ?)",
                (body['key'], json.dumps(body['value']), 
                 json.dumps(body.get('tags', [])),
                 body['key'], now, now)
            )
            conn.commit()
            conn.close()
            self._respond({"status": "ok"})
        
        elif self.path == '/search':
            conn = sqlite3.connect(DB_PATH)
            query = f"%{body.get('query', '')}%"
            rows = conn.execute(
                "SELECT key, value, tags, updated_at FROM memories "
                "WHERE key LIKE ? OR value LIKE ? OR tags LIKE ? LIMIT 10",
                (query, query, query)
            ).fetchall()
            conn.close()
            results = [{"key": r[0], "value": json.loads(r[1]), 
                       "tags": json.loads(r[2]), "updated_at": r[3]} for r in rows]
            self._respond({"results": results})
    
    def _respond(self, data, code=200):
        self.send_response(code)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps(data).encode())

if __name__ == '__main__':
    init_db()
    server = HTTPServer(('localhost', 8090), MemoryHandler)
    print("Memory server running on :8090")
    server.serve_forever()

不到 80 行代码,一个可用的记忆服务器就启动了。这个服务器支持两个操作:

  • POST /store --- 写入记忆(key-value + tags)
  • POST /search --- 全文搜索记忆

启动它:

bash 复制代码
python3 mcp_memory_server.py &

测试写入和读取:

bash 复制代码
# 写入一条记忆
curl -X POST http://localhost:8090/store \
  -H "Content-Type: application/json" \
  -d '{"key": "project:ecommerce:db_config", "value": {"host": "localhost", "port": 5432, "db": "shop"}, "tags": ["postgres", "ecommerce"]}'

# 搜索记忆
curl -X POST http://localhost:8090/search \
  -H "Content-Type: application/json" \
  -d '{"query": "ecommerce"}'

有这个记忆层之后,多个 Agent 就不会"各自为政"了。它们写入和读取的是同一个数据源。

Step 2:写编排器(Orchestrator)

有了记忆,下一步是编排器。它不直接做具体工作------它负责把任务拆成子任务、派给子 Agent、收集结果、写回记忆。

完整的解决方案就在后半部分,包含可直接复用的代码模板【关注后可见】📦

python 复制代码
# orchestrator.py --- 任务编排器
import json
import urllib.request
import time

MEMORY_URL = "http://localhost:8090"

class Orchestrator:
    def __init__(self, workflow_id: str):
        self.workflow_id = workflow_id
        self.state = {"tasks": [], "results": {}, "errors": []}
    
    def define_workflow(self, tasks: list):
        """定义工作流步骤"""
        self.state["tasks"] = tasks
        # 把工作流定义写入记忆
        self._store(f"workflow:{self.workflow_id}:definition", {
            "tasks": tasks,
            "created_at": time.time()
        })
    
    def execute(self, agent_executor):
        """按顺序执行任务"""
        for i, task in enumerate(self.state["tasks"]):
            print(f"[{i+1}/{len(self.state['tasks'])}] 执行: {task['name']}")
            
            # 1. 从记忆读取上下文
            context = self._search(task.get("context_query", ""))
            
            # 2. 执行任务
            result = agent_executor(task, context)
            
            # 3. 保存结果到记忆
            self.state["results"][task["name"]] = result
            self._store(f"workflow:{self.workflow_id}:result:{task['name']}", {
                "result": result,
                "completed_at": time.time()
            })
            
            # 4. 检查是否需要继续
            if result.get("status") == "blocked":
                print(f"  ⚠️ {task['name']} 被阻塞,需要人工介入")
                self.state["errors"].append({
                    "task": task["name"],
                    "reason": result.get("reason", "unknown")
                })
                break
        
        # 5. 汇总写入最终记忆
        self._store(f"workflow:{self.workflow_id}:final", {
            "total_tasks": len(self.state["tasks"]),
            "completed": len(self.state["results"]),
            "errors": self.state["errors"],
            "status": "complete" if not self.state["errors"] else "partial"
        })
        return self.state
    
    def _store(self, key, value):
        data = json.dumps({"key": key, "value": value, "tags": [self.workflow_id]}).encode()
        urllib.request.urlopen(f"{MEMORY_URL}/store", data=data)
    
    def _search(self, query):
        if not query:
            return []
        data = json.dumps({"query": query}).encode()
        resp = urllib.request.urlopen(f"{MEMORY_URL}/search", data=data)
        return json.loads(resp.read()).get("results", [])

这个编排器实现了"定义 → 执行 → 存储"的闭环。每个子 Agent 的执行结果被自动保存到记忆服务器,后续步骤可以随时查看上下文。

使用示例:

python 复制代码
# 定义一个三步骤工作流
orch = Orchestrator("deploy-v2")
orch.define_workflow([
    {"name": "代码审查", "context_query": "project:规范", "agent": "reviewer"},
    {"name": "运行测试", "context_query": "project:配置", "agent": "tester"},
    {"name": "部署预览", "context_query": "workflow:deploy-v2:result:运行测试", "agent": "deployer"},
])

Step 3:子 Agent 接入 MCP 工具

编排器有了,子 Agent 需要能调用工具。MCP 协议在这里发挥作用------每个子 Agent 通过 MCP 访问记忆服务器和其他工具。

在 OpenClaw 的 config.yaml 中注册 MCP 记忆工具:

yaml 复制代码
# ~/.config/openclaw/config.yaml
mcpServers:
  memory:
    command: python3
    args:
      - /path/to/mcp_memory_client.py
    env:
      MEMORY_SERVER_URL: "http://localhost:8090"
  
  filesystem:
    command: npx
    args:
      - -y
      - "@modelcontextprotocol/server-filesystem"
      - /home/ubuntu/projects

对应的 MCP 客户端封装:

python 复制代码
# mcp_memory_client.py --- 将记忆服务器包装为 MCP 工具
import json
import sys
import urllib.request

MEMORY_URL = "http://localhost:8090"

def handle_request(request):
    method = request.get("method", "")
    
    if method == "tools/list":
        return {
            "tools": [
                {
                    "name": "memory_store",
                    "description": "存储记忆到持久化层",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "key": {"type": "string"},
                            "value": {"type": "object"},
                            "tags": {"type": "array", "items": {"type": "string"}}
                        }
                    }
                },
                {
                    "name": "memory_search",
                    "description": "搜索已有记忆",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "query": {"type": "string"},
                            "limit": {"type": "number", "default": 5}
                        }
                    }
                }
            ]
        }
    
    elif method == "tools/call":
        tool = request["params"]["name"]
        args = request["params"]["arguments"]
        
        if tool == "memory_store":
            data = json.dumps({
                "key": args["key"],
                "value": args["value"],
                "tags": args.get("tags", [])
            }).encode()
            urllib.request.urlopen(f"{MEMORY_URL}/store", data=data)
            return {"content": [{"type": "text", "text": "stored"}]}
        
        elif tool == "memory_search":
            data = json.dumps({"query": args["query"]}).encode()
            resp = urllib.request.urlopen(f"{MEMORY_URL}/search", data=data)
            results = json.loads(resp.read()).get("results", [])
            return {"content": [{"type": "text", "text": json.dumps(results, indent=2)}]}

# 标准 MCP stdio 协议
if __name__ == "__main__":
    while True:
        line = sys.stdin.readline()
        if not line:
            break
        request = json.loads(line)
        response = handle_request(request)
        response["id"] = request.get("id")
        sys.stdout.write(json.dumps(response) + "\n")
        sys.stdout.flush()

这个 MCP 客户端通过 stdio 与 OpenClaw 通信,提供 memory_storememory_search 两个工具。子 Agent 可以在 prompt 中随意调用它们。

Step 4:实战------三 Agent 协作完成代码迁移

理论说够了,来一个真实的编排案例。假设我们要把项目从 JavaScript 迁移到 TypeScript,分为三步走。

python 复制代码
# migration_workflow.py --- 完整的多Agent编排
from orchestrator import Orchestrator
import subprocess
import json

def run_agent(agent_name: str, task: dict, context: list):
    """调用 OpenClaw 执行子任务"""
    prompt = f"""
    你是一个 {agent_name} Agent。
    你的任务: {task['name']}
    上下文: {json.dumps(context, indent=2)[:500]}
    
    请完成任务并返回 JSON: {{"status": "ok|blocked", "summary": "...", "files_changed": [...]}}
    """
    
    # 用 OpenClaw CLI 执行
    result = subprocess.run(
        ["openclaw", "run", "-p", prompt],
        capture_output=True, text=True, timeout=120
    )
    
    try:
        return json.loads(result.stdout.strip())
    except:
        return {"status": "blocked", "reason": f"Agent 输出解析失败: {result.stdout[:200]}"}

# 定义工作流
orch = Orchestrator("js-to-ts-migration")
orch.define_workflow([
    {
        "name": "扫描项目结构,列出所有需要迁移的 JS 文件",
        "context_query": "project:目录结构",
        "agent": "scanner"
    },
    {
        "name": "分析依赖关系,确定迁移顺序",
        "context_query": "project:依赖关系",
        "agent": "analyzer"
    },
    {
        "name": "逐个文件从 JS 迁移到 TS,添加类型定义",
        "context_query": "workflow:js-to-ts-migration:result:分析依赖关系",
        "agent": "migrator"
    }
])

# 执行(依次启动子 Agent)
state = orch.execute(lambda task, ctx: run_agent(task["agent"], task, ctx))

print(f"✅ 完成 {len(state['results'])}/{len(state['tasks'])} 个任务")
if state["errors"]:
    print(f"❌ {len(state['errors'])} 个错误需要处理")

三个 Agent 各司其职:

Agent 职责 输入 输出
Scanner 扫描项目 项目路径 文件清单 + 类型统计
Analyzer 分析依赖 文件清单 迁移顺序图
Migrator 逐个迁移 迁移顺序 + 记忆 类型定义文件

关键的设计决策:Analyzer 的输出自动存入记忆,Migrator 从记忆读取 Analyzer 的结果。这就避免了那个"三个 Agent 抢文件"的问题。

Step 5:状态持久化------让工作流"断电不停"

多 Agent 工作流的一个常见场景是跑着跑着 Agent 崩了(或者你关掉了终端)。如何在恢复后继续?

python 复制代码
# 恢复工作流 --- 从记忆读取上一个状态
def resume_workflow(workflow_id: str):
    """从中断点恢复工作流"""
    orch = Orchestrator(workflow_id)
    
    # 从记忆读取工作流定义
    final = orch._search(f"workflow:{workflow_id}:final")
    if final:
        print(f"ℹ️ 工作流 {workflow_id} 已完成,跳过恢复")
        return None
    
    # 读取已完成的步骤
    completed_results = orch._search(f"workflow:{workflow_id}:result:")
    
    # 读取工作流定义
    definition = orch._search(f"workflow:{workflow_id}:definition")
    if not definition:
        print("❌ 找不到工作流定义")
        return None
    
    tasks = json.loads(definition[0]["value"])["tasks"]
    completed_count = len(completed_results)
    
    print(f"🔄 恢复工作流: 已完成 {completed_count}/{len(tasks)}")
    remaining_tasks = tasks[completed_count:]
    
    # 从断点继续执行
    orch.state["tasks"] = remaining_tasks
    orch.state["results"] = {t["name"]: json.loads(r["value"]) 
                              for t, r in zip(tasks[:completed_count], completed_results)}
    
    return orch

# 使用
orch = resume_workflow("js-to-ts-migration")
if orch:
    agent_fn = lambda task, ctx: run_agent(task["agent"], task, ctx)
    state = orch.execute(agent_fn)

这段代码的关键是:所有中间结果都通过 _store 写入了记忆服务器。所以即使主进程挂了,重启后读取记忆就能知道"干到哪了、干了什么、下一步是什么"。

架构总览

把以上组件串起来,完整的架构图如下:

复制代码
┌─────────────────────────────────────────────────────┐
│                   Orchestrator                       │
│  拆任务 → 派 Agent → 收结果 → 决定下一步              │
└────┬────────┬────────┬────────┬──────────────────────┘
     │        │        │        │
     ▼        ▼        ▼        ▼
┌────────┐┌────────┐┌────────┐┌──────────────┐
│Agent 1 ││Agent 2 ││Agent 3 ││MCP Tool 注册  │
│Scanner ││Analyzer││Migrator││memory/filesys │
└───┬────┘└───┬────┘└───┬────┘└──────┬───────┘
    └──────────┴──────────┴─────────────┘
                    │
                    ▼
           ┌────────────────┐
           │  MCP Memory    │
           │  Server (:8090)│
           │  SQLite + Tags │
           └────────────────┘

所有 Agent 读写同一个记忆后端,Orchestrator 控制流程,MCP 提供工具接口。

踩过的坑

坑 1:Agent 之间锁竞争

两个 Agent 同时 memory_store 同一个 key,后写覆盖先写。解法很简单------在编排器中加一个写锁

python 复制代码
import threading
_write_lock = threading.Lock()

def safe_store(self, key, value):
    with _write_lock:
        self._store(key, value)

坑 2:Agent 上下文爆炸

每个 Agent 在执行前会加载所有记忆,但如果记忆太多,prompt 会爆炸。需要在编排器里加一个记忆摘要层

python 复制代码
def summarize_context(self, results: list, max_tokens=2000):
    """压缩记忆上下文"""
    total = sum(len(json.dumps(r)) for r in results)
    if total > max_tokens:
        # 只保留最近的3条结果
        return results[-3:]
    return results

坑 3:Agent "忘记"调用工具

不是每个 Agent 都记得调用 memory_search。我加了一条系统 prompt 指令来完成这个任务:

复制代码
在你执行任何操作前,先调用 memory_search 检查是否有相关上下文。
完成操作后,调用 memory_store 保存结果。

适合什么场景

这套架构不是万能的,但它解决了一个真实的问题:你的 Agent 不需要每次都在空白状态下工作

场景 适合 不适合
代码迁移/重构 ✅ 分步执行,记忆衔接 ❌ 实时聊天(延迟高)
CI/CD 流水线 ✅ 按步骤执行+断点续传 ❌ 毫秒级响应要求
数据管道处理 ✅ 分阶段处理+中间结果持久化 ❌ 单次简单查询
多 Agent 并行开发 ✅ 协调多个 AI 编码助手 ❌ Agent 之间频繁通信

写在最后

回到开头那个"三个 Agent 打架"的故事。问题的根源不是 Agent 不够聪明,是没有给它们一套协同的规则

我搭完这套系统之后,最大的感受不是"哇三倍效率",而是------每个 Agent 都知道自己在干什么,以及别人干了什么。这种感觉比单纯的效率提升重要得多:Agent 不再是孤立运行的"黑箱",而是被编排进了可观察、可恢复、可控制的工作流。

如果你已经在用单 Agent 做日常开发,下一步不是"再加一个 Agent",而是先搭好记忆层。有了共享记忆,多 Agent 协作就是一个顺理成章的结果。

下一步我会尝试把 LLM 本身的推理也纳入编排------让 Orchestrator 不只是"按顺序派活",而是根据子 Agent 的反馈动态调整任务分解策略。这个坑挖好了,下次填。

延伸阅读:Dynamic Workflows教程:Claude Opus 4.8的编排机制Kimi Work 300Agent实测:多Agent并行的真实体验


📌 系列文章

看完有收获?点个关注 👆 我会持续分享更多AI编程实战教程。

相关推荐
石一峰6993 小时前
SQLite 与 db_manager 集成关键概念详解
jvm·数据库·sqlite
布朗克16820 小时前
34 JVM深入理解
java·jvm
eggrall21 小时前
Linux线程:并发编程的双刃剑
jvm
程序员晨曦1 天前
深入浅出JVM内存结构
jvm·面试·职场和发展
cfm_29141 天前
JVM对象创建与内存分配机制深度解析
jvm
wuminyu1 天前
Java锁膨胀机制之偏向锁到轻量级锁源码剖析
java·linux·c语言·jvm·c++
cfm_29141 天前
JVM内存模型深度剖析与性能优化
jvm·性能优化
cfm_29141 天前
JVM对象逃逸分析深度详解
java·开发语言·jvm
Full Stack Developme1 天前
JVM 与 Linux 交互的核心原理
linux·运维·jvm