1000行代码实现极简版openclaw(附源码)(5)
·
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);
}
用途:
- 防止并发:如果正在 thinking,不能接受新请求
- UI 反馈:显示当前状态给用户
- 调试:追踪 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 条消息。
更多推荐



所有评论(0)