所有顶级的 Agent 引擎(无论是早期的 AutoGPT,还是如今最先进的 Claude Code、OpenClaw),它们表面上看起来像魔法一样能在你的本地项目里来回穿梭、修改代码、执行测试。但在代码的最底层,
它们都在跑着一个极其朴素、但极其强健的无限循环。这个循环,在学术界通常被称为 ReAct (Reason + Act) 范式,而在工程界,我们通常称之为 Agent Loop 或 Main Loop。
在传统的软件开发中,程序的执行流是确定且线性的(如下图所示)。你写下 if-else,程序就严格按照路径执行。
但大模型(LLM)面对的是一个开放的、动态的、需要不断探索的环境。当它拿到一个宏大的任务(比如:“找出项目中计算错误的原因并修复”)时,它不可能像传统的纯问答(QA)机器人助手那样,在一次 API 调用中就吐出最终的完美代码。因为它缺少实时信息——它不知道当前目录下有什么文件,也不知道运行 go test 会报什么错。
所有的Agent的设计都是任务导向的,最终目的都是为了完成任务
为了解决大模型在执行任务过程中信息不足,以及在信息不足的情况下就执行操作的问题,经历过几次范式的改变
纯推理(Reasoning Only)与纯行动(Acting Only)的局限性 纯推理模式(如 Chain of Thought, CoT):通过在 Prompt 中加入“Let’s think step by step”,强迫模型把思考过程写出来。这极大地提升了模型的逻辑推导能力,但致命缺陷是它无法与外部世界交互。如果代码库更新了,或者报错信息变了,模型依然在用过时的、基于训练数据的“幻觉”在推理。
纯行动模式(Acting Only):直接给模型一堆工具(Tools),让它直接预测下一个要执行的动作。这种模式下,模型缺乏深度的状态跟踪和自我反思,往往就像一个横冲直撞的莽夫,很容易因为上一步的报错而陷入迷茫。
ReAct:智能体的觉醒时刻 直到 2022 年 10 月,普林斯顿大学博士生 Shunyu Yao(在 Google 实习期间)与 Google 研究人员联合发表了预印本论文《ReAct: Synergizing Reasoning and Acting in Language Models》,并于 2023 年正式发表在 ICLR 2023 上。
ReAct 核心思想这篇论文提出了一个极其优雅但影响深远的范式:将“思考(Reasoning)”与“行动(Acting)”在一个循环中交织起来。ReAct 范式认为,一个真正的智能体,必须像人类解决问题一样,在每次行动前先思考,在每次行动后观察结果:
思考(Reason / Thought) :分析当前拿到的线索,规划下一步的意图。例如:“我看到了 calc.go 这个文件,里面可能有 Bug,下一步我要读取它。
”行动(Act / Action) :向外部环境发出指令。例如:调用 read_file 工具。
观察(Observe / Observation) :外部环境(比如我们的 Harness 引擎)将工具执行的结果返回给模型。例如返回了 calc.go 的具体代码。
然后再回到第 1 步,结合新获得的 Observation 再次思考,形成闭环。
在驾驭工程(Harness Engineering)中,我们将这套理论抽象为一个底层的 for 循环。我们可以用下面这张状态机图来精确描述它
只要大模型返回的结果中包含“工具调用请求(Tool Call Request)”,这个 Loop 就会一直循环下去。每一次从“组装 Prompt”到“追加观察结果”,我们称之为一个 Turn(轮次)。
在顶级引擎(如 Claude Code、OpenClaw)中,这个 Main Loop 的设计有几个极其鲜明的特征:
极度纯粹,没有预设分支:循环中没有业务逻辑,全凭模型决定走向。
不设硬性的最大步骤限制:传统的玩具框架喜欢设置 max_turns=10,但真实的工业任务可能需要 50 步。顶级引擎不在此处做生硬的截断,而是依赖后续我们将会讲到的 Context Compaction(内存压缩) 和 System Reminders(系统级防死循环干预)来维持稳定。
上下文(Context)是唯一的记忆载体:在这个循环中,数据会像滚雪球一样不断累加,记录下每一次的思考、动作和观察结果。
以下是核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 package engineimport ( "context" "fmt" "log" "github.com/yourname/go-tiny-claw/internal/provider" "github.com/yourname/go-tiny-claw/internal/schema" "github.com/yourname/go-tiny-claw/internal/tools" ) type AgentEngine struct { provider provider.LLMProvider registry tools.Registry WorkDir string } func NewAgentEngine (p provider.LLMProvider, r tools.Registry, workDir string ) *AgentEngine { return &AgentEngine{ provider: p, registry: r, WorkDir: workDir, } } func (e *AgentEngine) Run(ctx context.Context, userPrompt string ) error { log.Printf("[Engine] 引擎启动,锁定工作区: %s\n" , e.WorkDir) contextHistory := []schema.Message{ { Role: schema.RoleSystem, Content: "You are go-tiny-claw, an expert coding assistant. You have full access to tools in the workspace." , }, { Role: schema.RoleUser, Content: userPrompt, }, } turnCount := 0 for { turnCount++ log.Printf("========== [Turn %d] 开始 ==========\n" , turnCount) availableTools := e.registry.GetAvailableTools() log.Println("[Engine] 正在思考 (Reasoning)..." ) responseMsg, err := e.provider.Generate(ctx, contextHistory, availableTools) if err != nil { return fmt.Errorf("模型生成失败: %w" , err) } contextHistory = append (contextHistory, *responseMsg) if responseMsg.Content != "" { fmt.Printf("🤖 模型: %s\n" , responseMsg.Content) } if len (responseMsg.ToolCalls) == 0 { log.Println("[Engine] 任务完成,退出循环。" ) break } log.Printf("[Engine] 模型请求调用 %d 个工具...\n" , len (responseMsg.ToolCalls)) for _, toolCall := range responseMsg.ToolCalls { log.Printf(" -> 🛠️ 执行工具: %s, 参数: %s\n" , toolCall.Name, string (toolCall.Arguments)) result := e.registry.Execute(ctx, toolCall) if result.IsError { log.Printf(" -> ❌ 工具执行报错: %s\n" , result.Output) } else { log.Printf(" -> ✅ 工具执行成功 (返回 %d 字节)\n" , len (result.Output)) } observationMsg := schema.Message{ Role: schema.RoleUser, Content: result.Output, ToolCallID: toolCall.ID, } contextHistory = append (contextHistory, observationMsg) } } return nil }
问题:大模型很“冲动”看似我们已经掌握了智能体运行的终极密码。但是,如果现在就把真实的前沿大模型(如 Claude 4.x 或 GPT-5.x)接入这个基础循环,并且给它挂载上能够修改本地代码的 edit 和 bash 工具,你会遭遇一个极其普遍但又令人抓狂的现象:大模型变得极其“冲动”。
当你给它一个复杂的任务:“帮我分析整个订单模块的并发逻辑,并重构它”时。它可能连其他文件都没看,瞬间就发出了一个 edit 工具的调用请求,去盲目修改它看到的第一个文件。
诺贝尔经济学奖得主丹尼尔·卡尼曼在《思考,快与慢》中提出,人类的大脑包含两套系统:
系统 1(快思考):直觉的、本能的、自动的。比如你看到 2+2 立刻就能想到 4。
系统 2(慢思考):逻辑的、深思熟虑的、需要消耗精力的。比如让你计算 17 × 24,你必须拿出一张草稿纸,一步步推导。
大语言模型的本质是预测下一个 Token。从架构上看,它天生就是一个完美的“系统 1”。
当你向大模型发起一次普通的 API 请求时,它只能顺着当前的上下文,凭借概率直觉“一口气”把答案生成出来。如果问题极其复杂,它无法在生成第一个字之前,在脑海里预演几十步的完整计划。
“冲动的解决方案” 强制CoT为了激发大模型的“系统 2(慢思考)”,学术界发明了 Chain of Thought(思维链,CoT) 技术。我们会在提示词里加上一句咒语:“Let’s think step by step(让我们一步步思考)”。这就相当于给了大模型一张“草稿纸”,让它在输出最终答案前,先把中间推理过程写出来。
但在 Agent 的工具调用(Function Calling)场景下,这种单纯的提示词工程彻底破产了。因为 AI 工程师在构建长程 Coding Agent 时,发现了一个致命的规律:
“When tools are available, models tend to act quickly rather than think deeply.”(当工具可用时,模型倾向于迅速采取行动,而不是深入思考。)
Harness(机制决定行为)流程设计与管理这个科目中有个核心哲学:功能蕴含在流程之中,一个系统的功能由其流程实现,也由其功能体现。
如果你在系统提示词里写:“请你先仔细规划,然后再调用工具”。大模型往往会无视这句话。只要它在上下文的 Schema 里看到了诱人的 bash 或 edit 工具,它的预测概率就会瞬间坍塌,转而生成一段 JSON 参数去调用工具。
如何解决呢?既然提示词管不住它的“手”,那我们就用架构锁住它的“手”!
驾驭工程(Harness Engineering)给出的解法是:机制决定行为。
在每一次大模型采取行动前,Harness 引擎会向它发起一次没有附带任何工具 Schema 的纯文本 API 请求。在这个绝对没有工具诱惑的“小黑屋”里,模型别无选择,只能乖乖地输出一段纯文本的深度推理与规划。
等它想清楚了,Harness 会把这段推理记录追加到上下文中,然后再发起第二次附带工具的请求,让它去执行。
这就是工业级 Agent 循环中的 Two-Stage ReAct(两阶段 ReAct 循环) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 func (e *AgentEngine) Run(ctx context.Context, userPrompt string ) error { log.Printf("[Engine] 引擎启动,锁定工作区: %s\n" , e.WorkDir) log.Printf("[Engine] 慢思考模式 (Thinking Phase): %v\n" , e.EnableThinking) contextHistory := []schema.Message{ { Role: schema.RoleSystem, Content: "You are go-tiny-claw, an expert coding assistant. You have full access to tools in the workspace." , }, { Role: schema.RoleUser, Content: userPrompt, }, } turnCount := 0 for { turnCount++ log.Printf("\n========== [Turn %d] 开始 ==========\n" , turnCount) availableTools := e.registry.GetAvailableTools() if e.EnableThinking { log.Println("[Engine][Phase 1] 剥夺工具访问权,强制进入慢思考与规划阶段..." ) thinkResp, err := e.provider.Generate(ctx, contextHistory, nil ) if err != nil { return fmt.Errorf("Thinking 阶段生成失败: %w" , err) } if thinkResp.Content != "" { fmt.Printf("🧠 [内部思考 Trace]: %s\n" , thinkResp.Content) contextHistory = append (contextHistory, *thinkResp) } } log.Println("[Engine][Phase 2] 恢复工具挂载,等待模型采取行动..." ) actionResp, err := e.provider.Generate(ctx, contextHistory, availableTools) if err != nil { return fmt.Errorf("Action 阶段生成失败: %w" , err) } contextHistory = append (contextHistory, *actionResp) if actionResp.Content != "" { fmt.Printf("🤖 [对外回复]: %s\n" , actionResp.Content) } if len (actionResp.ToolCalls) == 0 { log.Println("[Engine] 模型未请求调用工具,任务宣告完成。" ) break } log.Printf("[Engine] 模型请求调用 %d 个工具...\n" , len (actionResp.ToolCalls)) for _, toolCall := range actionResp.ToolCalls { log.Printf(" -> 🛠️ 执行工具: %s, 参数: %s\n" , toolCall.Name, string (toolCall.Arguments)) result := e.registry.Execute(ctx, toolCall) if result.IsError { log.Printf(" -> ❌ 工具执行报错: %s\n" , result.Output) } else { log.Printf(" -> ✅ 工具执行成功 (返回 %d 字节)\n" , len (result.Output)) } observationMsg := schema.Message{ Role: schema.RoleUser, Content: result.Output, ToolCallID: toolCall.ID, } contextHistory = append (contextHistory, observationMsg) } } return nil }
Two-Stage ReAct的一点实现细节上面的代码其实有个问题,如果在Think阶段完全不传递tool的信息,大模型的推理也是有问题的,所以一般的做法是通过prompt的方式传入而非通过 tools 字段传入结构化的 JSON Schema
在 Phase 1 时,我们将工具的宏观描述(自然语言)作为 System Prompt 或上下文的一部分发给大模型,让它知道自己有什么能力去“做规划”; 但是,我们故意不传结构化的 tools 字段。这就相当于给模型戴上了“手铐”,逼迫它必须用纯文本把步骤想清楚,而无法直接“扣动扳机”