用OpenClaw的架构思路,我给公司搭了一套内部AI Agent系统,替代了3个外包岗位

用OpenClaw的架构思路,我给公司搭了一套内部AI Agent系统,替代了3个外包岗位

背景:为什么不直接用OpenClaw

先说结论:OpenClaw本身不适合直接丢进企业生产环境。

原因有三:应用层沙箱扛不住安全审计(CVE-2026-25253已经说明问题)、单体架构没法做高可用、记忆系统的本地文件方案不支持多实例共享。

但它的架构思路------消息驱动、三层记忆、工具调用链------确实是目前个人AI Agent框架里设计得最清晰的。我把这套思路抽出来,用公司现有的技术栈重新实现了一版,跑在内部的钉钉群里,接管了三个外包同事之前干的活:客户工单自动分类、日报周报自动汇总、竞品价格监控。

这篇文章把整个过程拆开讲,包括架构设计、核心模块实现、踩过的坑和最终的性能数据。


架构设计

整体思路

直接上架构图:

scss 复制代码
┌──────────────────────────────────────────────────────────┐
│                     Channel Adapter                       │
│              (钉钉Bot / 企微Bot / HTTP API)                │
└─────────────────────────┬────────────────────────────────┘
                          │ 消息事件
                          ▼
┌──────────────────────────────────────────────────────────┐
│                    Message Router                         │
│         (会话管理 / 限流 / 消息队列 / 状态机)              │
│         ┌─────────────┐                                   │
│         │  BullMQ队列  │  ← OpenClaw用进程内事件循环       │
│         └─────────────┘    我们换成了持久化消息队列         │
└─────────────────────────┬────────────────────────────────┘
                          │ 任务分发
                          ▼
┌──────────────────────────────────────────────────────────┐
│                   Agent Runtime Pool                      │
│         (多Worker进程 / 上下文组装 / 模型调用)             │
│                                                           │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                  │
│  │ Worker 1 │  │ Worker 2 │  │ Worker 3 │  ← 可水平扩展   │
│  └─────────┘  └─────────┘  └─────────┘                  │
└─────────────────────────┬────────────────────────────────┘
                          │ 工具调用 / 记忆读写
                          ▼
┌──────────────────────────────────────────────────────────┐
│                  Infrastructure Layer                     │
│                                                           │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ 记忆系统  │  │ 工具沙箱  │  │ Skill注册│  │ 监控告警  │ │
│  │ (PG+pgvec)│  │ (Docker)  │  │  中心    │  │(Prometheus│ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
└──────────────────────────────────────────────────────────┘

跟OpenClaw原版架构的关键区别:

模块 OpenClaw 我们的方案 改造原因
消息队列 进程内EventEmitter BullMQ + Redis 需要持久化,防丢消息
Agent执行 单进程单线程 Worker进程池 需要并发,一个Worker挂了不影响其他
记忆存储 本地Markdown文件 PostgreSQL + pgvector 多实例共享,支持事务
工具沙箱 应用层路径白名单 Docker容器隔离 安全审计要求
模型调用 直连API 内部网关统一代理 统一计费、限流、审计日志

不是说OpenClaw的方案不好------对个人用户来说那套方案恰到好处。但企业场景的需求不一样,照搬必翻车。


技术选型

复制代码
运行时:Node.js 20 LTS(跟OpenClaw保持一致,方便参考源码)
消息队列:BullMQ 5.x + Redis 7.x
数据库:PostgreSQL 16 + pgvector扩展
容器:Docker(工具沙箱)
模型:Claude Sonnet 4.6(主力) / GPT-4o(备用)
监控:Prometheus + Grafana
部署:公司内网K8s集群

选Node.js不是因为它最适合,而是因为OpenClaw的源码可以直接参考。如果从零开始,Python + FastAPI可能是更主流的选择。


核心模块实现

1. Channel Adapter:钉钉机器人接入

钉钉的Stream模式比传统Webhook好用得多------不需要公网IP,不需要配回调地址,直接WebSocket长连接。

php 复制代码
// src/channels/dingtalk.ts
import { DWClient, EventAck, TOPIC_ROBOT } from 'dingtalk-stream';

class DingTalkChannel implements Channel {
  private client: DWClient;

  async connect(config: DingTalkConfig): Promise<void> {
    this.client = new DWClient({
      clientId: config.appKey,
      clientSecret: config.appSecret,
    });

    this.client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
      const data = JSON.parse(res.data);
      const message: IncomingMessage = {
        id: data.msgId,
        conversationId: data.conversationId,
        senderId: data.senderStaffId,
        content: data.text.content.trim(),
        platform: 'dingtalk',
        timestamp: Date.now(),
        // 钉钉特有:区分单聊和群聊
        isGroup: data.conversationType === '2',
        groupId: data.conversationId,
      };

      await this.messageHandler(message);
      return EventAck.SUCCESS;
    });

    await this.client.start();
  }

  async sendMessage(conversationId: string, content: string): Promise<void> {
    // 钉钉的回复需要用webhook URL
    await this.client.sendMessage({
      conversationId,
      content: JSON.stringify({
        msgtype: 'markdown',
        markdown: { title: 'AI助手', text: content }
      })
    });
  }
}

这里有个细节:钉钉群聊里@机器人的消息,content前面会带一个@xxx的前缀,需要strip掉,不然模型会困惑。

typescript 复制代码
// 清理@前缀
function cleanMention(content: string): string {
  return content.replace(/^@\S+\s*/, '').trim();
}

2. Message Router:基于BullMQ的任务调度

OpenClaw用进程内EventEmitter做消息调度,简单但脆弱------进程崩了消息就丢了。我们换成了BullMQ,关键改动是加了优先级队列死信处理

typescript 复制代码
// src/router/message-router.ts
import { Queue, Worker } from 'bullmq';

class MessageRouter {
  private queue: Queue;
  private connection = { host: 'redis-host', port: 6379 };

  async init() {
    this.queue = new Queue('agent-tasks', {
      connection: this.connection,
      defaultJobOptions: {
        attempts: 3,                    // 最多重试3次
        backoff: { type: 'exponential', delay: 2000 },
        removeOnComplete: { age: 86400 },  // 完成的任务保留24小时
        removeOnFail: { age: 604800 },     // 失败的保留7天
      }
    });

    // 启动Worker池
    for (let i = 0; i < 3; i++) {
      this.createWorker(i);
    }
  }

  async dispatch(message: IncomingMessage): Promise<void> {
    // 根据消息类型设置优先级
    const priority = this.calculatePriority(message);
    await this.queue.add('process-message', message, { priority });
  }

  private calculatePriority(msg: IncomingMessage): number {
    // 优先级:1最高,数字越大优先级越低
    if (msg.content.startsWith('/urgent')) return 1;  // 紧急指令
    if (!msg.isGroup) return 2;                        // 单聊优先
    return 5;                                          // 群聊消息
  }

  private createWorker(id: number) {
    const worker = new Worker('agent-tasks', async (job) => {
      const message = job.data as IncomingMessage;

      try {
        const runtime = new AgentRuntime(this.config);
        const response = await runtime.process(message);
        await this.channelManager.reply(message, response);
      } catch (err) {
        // 区分可重试和不可重试的错误
        if (err instanceof ModelAPIError && err.statusCode === 429) {
          throw err; // 429限流,BullMQ会自动重试
        }
        if (err instanceof ModelAPIError && err.statusCode === 403) {
          // API Key无效,不重试,直接回复错误
          await this.channelManager.reply(message, '模型服务暂时不可用,请联系管理员');
          return; // 不throw,标记任务完成
        }
        throw err;
      }
    }, {
      connection: this.connection,
      concurrency: 2,  // 每个Worker的并发数
      limiter: { max: 10, duration: 60000 }, // 每分钟最多处理10个任务
    });

    worker.on('failed', (job, err) => {
      console.error(`Worker ${id} 任务失败: ${job?.id}`, err.message);
      // 发到监控系统
      metrics.increment('agent.task.failed');
    });
  }
}

为什么用3个Worker、每个并发2?因为我们的主力模型Claude Sonnet的API并发限制是Tier 2的5 req/min,3×2=6刚好卡在限制边缘。实际跑的时候靠BullMQ的limiter兜底。

3. 记忆系统:从Markdown到PostgreSQL + pgvector

这是改动最大的模块。OpenClaw的三层记忆模型设计得很好,我们保留了这个分层思路,但把存储换成了PostgreSQL。

sql 复制代码
-- 建表
CREATE EXTENSION IF NOT EXISTS vector;

-- 短期记忆(会话消息)
CREATE TABLE short_term_memory (
  id SERIAL PRIMARY KEY,
  conversation_id VARCHAR(64) NOT NULL,
  role VARCHAR(16) NOT NULL,      -- 'user' | 'assistant' | 'tool'
  content TEXT NOT NULL,
  tool_calls JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  -- 自动清理:只保留最近的消息
  INDEX idx_stm_conv_time (conversation_id, created_at DESC)
);

-- 中期记忆(会话摘要)
CREATE TABLE mid_term_memory (
  id SERIAL PRIMARY KEY,
  conversation_id VARCHAR(64) NOT NULL,
  user_id VARCHAR(64) NOT NULL,
  summary TEXT NOT NULL,
  key_facts JSONB,                -- 结构化的关键信息
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ,         -- 过期时间,到期自动清理
  INDEX idx_mtm_user (user_id, created_at DESC)
);

-- 长期记忆(带向量索引)
CREATE TABLE long_term_memory (
  id SERIAL PRIMARY KEY,
  user_id VARCHAR(64) NOT NULL,
  content TEXT NOT NULL,
  category VARCHAR(32),           -- 'preference' | 'fact' | 'config'
  embedding vector(1536) NOT NULL, -- OpenAI text-embedding-3-small维度
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 向量相似度检索索引(IVFFlat,适合10万级数据)
CREATE INDEX idx_ltm_embedding ON long_term_memory
  USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

检索的时候,语义查询走pgvector:

typescript 复制代码
// src/memory/long-term-store.ts
class LongTermStore {
  async retrieve(query: string, userId: string, limit = 5): Promise<MemoryEntry[]> {
    // 1. 生成查询向量
    const embedding = await this.embeddings.embed(query);

    // 2. pgvector相似度检索 + 用户过滤
    const results = await this.db.query(`
      SELECT id, content, category,
             1 - (embedding <=> $1::vector) AS similarity
      FROM long_term_memory
      WHERE user_id = $2
      ORDER BY embedding <=> $1::vector
      LIMIT $3
    `, [JSON.stringify(embedding), userId, limit * 2]);

    // 3. 过滤低相似度结果(阈值0.7)
    const filtered = results.rows.filter(r => r.similarity > 0.7);

    // 4. 如果候选太多,用模型做reranking
    if (filtered.length > limit) {
      return this.rerank(query, filtered, limit);
    }
    return filtered.slice(0, limit);
  }
}

为什么不用专门的向量数据库(Pinecone、Milvus)?因为公司内部数据不允许出内网,自建Milvus运维成本太高,pgvector刚好------数据库本来就有PostgreSQL,加个扩展就行。10万级别以内的数据量,IVFFlat索引的检索速度完全够用(P99 < 50ms)。

4. 工具沙箱:Docker容器隔离

这是跟OpenClaw差异最大的地方。OpenClaw的应用层沙箱(路径白名单+命令黑名单)过不了安全审计,我们用Docker做了真正的隔离。

typescript 复制代码
// src/sandbox/docker-sandbox.ts
import Docker from 'dockerode';

class DockerSandbox {
  private docker = new Docker();

  async execute(
    command: string,
    args: string[],
    options: SandboxOptions
  ): Promise<SandboxResult> {
    const container = await this.docker.createContainer({
      Image: 'openclaw-sandbox:latest', // 预构建的精简镜像
      Cmd: [command, ...args],
      HostConfig: {
        Memory: 256 * 1024 * 1024,       // 内存限制256MB
        CpuPeriod: 100000,
        CpuQuota: 50000,                 // CPU限制50%
        NetworkMode: options.allowNetwork ? 'bridge' : 'none',
        ReadonlyRootfs: true,            // 只读根文件系统
        SecurityOpt: ['no-new-privileges'],
        Binds: options.mounts || [],     // 只挂载必要的目录
      },
      StopTimeout: options.timeout || 30,
    });

    await container.start();

    // 等待执行完成,带超时
    const result = await Promise.race([
      this.waitForCompletion(container),
      this.timeoutReject(options.timeout || 30000)
    ]);

    // 执行完毕立即销毁容器
    try { await container.remove({ force: true }); } catch {}

    return result;
  }

  private async waitForCompletion(container: Docker.Container): Promise<SandboxResult> {
    const stream = await container.logs({ follow: true, stdout: true, stderr: true });
    let stdout = '', stderr = '';

    return new Promise((resolve) => {
      stream.on('data', (chunk: Buffer) => {
        // Docker日志流的前8字节是header
        const payload = chunk.slice(8).toString();
        const streamType = chunk[0]; // 1=stdout, 2=stderr
        if (streamType === 1) stdout += payload;
        else stderr += payload;
      });

      stream.on('end', () => {
        resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 });
      });
    });
  }
}

沙箱镜像是基于Alpine Linux精简的,只包含Python 3.11、Node.js 20和常用的CLI工具。镜像大小控制在150MB以内。

sql 复制代码
# Dockerfile.sandbox
FROM alpine:3.19
RUN apk add --no-cache nodejs npm python3 py3-pip curl jq
RUN adduser -D -s /bin/sh sandbox
USER sandbox
WORKDIR /workspace

每次工具调用都创建一个新容器、执行完立即销毁。听起来开销大,但实测容器创建+启动在200ms左右(Docker的轻量级容器很快),对于Agent任务的总延迟(通常3-10秒)来说可以接受。


实际业务场景

跑了两周,三个场景的效果:

场景1:客户工单自动分类

之前一个外包同事每天花4小时人工看工单、打标签、分派。现在Agent从钉钉群里自动读取工单消息,调用模型分类后写入工单系统。

erlang 复制代码
处理量:日均120-150条工单
准确率:93.7%(抽样500条人工复核)
处理延迟:平均2.3秒/条
人工干预率:6.3%(模型不确定的会@人工确认)

场景2:日报周报自动汇总

从钉钉群的日报消息中提取关键信息,每周五自动生成团队周报。用到了中期记忆------Agent需要"记住"本周每天的日报内容,周五时汇总。

erlang 复制代码
覆盖范围:3个项目组,27人
生成时间:周报约45秒(需要检索5天的记忆)
人工修改率:约15%的周报需要微调措辞

场景3:竞品价格监控

每4小时自动抓取竞品在各电商平台的价格,变动超过阈值时在钉钉群里@运营。这个场景用到了Docker沙箱------抓取脚本在隔离环境里跑。

objectivec 复制代码
监控商品数:86个SKU,3个平台
检查频率:每4小时
价格变动捕获率:100%(跟人工每日检查对比两周,无遗漏)
月度成本:API调用约¥400 + 服务器资源约¥200

成本对比

项目 之前(3个外包) 现在(Agent系统)
月度人力成本 ¥36,000 ¥0
服务器/API ¥0 ¥600
开发投入(一次性) - 约3人周
月度维护 - 约2小时/月
月度总成本 ¥36,000 ¥600

三个外包岗的月成本是3.6万,Agent系统的月运行成本600块。开发投入约3人周,按内部人力成本算不到2万。不到一个月就回本了。

不过说句公道话,Agent干的是这三个岗位中重复性最高的那部分工作。真正需要判断力和沟通能力的事情,还是得人来。


踩坑记录

几个值得记录的坑:

坑1:pgvector的IVFFlat索引需要先有数据才能建

空表上建IVFFlat索引会报错,需要先插入一批数据(至少跟lists参数数量相当),再建索引。我们的做法是先用一批测试数据灌进去,建好索引,再清掉测试数据。

坑2:BullMQ的Worker并发和模型API限流容易打架

Worker并发设太高,API返回429;设太低,消息堆积。最后用了动态调整:监控429的频率,超过阈值时自动降低Worker并发。

ini 复制代码
// 简化的动态限流
let currentConcurrency = 2;
const RATE_LIMIT_WINDOW = 60000;
let rateLimitHits = 0;

worker.on('failed', (job, err) => {
  if (err.message.includes('429')) {
    rateLimitHits++;
    if (rateLimitHits > 3 && currentConcurrency > 1) {
      currentConcurrency--;
      worker.concurrency = currentConcurrency;
      console.warn(`触发限流保护,并发降至${currentConcurrency}`);
    }
  }
});

// 每分钟重置计数器,尝试恢复并发
setInterval(() => {
  if (rateLimitHits === 0 && currentConcurrency < 3) {
    currentConcurrency++;
    worker.concurrency = currentConcurrency;
  }
  rateLimitHits = 0;
}, RATE_LIMIT_WINDOW);

坑3:Docker沙箱的DNS解析

NetworkMode: 'none' 的容器完全没网络,但有些工具调用需要访问内网API。折中方案是用自定义网络+iptables规则,只允许访问内网IP段:

sql 复制代码
docker network create --subnet=172.20.0.0/16 sandbox-net
iptables -I DOCKER-USER -s 172.20.0.0/16 -d 10.0.0.0/8 -j ACCEPT
iptables -I DOCKER-USER -s 172.20.0.0/16 -d 0.0.0.0/0 -j DROP

坑4:记忆系统的"幻觉放大"问题

Agent从长期记忆中检索到的信息如果本身有误(比如之前一次对话中模型的错误判断被存进了记忆),后续对话会反复引用这个错误信息。我们加了一个"记忆衰减"机制------长时间没被引用的记忆条目权重自动降低:

sql 复制代码
-- 记忆衰减:每次检索命中时更新时间戳
UPDATE long_term_memory SET updated_at = NOW() WHERE id = $1;

-- 查询时加入时间衰减因子
SELECT *, 
  (1 - (embedding <=> $1::vector)) * 
  -- 30天内的记忆权重为1,之后线性衰减,90天后权重0.5
  GREATEST(0.5, 1.0 - EXTRACT(EPOCH FROM NOW() - updated_at) / (90*86400) * 0.5)
  AS weighted_similarity
FROM long_term_memory
WHERE user_id = $2
ORDER BY weighted_similarity DESC
LIMIT $3;

性能数据

在公司内网K8s集群(3个Worker Pod)上的实测数据:

指标 数值
简单对话延迟(P50 / P99) 1.8s / 4.2s
带工具调用延迟(P50 / P99) 3.5s / 9.8s
记忆检索延迟(P50 / P99) 35ms / 85ms
Docker沙箱启动延迟 ~200ms
日处理消息量 400-600条
系统可用性(两周) 99.7%(一次Redis连接中断导致5分钟不可用)
单Worker内存占用 220MB
月度API成本 ¥400(Claude Sonnet为主)

瓶颈在模型API调用,本地处理几乎不是问题。如果用国产模型(通义千问/DeepSeek),延迟能再降30-40%,成本降80%,但输出质量有差距,需要更多的prompt engineering。


写在最后

这套系统跑了两周,整体效果超出预期。最大的收获不是省了多少钱,而是验证了一件事:当前的大模型能力,已经足够覆盖企业内部大量的重复性信息处理工作。

OpenClaw的贡献在于,它用一个足够简洁的架构,把"AI Agent能干什么"这件事演示得很清楚。虽然它本身不适合直接用于生产,但它的架构设计------消息驱动、三层记忆、工具调用链------是一套值得学习的参考模型。

如果你也在考虑给团队搭一套类似的系统,希望这篇文章能让你少走一些弯路。

有问题欢迎评论区交流。项目后续如果开源了会更新链接。

相关推荐
balmtv1 小时前
2026年Gemini 3 Pro技术拆解:深度推理、空间智能与Agentic系统的架构革命
人工智能·gpt·架构
Shining05962 小时前
前沿模型系列(四)《大模型前沿架构》
人工智能·学习·其他·ai·架构·大模型·infinitensor
进击的野人2 小时前
Prompt工程入门指南:写给AI学习新手的提示词秘籍
人工智能·aigc·ai编程
Ekehlaft2 小时前
同题画图大考,AiPy 适配性拉满,OpenClaw 全程 “哑火”
人工智能·ai·openclaw·aipy
wáng bēn2 小时前
2025 AI 打卡 Day5:Seaborn 数据可视化基础(Matplotlib 升级版 + Titanic 真实业务全案例 + 完整参数调优)
人工智能·机器学习·信息可视化·matplotlib·seaborn
golang学习记2 小时前
VS Code 1.110 AI大升级:让AI真正实用!
人工智能·visual studio code
程序员小明儿2 小时前
OpenClaw-RL 实战 05|加权损失融合:为什么“评估”+“指导”双信号能让Agent聪明一倍?
人工智能
_遥远的救世主_2 小时前
Claude HUD:给你的 Claude Code 装一块「仪表盘」
人工智能
用户4815930195912 小时前
Token 到底是什么?揭开大模型背后"文字压缩术"的神秘面纱
人工智能