schoober-ai-sdk:核心ReAct 引擎的实现

schoober-ai-sdk:核心ReAct 引擎的实现

github:Schoober AI SDK GitHub 仓库

各位看官求🌟一下,小的先在此谢过

1. 从论文到工程:ReAct 模式回顾

ReAct(Reasoning + Acting)源自 2022 年的同名论文,核心思想是让 LLM 在推理(Reason)和行动(Act)之间交替进行,而非一次性生成最终答案。每一轮循环包含三个阶段:

复制代码
Reason  →  LLM 分析当前状态,决定下一步行动
Act     →  调用外部工具执行具体操作
Observe →  获取工具返回的结果,作为下一轮推理的输入

这个循环持续进行,直到 LLM 判断任务已完成(或无法继续)。

如下图所示:

在 schoober-ai-sdk 中,这个循环由三个核心模块协作完成:

模块 职责
ReActEngine 驱动 Reason→Act→Observe 循环,控制启停
ExecutionManager 执行单次 LLM 请求,处理流式响应
ErrorTracker 追踪错误,决定是否暂停循环

下面逐层拆解实现。

2. ReActEngine:循环的驱动者

2.1 循环结构

ReActEngine.run() 是整个 Agent 的心跳。它的结构出乎意料地简单------一个 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;
        }

        // 执行单步 ReAct
        const result = await this.executeStep();

        // 根据执行结果决定下一步...
    }
}

循环的退出条件不是内部标志,而是直接检查任务状态。这意味着当工具调用了 attempt_completion 将状态设为 COMPLETED,或者外部调用了 task.abort(),循环会在下一次检查时自然停止。这种设计避免了内部状态和外部状态不一致的问题。

2.2 单步执行:executeStep()

每一轮 executeStep() 做了四件事:

复制代码
1. 收集上下文  →  获取消息历史 + 动态生成 systemPrompt
2. 调用 LLM    →  通过 ExecutionManager 发起流式请求
3. 解析响应    →  将流式输出分离为 text(推理)和 tool_use(行动)
4. 执行工具    →  调用对应工具,将结果写回消息历史

其中最关键的是第 1 步------每一轮循环都会重新生成 systemPrompt:

typescript 复制代码
// 获取当前任务状态快照
const taskState = this.callbacks.getTaskState();

// 动态生成系统提示词(每轮都会重新生成)
const systemPrompt = await this.callbacks.buildSystemPrompt(taskState);

// 动态生成环境变量提示词(注入实时上下文)
const envPrompt = await this.callbacks.buildEnvironmentPrompt(taskState);

为什么每轮都要重新生成?因为任务状态在不断变化------工具注册/注销、子任务状态更新、重试计数增加------这些变化需要实时反映到 LLM 的输入中。systemPrompt 的组合顺序是:

复制代码
coreSystemPrompt(行为规范)
    ↓
rolePromptBuilder(动态角色信息,如重试次数)
    ↓
工具定义列表(Zod Schema → 可读文本)
    ↓
子 Agent 信息(如果有)

environmentPromptBuilder 的输出则作为消息队列末尾的一条 user 消息注入,适合放置实时上下文(如当前时间、环境变量)。

3. ExecutionManager:流式响应的处理

3.1 流式处理流程

ExecutionManager.execute() 负责与 LLM Provider 交互。整个流程是:

复制代码
构建 StreamConfig → 创建流 → 逐 chunk 处理 → 等待工具执行 → 完成

流式响应的处理在 handleStream() 中完成,这是整个引擎最复杂的部分。它需要同时处理四种类型的 chunk:

typescript 复制代码
for await (const chunk of stream) {
    switch (chunk.type) {
        case 'text':    // LLM 的文本输出(推理过程)
        case 'usage':   // Token 使用统计
        case 'error':   // 流级别错误
        case 'end':     // 流结束信号
    }
}

3.2 文本与工具调用的分离

text 类型的 chunk 是最核心的。每个 text chunk 到达后,会经过 MessageParser 解析,产出两种内容:

  • TextContent:LLM 的推理文本,直接流式推送给前端
  • ToolUse:工具调用指令,包含工具名、参数、requestId
typescript 复制代码
case 'text':
    // 解析 chunk,可能产出 text 或 tool_use
    const parsedContents = this.messageParser.parseChunk(chunk.text);
    const content = parsedContents[processContentIndex];

    if (content.type === 'text') {
        // 文本:await 保证顺序输出
        await callbacks.onTextContent(textContent.text);
    } else if (content.type === 'tool_use') {
        // 工具调用:不 await,收集 Promise 并行执行
        const promise = callbacks.onToolUse(toolUse);
        toolExecutionPromises.push(promise);
    }

    // 当前内容块完成,移动到下一个
    if (content.partial === false) {
        processContentIndex++;
    }

注意这里的设计差异:文本输出是 await (保证流式输出的顺序),而工具调用不 await(收集 Promise,允许多个工具并行执行)。

3.3 工具的并行执行与同步等待

当一次 LLM 响应中包含多个工具调用时(比如同时调用搜索工具和数据库查询工具),它们会被并行执行:

typescript 复制代码
// 流结束后,等待所有工具执行完成
if (toolExecutionPromises.length > 0) {
    const results = await Promise.allSettled(toolExecutionPromises);

    // 记录失败的工具执行(不影响其他工具)
    results.forEach((result, index) => {
        if (result.status === 'rejected') {
            // 记录错误日志
        }
    });
}

// 所有工具完成后,才调用流结束回调
if (callbacks?.onStreamEnd) {
    await callbacks.onStreamEnd();
}

使用 Promise.allSettled 而非 Promise.all,确保单个工具失败不会阻断其他工具的执行。

3.4 API 消息的记录时序

还有一个容易忽略的细节:API 消息(LLM 的原始返回)的记录发生在工具执行之前

typescript 复制代码
// 先完成 API 消息记录
if (callbacks?.onApiMessageFinalize) {
    await callbacks.onApiMessageFinalize(apiMessageContent);
}

// 再等待工具执行
await Promise.allSettled(toolExecutionPromises);

这个顺序保证了即使工具执行失败或超时,LLM 的原始响应也已经被持久化,不会丢失。

4. 兜底策略:防止 LLM 空转

LLM 并不总是"听话"地调用工具。有时它会生成一段文本但不调用任何工具,导致循环空转、浪费 token。ReActEngine 对此有两层防护:

4.1 提醒机制

当一轮执行没有任何工具调用时,引擎会向消息历史中插入一条提醒:

typescript 复制代码
if (!result.hasToolExecution) {
    consecutiveNoToolExecutionCount++;

    await this.callbacks.insertReminderMessage(
        'You must call a tool. Please select an appropriate tool ' +
        'based on the current task progress, or use the ' +
        'attempt_completion tool to complete or abort the task.'
    );
    continue; // 继续下一轮循环
}

这条消息会作为 user 角色出现在消息历史中,在下一轮 LLM 请求时被"看到",引导 LLM 回到正轨。

4.2 硬性阈值

如果连续 3 次都没有工具调用,说明 LLM 可能陷入了无法恢复的状态(比如不理解工具的使用方式,或者 systemPrompt 的指令被忽略)。此时引擎会强制暂停任务:

typescript 复制代码
if (consecutiveNoToolExecutionCount >= 3) {
    await this.callbacks.pauseTask();
    break; // 退出循环
}

暂停而非终止,是为了给外部(用户或上层系统)一个介入的机会------可以调整 prompt、更换模型、或手动恢复。

5. 中止机制:AbortController

ReActEngine 的中止基于 Web 标准的 AbortController

typescript 复制代码
// 引擎启动时创建
this.abortController = new AbortController();

// 外部调用 abort()
abort(): void {
    if (this.abortController) {
        this.abortController.abort();
    }
}

AbortSignal 会传递给 ExecutionManager,后者再传递给 LLM Provider。这意味着中止会层层传播:

复制代码
task.abort()
  → ReActEngine.abort()
    → AbortController.abort()
      → ExecutionManager 检测到 signal.aborted,停止处理流
        → LLM Provider 中止网络请求

中止后,run() 方法中的 finally 块会清理 AbortController

typescript 复制代码
try {
    // ... 循环逻辑
} finally {
    this.abortController = undefined;
}

6. 错误追踪:ErrorTracker

并非所有错误都应该终止任务。网络抖动、LLM 偶发超时都是正常现象。ErrorTracker 通过错误签名和计数来决定何时真正暂停:

typescript 复制代码
class ErrorTracker {
    private errorHistory: Map<string, ErrorRecord> = new Map();
    private maxSameErrorCount: number = 3; // 相同错误的最大重复次数

    trackError(error: Error): boolean {
        const signature = this.getErrorSignature(error);
        const record = this.errorHistory.get(signature);

        if (record) {
            record.count++;
            // 相同错误达到 3 次,返回 true → 暂停任务
            return record.count >= this.maxSameErrorCount;
        } else {
            // 首次出现,记录但不暂停
            this.errorHistory.set(signature, { signature, count: 1, ... });
            return false;
        }
    }
}

错误签名的生成策略有两种:

  • simpleerror.name + error.message,适合大多数场景
  • detailed:还包含堆栈前 3 行,用于区分同一错误消息但不同调用点的情况

在 ReActEngine 中,错误追踪贯穿两个层面:

typescript 复制代码
// 层面 1:单步执行中的工具错误
onToolUse: async (toolUse) => {
    try {
        await this.callbacks.handleToolExecution(toolUse);
    } catch (error) {
        const shouldPause = this.callbacks.trackError(errorObj);
        if (shouldPause) {
            await this.callbacks.pauseTask();
            return;
        }
        // 未达阈值:将错误写入消息历史,让 LLM 自行修正
        await this.callbacks.insertReminderMessage(
            `工具执行失败: ${toolUse.name}\n错误: ${errorObj.message}`
        );
    }
}

// 层面 2:循环级别的 LLM 请求错误
while (status === RUNNING) {
    try {
        await this.executeStep();
    } catch (error) {
        const shouldPause = this.callbacks.trackError(errorObj);
        if (shouldPause) {
            await this.callbacks.pauseTask();
            break;
        }
        continue; // 继续下一轮,尝试恢复
    }
}

设计思路是:偶发错误交给 LLM 自愈,持续错误交给外部处理

7. 完整数据流

将以上模块串联起来,一次完整的 ReAct 循环数据流如下:

复制代码
用户输入: "查一下北京的天气"
    │
    ▼
┌─ ReActEngine.run() ─────────────────────────────────────┐
│                                                          │
│  Loop 1:                                                 │
│  ┌─ executeStep() ─────────────────────────────────┐    │
│  │                                                   │    │
│  │  1. buildSystemPrompt(taskState)                  │    │
│  │     → corePrompt + rolePrompt + 工具定义          │    │
│  │                                                   │    │
│  │  2. ExecutionManager.execute(messages, options)    │    │
│  │     → LLM 返回:                                   │    │
│  │       text: "我来查一下北京的天气"                  │    │
│  │       tool_use: get_weather({city: "北京"})        │    │
│  │                                                   │    │
│  │  3. 流式处理:                                      │    │
│  │     text → 推送给前端(await,保证顺序)            │    │
│  │     tool_use → 收集 Promise(并行执行)             │    │
│  │                                                   │    │
│  │  4. 工具执行:                                      │    │
│  │     get_weather({city: "北京"})                    │    │
│  │     → 返回: {temperature: 22, condition: "晴"}     │    │
│  │     → setToolResult() 写入消息历史                  │    │
│  │                                                   │    │
│  └───────────────────────────────────────────────────┘    │
│  result: { hasToolExecution: true }                       │
│  → 重置 consecutiveNoToolExecutionCount = 0               │
│                                                          │
│  Loop 2:                                                 │
│  ┌─ executeStep() ─────────────────────────────────┐    │
│  │                                                   │    │
│  │  消息历史现在包含了天气查询的结果                    │    │
│  │  LLM 看到结果后调用 attempt_completion:            │    │
│  │  tool_use: attempt_completion({                    │    │
│  │    result: "北京当前天气:22°C,晴"                │    │
│  │  })                                               │    │
│  │  → 任务状态变为 COMPLETED                          │    │
│  │                                                   │    │
│  └───────────────────────────────────────────────────┘    │
│                                                          │
│  while 条件检查: status !== RUNNING → 退出循环            │
│                                                          │
└──────────────────────────────────────────────────────────┘

8. 小结

ReActEngine 的设计遵循了几个原则:

  • 状态外置:循环条件基于外部任务状态,而非内部标志,避免状态不一致
  • 关注点分离:ReActEngine 只管循环控制,ExecutionManager 管 LLM 交互,ErrorTracker 管错误策略
  • 渐进式降级:偶发错误 → LLM 自愈;持续错误 → 暂停等待外部介入;外部中止 → 层层传播到网络层
  • 流式优先:文本保证顺序输出,工具允许并行执行,API 消息在工具执行前持久化
相关推荐
龙文浩_8 小时前
AI深度学习中的自动微分与梯度下降机制解析
人工智能·深度学习
conlin day8 小时前
Spring AI学习(一)
人工智能·学习·spring
网络安全学习库8 小时前
很喜欢Vue,但还是选择了React: AI时代的新考量
vue.js·人工智能·react.js·小程序·aigc·产品经理·ai编程
STLearner8 小时前
WWW 2026 | 时空数据(Spatial Temporal)论文总结(交通预测,人群移动,轨迹表示,信控等)
大数据·论文阅读·人工智能·深度学习·机器学习·数据挖掘·自动驾驶
xixixi777778 小时前
微软推出 Critique 双模型协作系统:GPT + Claude 协同,开启“生成 + 审查”新范式
人工智能·安全·ai·微软·大模型·多模态·合规
CV-deeplearning8 小时前
Claw Code:Better Harness Tools,让 AI 真正干实事
人工智能·agent·智能体·openclaw·claw code
龙文浩_8 小时前
AI深度学习中参数初始化方法及其与激活函数的协同优化
人工智能·深度学习·神经网络
多年小白8 小时前
2026年AI智能体“三国杀“:腾讯龙虾矩阵、阿里千问生态与字节豆包的技术架构全解析
网络·人工智能·科技·矩阵·notepad++
wangguanghou18 小时前
LLM、Agent、Skill的区别与关系
人工智能