从零实现 Harness Agent:模型无关的 ReAct 主循环
系列导航:系列目录 | 上一篇:从零实现 Harness Agent:搭建分层 Python Agent CLI 框架 | 下一篇:从零实现 Harness Agent:设计模型 Provider 适配层
本节目标
导读:本篇属于第一部分「基础运行时」,聚焦控制流核心:让
MainLoop做调度中心,而不是模型 SDK、工具实现和状态管理的混合体。
本节要实现的是 tiny-claw 的 Agent 主循环:一个可以接收用户请求、构建上下文、调用 Provider、执行工具调用、追加 observation,并在多轮 ReAct 流程中返回最终结果的编排核心。
完成这一节后,系统会具备下面这些能力:
tiny-claw run "..."可以进入稳定的多轮 Agent 主循环。- 主循环可以根据
act、think、plan、plan-act切换工具策略。 - 模型返回 tool calls 时,主循环能把它们交给
ToolExecutor,再把工具结果送回下一轮模型请求。 - OpenAI、Claude、Echo 或 FakeProvider 都可以接入同一个主循环,而不需要改控制流。
这一节的关键目标是把 MainLoop 做成“调度中心”,而不是模型 SDK、工具实现和状态管理的混合体。
摘要
Agent 主循环最容易膨胀成系统里的“万能类”:既懂模型 SDK,又懂工具实现,还负责保存状态。tiny-claw 刻意把 MainLoop 限定为编排层:它只决定模型本轮看到什么、能调用什么、工具结果如何进入下一轮。本文介绍 MainLoop 如何只依赖内部协议和抽象接口,而不直接绑定 OpenAI、Claude 或具体工具实现。
背景与问题
一个 ReAct Agent 至少要处理三类对象:模型消息、工具定义、工具调用结果。最直接的实现方式是让主循环直接拼 OpenAI payload、读取工具列表、执行工具函数,再把结果拼回模型请求。
这种写法的问题很快会显现:
- 接入 Claude 时,需要重写主循环里的消息转换。
- 测试时只能调用真实模型,难以构造确定性的 tool calls。
- 工具实现和工具调度混在一起,难以做并发、错误兜底和权限过滤。
- 新增
plan、think、plan-act运行模式时,控制流会和厂商 SDK 细节缠在一起。
tiny-claw 的做法是把主循环变成 provider-neutral、tool-neutral 的编排层。
设计目标
- Provider-neutral:主循环只认识
LLMProvider协议和内部 schema。 - Tool-neutral:主循环不执行具体工具逻辑,只调用
ToolExecutor。 - 模式清晰:
act、think、plan、plan-act有明确工具策略。 - 可测试:可以用 FakeProvider 构造稳定多轮响应。
- 可观测:每轮请求、响应、工具调用和停止原因都有日志入口。
- 可恢复:会话记忆和 plan 文件由 session 层提供,不硬编码在主循环里。
整体方案
MainLoop.run() 的输入是一次用户请求、运行模式、最大轮数和 SessionRef。它先通过上下文层生成本轮消息,再按模式决定是否暴露工具。模型返回 tool calls 时,主循环交给 ToolExecutor 执行,并把 observation 作为 Role.TOOL 消息追加回原始消息列表。
1 | sequenceDiagram |
核心实现
核心文件是 src/tiny_claw/_internal/engine/main_loop.py。
运行模式定义为:
1 | class RunMode(StrEnum): |
工具策略只区分两种:
1 | class ToolPolicy(StrEnum): |
MainLoop 的依赖都是抽象或项目内部协议:
1 |
|
在每轮模型请求前,主循环会基于当前 phase 选择是否暴露工具:
1 | request_tool_definitions = ( |
Provider 请求使用项目内部 LLMRequest,不是厂商 SDK payload:
1 | response = self.provider.complete( |
这让 OpenAI、Claude、Echo 的差异被限制在 provider/ 目录中。
使用方式
用户通常通过 CLI 触发主循环:
1 | tiny-claw run "解释当前项目结构" |
工具暴露由配置控制:
1 | TINY_CLAW_ENABLED_TOOLS=read,edit tiny-claw run "修改 README 中的一段文字" |
如果不显式启用写类工具,默认只启用 read。
测试与验证
主循环适合用 FakeProvider 测试。测试可以精确控制模型每一轮返回什么:
1 | FakeProvider -> assistant tool_call(read) |
推荐验证命令:
1 | uv run pytest tests/test_engine.py |
全量验证:
1 | uv run ruff check . |
设计取舍与注意事项
MainLoop 最重要的取舍,是拒绝成为“什么都知道”的对象。它不读取 .env,因为配置来源属于 Settings 和 app.py;它不直接执行工具,因为工具并发、错误兜底和 channel 通知属于 ToolExecutor;它也不理解 OpenAI 或 Claude 的 SDK 类型,因为厂商翻译属于 Provider。
运行模式也是边界的一部分。think 和 plan 阶段隐藏工具定义,不只是为了省 token,而是为了保证“只分析”和“只规划”的语义不会被模型绕过。如果模型仍然返回 tool calls,主循环会用工具策略阻止执行。
上下文压缩同样保持在请求视图层:ContextCompactor 只影响发给 provider 的临时 messages,不修改主循环里的原始历史。最后,max_steps 是硬边界,避免模型和工具在异常情况下无限循环。
总结
MainLoop是编排层,不是 Provider 适配层,也不是工具实现层。- 内部 schema 让模型厂商差异保持在 provider 目录里。
- FakeProvider 让多轮 Agent 行为可以稳定测试。
- 运行模式和工具策略分离,使
act、think、plan、plan-act的边界更清楚。
按编号继续阅读:03:Provider 适配层 会把模型厂商差异收敛到主循环之外。
来源:本文整理自
tiny-claw/docs/tutorial/02-模型无关-react-主循环.md。
项目地址:barry166/tiny-claw。









