
0. 为什么要手搓 OpenClaw
OpenClaw 很强,但完整工程体量也很大。对于大多数开发者来说,直接阅读全量代码会有三个痛点:
- 模块多:Gateway、Agent、Tools、Sessions、Channels 互相耦合
- 路径长:一条消息从输入到回复,跨越多个子系统
- 调试难:没有自己的"最小版本",很难定位问题
所以这个系列采用一个更实用的学习路径:
先做最小闭环,再逐步补齐能力。
1. 目标
用 Python 从 0 到 1 复现 OpenClaw 的核心能力:
- Agent Loop(工具调用 + 多轮推理)
- Session 与并发隔离
- 记忆系统(短期 + 长期)
- Skills 系统(分层加载)
- Web/Telegram 等渠道接入
第一篇的阶段目标是:
- 跑起 FastAPI 服务
- 打通一个最小
/v1/chat对话接口 - 具备会话隔离与并发控制(每会话锁 + 全局信号量)
2. 目标架构
用户输入: CLI/Web/Telegram/Discord
Gateway Server
SessionManager
Session Lock + Global Semaphore
Agent Loop
Prompt Builder
LLM Provider Adapter
Tool Runtime
exec/web/search/read/write...
Memory Manager
短期会话历史
长期记忆: MEMORY.md + 日志
Knowledge RAG
BM25 + Embedding + RRF + Rerank
Skill Registry
L1 元数据
L2 指令加载
L3 资源加载
Cron Scheduler
3. 本篇目标
把工具能力从"占位函数"升级为可扩展的 Tool Runtime:有协议、有注册中心、有执行超时和错误边界。
2. 本篇范围
做什么
- 工具协议:
name、description、schema、run() - 参数校验:Pydantic
- 执行约束:超时、异常包装、结果裁剪
- 内置最小工具:
echo、read_file(受限目录)、web_fetch(可选)
3. 实现步骤
- 定义
ToolSpec与ToolRuntime - 给每个工具增加输入/输出模型
- 在 Agent Loop 中接入工具调度
- 统一 tool event 日志(start/success/fail)
3.1 目标
到第四篇结束,具备 3 个可被模型调用并可演示 的工具:
echo:最小闭环工具(验证协议链路)read_file:读取文件(只读 + 沙箱路径限制)write_file:写入文件(沙箱限制 + 覆盖策略)list_files(目录浏览)
每个工具都有:
- JSON Schema(给模型)
- Pydantic 入参校验(给执行器)
- 错误语义(参数错误、权限错误、执行错误)
- 演示 case(成功 + 失败)
3.2 项目代码
这里贴出了项目里一些片段。
A. 工具协议定义:ToolSpec
文件:openclaw_py/app/core/tools.py
python
@dataclass
class ToolSpec:
name: str
description: str
args_model: type[BaseModel]
handler: ToolHandler
timeout_seconds: float = 8.0
def to_openai_tool_schema(self) -> dict[str, Any]:
schema = self.args_model.model_json_schema()
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": schema.get("properties", {}),
"required": schema.get("required", []),
},
},
}
这段代码把"工具给谁看"的两个侧面统一了:
- 给模型看:
to_openai_tool_schema()(用于工具选择) - 给执行器看:
args_model+handler(用于实际执行)
B. 执行入口:ToolRuntime.execute()
文件:openclaw_py/app/core/tools.py
python
async def execute(self, call: ProviderToolCall) -> str:
spec = self._specs.get(call.name)
if spec is None:
return f"tool_error: unknown tool '{call.name}'"
try:
payload = json.loads(call.arguments or "{}")
if not isinstance(payload, dict):
return "tool_error: arguments must be a JSON object"
except json.JSONDecodeError:
return "tool_error: arguments must be valid JSON"
try:
args = spec.args_model.model_validate(payload)
except ValidationError as exc:
return f"tool_error: invalid arguments: {exc.errors()}"
try:
result = await asyncio.wait_for(
spec.handler(args),
timeout=spec.timeout_seconds,
)
except asyncio.TimeoutError:
return f"tool_error: tool '{spec.name}' timed out"
这就是"先验参、再执行、最后兜底"的主链路,基本覆盖了真实工具系统的最小可用闭环。
C. 沙箱限制:防止路径越权
文件:openclaw_py/app/core/tools.py
python
def _resolve_in_sandbox(sandbox_root: Path, raw_path: str) -> Path:
raw = Path(raw_path)
resolved = (sandbox_root / raw).resolve() if not raw.is_absolute() else raw.resolve()
try:
resolved.relative_to(sandbox_root)
except ValueError as exc:
raise PermissionError(
f"path '{raw_path}' escapes sandbox root '{sandbox_root}'",
) from exc
return resolved
像 read_file / write_file 这类工具,安全边界通常比"功能跑通"更重要。
这个检查可以直接拦住 ../ 路径穿越。
D. 内置 4 个演示工具注册
文件:openclaw_py/app/core/tools.py
python
runtime.register(ToolSpec(name="echo", ...))
runtime.register(ToolSpec(name="read_file", ...))
runtime.register(ToolSpec(name="write_file", ...))
runtime.register(ToolSpec(name="list_files", ...))
这四个足够覆盖:
- 协议链路验证(
echo) - 只读文件能力(
read_file) - 写文件能力(
write_file) - 目录可见性(
list_files)
E. Agent 侧如何接入 Tool Runtime
文件:openclaw_py/app/core/agent.py
python
tool_schemas = self.tool_runtime.schemas()
turn = await self.provider.chat(
messages=self.history,
tools=tool_schemas,
tool_choice="auto",
)
for call in turn.tool_calls:
tool_result = await self.tool_runtime.execute(call)
self.history.append({
"role": "tool",
"tool_call_id": call.id,
"content": tool_result,
})
这段把第三篇和第四篇真正串起来了:
- 第三篇负责循环编排(何时调用、何时收敛)
- 第四篇负责工具执行(怎么验参、怎么运行、怎么报错)
F. 一次完整调用时序图(请求 -> 工具 -> 回答)
Tool(read/write/list) ToolRuntime Provider Agent FastAPI /v1/chat User Tool(read/write/list) ToolRuntime Provider Agent FastAPI /v1/chat User loop [for each tool_call] POST /v1/chat {session_id, message} agent.chat(message) chat(messages, tools=schema, tool_choice=auto) assistant + tool_calls execute(tool_call) JSON parse + Pydantic validate sandbox check + timeout guard handler(args) result/error tool_result append role=tool to history chat(updated_messages, tools=schema, tool_choice=auto) final assistant text reply {"session_id": "...", "reply": "..."}
这个时序图对应当前项目的真实实现路径,读者可以一边看图,一边对照:
openclaw_py/app/core/agent.pyopenclaw_py/app/core/tools.pyopenclaw_py/app/core/llm_provider.py
4. 计划修改文件
openclaw_py/app/core/tools.pyopenclaw_py/app/core/agent.pyopenclaw_py/tests/(工具参数与超时测试)
6. 测试
编写了一个测试run_demo.py
python
from __future__ import annotations
import json
import httpx
BASE_URL = "http://127.0.0.1:7788"
SESSION_ID = "demo-tool-runtime"
def _print_title(title: str) -> None:
print(f"\n== {title} ==")
def send_chat(client: httpx.Client, message: str) -> str:
resp = client.post(
f"{BASE_URL}/v1/chat",
json={"session_id": SESSION_ID, "message": message},
timeout=60,
)
resp.raise_for_status()
return resp.json()["reply"]
def main() -> None:
with httpx.Client() as client:
_print_title("1) Health check")
health = client.get(f"{BASE_URL}/health", timeout=10)
health.raise_for_status()
print(json.dumps(health.json(), ensure_ascii=False, indent=2))
_print_title("2) echo tool")
print(
send_chat(
client,
"你现在是工具调用代理。请调用 echo 工具,参数 text 为 "
"'hello-openclaw-tool-runtime'。调用完成后只返回一句确认结果。",
),
)
_print_title("3) write_file tool")
print(
send_chat(
client,
"请调用 write_file 工具,在路径 'demo/runtime-demo.txt' 写入以下内容:"
"第一行 OpenClaw Tool Runtime Demo;"
"第二行 This file is generated by tool call."
"如果文件已存在,请覆盖。完成后简短确认写入成功。",
),
)
_print_title("4) read_file tool")
print(
send_chat(
client,
"请调用 read_file 工具读取 'demo/runtime-demo.txt',"
"然后把读取到的内容原样返回给我。",
),
)
_print_title("5) list_files tool (optional)")
print(
send_chat(
client,
"请调用 list_files 工具查看目录 'demo',"
"并告诉我是否看到了 runtime-demo.txt。",
),
)
print("\nDemo finished.")
if __name__ == "__main__":
main()
首先 python run.py 把服务跑起来, 然后运行 python demo/run_demo.py , 大概看到的是如下结果
shell
== 1) Health check ==
{
"ok": true,
"service": "openclaw-py",
"active_sessions": 0
}
== 2) echo tool ==
工具调用成功,已确认结果。
== 3) write_file tool ==
文件已成功写入demo/runtime-demo.txt。
== 4) read_file tool ==
OpenClaw Tool Runtime Demo
This file is generated by tool call.
== 5) list_files tool (optional) ==
是的,在demo目录中看到了runtime-demo.txt文件。
Demo finished.
5. 下一篇衔接
第 5 篇单独讲 Session 并发:为什么"同会话串行"是必要条件,以及如何验证不串线。
6. 看到这里,不妨支持一下
如果你已经看到这里,说明你对"手搓 OpenClaw"是真的感兴趣。
这套系列会持续把代码和踩坑都开源出来,不走玄学,尽量做到每篇都能复现。
如果这篇对你有帮助,欢迎随手支持一下:
- 点个赞,让我知道这条路线是有价值的
- 点个关注,后续 3~12 篇更新不会错过
- 点个收藏,后面实操时可以随时回来看代码片段
- 有余力的话,来个打赏,我会把更多时间投入到高质量连载里
你的每一次反馈,都会直接影响这个系列更新的速度和深度。 我们下一篇见。