在这里插入图片描述

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. 本篇目标

实现一个“可持续多轮”的 Agent Loop:消息输入 -> 模型推理 ->(可选)工具调用 -> 最终回复。


4. 本篇范围

做什么

  • 定义 Agent 循环状态机
  • 支持最多 N 轮工具调用(如 8 或 12)
  • 增加循环退出条件(无工具调用/达到最大轮次/异常)
  • 持久化最小消息历史(内存版)

不做什么

  • 不实现复杂工具协议(留给以后做)
  • 不做上下文压缩(留给以后做)

5. 关键设计

  • MAX_TOOL_ROUNDS:防止死循环
  • 每轮输出统一结构:assistant_text, tool_calls, usage
  • 错误策略:工具失败可回传模型继续,模型失败直接终止并返回错误说明
  • 明确本篇只做“编排”,不做工具细节:Agent 负责循环与状态机,Tool Runtime 负责执行与校验

6. 修改的文件

​ 代码下载路径:openclaw_py

  • openclaw_py/app/core/agent.py

  • openclaw_py/app/core/llm_provider.py

  • openclaw_py/tests/(Agent Loop 行为测试)

  • 一张 Agent Loop 时序图(sequenceDiagram)

    如何实现了agent的工具调用的循环状态机呢, 如下图

Tool Runtime LLM Provider Agent User Tool Runtime LLM Provider Agent User continue next round with updated history alt [model_text is normal answer] [model_text starts with TOOL_CALL:] loop [round <= MAX_TOOL_ROUNDS] chat("how is weather") 1 append history(user) 2 complete(history) 3 model_text 4 append history(assistant) 5 return final answer 6 parse TOOL_CALL(name,arg) 7 append history(assistant: TOOL_CALL...) 8 run(name, arg) 9 tool_result 10 append history(tool) 11 append fallback(max rounds reached) 12 return fallback 13

上面的时序图与 test_agent_tool_call_then_final_answer() 一一对应:

  1. 用户发起 how is weather,Agent 先写入一条 user 历史。
  2. 第 1 次调用模型返回 TOOL_CALL: echo weather in beijing
  3. Agent 解析后执行工具,并写入 tool 历史(对应测试里 assert "tool" in roles)。
  4. 第 2 次调用模型返回普通文本 The weather tool has finished.
  5. Agent 直接结束并返回最终答案(对应测试里 assert provider.calls == 2 与最终 reply 断言)。

补充说明:test_agent_tool_call_then_final_answer() 位于项目代码 openclaw_py/tests/test_agent_loop.py
这个版本实现的是一个最简单的文本工具调用协议:当模型输出以 TOOL_CALL: 开头时,Agent 认为“这是一次工具调用请求”;TOOL_CALL: 后面是调用参数, 否则就认为“这是最终回复文本”并直接结束。

协议解析核心代码在 openclaw_py/app/core/agent.py

@staticmethod
def _parse_tool_call(text: str) -> ToolCall | None:
    line = text.strip()
    prefix = "TOOL_CALL:"
    if not line.startswith(prefix):
        return None
    payload = line.removeprefix(prefix).strip()
    if not payload:
        return None
    parts = payload.split(" ", 1)
    name = parts[0].strip()
    arg = parts[1].strip() if len(parts) > 1 else ""
    if not name:
        return None
    return ToolCall(name=name, arg=arg)

基于这段解析逻辑,Agent 就形成了一个可运行的工具调用循环状态机:

  1. provider.complete(history) 拿到模型输出;
  2. _parse_tool_call() 返回 None:判定为普通文本,直接结束;
  3. _parse_tool_call() 返回 ToolCall:执行 tools.run(name, arg)
  4. 把工具结果写回 historytool 消息;
  5. 进入下一轮,再次调用模型,直到拿到普通文本或达到 MAX_TOOL_ROUNDS

实际langchain或者openclaw中的工具调用逻辑跟我们这个简单版的过程是一样的,等下一篇,我们就实现工具调用功能协议,实际写几个工具, 调用一下. 注意, 这个版本 没有采用openai 的 tools schema,知识采用简单的 "TOOL_CALL:"来标记, 下一篇咱们就用标准的tool schema来做.

7. 下一篇衔接

第 4 篇专讲 Tool Runtime,定义工具协议、参数校验、执行器隔离与超时控制等。

8. 看到这里,不妨支持一下

如果你已经看到这里,说明你对“手搓 OpenClaw”是真的感兴趣。
这套系列会持续把代码和踩坑都开源出来,不走玄学,尽量做到每篇都能复现。

如果这篇对你有帮助,欢迎随手支持一下:

  • 点个,让我知道这条路线是有价值的
  • 点个关注,后续 3~12 篇更新不会错过
  • 点个收藏,后面实操时可以随时回来看代码片段
  • 有余力的话,来个打赏,我会把更多时间投入到高质量连载里

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐