龙虾-OpenClaw一文详细了解-手搓OpenClaw-3 Agent Loop

0. 为什么要手搓OpenClaw
OpenClaw 很强,但完整工程体量也很大。对于大多数开发者来说,直接阅读全量代码会有三个痛点:
- 模块多:Gateway、Agent、Tools、Sessions、Channels 互相耦合
- 路径长:一条消息从输入到回复,跨越多个子系统
- 调试难:没有自己的“最小版本”,很难定位问题
所以这个系列采用一个更实用的学习路径:
先做最小闭环,再逐步补齐能力。
1. 目标
用 Python 从 0 到 1 复现 OpenClaw 的核心能力:
- Agent Loop(工具调用 + 多轮推理)
- Session 与并发隔离
- 记忆系统(短期 + 长期)
- Skills 系统(分层加载)
- Web/Telegram 等渠道接入
第一篇的阶段目标是:
- 跑起 FastAPI 服务
- 打通一个最小
/v1/chat对话接口 - 具备会话隔离与并发控制(每会话锁 + 全局信号量)
2. 目标架构
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的工具调用的循环状态机呢, 如下图
上面的时序图与 test_agent_tool_call_then_final_answer() 一一对应:
- 用户发起
how is weather,Agent 先写入一条user历史。 - 第 1 次调用模型返回
TOOL_CALL: echo weather in beijing。 - Agent 解析后执行工具,并写入
tool历史(对应测试里assert "tool" in roles)。 - 第 2 次调用模型返回普通文本
The weather tool has finished.。 - 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 就形成了一个可运行的工具调用循环状态机:
provider.complete(history)拿到模型输出;_parse_tool_call()返回None:判定为普通文本,直接结束;_parse_tool_call()返回ToolCall:执行tools.run(name, arg);- 把工具结果写回
history的tool消息; - 进入下一轮,再次调用模型,直到拿到普通文本或达到
MAX_TOOL_ROUNDS。
实际langchain或者openclaw中的工具调用逻辑跟我们这个简单版的过程是一样的,等下一篇,我们就实现工具调用功能协议,实际写几个工具, 调用一下. 注意, 这个版本 没有采用openai 的 tools schema,知识采用简单的 "TOOL_CALL:"来标记, 下一篇咱们就用标准的tool schema来做.
7. 下一篇衔接
第 4 篇专讲 Tool Runtime,定义工具协议、参数校验、执行器隔离与超时控制等。
8. 看到这里,不妨支持一下
如果你已经看到这里,说明你对“手搓 OpenClaw”是真的感兴趣。
这套系列会持续把代码和踩坑都开源出来,不走玄学,尽量做到每篇都能复现。
如果这篇对你有帮助,欢迎随手支持一下:
- 点个赞,让我知道这条路线是有价值的
- 点个关注,后续 3~12 篇更新不会错过
- 点个收藏,后面实操时可以随时回来看代码片段
- 有余力的话,来个打赏,我会把更多时间投入到高质量连载里
你的每一次反馈,都会直接影响这个系列更新的速度和深度。
我们下一篇见。
更多推荐
所有评论(0)