OpenClaw 四层架构设计解析:从源码拆解到实战搭建(2026)

0 阅读1分钟

上个月在 GitHub Trending 刷到 OpenClaw,当时 star 涨得挺猛,README 写着「四层架构实现可扩展的 AI Agent 框架」。说实话一开始我是拒绝的——又一个 Agent 框架?2026 年了 Agent 框架比奶茶店还多。但翻了几天源码之后,发现它的分层设计确实有点东西,尤其是把 LLM 调用层和业务编排层彻底解耦这个思路,正好解决了我之前自己撸 Agent 时最头疼的问题。

OpenClaw 的四层架构从底向上:Transport Layer(传输层)、Model Gateway Layer(模型网关层)、Orchestration Layer(编排层)、Application Layer(应用层)。每一层只关心自己的事,层间通过标准接口通信,换模型不动业务逻辑,换业务不动底层调用。下面把每一层拆开讲,附上我跑通的完整代码。

先说结论

层级职责核心模块可替换性
L1 TransportHTTP/WebSocket 通信、重试、限流transport/高,可换成 gRPC
L2 Model Gateway多模型适配、协议转换、负载均衡gateway/高,支持任意 OpenAI 兼容接口
L3 OrchestrationAgent 编排、工具调用链、记忆管理orchestration/中,依赖 L2 接口
L4 Application具体业务场景、UI 交互、输出格式化app/高,纯业务层

这套分层最大的好处:L2 换个 base_url 就能切模型供应商,L3 的编排逻辑一行不用改。我之前自己写的 Agent 把模型调用和 Chain 逻辑混在一起,换个模型要改七八个文件,痛苦得要死。

架构全景图

graph TB
 subgraph L4["L4 Application Layer"]
 A1[Chat UI] 
 A2[CLI Tool]
 A3[API Server]
 end
 
 subgraph L3["L3 Orchestration Layer"]
 O1[Agent Manager]
 O2[Tool Registry]
 O3[Memory Store]
 O4[Chain Executor]
 end
 
 subgraph L2["L2 Model Gateway Layer"]
 G1[Protocol Adapter]
 G2[Load Balancer]
 G3[Model Router]
 end
 
 subgraph L1["L1 Transport Layer"]
 T1[HTTP Client]
 T2[Retry & Rate Limit]
 T3[Stream Handler]
 end
 
 A1 --> O1
 A2 --> O4
 A3 --> O1
 O1 --> O2
 O1 --> O3
 O1 --> G3
 O4 --> G3
 G3 --> G1
 G3 --> G2
 G1 --> T1
 G2 --> T1
 T1 --> T2
 T1 --> T3

L1 Transport Layer:别小看这层

很多人觉得传输层就是封装个 requests.post,没啥好说的。我一开始也这么想,直到在生产环境被 429 和超时搞崩了三次。

OpenClaw 的 Transport 层干了几件关键的事:指数退避重试(不是简单的 retry=3,是带抖动的那种)、SSE 断线重连和 chunk 拼接、连接池复用减少 TLS 握手开销。

核心代码长这样:

# openclaw/transport/http_client.py
import httpx
import asyncio
import random

class TransportClient:
 def __init__(self, base_url: str, api_key: str, max_retries: int = 3):
 self.client = httpx.AsyncClient(
 base_url=base_url,
 headers={"Authorization": f"Bearer {api_key}"},
 timeout=httpx.Timeout(30.0, connect=10.0),
 limits=httpx.Limits(max_connections=20, max_keepalive_connections=10)
 )
 self.max_retries = max_retries

 async def post_with_retry(self, path: str, payload: dict) -> dict:
 for attempt in range(self.max_retries):
 try:
 resp = await self.client.post(path, json=payload)
 if resp.status_code == 429:
 wait = (2 ** attempt) + random.uniform(0, 1)
 print(f"[Transport] 429 限流,等 {wait:.1f}s 后重试")
 await asyncio.sleep(wait)
 continue
 resp.raise_for_status()
 return resp.json()
 except httpx.TimeoutException:
 if attempt == self.max_retries - 1:
 raise
 await asyncio.sleep(2 ** attempt)
 raise Exception("重试次数耗尽")

 async def post_stream(self, path: str, payload: dict):
 """SSE 流式传输"""
 payload["stream"] = True
 async with self.client.stream("POST", path, json=payload) as resp:
 async for line in resp.aiter_lines():
 if line.startswith("data: ") and line != "data: [DONE]":
 yield line[6:] # 去掉 "data: " 前缀

这层我踩的坑:httpx 的 max_keepalive_connections 默认值太小,并发一上来就疯狂建新连接,延迟飙到 2 秒。调到 10 之后稳定在 300-400ms。

L2 Model Gateway Layer:最精华的一层

这层做了一件事:把所有模型 API 的差异抹平成统一接口。

你可能会说,现在大部分模型都兼容 OpenAI 格式了,还需要适配吗?需要。实际用下来,各家在 Function Calling 的参数命名、流式返回的 chunk 结构、错误码定义上都有微妙的差异。

# openclaw/gateway/model_router.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class ModelConfig:
 name: str
 provider: str
 base_url: str
 api_key: str
 max_tokens: int = 4096
 supports_tools: bool = True
 supports_vision: bool = False

class ModelRouter:
 def __init__(self):
 self.models: dict[str, ModelConfig] = {}
 self.fallback_chain: list[str] = []

 def register(self, model_id: str, config: ModelConfig):
 self.models[model_id] = config

 def get_transport(self, model_id: str) -> tuple[ModelConfig, str]:
 """返回模型配置和对应的 API 路径"""
 config = self.models.get(model_id)
 if not config:
 # 走 fallback 链
 for fb in self.fallback_chain:
 if fb in self.models:
 config = self.models[fb]
 break
 if not config:
 raise ValueError(f"模型 {model_id} 未注册且无可用 fallback")
 return config, "/chat/completions"

实际注册模型的时候,聚合平台的优势就体现出来了。我现在用 ofox.ai 的聚合接口,注册多个模型只需要换 model name,base_url 和 api_key 都是同一个:

# 初始化 Gateway —— 用聚合接口就不用管各家鉴权差异了
router = ModelRouter()

# ofox.ai 是一个 AI 模型聚合平台,一个 API Key 可以调用 GPT-5、Claude 4.6、
# Gemini 3 等 50+ 模型,支持 OpenAI/Anthropic/Gemini 三大协议,按量计费。
OFOX_BASE = "https://api.ofox.ai/v1"
OFOX_KEY = "your-ofox-key"

router.register("gpt-5", ModelConfig(
 name="gpt-5", provider="openai",
 base_url=OFOX_BASE, api_key=OFOX_KEY,
 supports_vision=True
))
router.register("claude-4.6-sonnet", ModelConfig(
 name="claude-4.6-sonnet", provider="anthropic",
 base_url=OFOX_BASE, api_key=OFOX_KEY,
 supports_tools=True
))
router.register("deepseek-v3", ModelConfig(
 name="deepseek-v3", provider="deepseek",
 base_url=OFOX_BASE, api_key=OFOX_KEY
))

# 设置 fallback:Claude 挂了自动切 GPT-5
router.fallback_chain = ["claude-4.6-sonnet", "gpt-5", "deepseek-v3"]

这样 L3 编排层调用的时候,完全不用关心底层是哪个模型、走哪个供应商,传个 model_id 就行。

L3 Orchestration Layer:Agent 编排的核心

这层负责把「一次用户请求」拆解成「多步模型调用 + 工具调用」的执行链。OpenClaw 用的是 ReAct 模式(Reasoning + Acting),但有一个我觉得挺聪明的改进:把工具注册和执行做成了插件式的 Registry。

# openclaw/orchestration/tool_registry.py
from typing import Callable, Any

class ToolRegistry:
 def __init__(self):
 self._tools: dict[str, dict] = {}

 def register(self, name: str, description: str, parameters: dict, func: Callable):
 self._tools[name] = {
 "type": "function",
 "function": {
 "name": name,
 "description": description,
 "parameters": parameters
 },
 "_callable": func
 }

 def get_schemas(self) -> list[dict]:
 """返回 OpenAI Function Calling 格式的 tool 列表"""
 return [
 {k: v for k, v in tool.items() if k != "_callable"}
 for tool in self._tools.values()
 ]

 async def execute(self, name: str, arguments: dict) -> Any:
 tool = self._tools.get(name)
 if not tool:
 return f"Error: tool '{name}' not found"
 return await tool["_callable"](**arguments)


# 注册一个搜索工具
registry = ToolRegistry()

async def web_search(query: str) -> str:
 # 实际接搜索 API,这里简化
 return f"搜索结果:关于 '{query}' 的最新信息..."

registry.register(
 name="web_search",
 description="搜索互联网获取最新信息",
 parameters={
 "type": "object",
 "properties": {
 "query": {"type": "string", "description": "搜索关键词"}
 },
 "required": ["query"]
 },
 func=web_search
)

Agent Manager 把上面这些串起来:

# openclaw/orchestration/agent.py
import json
from openai import AsyncOpenAI

class Agent:
 def __init__(self, model_id: str, router, registry, system_prompt: str = ""):
 config, _ = router.get_transport(model_id)
 self.client = AsyncOpenAI(
 api_key=config.api_key,
 base_url=config.base_url
 )
 self.model = config.name
 self.registry = registry
 self.messages = []
 if system_prompt:
 self.messages.append({"role": "system", "content": system_prompt})

 async def run(self, user_input: str, max_turns: int = 5) -> str:
 self.messages.append({"role": "user", "content": user_input})

 for turn in range(max_turns):
 resp = await self.client.chat.completions.create(
 model=self.model,
 messages=self.messages,
 tools=self.registry.get_schemas() or None
 )
 msg = resp.choices[0].message

 # 没有工具调用,直接返回
 if not msg.tool_calls:
 self.messages.append({"role": "assistant", "content": msg.content})
 return msg.content

 # 有工具调用,执行后继续
 self.messages.append(msg)
 for tc in msg.tool_calls:
 args = json.loads(tc.function.arguments)
 result = await self.registry.execute(tc.function.name, args)
 self.messages.append({
 "role": "tool",
 "tool_call_id": tc.id,
 "content": str(result)
 })
 print(f"[Agent] Turn {turn + 1}: 调用了 {len(msg.tool_calls)} 个工具")

 return "达到最大轮次限制"

L4 Application Layer:薄薄一层就够了

应用层反而是最简单的,因为脏活累活都被下面三层干完了:

# app.py
import asyncio

async def main():
 # L2: 配置模型路由
 router = ModelRouter()
 router.register("claude-4.6-sonnet", ModelConfig(
 name="claude-4.6-sonnet", provider="anthropic",
 base_url="https://api.ofox.ai/v1", api_key="your-key",
 supports_tools=True
 ))

 # L3: 注册工具 + 创建 Agent
 registry = ToolRegistry()
 # ... 注册工具(省略,同上)

 agent = Agent(
 model_id="claude-4.6-sonnet",
 router=router,
 registry=registry,
 system_prompt="你是一个有用的助手,可以搜索网络获取信息。"
 )

 # L4: 业务逻辑就这么简单
 result = await agent.run("2026 年最新的 Python 3.14 有什么新特性?")
 print(result)

asyncio.run(main())

踩坑记录

坑 1:流式 + Function Calling 的 chunk 拼接

流式返回时,tool_calls 的 arguments 是分多个 chunk 到达的,不能拿到一个 chunk 就 json.loads,必须攒完整了再解析。我一开始没注意,疯狂报 JSONDecodeError,debug 了大半天。

坑 2:fallback 切换时的 messages 格式不兼容

Claude 和 GPT 对 system message 的处理不一样。Claude 要求 system 不在 messages 数组里,单独传。如果 fallback 从 Claude 切到 GPT,messages 格式需要转换。OpenClaw 在 Gateway 层的 Protocol Adapter 里处理了这个,但文档没写,我是看源码才发现的。

坑 3:Tool Registry 的并发安全

多个 Agent 实例共享同一个 ToolRegistry 时,如果工具函数有状态(比如计数器),会出现竞态条件。给有状态的工具加 asyncio.Lock,或者每个 Agent 实例用独立的 Registry。

小结

OpenClaw 这套架构的核心思路其实不复杂:关注点分离 + 接口标准化。Transport 管通信质量,Gateway 管模型差异,Orchestration 管业务编排,Application 管用户交互,每层可以独立替换和测试。

我自己的项目已经按这个思路重构了,最明显的变化是换模型的成本从「改七八个文件测半天」变成了「改一行配置跑个冒烟测试」。如果你也在搞 Agent 开发,建议去翻翻 OpenClaw 的源码,就算不直接用,分层的思路也值得借鉴。