从 QQ 机器人到多租户 Agent 平台,LangBot 用一条流水线接住了 17 个 IM

大家好,我是若风。

2022 年 12 月,RockChinQ 在 GitHub 上传了 LangBot 的第一个 commit。那时候它还叫 QChatGPT,干的事很直接,就是把 ChatGPT 塞进 QQ 群,核心代码没几行。三年半过去,这个项目跑到 v4.10.6,已经是一个支持 17 个 IM 平台、自带 Web 管理面板、有 Box 沙箱、有多租户 RLS、还同时是 MCP server 和 MCP client 的生产级平台。17,292 个 star,Apache 2.0 协议。

这个演进路径本身就值得讲。很多团队一上来就想做「通用 Agent 平台」,结果架构没扛住,半年就重写。LangBot 反着来,先从一个 QQ 机器人长起来,再慢慢把 pipeline、插件系统、沙箱、多租户一层层加进来。它现在这套架构,几乎每一层都能看到「先有功能、再抽象」的痕迹,不是 PPT 里画出来的。

今天这篇就拆一拆,它到底怎么用一条流水线接住这么多平台,以及它在走向「平台」这条路上踩了哪些坑、付了什么代价。

一句话定位

LangBot 是一个把 LLM 能力接入即时通讯平台的开源运行时。你给它一个 Discord token、一个微信账号、或者一个钉钉应用,再配上一个模型 provider,它就能让这些 IM 里的用户和机器人对话、调工具、查知识库、跑 Agent。

它不是 SDK,不是库,是一个跑起来的服务。一个 LangBot 进程会同时持有 5300 端口的 HTTP 服务和 Web UI、十几个 IM 适配器、一个 pipeline 引擎、持久化、向量库、遥测、以及到插件运行时和 Box 沙箱运行时的桥接代码。

为什么不是「一个 if-else 写死所有平台」

接 IM 平台这件事,听起来简单,做起来全是脏活。每个平台的鉴权方式不一样,消息格式不一样,群和私聊的概念不一样,多媒体消息的处理不一样,连「发消息」这个动作的 API 命名都不一样。Discord 叫 send,Telegram 叫 sendMessage,微信叫 sendmsg,钉钉要走回调。

最容易想到的做法是写一个大的 switch,每个平台一个分支。早期 QChatGPT 大概就是这么干的。但当你接第三个、第四个平台的时候,这个 switch 就会膨胀成无法维护的怪物,每加一个功能都要在十几个分支里同步改。

LangBot 的解法是分两层抽象。先看它的平台层代码,在 pkg/platform/sources/ 下,每一个平台都是一个独立的文件加一个 YAML 元数据:

bash 复制代码
src/langbot/pkg/platform/sources/
├── aiocqhttp.py / aiocqhttp.yaml      # QQ
├── discord.py / discord.svg / discord.yaml
├── telegram.py / telegram.yaml
├── lark.py / lark.yaml                 # 飞书
├── dingtalk.py / dingtalk.yaml
├── wecom.py / wecom.yaml               # 企业微信
├── wechatpad.py / wechatpad.yaml       # 个人微信
├── slack.py / slack.yaml
├── kook.py / kook.yaml
├── line.py / line.yaml
├── matrix.py / matrix.yaml
└── ... 还有 officialaccount、wecombot、qqofficial、satori 等

每个适配器都继承自 SDK 里的 AbstractMessagePlatformAdapter,把平台特有的 API 调用、事件回调、消息格式,统一翻译成 LangBot 共享的 MessageChain 消息链和事件实体。这一层的纪律是,平台层只做翻译,不碰 LLM 逻辑,也不碰业务逻辑。

但真正把 17 个平台接住的,不是这一层适配器,而是它后面那条 pipeline。

pipeline 引擎,把消息处理做成责任链

LangBot 最核心的设计是它的 pipeline 引擎。ARCHITECTURE.md 里画了这么一条运行时图:

vbscript 复制代码
Platform adapter
  → RuntimeBot
  → MessageAggregator
  → QueryPool
  → Controller
  → RuntimePipeline
  → PipelineStage chain
  → RequestRunner / ToolManager / PluginRuntimeConnector / BoxService
  → response via adapter

一条 IM 消息进来,先被平台适配器翻译成统一格式,然后进 RuntimeBot 做路由判断,能处理的丢给 MessageAggregator 做批处理和归一化,再加成一个 Query 塞进 QueryPool。接下来 Controller 从池子里取 query,丢进 RuntimePipeline 跑一遍配置好的 stage 链,最后把响应原路返回。

关键的工程价值在 pipelinemgr.py 里。打开这个文件,你会看到 RuntimePipeline 类,它在构造的时候通过一段 importutil.import_modules_in_pkgs 动态导入了十个内置 stage 家族:

python 复制代码
importutil.import_modules_in_pkgs([
    resprule, bansess, cntfilter, process, longtext,
    respback, wrapper, preproc, ratelimit, msgtrun,
])

这十个家族对应十类处理逻辑,比如响应规则(resprule)、封禁会话(bansess)、内容过滤(cntfilter)、消息截断(msgtrun)、长文本处理(longtext)、限流(ratelimit)等等。每个家族都是可插拔的,pipeline 配置驱动,你想加一段敏感词过滤或者把超长消息转成图片,就是在配置里挂一个 stage,而不是去改主流程代码。

坦白讲,这种责任链加配置驱动的设计在中间件领域并不新鲜,Scrapy 的 middleware、Express 的中间件都是这个套路。但 LangBot 把它用在 IM bot 上,并且每个 stage 都支持生成器风格(typing.AsyncGenerator),意味着你可以写一个 stage 边接收消息边往外吐中间结果,这种流式处理在「机器人一边思考一边打字」这种交互上很关键。

并发控制也值得点出来。controller.py 里用的是两级信号量,一个是全局 pipeline 并发的 asyncio.Semaphore(self.ap.instance_config.data['concurrency']['pipeline']),一个是每个 session 自己的 session._semaphore。一条消息要被处理,得同时拿到全局槽位和当前会话槽位。这套设计保证你配置了「全局最多同时处理 20 条消息、每个群最多并发 2 条」这种规则时,不会因为某个群被刷屏把整个进程的资源都吃光。

把整条链路画出来

上面这些零散的模块拼在一起,就是 LangBot 的五层架构。从 IM 消息进来,到最后执行完返回,中间经过的每一层都有明确的职责边界。

最上面是三条入口(IM 适配器、HTTP/Web、MCP server),消息进来后被调度层做批处理、并发控制和多租户守卫,再交给 pipeline 引擎跑责任链。能力层的 ToolManager 聚合四路工具,最底层是 Box 沙箱和多租户持久化。这张图里每一层的模块名都是源码里真实存在的文件路径,不是目录名的猜测。

ToolManager,四路来源的工具聚合

LLM 光能聊天还不够,得能调工具。LangBot 的 ToolManager 把工具来源分成了四路,这个分类很能看出它的设计取舍:

python 复制代码
class ToolManager:
    native_tool_loader: native_loader.NativeToolLoader
    plugin_tool_loader: plugin_loader.PluginToolLoader
    mcp_tool_loader: mcp_loader.MCPLoader
    skill_tool_loader: skill_authoring_loader.SkillToolLoader

native 是内置工具,plugin 是插件运行时提供的工具,mcp 是接外部 MCP server 的工具,skill_authoring 是「让 Agent 自己写 skill」的工具。四路工具最后都归一成 SDK 里的 LLMTool,喂给 RequestRunner 让 LLM 调用。

这里有个细节特别有意思,在 get_all_tools 被调用前,ToolManager 会先跑一步 _bind_plugin_workspace(context),注释写得很直白:

python 复制代码
async def _bind_plugin_workspace(self, context: TenantContext) -> None:
    """Select the tenant before any plugin catalog lookup.

    Tool discovery happens before invocation, so relying on ``call_tool``
    to bind the Workspace is too late and can expose another task's
    catalog in a shared Runtime.
    """

意思是工具发现发生在工具调用之前,如果你等到 call_tool 的时候才绑定租户上下文,就晚了,在共享运行时里可能把别的任务的工具目录暴露出去。这种「越权防护要前移到发现阶段」的教训,大概率是被真实 bug 教出来的。

Box 沙箱,让 Agent 真正能动手

光调工具还不够,Agent 时代需要的是能跑代码、能起进程的沙箱。LangBot 把这块单独抽成了 Box Runtime,放在兄弟仓库 langbot-plugin-sdk 里,主仓库通过 pkg/box/service.pyconnector.py 连过去。

Box 支持三种后端,backend.pynsjail_backend.pye2b_backend.py,对应本地进程隔离、Linux namespace 隔离、和 E2B 远程沙箱。它服务的东西包括原生 Agent 工具、stdio MCP server、skill 编写、托管进程。

打开 box/service.py,你会看到一个叫 placement_generation 的概念。这是多租户场景下用来隔离「放置代」的,沙箱会话和托管进程都是 generation 级作用域,但 Workspace 存储跨代共享。LangBot 在 MCP stdio relay 接入前会校验当前执行绑定,把 Workspace 和 generation 绑定放进鉴权 header 里,这样当租户切换发生时,旧的进程会被自然退休,已经接上的 relay 也会被关掉。

这种设计不是一开始就有的。你看代码里那些 O_NOFOLLOW 探针、O_EXCL 创建、fsync 强制落盘的操作,全都是为了防「共享卷里被别的租户塞东西」做的硬隔离。_DEFAULT_MAX_WORKSPACE_ENTRIES = 100_000_HARD_MAX_WORKSPACE_ENTRIES = 1_000_000 这种硬上限,也是在防一个租户把共享存储写爆。这些都是真刀真枪在生产环境里被坑过才会写的代码。

多租户是怎么硬加进来的

这是我觉得 LangBot 最值得讲的一段。它本来是个单实例机器人框架,后来要做云服务(LangBot Space),就得多租户。多租户这种东西,从架构第一天就设计进去和后期硬加,完全是两种代价。LangBot 是后者。

最直观的证据在 alembic/versions/ 里,三个连续的迁移:

erlang 复制代码
0009_workspace_tenancy_kernel.py
0010_scope_tenant_resources.py
0011_postgres_tenant_rls.py
...
0013_tenant_pgvector.py

0011_postgres_tenant_rls.py 是在 PostgreSQL 上启用行级安全(Row Level Security),0013_tenant_pgvector.py 是把向量库也做租户隔离。也就是说,从数据库这一层就开始把租户边界焊死。

但更狠的是侵入到了运行时每一个核心对象。你看 botmgr.pyRuntimeBot 的构造函数,前面十几行全是守卫:

python 复制代码
if not isinstance(execution_context, ExecutionContext):
    raise WorkspaceRequiredError('RuntimeBot requires an ExecutionContext')
if not execution_context.instance_uuid.strip() or not execution_context.workspace_uuid.strip():
    raise WorkspaceRequiredError('RuntimeBot requires an instance and Workspace')
if execution_context.placement_generation <= 0:
    raise WorkspaceRequiredError('RuntimeBot requires a positive placement generation')
...

RuntimePipeline 的构造一模一样,一上来就是四个 WorkspaceRequiredError 检查。Pipeline 实体的 workspace_uuid 要和执行上下文对得上,pipeline uuid 要对得上,placement_generation 必须是正数。任何一个对不上直接抛错。

这套防御的代价是,整个运行时的每个核心类都背了一个 ExecutionContext,带着 instance_uuid、workspace_uuid、placement_generation、bot_uuid、pipeline_uuid 这一堆字段到处走。ARCHITECTURE.md 里自己也承认:

The central runtime object is pkg/core/app.py::Application. It is a service locator for long-lived managers. That is not elegant, but it is the current architectural center.

「That is not elegant」,作者自己说的。这种诚实本身就说明项目是真的在生产里跑过、被真实需求逼着长成现在这样的,而不是为了好看的设计。

两个方向的 MCP

LangBot 对 MCP 的用法,能单独拎出来讲,因为它同时是 MCP server 又是 MCP client,这两个方向经常被搞混。

作为 server,它把一部分 HTTP API 挂在 /mcp 路径下,pkg/api/mcp/mount.py 负责挂载。这个 MCP server 故意只暴露精选的 API 子集,工具直接调 service 层的类,而不是 HTTP 回调自己。这样 Agent 可以用 MCP 协议管理机器人、配 pipeline、装插件、配模型,全程不用浏览器登录,一个 API key 就够。

作为 client,pkg/provider/tools/loaders/mcp.py 负责连外部 MCP server,把这些外部工具汇到 ToolManager 里给 LLM 用。还有 mcp_stdio.py 处理 stdio 形式的 MCP server,这种要靠 Box 沙箱跑起来。

有意思的是 issue 区有一条 #2363 feat(mcp): detect OAuth-protected remote MCP servers,说明他们正在补对 OAuth 保护的远程 MCP server 的支持,这块还在演进。

这种「双向 MCP」其实是个模式。一个 Agent 平台,既要让别的 Agent 来操作它(server 侧),又要让它内置的 Agent 去操作别的工具(client 侧),MCP 协议同时服务这两个方向。LangBot 是我见过的把这两个方向做得最清楚的项目之一,ARCHITECTURE.md 里专门用大写提醒「Do not confuse LangBot's MCP client side with LangBot's own MCP server at /mcp; they are different surfaces.」。

跑起来到底什么样

说了这么多架构,落地到底难不难。LangBot 现在主打三种部署方式,最轻的是一行命令:

bash 复制代码
uvx langbot

要求机器装了 uv,启动完访问 localhost:5300 就能开干。复杂一点用 Docker Compose,仓库 docker/ 目录里有现成的 compose 文件,docker compose --profile all up -d 一把起。再复杂的,它还支持 Zeabur、Railway 一键部署,以及 Kubernetes。

配置不在 YAML 里硬写,走 Web 面板。你登进去之后,配 IM 平台的 token、配模型 provider、配 pipeline、装插件,全在浏览器里点。这点对非技术用户友好,也是它能从纯技术项目长成「被企业信任」的重要原因之一。

它还暴露了一个 demo 环境,demo.langbot.dev,账号 demo@langbot.app,密码 langbot123456,可以直接进去点。不过 README 里明确写了「Public demo environment. Do not enter sensitive information.」,这点挺诚实。

它的真实痛点在哪

说了这么多好的,也该聊聊它的边界和问题,不然就成软文了。

先说维护结构。30 个贡献者里,主作者 RockChinQ 一个人 2783 次 commit,第二名 fdc310 只有 163 次,第三名 wangcham 142 次。这个 bus factor 严格说不太健康,项目高度依赖一个人。值得欣慰的是贡献者列表里出现了 Copilot(47 次)和 github-actions bot(29 次),说明团队已经开始用 AI 辅助和自动化测试,这是缓解 bus factor 的正路子。

再说说 IM 集成的实际表现。issue 区的真实痛点很集中,排前几的高赞 issue 全在围绕几个国内平台打转。#1715 25 条评论,企业微信智能机器人用 dify 的 chatflow 不能用了;#1872 25 条评论,企业微信智能机器人无法识别文件;#2030 16 条评论,钉钉接 dify 知识库内容没及时更新;#2132 11 条评论,langbot 接收企微图片异常。

这些不是偶然。企业微信和钉钉这两个平台,API 本身就难用,鉴权复杂,回调机制各有各的坑,文档还经常对不上实际行为。LangBot 把这些做成了一等公民适配器,代码里 libs/wecom_api/ 还 vendored 了一个 WXBizMsgCrypt3.py 加密解密库,这种东西不维护会一直出问题。你看 platform/sources/ 下 wecom(企业微信)、wecombot(企微智能机器人)、wecomcs(企微客服)三个文件并存,就知道这一家平台就有好几种接入方式,每一种都得单独适配。

还有一条隐性成本,它的多租户能力是 Apache 2.0 开源的,但 LangBot Space 这个云服务是闭源商业的。也就是说,如果你拿开源版自己做多租户 SaaS,你得自己扛下那些 RLS、placement generation、共享卷隔离的复杂度。ARCHITECTURE.md 里那些「not elegant」的自嘲,和 issue 区那些企业微信的报错,都得你自己处理。

看完能带走什么

拆完 LangBot,我觉得最值得提炼的是两个模式。

第一个是 Pipeline-as-Runtime。当你面对「同一类业务要适配十几个外部系统」这种场景时,不要写 switch,也不要写 SDK 就完事。把处理流程抽象成一条配置驱动的责任链,每个适配器只负责翻译,业务逻辑全做成可插拔的 stage。LangBot 这套 pipeline 加 stage 家族的设计,核心思路就是把「接 IM 平台」这件事做成了一个可组合的运行时,而不是一堆散落的脚本。这个模式可以直接迁移到客服路由、CI 流水线、数据 ETL 这类场景。

第二个是 双协议表面模式 。一个 Agent 平台天然有两个身份,既是被操作的对象(要给别的 Agent 提供 MCP),又是 Agent 的容器(要去调外部 MCP 工具)。把这两个方向一开始就用不同的代码路径实现清楚,比后期硬拆要省太多事。LangBot 的 api/mcp/mount.pytools/loaders/mcp.py 就是这两条路径,命名上刻意区分,注释里反复强调,这种自觉性很值得学。

至于什么场景该选 LangBot,我的建议是,如果你要做的是一个多平台 IM bot,而且要长期维护、要上生产,它几乎是开源里最完整的选择。但如果你只是想让一个 LLM 接进一个 QQ 群玩玩,它这套架构其实是 over-engineered 的,用更轻的 nonebot 加个 OpenAI 插件反而更省心。工具选型,匹配场景比追求功能多更重要。

相关推荐
墨风如雪1 小时前
Hermes Agent 实战:接入微信两周后,我留下了这 4 个后台任务
aigc
大地之灯1 小时前
从零做一个自己的 CLI
python·agent
孙启超1 小时前
【AI应用开发】怎么降低 Agent 幻觉?有几种可行方案?
大数据·人工智能·llm·agent·rag·幻觉·ai应用开发
酱学编程1 小时前
Harness Engineering - 是什么、怎么设计、往哪走
ai·agent·harness
ZJPRENO2 小时前
Open‑AI GPT‑5.6 Luna永久性降价80%
ai编程
武子康2 小时前
GPT-5.6 Sol 的 ARC-AGI-3 分数为何翻近三倍:Agent 评测必须记录整套运行合同
人工智能·chatgpt·agent
weixin_431600442 小时前
做 Agent 会用到的 Node API(4):取消与 AbortController
前端·学习·ai·agent·ai编程
苏灿烤鱼2 小时前
今日 GitHub 热门|Agent 云端电脑登顶,+1,891 项目却只排第三(08.06)
github·agent·资讯
苏灿烤鱼2 小时前
GitHub Trending 榜首|GitHub #1 拆解|为什么「持久工作台」比临时沙箱更值得关注?(08.06)技术拆解
人工智能·typescript·agent