ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Node.js与React的AI Agent框架:从零构建思考-行动-观察循环

基于Node.js与React的AI Agent框架:从零构建思考-行动-观察循环 1. 从 paperclip 这个名字说起它到底想解决什么问题第一次看到paperclip这个项目名我脑子里蹦出来的不是回形针而是那个经典的“回形针最大化”思想实验——一个看起来无害的小工具如果目标设定得足够明确就能撬动远超预期的连锁反应。放到 AI Agent 这个语境里这个名字其实挺妙的它暗示的是一种“轻量、通用、可插拔”的定位就像回形针一样便宜、随处可见、但几乎什么都能夹住。paperclip这个项目从标题和关联热词来看核心定位是一个基于 Node.js 和 React 构建的 AI Agent 运行框架它试图把“能思考”和“能行动”这两件事拆开又缝合起来。说人话就是你给它一个目标它不只是回你一段文字而是会规划步骤、调用工具、执行操作、观察结果然后根据结果决定下一步。这跟传统的“问答式”AI 有本质区别——问答式 AI 是“你问我答”Agent 是“你给目标我自己想办法”。为什么这件事值得单独拿出来讲因为现在市面上大部分所谓的 Agent 框架要么太重一上来就是一堆抽象层跑个 demo 要装半天要么太轻本质上就是个 prompt 模板根本没有真正的执行闭环。paperclip想走的是中间路线用 Node.js 做运行时用 React 做交互层把 Agent 的“思考-行动-观察”循环做成可复用的模块。这个思路其实很务实因为 Node.js 的异步 I/O 天然适合处理“等待工具返回结果”这种场景而 React 的组件化思维又能让 Agent 的状态可视化变得很自然。适合谁来参考这个项目三类人第一类是想自己搭一个 Agent 但不想从零造轮子的开发者第二类是对“AI 怎么真正干活”好奇、想看看执行链路长什么样的产品经理或技术爱好者第三类是在做类似 OpenClaw 这类工具、想对比不同实现思路的工程师。不管你基础如何只要你能跑起npm install这篇文章里的东西你都能上手试。2. 整体架构拆解为什么是 Node.js React 这个组合2.1 Node.js 在 Agent 场景里的真实优势很多人一提到 Node.js第一反应是“写后端的”或者“做前端的构建工具”。但在 Agent 这个场景里Node.js 有一个被低估的优势事件循环机制天然适配“工具调用-等待-回调”的模式。Agent 执行一个任务时经常要等外部 API 返回、等文件读写完成、等命令执行结束这些操作如果用一个线程去阻塞等待效率会非常低。Node.js 的非阻塞 I/O 让它在等待的时候可以继续处理其他事情比如同时跑多个 Agent 实例、或者在前端展示实时进度。另一个实际原因是生态。paperclip要调用的工具大概率包括文件操作、网络请求、命令行执行这些Node.js 的fs、http、child_process模块都是现成的不需要额外装一堆依赖。而且 npm 上的工具库极其丰富想接什么第三方服务基本都能找到对应的包。这一点在快速迭代阶段特别重要——你不需要为了一个功能去写底层实现直接npm install就完事了。还有一个容易被忽略的点Node.js 的调试体验对 Agent 开发很友好。Agent 的执行链路往往很长中间涉及多次状态转换如果运行时本身太重排查问题会很痛苦。Node.js 的console.log加上--inspect断点调试基本能覆盖大部分场景。我试过用一些 Python 的 Agent 框架光是环境隔离和依赖冲突就能折腾半天Node.js 在这方面确实省心不少。2.2 React 不只是 UI它是 Agent 状态的“显示器”paperclip用 React 这件事乍看有点奇怪——Agent 框架要 UI 干嘛但仔细想想就通了Agent 的执行过程是黑盒的用户需要看到它在干什么。如果只是命令行输出信息密度太低而且很难展示“当前在第几步、调用了什么工具、返回了什么结果”这种结构化信息。React 的组件化模型刚好适合做这件事每个步骤是一个组件状态变化驱动视图更新用户能实时看到 Agent 的“思考轨迹”。更深一层React 的useState和useEffect这套 hooks 机制其实和 Agent 的状态管理有很强的对应关系。Agent 的每一步执行都可以看作一次状态更新当前目标是什么、已经完成了哪些子任务、下一步该调用什么工具。用 React 来管理这些状态比手写一个状态机要直观得多。而且 React 的生态里有大量现成的图表库、日志展示组件、时间线组件拿来就能用省去了很多造轮子的时间。提示如果你之前没接触过 React不用慌。paperclip里用到的 React 知识基本就是组件、props、state、hooks 这几样花一个下午看看官方文档的“Main Concepts”部分就能跟上。重点理解“状态变化驱动视图更新”这个核心思想就够了。2.3 为什么不是 Python 或 Go这个问题我被问过很多次。Python 在 AI 领域确实是主流但 Agent 框架和模型训练是两回事。模型训练需要 Python 是因为 PyTorch、TensorFlow 这些库只支持 Python但 Agent 框架的核心是“调度”和“编排”不是“计算”。调度这件事Node.js 的异步模型反而更顺手。Go 的并发性能确实强但生态相对封闭想接个第三方 API 经常要自己写 SDK开发效率会打折扣。paperclip选择 Node.js React本质上是在开发效率、运行效率、生态丰富度这三者之间找了一个平衡点。它不是性能最优解但它是“能快速跑起来、能快速改、能快速看到效果”的解。对于 Agent 这种还在快速演进的领域快速迭代比极致性能重要得多。3. 核心机制Agent 的“思考-行动-观察”循环怎么落地3.1 从 ReAct 模式说起paperclip的底层逻辑大概率参考了ReActReasoning Acting模式。这个模式的核心思想很简单让模型在每一步都先“想一下”Reasoning然后决定“做什么”Acting执行完后再“看一下结果”Observation循环往复直到任务完成。听起来像废话但真正落地的时候难点在于怎么把这三个步骤串起来并且让模型知道当前处于哪一步。一个典型的 ReAct 循环长这样Thought模型输出一段思考比如“用户想让我查一下今天的天气我需要调用天气 API”Action模型输出要调用的工具名和参数比如get_weather(city北京)Observation框架执行工具调用把结果返回给模型比如{temperature: 25, condition: 晴}重复模型看到结果后继续思考直到输出最终答案paperclip要做的就是把这个循环封装成一个可复用的执行器。开发者只需要定义好工具Tool框架会自动处理“解析模型输出 → 调用工具 → 把结果塞回上下文”这一整套流程。3.2 工具定义Agent 的“手”和“脚”Agent 能不能干活取决于它有没有工具可用。paperclip里的工具定义我推测大概是这样的结构const tools [ { name: read_file, description: 读取指定路径的文件内容, parameters: { type: object, properties: { path: { type: string, description: 文件路径 } }, required: [path] }, execute: async ({ path }) { return await fs.promises.readFile(path, utf-8); } }, { name: run_command, description: 执行 shell 命令并返回输出, parameters: { type: object, properties: { command: { type: string, description: 要执行的命令 } }, required: [command] }, execute: async ({ command }) { return await execPromise(command); } } ];这个结构的关键在于description 和 parameters 的写法。模型是根据这两个字段来决定“什么时候用这个工具、怎么传参数”的所以描述必须清晰、参数必须明确。我踩过的一个坑是工具描述写得太模糊模型要么不用这个工具要么传错参数。比如把read_file的描述写成“读取文件”模型可能会在需要写文件的时候也调用它。后来改成“读取指定路径的文本文件内容返回字符串”准确率明显提升。注意工具的参数定义尽量用 JSON Schema 的格式这样模型能更准确地理解每个参数的类型和含义。如果参数是可选的一定要在 description 里说明默认值是什么。3.3 执行循环的代码骨架paperclip的核心执行循环我猜大概是这个逻辑async function runAgent(goal, tools, maxSteps 10) { let messages [ { role: system, content: 你是一个能调用工具的 AI 助手... }, { role: user, content: goal } ]; for (let step 0; step maxSteps; step) { // 1. 调用模型获取下一步动作 const response await callModel(messages); // 2. 如果模型输出了最终答案结束循环 if (response.type final_answer) { return response.content; } // 3. 如果模型要调用工具执行它 if (response.type tool_call) { const tool tools.find(t t.name response.toolName); const result await tool.execute(response.parameters); // 4. 把工具结果塞回上下文 messages.push({ role: assistant, content: response.raw }); messages.push({ role: tool, content: JSON.stringify(result) }); } } return 达到最大步数限制任务未完成; }这个骨架看起来简单但实际落地的时候有几个细节很关键。maxSteps 的设置就是一个设太小复杂任务跑不完设太大模型可能陷入死循环反复调用同一个工具。我的经验是简单任务设 5 步中等复杂度设 10 步特别复杂的可以设 20 步但一定要配合超时机制。另一个细节是消息历史的裁剪。Agent 跑多步之后上下文会越来越长如果超过模型的 token 限制就会报错。常见的做法是保留最近的 N 条消息或者把早期的工具调用结果压缩成摘要。paperclip如果要做生产级应用这块肯定得处理。4. 实操从零跑起一个 paperclip 风格的 Agent4.1 环境准备与依赖安装先把基础环境搭好。你需要 Node.js 18 以上版本推荐用 LTS 版本稳定性和兼容性都更好。安装完之后在终端里验证一下node -v npm -v如果版本号正常输出就可以开始建项目了。我习惯用 Vite 来初始化 React 项目因为它快而且配置简单npm create vitelatest paperclip-demo -- --template react cd paperclip-demo npm install然后装几个核心依赖一个是用来调用模型的 SDK具体用哪家看你的选择一个是用来处理工具调用的库。如果你想像paperclip那样把 Agent 逻辑和 UI 分开可以再装一个状态管理库不过对于 demo 来说React 自带的useState和useReducer就够了。提示Node.js 版本别用太新的有些实验性版本可能会有兼容性问题。我实测下来18.x 和 20.x 的 LTS 版本最稳。如果你之前装过多个版本可以用 nvm 来切换。4.2 定义你的第一个工具工具是 Agent 的手脚先从最简单的开始。我建议第一个工具做“获取当前时间”因为它不依赖外部服务调试起来最方便// tools/getTime.js export const getTimeTool { name: get_current_time, description: 获取当前的日期和时间返回 ISO 格式字符串, parameters: { type: object, properties: {}, required: [] }, execute: async () { return new Date().toISOString(); } };这个工具没有参数所以properties是空的。执行逻辑就是返回当前时间的 ISO 字符串。别看它简单它能帮你验证整个“模型输出工具调用 → 框架解析 → 执行 → 返回结果”的链路是否通畅。第二个工具可以做一个“计算器”用来验证带参数的工具调用// tools/calculator.js export const calculatorTool { name: calculate, description: 执行简单的数学计算支持加减乘除, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如 2 3 * 4 } }, required: [expression] }, execute: async ({ expression }) { // 注意生产环境不要直接用 eval这里只是演示 const result Function(use strict; return (${expression}))(); return { expression, result }; } };注意eval和Function都有安全风险生产环境一定要用专门的表达式解析库比如mathjs。这里只是为了演示方便别直接抄到线上代码里。4.3 把 Agent 循环接进 React 组件现在把执行循环和 UI 接起来。核心思路是用一个useState存消息历史用一个useState存当前状态空闲/运行中/完成然后在按钮点击时触发 Agent 循环import { useState } from react; import { getTimeTool } from ./tools/getTime; import { calculatorTool } from ./tools/calculator; const tools [getTimeTool, calculatorTool]; export default function AgentPanel() { const [goal, setGoal] useState(); const [messages, setMessages] useState([]); const [status, setStatus] useState(idle); const runAgent async () { setStatus(running); let history [ { role: system, content: 你是一个能调用工具的助手请逐步完成任务。 }, { role: user, content: goal } ]; for (let step 0; step 10; step) { const response await callModel(history, tools); if (response.type final) { setMessages(prev [...prev, { type: answer, content: response.content }]); setStatus(done); return; } if (response.type tool_call) { setMessages(prev [...prev, { type: tool, name: response.toolName, params: response.parameters }]); const tool tools.find(t t.name response.toolName); const result await tool.execute(response.parameters); setMessages(prev [...prev, { type: observation, content: result }]); history.push({ role: assistant, content: response.raw }); history.push({ role: tool, content: JSON.stringify(result) }); } } setStatus(done); }; return ( div input value{goal} onChange{e setGoal(e.target.value)} / button onClick{runAgent} disabled{status running} {status running ? 运行中... : 执行} /button div {messages.map((msg, i) ( div key{i}{JSON.stringify(msg)}/div ))} /div /div ); }这个组件跑起来之后你输入“现在几点了”应该能看到 Agent 调用get_current_time工具然后把结果返回给你。输入“帮我算一下 23 乘以 47”应该能看到它调用calculate工具。这就是一个最小可用的 Agent 闭环。4.4 关键参数的计算与选择Agent 循环里有几个参数需要根据实际情况调整我列一下我的经验值参数推荐值说明maxSteps5-20简单任务 5中等 10复杂 20超时时间30-60秒单步工具调用的超时防止卡死上下文窗口保留最近 10-20 条太多会超 token 限制太少会丢上下文温度参数0.1-0.3Agent 场景需要确定性温度别太高温度参数这个事值得多说一句。很多人调模型习惯用默认温度通常是 0.7 或 1.0但在 Agent 场景里温度太高会导致模型“胡思乱想”比如该调用工具的时候不调用或者传一些奇怪的参数。我实测下来0.1 到 0.3 之间比较稳既能保持一定的灵活性又不会太发散。5. 常见问题与排查技巧实录5.1 模型不调用工具怎么办这是最常见的问题。你明明定义好了工具模型却直接用自己的知识回答根本不走工具调用。原因通常有三个工具描述不够清晰、系统提示词没强调要用工具、模型本身不支持工具调用。排查顺序是这样的先看工具描述确保每个工具的description都写清楚了“什么时候用、用来干什么”。比如get_current_time的描述如果只写“获取时间”模型可能觉得“我自己也知道大概时间”就不调用了。改成“获取当前的精确日期和时间当用户询问现在几点、今天几号时使用”调用率会明显提升。然后在系统提示词里加一句“你有以下工具可以使用当任务需要这些工具时必须调用它们不要凭记忆回答。”这句话看起来简单但效果很明显。最后确认你用的模型是否支持 function calling。有些轻量级模型或者老版本模型不支持这个能力那就只能靠 prompt 工程来模拟效果会差很多。5.2 工具调用参数传错模型传错参数的情况也很常见比如该传字符串的传了数字该传数组的传了对象。解决办法是在参数定义里把类型写清楚并且在 description 里给例子parameters: { type: object, properties: { city: { type: string, description: 城市名称例如 北京、上海 } } }如果模型还是传错可以在执行工具之前加一层参数校验用ajv这类库做 JSON Schema 验证不合法就直接返回错误信息给模型让它重新传。这样虽然多一步但能避免很多莫名其妙的 bug。5.3 循环停不下来Agent 反复调用同一个工具、或者一直在“思考”不输出最终答案这种情况通常是任务目标太模糊或者maxSteps 设太大。解决办法有两个一是把任务拆细给 Agent 更明确的目标二是设置一个“重复检测”机制如果连续三次调用同一个工具且参数相同就强制终止。我踩过的一个坑是让 Agent “帮我整理一下桌面文件”结果它反复调用list_files工具每次返回的结果都一样但它就是不停。后来改成“列出桌面所有 .txt 文件然后把它们移动到 documents 文件夹”任务就顺利完成了。目标越具体Agent 越不容易跑偏。5.4 常见问题速查表问题现象可能原因解决方法模型不调用工具工具描述模糊、系统提示没强调优化 description加“必须调用”提示参数传错类型定义不清、缺少示例补 JSON Schema加参数校验循环停不下来目标模糊、maxSteps 太大拆细任务加重复检测上下文超限消息历史太长裁剪历史保留最近 N 条工具执行超时外部服务慢、没设超时加 timeout异步执行结果不稳定温度太高降到 0.1-0.3提示排查 Agent 问题的时候把每一步的输入输出都打日志。很多时候问题不在模型而在你的代码逻辑。日志越详细定位越快。6. 从 paperclip 延伸出去Agent 框架的选型思考6.1 自建还是用现成的paperclip这种自建框架的好处是完全可控你想怎么改就怎么改不用受限于别人的抽象层。但代价是什么都要自己写工具管理、错误处理、日志、监控这些在成熟框架里都是现成的。我的建议是如果你只是想快速验证一个想法用现成框架如果你想深入理解 Agent 的运行机制或者有特殊需求现成框架满足不了那就自建。OpenClaw 这类工具之所以火是因为它们把“安装-配置-使用”这条链路做得足够短用户不需要懂代码就能跑起来。但如果你要定制化还是得回到代码层面。paperclip的定位更像是“给你一套积木你自己搭”适合愿意动手的人。6.2 工具生态才是护城河Agent 框架本身的技术门槛其实不高真正难的是工具生态。你有 100 个工具Agent 能做的事就多 100 种。paperclip如果要做大关键不在于循环写得多优雅而在于能不能吸引开发者贡献工具。这一点上MCPModel Context Protocol这类标准的出现其实是好事它让工具的定义和调用有了统一规范不同框架之间的工具可以复用。我个人的判断是未来 Agent 框架的竞争会从“谁的循环写得好”转向“谁的工具多、谁的工具好用”。paperclip如果想在这个赛道站稳工具市场的建设比核心代码的优化更重要。6.3 安全边界不能忽视Agent 能执行命令、能读写文件这意味着它也能删库跑路。paperclip这类框架必须考虑安全边界哪些工具是只读的、哪些是写操作的、哪些需要用户确认。我的做法是给工具加一个dangerous标记执行前弹窗确认或者限制在沙箱环境里运行。注意永远不要给 Agent 无限制的 shell 权限。哪怕只是 demo也要限制它能执行的命令范围。我见过有人让 Agent 直接跑rm -rf结果把整个项目目录删了这种坑千万别踩。7. 我个人的一些实操体会跑通paperclip这套逻辑之后最大的感受是Agent 的难点不在“智能”而在“工程”。模型本身的能力已经足够强了真正花时间的是工具定义、错误处理、状态管理、日志这些“脏活累活”。一个能跑的 demo 可能半天就写完了但要让它稳定运行、能处理各种边界情况可能需要几周的打磨。另一个体会是别追求一步到位。我一开始想做一个“全能 Agent”什么工具都往里塞结果调试的时候根本不知道问题出在哪。后来改成一次只加一个工具跑通了再加下一个效率反而高很多。Agent 开发是个迭代的过程先跑通最小闭环再逐步扩展比一上来就搞大而全要靠谱得多。最后分享一个小技巧给 Agent 加一个“思考日志”面板把每一步的 Thought、Action、Observation 都展示出来。这不仅方便调试也能让用户理解 Agent 在干什么。很多时候用户觉得 Agent “不智能”其实是因为它干了什么用户看不到。把过程透明化信任感会强很多。这个方向后续还可以往“多 Agent 协作”扩展比如一个 Agent 负责规划、一个负责执行、一个负责检查三个角色互相配合。不过那是另一个话题了等我把单 Agent 跑稳了再折腾。
返回列表