news 2026/4/29 13:06:25

第3节:核心心脏,手写 Agent 的 Main Loop

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第3节:核心心脏,手写 Agent 的 Main Loop

Agent Harness 专题

上一节:第2节:从Framework到Harness,Agent需要怎样的底层支撑?

本节:第3节:核心心脏,手写 Agent 的 Main Loop

下一节:待更新


这一节,我们来亲手实现整个系统里最核心的部分:Main Loop

所有顶级的 Agent 引擎,表面上看起来像魔法一样,能在本地项目里来回穿梭、读代码、改文件、跑测试。

但如果你把这些系统一层层拆开,最终都会看到一个极其朴素、却又极其强健的东西:

一个持续运转的循环。

这个循环,在学术界通常被称为ReAct(Reason + Act)范式;而在工程实践里,我们更习惯叫它Agent Loop,或者Main Loop

如果说 Harness 是操作系统,那么 Main Loop 就是它的“心脏起搏器”。


为什么 Agent 必须依赖 Main Loop?

大模型面对的,不是一个静态题目,而是一个开放、动态、需要不断探索的环境。

当它拿到一个宏大的任务时,它不可能像传统问答机器人那样,只靠一次 API 调用就输出最终答案。

原因很简单:

  • 它不知道当前目录下有什么文件
  • 它不知道代码运行后会报什么错
  • 它不知道工具执行后会返回什么结果
  • 它也不知道前一步的尝试会不会失败

换句话说,大模型天然带着一种“针眼瞎”式的信息缺口。

它必须一边思考,一边行动,再根据行动结果修正下一步判断。

这就是 Main Loop 存在的根本原因。


从 CoT 到 ReAct:智能体范式是怎么演进的?

为了让大模型真正具备“解决问题”的能力,研究界和工程界其实走过了几条不同的路。

1. 纯推理模式的局限

最典型的代表,就是 Chain of Thought(CoT)。

这种方法会在 Prompt 里加入类似 “Let’s think step by step” 的提示,要求模型把思考过程显式写出来。

它确实大幅提升了逻辑推导能力,但也有一个非常明显的问题:

它只能思考,不能感知真实世界。

如果代码库变了、报错信息变了、外部环境变了,模型依然只能基于训练数据和当前 Prompt 做推断,很容易滑向幻觉。

2. 纯行动模式的局限

另一条路线,是直接给模型一堆工具,让它预测“下一步该执行什么动作”。

这种方式的优点是能动起来,但缺点同样明显:

它会很像一个横冲直撞的莽夫。

模型虽然会调用工具,却缺少稳定的状态跟踪和自我反思能力,遇到报错之后也常常不知道为什么错、该怎么调整。

3. ReAct:让思考与行动交织起来

ReAct 的关键突破,在于它不再把“思考”和“行动”分开,而是把两者编织进同一个闭环。

一个真正的智能体,在每一轮里都要完成 4 件事:

  1. 思考(Reason / Thought)
    分析当前线索,决定下一步意图。
    比如:“我看到了 calc.go 这个文件,下一步应该先读取它。”

  2. 行动(Act / Action)
    向外部环境发出操作请求。
    比如:调用 read_file 工具。

  3. 观察(Observe / Observation)
    外部环境把行动结果回传给模型。
    比如:返回 calc.go 的具体代码内容。

  4. 继续下一轮
    模型把新的 Observation 纳入上下文,再次思考、再次行动,形成闭环。

图 1:Main Loop 的整体心跳结构

图 2:ReAct 让 Agent 从一次性回答,变成持续探索


Harness 视角下,Main Loop 有什么不同?

从 Harness 的角度看,Main Loop 不是一个“业务流程图”,而是一个高度克制的运行时循环。

它最重要的特征,通常有 3 个:

  1. 极度纯粹,没有预设分支
    Loop 本身不承担业务逻辑,不写死“先做什么、再做什么”。路径完全由模型的实时推理决定。

  2. 不在这里设置生硬的最大步数
    很多玩具框架喜欢限制最大轮数,比如 5 步、10 步、20 步。
    但真实工业任务很可能需要几十轮。顶级引擎不会在这里简单粗暴截断,而是依赖后续的内存压缩、死循环干预和风险控制机制来维持稳定。

  3. 上下文是唯一的记忆载体
    在 Main Loop 里,系统最核心的状态,就是不断累加的上下文。
    每一次 Thought、Action、Observation,都会被追加进去,成为下一轮推理的依据。

正因为它足够朴素,才足够稳定。


项目开发:先定义统一的数据血液

在 Harness 引擎中,各个组件之间传递的核心数据,其实就是上下文。

大模型、工具系统、主循环,三者都围绕这一份上下文在协作。

但问题在于,不同模型供应商的 API 格式差异非常大。Claude、OpenAI、Gemini,各自都有不同的字段设计、消息结构、工具调用格式。

所以,在正式写 Main Loop 之前,我们必须先做一件事:

定义一套属于我们自己的统一 Schema。

它要能承载 ReAct 范式里最重要的几类信息:

  • 消息角色
  • 文本内容
  • 工具调用请求
  • 工具执行结果
  • 轮次间持续追加的上下文历史

只有先统一“血液”,后面的 Provider、Registry、Engine 才能真正解耦。


第 1 步:实现核心引擎

下面这段代码,就是我们这个项目当前的 Main Loop 雏形。

/** * ReAct 引擎核心(Main Loop) */ public class AgentEngine { private static final Logger log = Logger.getLogger(AgentEngine.class.getName()); private final LLMProvider provider; private final Registry registry; private final String workDir; public AgentEngine(LLMProvider provider, Registry registry, String workDir) { this.provider = provider; this.registry = registry; this.workDir = workDir; } public void run(String userPrompt) { log.info("[Engine] 引擎启动,工作目录: " + workDir); // 初始化上下文 List<Message> contextHistory = new ArrayList<>(); Message systemMsg = new Message(); systemMsg.setRole(Role.SYSTEM); systemMsg.setContent("You are my-claw, an expert coding assistant. You have full access to tools in the workspace."); contextHistory.add(systemMsg); Message userMsg = new Message(); userMsg.setRole(Role.USER); userMsg.setContent(userPrompt); contextHistory.add(userMsg); int turnCount = 0; // ======== 主循环:ReAct 心跳 ======== while (true) { turnCount++; log.info("========== [Turn " + turnCount + "] 开始 =========="); // 获取可用工具 var availableTools = registry.getAvailableTools(); // 模型思考 log.info("[Engine] 正在思考 (Reasoning)..."); Message response = provider.generate(contextHistory, availableTools); // 追加到历史 contextHistory.add(response); // 输出模型思考 if (response.getContent() != null && !response.getContent().isEmpty()) { System.out.println("🤖 模型: " + response.getContent()); } // 退出条件:无工具调用 = 任务完成 if (response.getToolCalls() == null || response.getToolCalls().isEmpty()) { log.info("[Engine] 任务完成,退出循环。"); break; } // 执行工具 log.info("[Engine] 模型请求调用 " + response.getToolCalls().size() + " 个工具..."); for (ToolCall toolCall : response.getToolCalls()) { log.info(" -> 🛠️ 执行工具: " + toolCall.getName() + ", 参数: " + toolCall.getArguments()); ToolResult result = registry.execute(toolCall); if (result.isError()) { log.warning(" -> ❌ 工具执行报错: " + result.getOutput()); } else { log.info(" -> ✅ 工具执行成功 (返回 " + result.getOutput().getBytes().length + " 字节)"); } // 构造观察结果,加入上下文 Message observation = new Message(); observation.setRole(Role.USER); observation.setContent(result.getOutput()); observation.setToolCallId(toolCall.getId()); contextHistory.add(observation); } } } }

这段代码看上去不长,但已经具备了 Main Loop 的核心骨架:

  • 初始化系统消息和用户消息
  • 把历史上下文持续累加起来
  • 交给模型生成下一步决策
  • 判断是否存在工具调用
  • 执行工具并把结果作为 Observation 回写上下文
  • 若无工具调用,则退出循环

你可以把它理解为一个最小可运行的“Agent 心跳”。


第 2 步:写一个最小可验证的测试闭环

光有 Main Loop 还不够,我们还需要一个最小实验环境,来验证这颗“心脏”是否真的会跳。

所以这里我用了两个 Mock 组件:

  • 一个伪造的 LLMProvider
  • 一个伪造的 Tool Registry

前者负责模拟模型在第 1 轮发起工具调用,在第 2 轮输出最终结论。

后者负责模拟工具执行结果,比如返回一条假的文件列表。

@SpringBootApplication public class Main { private static final Logger log = LoggerFactory.getLogger(Main.class); // ========== 伪造 LLM Provider ========== static class MockProvider implements LLMProvider { private int turn = 0; @Override public Message generate(List<Message> messages, List<ToolDefinition> availableTools) { turn++; Message msg = new Message(); msg.setRole(Role.ASSISTANT); if (turn == 1) { msg.setContent("让我来看看当前目录下有什么文件。"); ToolCall call = new ToolCall(); call.setId("call_123"); call.setName("bash"); call.setArguments("{\"command\": \"ls -la\"}"); msg.setToolCalls(List.of(call)); } else { msg.setContent("我看到了文件列表,里面包含 main.go,任务完成!"); } return msg; } } // ========== 伪造 Tool Registry ========== static class MockRegistry implements Registry { @Override public List<ToolDefinition> getAvailableTools() { return List.of(); } @Override public ToolResult execute(ToolCall call) { ToolResult result = new ToolResult(); result.setToolCallId(call.getId()); result.setOutput("-rw-r--r-- 1 user group 234 Oct 24 10:00 main.go\n"); result.setError(false); return result; } } public static void main(String[] args) { SpringApplication.run(Main.class, args); log.info("欢迎来到 my-claw 引擎启动序列"); log.info("架构蓝图搭建完毕,等待各核心模块注入!"); log.info("========="); String workDir = System.getProperty("user.dir"); LLMProvider provider = new MockProvider(); Registry registry = new MockRegistry(); AgentEngine engine = new AgentEngine(provider, registry, workDir); engine.run("帮我检查当前目录的文件"); } }

这个测试环境的意义不在于“功能完整”,而在于它验证了最关键的一点:

模型发起行动 -> 工具返回结果 -> 模型继续推理 -> 循环自然结束。

只要这个闭环成立,后面接入真实模型、真实工具、真实上下文工程,才有意义。


第 3 步:看测试结果是否符合预期

下面是这段 Main Loop 跑起来之后的日志输出:

2026-04-28T12:05:33.394+08:00 INFO 29888 --- [ main] com.example.javaclaw.Main : 欢迎来到 my-claw 引擎启动序列 2026-04-28T12:05:33.394+08:00 INFO 29888 --- [ main] com.example.javaclaw.Main : 架构蓝图搭建完毕,等待各核心模块注入! 2026-04-28T12:05:33.394+08:00 INFO 29888 --- [ main] com.example.javaclaw.Main : ========= 2026-04-28T12:05:33.395+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : [Engine] 引擎启动,工作目录: D:\Code\Agent Harness\harness-learning\my-claw 2026-04-28T12:05:33.395+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : ========== [Turn 1] 开始 ========== 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : [Engine] 正在思考 (Reasoning)... 🤖 模型: 让我来看看当前目录下有什么文件。 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : [Engine] 模型请求调用 1 个工具... 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : -> 🛠️ 执行工具: bash, 参数: {"command": "ls -la"} 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : -> ✅ 工具执行成功 (返回 51 字节) 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : ========== [Turn 2] 开始 ========== 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : [Engine] 正在思考 (Reasoning)... 🤖 模型: 我看到了文件列表,里面包含 main.go,任务完成! 2026-04-28T12:05:33.396+08:00 INFO 29888 --- [ main] c.example.javaclaw.engine.AgentEngine : [Engine] 任务完成,退出循环。

如果把这段日志翻译成人话,其实就是:

  1. 模型先思考
  2. 然后决定调用工具
  3. 工具把结果返回回来
  4. 模型基于 Observation 继续思考
  5. 发现任务已经完成,于是自然退出

这就是一个最小版的 ReAct Main Loop。

它还不复杂,但已经足够真实。


这一节的结论

Agent 的核心,不是“会不会调工具”,而是它有没有一个稳定、透明、持续运转的 Main Loop。

这个 Loop 不需要一开始就做得很花哨。

恰恰相反,它越朴素、越克制、越像一个真正的操作系统心跳,后面就越容易扩展出:

  • 更强的上下文工程
  • 更完整的工具系统
  • 更稳的死循环干预
  • 更严格的安全审批与边界控制

所以,手写 Main Loop 的意义,不只是“把代码跑起来”。

更重要的是,它让我们第一次真正掌握了 Agent 的心脏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/29 13:01:51

Steam自动关机终极指南:告别下载后电脑空转的烦恼

Steam自动关机终极指南&#xff1a;告别下载后电脑空转的烦恼 【免费下载链接】SteamShutdown Automatic shutdown after Steam download(s) has finished. 项目地址: https://gitcode.com/gh_mirrors/st/SteamShutdown 还在为Steam游戏下载完成后电脑整夜运行而烦恼吗&…

作者头像 李华
网站建设 2026/4/29 13:00:04

抖音无水印视频下载终极指南:3分钟掌握免费批量下载技巧

抖音无水印视频下载终极指南&#xff1a;3分钟掌握免费批量下载技巧 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback sup…

作者头像 李华
网站建设 2026/4/29 12:59:24

Winhance中文版终极指南:如何让Windows系统管理变得简单直观

Winhance中文版终极指南&#xff1a;如何让Windows系统管理变得简单直观 【免费下载链接】Winhance-zh_CN A Chinese version of Winhance. C# application designed to optimize and customize your Windows experience. 项目地址: https://gitcode.com/gh_mirrors/wi/Winha…

作者头像 李华
网站建设 2026/4/29 12:58:21

分布式深度学习中的Cut-in-half调度优化方案

1. 项目背景与核心发现 在分布式深度学习训练领域&#xff0c;流水线并行&#xff08;Pipeline Parallelism, PP&#xff09;与专家并行&#xff08;Expert Parallelism, EP&#xff09;的协同设计一直是优化训练效率的关键挑战。Sea AI Lab团队在分析DualPipe调度方案时发现了…

作者头像 李华