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

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 循环:

asyncrun():Promise<void>{this.abortController =newAbortController();let loopCount =0;let consecutiveNoToolExecutionCount =0;while(this.callbacks.getStatus()=== TaskStatus.RUNNING){ loopCount++;// 检查是否被外部中止if(this.abortController.signal.aborted){break;}// 执行单步 ReActconst result =awaitthis.executeStep();// 根据执行结果决定下一步...}}

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

2.2 单步执行:executeStep()

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

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

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

// 获取当前任务状态快照const taskState =this.callbacks.getTaskState();// 动态生成系统提示词(每轮都会重新生成)const systemPrompt =awaitthis.callbacks.buildSystemPrompt(taskState);// 动态生成环境变量提示词(注入实时上下文)const envPrompt =awaitthis.callbacks.buildEnvironmentPrompt(taskState);

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

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

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

3. ExecutionManager:流式响应的处理

3.1 流式处理流程

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

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

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

forawait(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
case'text':// 解析 chunk,可能产出 text 或 tool_useconst parsedContents =this.messageParser.parseChunk(chunk.text);const content = parsedContents[processContentIndex];if(content.type ==='text'){// 文本:await 保证顺序输出await callbacks.onTextContent(textContent.text);}elseif(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 响应中包含多个工具调用时(比如同时调用搜索工具和数据库查询工具),它们会被并行执行:

// 流结束后,等待所有工具执行完成if(toolExecutionPromises.length >0){const results =awaitPromise.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 的原始返回)的记录发生在工具执行之前

// 先完成 API 消息记录if(callbacks?.onApiMessageFinalize){await callbacks.onApiMessageFinalize(apiMessageContent);}// 再等待工具执行awaitPromise.allSettled(toolExecutionPromises);

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

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

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

4.1 提醒机制

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

if(!result.hasToolExecution){ consecutiveNoToolExecutionCount++;awaitthis.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 的指令被忽略)。此时引擎会强制暂停任务:

if(consecutiveNoToolExecutionCount >=3){awaitthis.callbacks.pauseTask();break;// 退出循环}

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

5. 中止机制:AbortController

ReActEngine 的中止基于 Web 标准的 AbortController

// 引擎启动时创建this.abortController =newAbortController();// 外部调用 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

try{// ... 循环逻辑}finally{this.abortController =undefined;}

6. 错误追踪:ErrorTracker

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

classErrorTracker{private errorHistory: Map<string, ErrorRecord>=newMap();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,...});returnfalse;}}}

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

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

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

// 层面 1:单步执行中的工具错误onToolUse:async(toolUse)=>{try{awaitthis.callbacks.handleToolExecution(toolUse);}catch(error){const shouldPause =this.callbacks.trackError(errorObj);if(shouldPause){awaitthis.callbacks.pauseTask();return;}// 未达阈值:将错误写入消息历史,让 LLM 自行修正awaitthis.callbacks.insertReminderMessage(`工具执行失败: ${toolUse.name}\n错误: ${errorObj.message}`);}}// 层面 2:循环级别的 LLM 请求错误while(status ===RUNNING){try{awaitthis.executeStep();}catch(error){const shouldPause =this.callbacks.trackError(errorObj);if(shouldPause){awaitthis.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 消息在工具执行前持久化

Read more

前端保持和服务器时间同步的方法【使用vue3举例】

你只管努力!剩下的交给时间! 目录 * 引言: * 方法一: 轮询(定时请求服务器时间) * 优点: * 缺点: * 方法二:使用WebSocket * 优点: * 缺点: * 方法三:时间戳校正 * 优点: * 缺点: * 方法四: 使用NTP(网络时间协议) * 优点: * 缺点: * 方法五:使用SSE(Server-Sent Events) * 优点: * 缺点: * 总结: 引言: 保持前端与服务器时间同步是一个常见的需求,特别是在需要确保时间一致性的应用中,比如在线投票、实时聊天或游戏等。以下是一些方法来实现这一目标: 方法一: 轮询(定时请求服务器时间) 可以定时向服务器发送请求获取当前时间,以此来更新前端的时间显示。 <template><div><h1>当前时间:

By Ne0inhk
前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

🧑 博主简介:ZEEKLOG博客专家,「历代文学网」(公益文学网,PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。 🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图 ” 前端异常捕获与统一格式化:从 console.log(error) 到服务端上报 引言 在前端开发中,异常监控是保证应用稳定性的重要一环。当用户遇到页面白屏、功能不可用等问题时,如果能及时收集到详细的错误信息(包括堆栈、

By Ne0inhk
【AI深究】逻辑回归(Logistic Regression)全网最详细全流程详解与案例(附大量Python代码演示)| 数学原理、案例流程、代码演示及结果解读 | 决策边界、正则化、优缺点及工程建议

【AI深究】逻辑回归(Logistic Regression)全网最详细全流程详解与案例(附大量Python代码演示)| 数学原理、案例流程、代码演示及结果解读 | 决策边界、正则化、优缺点及工程建议

大家好,我是爱酱。本篇将系统讲解——逻辑回归(Logistic Regression)的原理、公式、案例流程、代码实现和工程建议。内容详细分步,便于新手和进阶读者理解和实操。 注:本文章含大量数学算式、详细例子说明及大量代码演示,大量干货,建议先收藏再慢慢观看理解。新频道发展不易,你们的每个赞、收藏跟转发都是我继续分享的动力! 注:本文章颇长近5000字、以及大量Python代码、非常耗时制作,建议先收藏再慢慢观看。新频道发展不易,你们的每个赞、收藏跟转发都是我继续分享的动力! 一、逻辑回归简介 逻辑回归是一种经典的线性分类算法,本质上是用Sigmoid函数将线性回归的输出“压缩”到0~1之间,输出为概率,常用于二分类任务。 与KNN(K-近邻算法)不同,逻辑回归是判别式模型,直接建模输入特征与类别之间的概率关系,适合特征和类别呈线性可分或近似线性关系的数据。 注:爱酱也有文章介绍了分类以及其他五大任务的技巧,有兴趣的也可以参考一下哦~ 分类任务文章传送门: 【算法解析1/5】分类任务深度拆解:

By Ne0inhk
用AI给老照片上色:算法对比与调参技巧

用AI给老照片上色:算法对比与调参技巧

用AI给老照片上色:算法对比与调参技巧 * 一、前言 * 二、传统上色算法与局限性 * 2.1 基于直方图匹配的上色算法 * 2.2 基于特征匹配的上色算法 * 三、基于深度学习的上色算法 * 3.1 基于 CNN 的端到端上色算法 * 3.2 基于 GAN 的上色算法 * 3.3 基于Transformer的上色算法 * 四、实用调参技巧 * 4.1 数据预处理调参 * 4.1.1 图像分辨率调整 * 5.1.2 降噪与增强参数 * 5.2 模型结构调参 * 5.2.1 CNN 模型调参 * 5.2.

By Ne0inhk