04 - Agent 核心:ReAct 循环(最重要)github 源码(欢迎star)

目标

完全理解 ReAct 循环的实现,这是整个 AI Agent 的核心机制。


1. 什么是 ReAct?

1.1 概念

ReAct = Reasoning(推理)+ Acting(行动)

一种让 AI 能够思考并执行操作的循环模式。

1.2 类比:厨师做菜

客人:做一道红烧肉

[THINK 思考]
- 需要什么材料?猪肉、酱油、糖...
- 需要什么步骤?切块、炒糖色、炖煮...

[ACT 行动]
- 去超市买猪肉

[OBSERVE 观察]
- 猪肉买到了,是五花肉

[THINK 思考]
- 材料齐了,开始做菜

[ACT 行动]
- 切肉、炒糖色...

[OBSERVE 观察]
- 肉炖好了,颜色很漂亮

[完成]
- 上菜!

1.3 在 AI 中的体现

用户:创建一个文件

[THINK] LLM 分析:用户需要创建文件,我应该调用 write_file
[ACT]  调用 write_file(path="test.txt", content="")
[OBSERVE] 工具返回:"已写入: test.txt"

[THINK] LLM 分析:文件已创建成功,可以告诉用户了
[完成] 回复用户:"文件已创建!"

2. 完整代码实现

创建文件 src/core/agent.ts

/**
 * Agent 运行时 - ReAct 范式实现
 * 
 * 核心思想:
 * Think(思考) → Act(行动) → Observe(观察) → Loop(循环)
 * 
 * 状态机:idle → thinking → acting → idle
 * 防御设计:maxDepth 防止无限循环
 */

import { AgentConfig, AgentState, Message, ToolCall, ToolResult, AgentThought, Session, LLMRequest } from './types.js';
import { ToolRegistry } from '../tools/registry.js';
import { createProvider } from './llm.js';
import { randomUUID } from 'crypto';

/** Agent 事件回调 */
export interface AgentCallbacks {
  onStateChange?: (state: AgentState) => void;
  onMessage?: (message: Message) => void;
  onToolCall?: (call: ToolCall) => void;
  onToolResult?: (result: ToolResult) => void;
  onError?: (error: Error) => void;
}

/** Agent 运行时 */
export class Agent {
  /** 当前状态 */
  private state: AgentState = 'idle';

  constructor(
    private config: AgentConfig,
    private tools: ToolRegistry,
    private callbacks: AgentCallbacks = {}
  ) {}

  getState(): AgentState { return this.state; }

  // ==============================================================================
  // 第一步:处理用户输入(入口)
  // ==============================================================================
  
  /**
   * 处理用户输入 - 主入口
   * 
   * 流程:
   * 1. 添加用户消息到会话历史
   * 2. 进入 ReAct 循环
   * 3. 返回最终回复
   */
  async process(input: string, session: Session): Promise<Message> {
    // 1. 添加用户消息
    const userMsg: Message = {
      id: randomUUID(),
      role: 'user',
      content: input,
      timestamp: Date.now(),
    };
    session.messages.push(userMsg);
    this.callbacks.onMessage?.(userMsg);

    // 2. 进入 ReAct 循环
    const response = await this.reactLoop(session);
    
    // 3. 保存助手回复
    session.messages.push(response);
    session.updatedAt = Date.now();
    
    return response;
  }

  // ==============================================================================
  // 第二步:ReAct 循环(核心)
  // ==============================================================================
  
  /**
   * ReAct 循环
   * 
   * 这是整个 Agent 的核心逻辑!
   * 
   * @param session 会话对象,包含消息历史
   * @param depth 当前递归深度,防止无限循环
   */
  private async reactLoop(session: Session, depth = 0): Promise<Message> {
    // 防御机制:最大递归深度
    // 如果超过 10 轮还没完成,说明任务太复杂或出错了
    if (depth >= (this.config.maxToolDepth || 10)) {
      return this.createMessage('assistant', '任务过于复杂,已达到最大处理深度。');
    }

    // ---------------------------------------------------------------------------
    // Step 1: THINK(思考)
    // ---------------------------------------------------------------------------
    this.setState('thinking');
    
    // 调用 LLM,让它决定下一步行动
    const thought = await this.think(session);

    // 如果 LLM 没有调用工具,说明任务已完成
    if (!thought.toolCalls?.length) {
      this.setState('idle');
      return this.createMessage('assistant', thought.content || '');
    }

    // ---------------------------------------------------------------------------
    // Step 2: ACT(行动)
    // ---------------------------------------------------------------------------
    this.setState('acting');

    // ★★★ 关键:添加 assistant 消息到历史 ★★★
    // 为什么重要?
    // 因为 LLM 需要知道它自己之前调用了什么工具
    // 否则下次调用时会重复调用同一个工具
    session.messages.push({
      id: randomUUID(),
      role: 'assistant',
      content: thought.content || '',
      timestamp: Date.now(),
      metadata: { toolCalls: thought.toolCalls },
    });

    // 执行每个工具调用
    const results: ToolResult[] = [];
    for (const call of thought.toolCalls) {
      this.callbacks.onToolCall?.(call);
      
      // 获取工具并执行
      const tool = this.tools.get(call.name);
      let result: ToolResult;
      
      if (!tool) {
        result = {
          toolCallId: call.id,
          output: '',
          error: `工具不存在: ${call.name}`,
        };
      } else {
        try {
          const output = await tool.execute(call.parameters);
          result = { toolCallId: call.id, output };
        } catch (error: any) {
          result = { toolCallId: call.id, output: '', error: error.message };
        }
      }
      
      results.push(result);
      this.callbacks.onToolResult?.(result);
    }

    // ---------------------------------------------------------------------------
    // Step 3: OBSERVE(观察)
    // ---------------------------------------------------------------------------
    // 将工具执行结果添加到历史
    for (const result of results) {
      session.messages.push({
        id: randomUUID(),
        role: 'tool',
        content: result.error ? `错误: ${result.error}` : result.output,
        timestamp: Date.now(),
        metadata: { toolCallId: result.toolCallId },
      });
    }

    // ---------------------------------------------------------------------------
    // Step 4: LOOP(循环)
    // ---------------------------------------------------------------------------
    // 递归调用,进入下一轮思考
    return this.reactLoop(session, depth + 1);
  }

  // ==============================================================================
  // 第三步:调用 LLM 思考
  // ==============================================================================
  
  /**
   * THINK - 调用 LLM 进行推理
   * 
   * @param session 包含完整消息历史的会话
   * @returns LLM 的思考结果
   */
  private async think(session: Session): Promise<AgentThought> {
    // 创建 LLM Provider
    const provider = createProvider(
      this.config.model,
      this.config.apiKey,
      this.config.baseUrl
    );

    // ★★★ 关键:构建消息历史 ★★★
    // 这是整个系统最核心的数据转换!
    
    const messages: LLMRequest['messages'] = [
      // 系统提示词
      { role: 'system', content: this.config.systemPrompt || '' },
    ];

    // 添加会话历史(最近 20 条)
    for (const msg of session.messages.slice(-20)) {
      if (msg.role === 'assistant' && msg.metadata?.toolCalls) {
        // 包含 tool_calls 的 assistant 消息
        messages.push({
          role: 'assistant',
          content: msg.content,
          tool_calls: (msg.metadata.toolCalls as any[]).map(tc => ({
            id: tc.id,
            type: 'function',
            function: {
              name: tc.name,
              arguments: JSON.stringify(tc.parameters),
            },
          })),
        });
      } else if (msg.role === 'tool') {
        // tool 结果消息,需要关联到 tool_call
        messages.push({
          role: 'tool',
          content: msg.content,
          tool_call_id: msg.metadata?.toolCallId as string,
        });
      } else {
        // 普通消息
        messages.push({
          role: msg.role,
          content: msg.content,
        });
      }
    }

    // 构建 LLM 请求
    const request: LLMRequest = {
      model: this.config.model.split('/').pop() || this.config.model,
      messages,
      tools: this.tools.toOpenAIFormat(),
      maxTokens: this.config.maxTokens,
      temperature: this.config.temperature,
    };

    try {
      // 调用 LLM
      const response = await provider.complete(request);

      // 解析工具调用
      const toolCalls = response.toolCalls?.map(tc => ({
        id: tc.id,
        name: tc.function.name,
        parameters: JSON.parse(tc.function.arguments),
      }));

      return {
        content: response.content,
        toolCalls,
        isComplete: !toolCalls || toolCalls.length === 0,
      };
    } catch (error: any) {
      this.callbacks.onError?.(error);
      return {
        content: `调用失败: ${error.message}`,
        isComplete: true,
      };
    }
  }

  // ==============================================================================
  // 辅助方法
  // ==============================================================================
  
  private setState(state: AgentState): void {
    this.state = state;
    this.callbacks.onStateChange?.(state);
  }

  private createMessage(role: 'assistant', content: string): Message {
    return {
      id: randomUUID(),
      role,
      content,
      timestamp: Date.now(),
    };
  }
}

3. 关键问题详解

3.1 为什么需要添加 assistant 消息?

问题场景

用户:创建一个文件

第 1 轮:
- LLM: 调用 write_file
- Agent: 执行 write_file
- 结果:文件已创建

第 2 轮(如果不添加 assistant 消息):
- 消息历史:[user: "创建文件"]
- LLM: ??? 用户要创建文件,我应该调用 write_file
- Agent: 再次执行 write_file(重复!)
- 无限循环...

第 2 轮(正确添加 assistant 消息):
- 消息历史:[user: "创建文件", assistant: "调用 write_file", tool: "已写入"]
- LLM: 看到历史里已经有 write_file 的结果了,任务完成
- 回复用户:"文件已创建"
- 结束

代码体现

// 执行工具前,先添加 assistant 消息
session.messages.push({
  role: 'assistant',
  content: thought.content || '',
  metadata: { toolCalls: thought.toolCalls },  // 关键!
});

// 然后执行工具
const result = await tool.execute(...);

// 再添加 tool 结果
session.messages.push({
  role: 'tool',
  content: result,
  metadata: { toolCallId: call.id },  // 关联到 tool_call
});

3.2 消息格式转换

为什么要转换?

不同角色有不同的格式要求:

// 普通 assistant 消息
{ role: 'assistant', content: '你好' }

// 包含 tool_calls 的 assistant 消息
{
  role: 'assistant',
  content: '',
  tool_calls: [{
    id: 'call_1',
    type: 'function',
    function: { name: 'write_file', arguments: '{}' }
  }]
}

// tool 结果消息
{
  role: 'tool',
  content: '已写入',
  tool_call_id: 'call_1'  // 必须关联到 tool_call
}

3.3 递归 vs 循环

我们使用递归实现 ReAct:

private async reactLoop(session: Session, depth = 0): Promise<Message> {
  // ...
  return this.reactLoop(session, depth + 1);  // 递归
}

为什么不用 while 循环?

// 循环版本(也可以)
while (true) {
  const thought = await this.think(session);
  if (!thought.toolCalls?.length) break;
  // ...
}

// 递归版本
// 更清晰,每次调用都是一个新的"思考-行动"周期
// 便于调试和追踪

3.4 状态机的作用

type AgentState = 'idle' | 'thinking' | 'acting';

private setState(state: AgentState) {
  this.state = state;
  this.callbacks.onStateChange?.(state);
}

用途

  1. 防止并发:如果正在 thinking,不能接受新请求
  2. UI 反馈:显示当前状态给用户
  3. 调试:追踪 Agent 的执行流程

4. 数据流追踪

4.1 创建文件任务的完整追踪

初始状态:
session.messages = []

═══════════════════════════════════════════════════════════
用户输入: "创建一个 hello.txt"
═══════════════════════════════════════════════════════════

Step 1: process() 添加用户消息
session.messages = [
  { role: 'user', content: '创建一个 hello.txt' }
]

═══════════════════════════════════════════════════════════
进入 reactLoop(depth=0)
═══════════════════════════════════════════════════════════

Step 2: THINK
状态: thinking
调用 LLM...

LLM 请求: {
  messages: [
    { role: 'system', content: 'You are helpful...' },
    { role: 'user', content: '创建一个 hello.txt' }
  ],
  tools: [write_file, read_file, list_files, ...]
}

LLM 响应: {
  content: '我来创建文件',
  toolCalls: [{
    id: 'call_1',
    name: 'write_file',
    parameters: { path: 'hello.txt', content: '' }
  }]
}

Step 3: ACT
状态: acting
添加 assistant 消息:
session.messages = [
  { role: 'user', content: '创建一个 hello.txt' },
  { 
    role: 'assistant', 
    content: '我来创建文件',
    metadata: { toolCalls: [{ id: 'call_1', name: 'write_file', ... }] }
  }
]

执行工具 write_file:
结果: '已写入: hello.txt'

添加 tool 消息:
session.messages = [
  { role: 'user', content: '创建一个 hello.txt' },
  { role: 'assistant', content: '我来创建文件', metadata: { toolCalls: [...] } },
  { role: 'tool', content: '已写入: hello.txt', metadata: { toolCallId: 'call_1' } }
]

═══════════════════════════════════════════════════════════
递归调用 reactLoop(depth=1)
═══════════════════════════════════════════════════════════

Step 4: THINK (第 2 轮)
状态: thinking
调用 LLM...

LLM 请求: {
  messages: [
    { role: 'system', content: 'You are helpful...' },
    { role: 'user', content: '创建一个 hello.txt' },
    { role: 'assistant', content: '我来创建文件', tool_calls: [{...}] },
    { role: 'tool', content: '已写入: hello.txt', tool_call_id: 'call_1' }
  ]
}

LLM 看到:用户要创建文件 -> 已调用 write_file -> 结果成功
LLM 响应: {
  content: '文件已创建完成!',
  toolCalls: []  // 无工具调用
}

Step 5: 完成
没有 toolCalls,返回回复
状态: idle

返回: { role: 'assistant', content: '文件已创建完成!' }

5. 常见错误

5.1 忘记添加 assistant 消息

// 错误:直接执行工具,不添加 assistant 消息
for (const call of thought.toolCalls) {
  const result = await tool.execute(call.parameters);
  session.messages.push({ role: 'tool', content: result });
}
// 结果:无限循环!

// 正确:先添加 assistant 消息
session.messages.push({
  role: 'assistant',
  metadata: { toolCalls: thought.toolCalls },  // 别忘了这个!
});

5.2 消息格式错误

// 错误:tool 消息没有 tool_call_id
{ role: 'tool', content: '结果' }

// 正确:需要关联到 tool_call
{ role: 'tool', content: '结果', tool_call_id: 'call_1' }

5.3 无限循环

// 忘记检查 depth
private async reactLoop(session: Session) {
  // 没有 depth 参数!
  return this.reactLoop(session);  // 永远循环
}

6. 练习

练习 1:追踪数据流

在代码中添加日志,追踪每次 reactLoop 的消息历史变化。

练习 2:添加调试模式

添加 debug 选项,打印每次 LLM 的请求和响应。

练习 3:优化消息历史

实现消息历史截断,只保留最近的 N 条消息。


Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐