OpenClaw一文详细了解-手搓OpenClaw-4 Tool Runtime

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. 本篇范围

做什么

  • 工具协议:namedescriptionschemarun()
  • 参数校验:Pydantic
  • 执行约束:超时、异常包装、结果裁剪
  • 内置最小工具:echoread_file(受限目录)、web_fetch(可选)

3. 实现步骤

  1. 定义 ToolSpecToolRuntime
  2. 给每个工具增加输入/输出模型
  3. 在 Agent Loop 中接入工具调度
  4. 统一 tool event 日志(start/success/fail)

3.1 目标

到第四篇结束,具备 3 个可被模型调用并可演示 的工具:

  1. echo:最小闭环工具(验证协议链路)
  2. read_file:读取文件(只读 + 沙箱路径限制)
  3. write_file:写入文件(沙箱限制 + 覆盖策略)
  4. 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.py
  • openclaw_py/app/core/tools.py
  • openclaw_py/app/core/llm_provider.py

4. 计划修改文件

  • openclaw_py/app/core/tools.py
  • openclaw_py/app/core/agent.py
  • openclaw_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 篇更新不会错过
  • 点个收藏,后面实操时可以随时回来看代码片段
  • 有余力的话,来个打赏,我会把更多时间投入到高质量连载里

你的每一次反馈,都会直接影响这个系列更新的速度和深度。 我们下一篇见。

相关推荐
波动几何2 小时前
你好,我是 Adaptive Skill Stack
人工智能
皮皮学姐分享-ppx2 小时前
1447上市公司数字化转型速度的计算(2000-2022年)
大数据·人工智能
克里普crirp2 小时前
电离层TEC地图中添加晨昏线/昼夜转换线
python
Dxy12393102162 小时前
Python使用PyEnchant详解:打造高效拼写检查工具
开发语言·python
张二娃同学2 小时前
Claude Code 使用教程:下载安装、CC Switch 配置、MiniMax API 获取与启动实操
人工智能·windows·深度学习·github·claude code
yitian_hm2 小时前
RAG实战:从原理到代码,构建企业级知识库问答系统
人工智能
AI品信智慧数智人2 小时前
文旅景区小程序集成数字人智能语音交互系统,山东品信解锁AI伴游新玩法✨
人工智能·小程序
Rick19932 小时前
LangChain和spring ai是什么关系?
人工智能·spring·langchain
AI创界者2 小时前
【首发】LTX-2.3-VBVR 增强版发布:8G 显存解锁无限时长,视频一致性与运动精度跨越式升级!
人工智能