1. schoober-ai-sdk:ReAct引擎深度解析
作为一名长期从事AI应用开发的工程师,我最近在schoober-ai-sdk项目中实现了一个完整的ReAct引擎。这个引擎不是简单的API封装,而是真正让LLM具备了动态决策和执行能力的核心组件。今天我想分享这个引擎的设计思路和实现细节,希望能给正在构建AI Agent的开发者一些启发。
ReAct模式之所以重要,是因为它解决了传统LLM应用的几个关键痛点:一次性输出导致的错误无法修正、复杂任务需要分步解决、以及需要结合外部工具获取实时信息。在我们的实现中,一个完整的ReAct循环包含三个核心模块:负责流程控制的ReActEngine、处理LLM交互的ExecutionManager,以及确保系统稳定性的ErrorTracker。这三个模块协同工作,使得AI Agent能够像人类一样思考-行动-观察循环推进任务。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReAct引擎架构设计
2.1 核心循环机制
引擎的核心是一个看似简单但精心设计的while循环。这个循环的特别之处在于它的退出条件不是内部标志,而是直接检查外部任务状态:
typescript复制async run(): Promise<void> {
this.abortController = new AbortController();
let loopCount = 0;
let consecutiveNoToolExecutionCount = 0;
while (this.callbacks.getStatus() === TaskStatus.RUNNING) {
loopCount++;
if (this.abortController.signal.aborted) {
break;
}
const result = await this.executeStep();
// 后续处理...
}
}
这种设计避免了内部状态和外部状态不一致的问题。在实际测试中,我们发现当任务状态由外部改变时(比如用户点击停止,或者子任务完成),这种设计能确保循环立即响应,而不会因为内部标志未更新导致延迟。
2.2 动态提示词生成
每一轮循环都会重新生成systemPrompt,这是我们的一个关键设计决策:
typescript复制const taskState = this.callbacks.getTaskState();
const systemPrompt = await this.callbacks.buildSystemPrompt(taskState);
const envPrompt = await this.callbacks.buildEnvironmentPrompt(taskState);
为什么需要动态生成?因为在复杂任务中,任务状态是不断变化的。比如:
- 新工具可能被动态注册
- 子任务状态会更新
- 错误计数会增加
- 环境变量会改变
这些变化都需要实时反映到LLM的输入中。我们的prompt组装顺序是:
- 核心行为规范(固定)
- 动态角色信息(如当前重试次数)
- 可用工具列表(转换为自然语言描述)
- 子Agent信息(如果存在)
这种结构既保持了核心指令的稳定性,又能灵活适应任务进展。
3. 流式响应处理机制
3.1 多类型chunk处理
ExecutionManager的核心职责是处理LLM的流式响应。我们设计了四种chunk类型:
typescript复制for await (const chunk of stream) {
switch (chunk.type) {
case 'text': // 文本输出
case 'usage': // Token统计
case 'error': // 流错误
case 'end': // 结束信号
}
}
其中text chunk的处理最为复杂,因为它可能包含普通文本或工具调用指令。我们使用专门的MessageParser来分离这两种内容:
typescript复制case 'text':
const parsedContents = this.messageParser.parseChunk(chunk.text);
const content = parsedContents[processContentIndex];
if (content.type === 'text') {
await callbacks.onTextContent(textContent.text); // 顺序输出
} else if (content.type === 'tool_use') {
const promise = callbacks.onToolUse(toolUse); // 并行收集
toolExecutionPromises.push(promise);
}
if (content.partial === false) {
processContentIndex++;
}
这里的关键设计点是:文本输出必须保持顺序(使用await),而工具调用可以并行执行(收集Promise)。这种差异处理显著提升了响应速度。
3.2 工具并行执行策略
当LLM一次性返回多个工具调用时,我们会并行执行它们:
typescript复制if (toolExecutionPromises.length > 0) {
const results = await Promise.allSettled(toolExecutionPromises);
results.forEach((result, index) => {
if (result.status === 'rejected') {
// 记录错误但不阻断其他工具
}
});
}
使用Promise.allSettled而不是Promise.all,确保了单个工具失败不会影响其他工具的执行。这在需要同时查询多个数据源的场景特别有用。
重要提示:API消息的记录必须在工具执行之前完成,这样即使工具执行失败,LLM的原始响应也不会丢失。
4. 稳定性保障机制
4.1 LLM空转防护
LLM有时会陷入只输出文本不调用工具的状态,我们实现了两层防护:
- 提醒机制 - 当没有工具调用时插入提示:
typescript复制if (!result.hasToolExecution) {
consecutiveNoToolExecutionCount++;
await this.callbacks.insertReminderMessage(
'You must call a tool...'
);
continue;
}
- 硬性阈值 - 连续3次无工具调用则暂停任务:
typescript复制if (consecutiveNoToolExecutionCount >= 3) {
await this.callbacks.pauseTask();
break;
}
在实际测试中,这种组合策略有效减少了约80%的无意义循环。
4.2 错误追踪与恢复
不是所有错误都应该终止任务。我们的ErrorTracker通过错误签名和计数来智能决策:
typescript复制class ErrorTracker {
private errorHistory = new Map<string, ErrorRecord>();
private maxSameErrorCount = 3;
trackError(error: Error): boolean {
const signature = this.getErrorSignature(error);
const record = this.errorHistory.get(signature);
if (record) {
record.count++;
return record.count >= this.maxSameErrorCount;
} else {
this.errorHistory.set(signature, { count: 1 });
return false;
}
}
}
错误签名有两种生成方式:
- 简单模式:error.name + error.message
- 详细模式:包含堆栈前三行
这种设计使得系统能够区分偶发错误和持续错误,前者让LLM尝试自愈,后者则暂停等待人工干预。
5. 完整工作流示例
让我们通过一个天气查询的例子看整个引擎如何协作:
- 用户输入:"查一下北京的天气"
- 第一轮循环:
- LLM返回:文本"我来查一下北京的天气" + 工具调用get_weather({city: "北京"})
- 并行执行天气查询工具
- 工具返回:
- 第二轮循环:
- LLM看到天气结果后调用attempt_completion
- 输出最终结果:"北京当前天气:22°C,晴"
- 任务状态变为COMPLETED,循环结束
这个流程展示了引擎如何自然地引导LLM完成任务,同时保持对执行过程的完全控制。
6. 关键设计原则总结
在实现这个ReAct引擎时,我们坚持了几个核心原则:
- 状态外置:所有重要状态都存储在引擎外部,避免内部状态不一致
- 关注点分离:引擎只控制流程,不处理具体LLM交互或错误策略
- 实时性:动态提示词确保LLM始终获得最新信息
- 弹性设计:错误处理和空转防护使系统具备自愈能力
这些原则不仅适用于ReAct引擎,也可以推广到其他AI系统设计中。
7. 实际开发中的经验教训
在开发过程中,我们踩过几个值得分享的坑:
-
流式处理的顺序保证:初期没有妥善处理文本输出的顺序,导致显示混乱。后来引入了严格的await机制解决。
-
工具超时问题:某些工具可能长时间不返回,我们最终添加了每个工具调用的超时控制:
typescript复制const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
);
await Promise.race([toolPromise, timeoutPromise]);
- 错误信息过载:最初我们把所有错误细节都传给LLM,结果导致confusion。后来改为只提供简洁的错误摘要。
这些经验表明,构建可靠的AI系统不仅需要好的架构,还需要大量实际场景的打磨。
