1. 项目概述一个极简的ReAct Agent实现如果你对AI Agent智能体感兴趣想亲手实现一个能“思考-行动”的循环但又觉得像LangChain、AutoGen这类框架过于庞大、依赖复杂那么这个项目就是为你准备的。Wscats/miniclaw或者说我更习惯叫它“Mini OpenClaw”是一个用纯JavaScript写成的、零外部依赖的ReAct Agent引擎。它的全部代码就一个文件230行却能完整地实现一个可以自主调用工具比如读文件、执行命令来完成任务的智能助手。简单来说ReActReasoning Acting是一种让大语言模型LLM像人一样工作的模式先思考Reasoning判断是否需要使用工具如果需要就行动Acting使用工具然后把工具的结果作为新的信息再继续思考如此循环直到解决问题。这个模式听起来高级但实现它的核心逻辑其实并不复杂。Mini OpenClaw的魅力就在于它用最直白、最精简的代码把这个核心逻辑清晰地呈现了出来没有任何多余的包装非常适合学习和理解ReAct Agent的底层工作原理。我自己在研究和教学Agent开发时经常需要向学生解释ReAct的运作机制。用那些成熟框架的例子学生往往被海量的抽象层和配置项淹没反而抓不住重点。而Mini OpenClaw就像一张清晰的解剖图把Agent的“骨骼”和“肌肉”都暴露在你面前。它不依赖任何npm包运行它只需要Node.js和一个能调用大模型的命令行工具CodeBuddy CLI。对于开发者尤其是前端或Node.js背景的开发者来说这降低了上手门槛让你能专注于理解Agent的思维流程本身。2. 核心架构与设计哲学2.1 单文件架构拆解整个项目的核心就是mini-openclaw.mjs这一个文件。这种极简设计并非偷懒而是一种强烈的教学和理念声明一个具备基础工具调用能力的Agent其核心循环可以如此紧凑。文件内部清晰地划分为四个逻辑部分我们可以逐一拆解工具定义这是Agent的“手”和“眼睛”。项目内置了四个最基础但极其实用的工具read_file读文件、write_file写文件、list_dir列目录和run_command执行Shell命令。每个工具都是一个简单的异步函数接收参数执行操作返回结果或错误。例如run_command就是利用Node.js的child_process.spawn来执行命令并捕获输出。这种设计让你一眼就能看懂工具是如何被封装和调用的。LLM调用层这是Agent的“大脑”。项目没有使用常见的HTTP API客户端如openai包而是选择通过child_process.spawn直接调用一个名为CodeBuddy的命令行工具。这样做的好处是零依赖你不需要安装任何额外的HTTP客户端库如axios,node-fetch。它通过命令行参数-p表示prompt模式和--output-format json来告诉CodeBuddy“我给你一段提示词你以JSON格式把回复吐出来”。这种设计巧妙地绕开了网络请求的复杂性将LLM交互简化为一个进程间通信问题。ReAct循环引擎这是项目的“心脏”也是代码最精妙的部分。它维护着一个不断增长的“对话历史”字符串。每次循环它都把当前的对话历史拼接到预设的系统提示词后面形成完整的prompt发送给LLM。然后它用正则表达式/tool_call(.*?)\/tool_call/s去“监听”LLM的回复。如果匹配到了tool_call标签就解析里面的JSON调用对应的工具函数把工具执行的结果以[工具结果]: ...的格式追加到对话历史中然后开启下一轮循环。如果没匹配到标签说明LLM认为不需要或无法使用工具已经给出了最终答案循环结束将答案输出给用户。交互式REPL这是Agent的“嘴巴”和“耳朵”。利用Node.js的readline模块它构建了一个简单的命令行交互界面。你可以持续输入问题Agent会保持对话历史记忆直到你输入/clear清空或者exit退出。这为测试和演示提供了极大的便利。注意这种基于正则表达式解析工具调用的方式虽然简单直观但对LLM输出的格式稳定性有较高要求。它要求模型必须严格遵循tool_call{...}/tool_call的格式。在实际使用中如果模型“不听话”输出了多余的解释或格式错误循环就会中断。这是极简设计在健壮性上做出的权衡。2.2 关键设计决策背后的考量为什么选择这些看似“非主流”的实现方式这背后有很实际的考量文本标签 vs. Function CallingOpenAI的Function Calling是一种更结构化、更可靠的方案但它绑定在特定的API协议里。使用tool_call这种自定义的文本标签使得这个Agent引擎与模型提供商解耦。理论上任何能遵循简单文本指令的LLM都可以驱动它兼容性更好。这也更贴近ReAct论文中描述的“Thought-Action-Observation”的文本交互模式。CLI调用 vs. HTTP SDK使用CLI工具CodeBuddy而不是直接调用HTTP API最大优势就是依赖最小化。你只需要在系统层面安装一个可执行程序项目本身没有任何package.json和node_modules。这对于分享、演示和快速启动来说非常友好。缺点是你被绑定在了CodeBuddy这个特定的CLI工具及其支持的模型上。纯文本对话历史将所有交互用户输入、助手回复、工具结果都用特定前缀User:Assistant:[工具结果]:拼接成一个长字符串是最简单、最兼容的上下文管理方式。它不需要维护复杂的状态对象直接将其作为prompt的一部分喂给LLMLLM天然就能理解这种对话格式。缺点是当对话轮数非常多时可能会触及模型的上下文长度限制且历史无法被结构化查询或修改。3. 环境准备与快速启动3.1 前置条件检查要运行Mini OpenClaw你需要准备两样东西它们都不复杂Node.js运行环境版本需要大于等于18。这是因为代码中使用了较新的ES模块语法.mjs后缀和一些稳定的API。你可以在终端输入node --version来检查。如果没有安装去Node.js官网下载LTS版本安装即可。CodeBuddy CLI及其API Key这是本项目与LLM通信的桥梁。安装CLI在终端运行npm install -g tencent-ai/codebuddy-code。这是一个腾讯云推出的命令行AI编程工具安装后你可以在任何地方使用codebuddy命令。获取API Key你需要一个以ck_开头的API Key。通常你需要注册腾讯云相关服务来获取。请务必妥善保管你的Key不要泄露。3.2 一步到位的启动流程准备好了环境启动它只需要三步# 1. 获取代码。你可以直接下载或者用git克隆推荐方便后续更新。 git clone https://github.com/wscats/enoclaw.git cd enoclaw # 2. 设置环境变量。将你的CodeBuddy API Key设置到当前终端会话中。 # 在Linux/macOS上 export CODEBUDDY_API_KEYck_your_actual_key_here # 在Windows PowerShell上 $env:CODEBUDDY_API_KEYck_your_actual_key_here # 3. 运行 node mini-openclaw.mjs如果一切顺利你会看到一个简洁的ASCII艺术logo和欢迎界面提示你模型已加载默认是deepseek-v3-2-volc工具已就绪光标在You后面等待你的输入。3.3 模型选择与配置默认使用的是deepseek-v3-2-volc模型。如果你希望尝试其他模型可以在启动前通过环境变量指定# 尝试混元2.0思考模型 MODELhunyuan-2.0-thinking node mini-openclaw.mjs # 或者GLM-5.0 MODELglm-5.0 node mini-openclaw.mjs可用的模型取决于你的CodeBuddy CLI版本和授权常见的有deepseek-v3-2-volc,hunyuan-2.0-thinking,glm-5.0,glm-4.7,minimax-m2.5,kimi-k2.5等。不同模型在工具调用的格式遵循能力、推理深度和速度上会有差异你可以根据任务需求进行选择。实操心得在第一次运行前建议先单独测试一下CodeBuddy CLI是否工作。在终端输入codebuddy -p Hello看看是否能正常返回AI的回复。这能帮你快速定位问题是出在Key配置、网络还是CLI安装上避免在运行Agent时一头雾水。4. 核心工作流程深度解析理解Mini OpenClaw如何工作最好的方式就是跟踪一次完整的工具调用。我们以“请列出当前目录下所有以.js结尾的文件”这个任务为例拆解每一步。4.1 初始状态与用户输入启动Agent后其内部维护的conversationHistory是一个空字符串。系统提示词包含工具定义和调用规则被预先定义好。当你输入问题后You 请列出当前目录下所有以.js结尾的文件程序会将你的输入加上User:前缀追加到对话历史中。此时准备发送给LLM的完整prompt是[系统提示词]...此处是详细的工具描述和调用规则... User: 请列出当前目录下所有以.js结尾的文件4.2 第一次LLM调用与思考这个完整的prompt通过codebuddy -p --output-format json命令发送给大模型。模型收到后会进行“思考”Reasoning。它分析系统提示词知道自己有list_dir这个工具可以列出目录内容。它也读到了用户的请求是“列出.js文件”。一个合理的推理路径是要列出特定类型的文件我需要先知道当前目录下有什么。因此它决定调用list_dir工具。根据系统提示词中规定的格式它必须在回复中只包含工具调用标签。于是它生成如下回复{ content: tool_call{\name\:\list_dir\,\args\:{\path\:\.\}}/tool_call }CodeBuddy CLI会把这个JSON对象输出到标准输出。4.3 工具调用与结果观察Mini OpenClaw的主循环函数比如叫react接收到这个JSON回复后会提取content字段。然后用正则表达式去匹配tool_call标签。成功匹配后解析出里面的JSON{name:list_dir,args:{path:.}}。接着它在预定义的工具字典里查找list_dir函数并以{path:.}为参数调用它。list_dir函数内部执行了Node.js的fs.readdir操作读取当前目录.并区分文件和文件夹。假设当前目录下有mini-openclaw.mjs,test.js,README.md和一个node_modules文件夹。工具执行成功返回一个结果字符串例如文件: mini-openclaw.mjs, test.js 目录: node_modules程序将这个结果以[工具结果]: 文件: mini-openclaw.mjs, test.js\n目录: node_modules的格式追加到对话历史中。此时对话历史变为User: 请列出当前目录下所有以.js结尾的文件 [工具结果]: 文件: mini-openclaw.mjs, test.js 目录: node_modules4.4 第二次LLM调用与最终回答由于检测到了工具调用循环继续。程序将更新后的对话历史再次拼接到系统提示词后形成新的prompt发送给LLM。这次LLM看到的上下文是用户问了.js文件然后工具列出了所有内容。它需要进行第二次“思考”从工具结果中筛选出以.js结尾的文件。它发现test.js符合要求而mini-openclaw.mjs虽然也是JavaScript模块但后缀是.mjs严格来说不符合用户“.js”的要求除非用户意图包含.mjs这取决于模型的判断。这次它判断不需要再调用工具因为信息已经足够。于是它生成一个自然语言回复{ content: 根据列出的目录内容当前目录下以.js结尾的文件是test.js。 }程序没有检测到tool_call标签因此判定循环结束。它将content字段中的文本输出到终端作为Assistant的最终回答 Assistant: 根据列出的目录内容当前目录下以.js结尾的文件是test.js。至此一个完整的“思考需要list_dir→ 行动执行list_dir→ 观察得到文件列表→ 再思考筛选并回答”的ReAct循环完成。4.5 循环终止与防呆设计循环不会无限进行下去。代码中设置了一个最大迭代次数比如15次。如果LLM在15轮内还在不停地请求调用工具却没有给出最终答案循环会被强制终止并提示“达到最大迭代次数”。这是一种重要的防呆机制防止模型陷入“工具调用-结果观察-再次调用同一工具”的死循环。此外系统提示词里有一条重要规则“如果对话历史中已有 [工具结果] 且与当前问题相关禁止再调用同一工具直接根据结果回答”。这条规则是“教”模型学会利用历史避免重复劳动。但在实际测试中模型并不总是严格遵守所以最大迭代次数的兜底措施是必要的。5. 内置工具详解与扩展指南5.1 四大基础工具实战Mini OpenClaw内置的四个工具覆盖了本地文件操作和系统交互的基本面。理解它们的实现和边界很重要。read_file(path)作用读取指定路径的文本文件内容。实现使用fs.readFile的Promise版本编码为utf-8。注意它只能读文本文件。如果尝试读取二进制文件如图片会得到乱码。路径可以是相对路径如./config.json或绝对路径。如果文件不存在或没有读取权限工具会返回错误信息这个错误信息也会被如实追加到对话历史中LLM需要能理解并处理这个错误。write_file(path, content)作用将文本内容写入指定路径的文件。如果文件已存在会覆盖如果不存在会创建前提是目录存在且有写入权限。实现使用fs.writeFile的Promise版本。注意这是一个有风险的工具因为它会直接覆盖磁盘文件。在更复杂的Agent系统中通常会对这种写操作进行确认或沙盒化。在这里它体现了极简设计的“信任”原则——信任LLM和用户的指令。切勿在重要项目目录或生产环境中随意测试此工具。list_dir(path)作用列出指定目录下的条目并区分文件和文件夹。实现使用fs.readdir获取所有条目名然后对每个条目使用fs.stat判断是文件还是目录。注意参数path默认为当前目录.。它只做浅层列出不会递归遍历子目录。返回的结果是格式化的字符串便于LLM阅读。run_command(command)作用在系统Shell中执行一条命令并返回其标准输出stdout和标准错误stderr。实现使用child_process.spawn并监听stdout和stderr流来收集输出。注意这是威力最大也最危险的工具。通过它Agent可以执行任何Shell命令。这意味着它理论上可以安装软件、删除文件、重启服务等。绝对不要在不受信任的环境或拥有高权限的账户下运行此类Agent。在Mini OpenClaw的教学场景中它展示了可能性但也必须强调其安全隐患。5.2 如何自定义和扩展工具扩展工具集是让这个迷你Agent变得更强大的关键。假设我们想添加一个http_get工具用于获取网页内容。步骤非常简单只需要修改mini-openclaw.mjs文件中的两个部分第一步在“工具定义”部分添加新的工具函数。// ... 其他工具定义之后 async function http_get(args) { const { url } args; if (!url) { return 错误缺少参数 url; } try { // 注意为了保持零依赖我们使用Node.js原生的https/http模块 // 这里以https为例 const https require(https); return new Promise((resolve, reject) { const req https.get(url, (res) { let data ; res.on(data, chunk data chunk); res.on(end, () { // 简单返回前1000个字符避免上下文过长 resolve(请求成功状态码${res.statusCode}:\n${data.substring(0, 1000)}); }); }); req.on(error, (err) { resolve(请求失败${err.message}); }); req.setTimeout(5000, () { req.destroy(); resolve(请求超时); }); }); } catch (error) { return 工具内部错误${error.message}; } } // 将新工具添加到 tools 对象中 const tools { read_file, write_file, list_dir, run_command, http_get, // 添加这一行 };第二步在系统提示词中更新工具描述。找到定义systemPrompt的地方在工具列表中添加对新工具的描述你是一个智能助手拥有以下工具 - read_file: 读取文件内容... - ... - http_get: 发起HTTP GET请求获取网页内容 参数: {url:string: 目标网址}第三步重启Agent即可使用。现在你可以问它“用http_get工具获取一下百度首页的标题”。LLM在思考后就会生成tool_call{name:http_get,args:{url:https://www.baidu.com}}/tool_call然后执行你刚定义的函数。扩展技巧工具函数的返回值最好是清晰的文本字符串方便LLM理解。对于复杂数据如JSON可以将其JSON.stringify后返回。工具函数内部要做好错误处理并将错误信息以自然语言形式返回这样LLM才能知道发生了什么并调整策略。6. 常见问题与排查技巧实录在实际运行和实验过程中你可能会遇到一些典型问题。下面是我踩过的一些坑和解决方法。6.1 启动与连接问题问题现象可能原因排查步骤运行node mini-openclaw.mjs后立即报错或退出1. Node.js版本过低2.CODEBUDDY_API_KEY环境变量未设置1. 运行node --version确认版本≥18。2. 运行echo $CODEBUDDY_API_KEY(Linux/macOS) 或echo %CODEBUDDY_API_KEY%(Windows CMD) 检查变量是否已设置且正确。启动后输入问题长时间无响应最后报超时或网络错误1. CodeBuddy CLI未安装或不在PATH中2. API Key无效或过期3. 网络连接问题无法访问CodeBuddy服务1. 在终端直接输入codebuddy --version看是否能识别命令。2. 单独运行codebuddy -p test测试CLI和Key是否正常工作。如果报错根据错误信息修复如重新安装CLI、申请新Key。3. 检查网络代理设置。提示MODEL not found或类似模型错误指定的MODEL环境变量值不在CodeBuddy支持列表中1. 不设置MODEL变量使用默认模型测试。2. 运行codebuddy --list-models(如果该命令支持) 或查阅文档确认可用的模型名称。6.2 工具调用与逻辑问题问题现象可能原因排查步骤与解决方案Agent陷入循环不断调用同一个工具不输出答案1. LLM没有理解“禁止重复调用”的规则。2. 工具返回的结果格式让LLM困惑认为问题未解决。1.检查系统提示词确保规则描述清晰。可以强化提示如“必须根据已有的工具结果直接回答禁止再次调用相同工具”。2.优化工具输出让工具返回更结构化、更清晰的结果。例如list_dir返回时明确说“以下是全部列表...”。3.启用调试在代码中临时加入console.log打印每一轮的对话历史和LLM回复观察模型“在想什么”。LLM回复中包含了自然语言但同时也包含了格式错误的tool_call标签LLM没有严格遵守“仅包含”工具调用标签的指令。这是提示工程Prompt Engineering的问题。需要强化系统提示词中的格式指令。可以尝试“你的回复必须且只能是以下两种格式之一1. 如果不需要工具直接给出最终答案。2. 如果需要工具回复必须完全且仅为tool_call{name:工具名,args:{参数}}/tool_call不要有任何其他文字、解释或Markdown格式。”工具执行出错如文件不存在、命令执行失败Agent不知道如何处理工具函数返回的错误信息没有被LLM有效理解或者LLM缺乏错误处理逻辑。1.改进工具错误信息让错误信息更友好例如“错误无法读取文件 /path/to/file原因文件不存在”。2.在系统提示词中教育LLM添加一条规则如“如果工具返回以‘错误’开头的消息说明操作失败。你应该分析错误原因并尝试其他方法或告知用户。”run_command执行某些命令没有输出或卡住有些命令是交互式的或会持续输出如top,python进入交互模式spawn的方式可能无法正确处理。1. 避免让Agent执行需要交互或永不结束的命令。2. 可以为run_command增加超时机制代码中已有简单实现并明确告知LLM命令执行可能有时限。6.3 性能与优化思考速度慢每次调用LLM都需要启动一个CodeBuddy CLI进程进程创建和模型加载都有开销。这对于快速交互体验有影响。但这在教学中不是大问题它让你更清晰地感知到每一次“思考-行动”的回合。上下文长度对话历史以纯文本形式不断增长如果进行非常长的多轮对话最终可能会超过模型的上下文窗口限制导致模型“失忆”。在生产系统中需要对历史进行摘要、裁剪或使用向量数据库。但在这个迷你版中这提醒我们“上下文管理”是Agent设计中的一个重要课题。工具可靠性文件读写和命令执行没有沙盒保护。一个“聪明”的LLM如果被恶意提示可能会执行危险操作。因此这个项目绝对只适合在安全的、隔离的开发环境中用于学习和实验。7. 从教学工具到生产原型的思考Mini OpenClaw作为一个教学工具无疑是成功的。它用200多行代码揭示了ReAct Agent的本质。但如果我们想把它变成一个更可靠、更强大的原型甚至用于轻量级生产有哪些方向可以探索呢这里分享一些我个人的实践和思考。1. 增强提示词Prompt的鲁棒性目前的系统提示词比较简单。我们可以让它更健壮加入少样本示例Few-shot Examples在提示词中直接给出几个正确调用工具和直接回答的例子。这能极大地提高模型输出格式的准确性。结构化输出要求除了要求工具调用用标签包裹还可以要求最终答案也用一个特定的标签包裹如final_answer.../final_answer这样程序能更精确地判断循环何时结束。分步骤思考Chain-of-Thought鼓励模型在调用工具前先在回复中输出它的“思考”过程可以用thought.../thought标签这不仅能提升任务完成的准确性也让我们更容易调试Agent的决策逻辑。2. 改进工具调用与执行参数验证与安全沙盒在工具函数被调用前对传入的参数进行类型和范围校验。对于run_command可以建立一个允许列表Allow List只允许执行少数几个安全的命令如ls,pwd,cat限制路径等或者在一个 Docker 容器或临时目录中执行命令。异步与并行工具调用目前的循环是严格串行的一次思考调用一个工具等待结果再思考。一些复杂的任务可能需要并行调用多个工具比如同时查询多个API。可以扩展协议让模型能在一个回复中指定多个tool_call然后由Agent并行执行。3. 升级通信层替换CLI为HTTP客户端虽然CLI实现了零依赖但HTTP客户端如fetch,axios能提供更稳定的连接、更灵活的配置超时、重试和更广泛的模型支持任何提供OpenAI兼容API的模型。你可以写一个简单的callLLM函数来替换掉spawnCodeBuddy的部分这样就能轻松接入GPT、Claude、国产大模型等各种API。支持流式输出目前是等待LLM生成完整回复后再处理。可以改为流式Streaming接收让模型一边“思考”一边输出用户能更快地看到部分结果体验更好。4. 引入记忆与状态管理短期记忆摘要当对话历史过长时可以触发一个摘要过程将旧的对话历史发送给LLM让它生成一段简短的摘要然后用摘要替换掉旧的历史释放上下文窗口。长期记忆向量库对于需要跨会话记忆的知识可以将重要的工具执行结果或对话结论通过嵌入模型Embedding转化为向量存入本地的轻量级向量数据库如chroma的本地模式。当遇到相关问题时先进行向量检索将相关内容作为上下文注入。实现这些改进中的任何一项都会让代码量增长背离“极简”的初衷。但这正是学习的路径先通过Mini OpenClaw理解核心原理然后根据实际需求亲手为它添砖加瓦。最终你可能会自己搭建出一个更适合你特定场景的Agent框架。这个从简到繁、从理解到创造的过程才是这个项目带给开发者最大的价值。