用Node.js+本地模型qwen3-1.7b实现ReAct循环多工具调用(收藏版)
发布时间:2026/10/20 5:32:17 锦皓数字建站
`)
本文通过Node.js和本地模型qwen3-1.7b实现了ReAct推理-行动-观察循环的多工具调用无需任何Agent SDK。文章详细解析了OpenAI tools协议和Agent框架的底层原理通过手写完整的工具调用循环摊开框架封装的底层协议细节。核心内容涵盖多工具调用的必要性、工具调用的本质是乒乓球循环、ReAct循环的实战解析、手写协议的核心实现以及并行与顺序执行模式的对比。本文旨在帮助程序员和小白深入理解大模型的工具调用机制为使用LangChain、openai/agents等框架打下坚实基础。用 Node.js 本地模型qwen3-1.7b实现 ReAct推理-行动-观察循环的多工具调用不用任何 Agent SDK手写完整的工具调用循环把框架封装掉的底层协议细节全部摊开。写作目的不为造轮子而是吃透 OpenAI tools 协议与 Agent 框架的底层原理——看懂这套“乒乓球”循环后LangChain、openai/agents 等框架的工具调用部分对你就再也没有黑盒。一、为什么需要多工具单个工具只能解决单一问题查天气、查新闻、看时间各管一摊。但真实需求往往是复合的“北京和上海今天哪里更热” → 要同时调两次天气工具并行“帮我看看今天 AI 新闻里第一条的正文” → 要先查新闻拿链接再读网页顺序依赖这就是多工具调用要解决的两类核心场景彼此独立的工具并行跑、有依赖关系的工具串行跑。而这两种模式在 OpenAI tools 协议里其实是同一套机制的不同表现——先看懂协议两种模式就都通了。二、工具调用的本质一场乒乓球循环模型自己不能执行代码它只会说想调什么工具。真正的执行在本地。一次带工具的问答实际是这样往返多次工具调用循环流程协议层面的四个关键点工具清单放在请求的tools参数里每个工具用 JSON Schema 描述名字、用途、参数结构——这是模型选工具、填参数的唯一依据模型决定调工具时响应的 message 里带tool_calls数组工具名 JSON 字符串参数。注意参数是字符串要本地JSON.parse工具执行结果以{ role: tool, tool_call_id, content }消息回写tool_call_id把结果和那次调用配对带tool_calls的 assistant 消息必须原样回写进 messages再带上 tool 结果重新请求直到模型不再返回tool_calls一次完整问答的消息数组最终长这样[system, user, assistant(tool_calls[A, B]), ← 模型第1轮要调 A 和 B tool(tool_call_idA), tool(tool_call_idB), assistant(tool_calls[C]), ← 模型第2轮再调 C tool(tool_call_idC), assistant(content最终回答)] ← 循环结束三、ReAct 实战用一个例子拆解推理-行动-观察上一节讲的是协议格式这一节回答“模型凭什么知道下一步干什么”——答案是 ReActReasoning Acting出自 Yao et al. 2022 论文让模型交替进行推理Thought和行动Action每次行动后观察Observation结果再继续推理直到任务完成。这个循环就是所有 Agent 框架的心脏。ReAct 循环推理-行动-观察ReAct LoopAI AgentReason Act论文ReAct: Synergizing Reasoning and Acting in Language Models (2022)Yao et al.ReAct Reasoning(Thought) Acting(Action)是大模型 Agent 最经典的循环范式交替做思考、调用工具、接收环境观察直到任务结束。 循环流程核心三步反复迭代Thought思考LLM 基于当前上下文分析现状、推理下一步策略写明思考过程。Action行动模型输出工具调用指令搜索 / 查库 / 计算器等交给外部环境执行。Observation观察外部环境返回执行结果把结果追加到上下文。回到第一步继续循环直到模型判断任务完成输出 Final Answer 终止循环。3.1 实例论文原貌用户提问“帮我看看今天 AI 新闻里第一条的正文。”按 ReAct 论文的经典格式模型的思考过程是这样Thought 1: 用户要看 AI 新闻正文但没给链接。 我需要先用 get_news 拿到新闻列表。 Action 1: get_news(topic人工智能) Observation 1: [{title:国产大模型...,link:https://www.thepaper.cn/newsDetail_forward_123}, ...共5条] Thought 2: 拿到列表了第一条的链接是 newsDetail_forward_123 用 read_url 读它的正文。 Action 2: read_url(urlhttps://www.thepaper.cn/newsDetail_forward_123) Observation 2: {title:国产大模型...,summary:今日某实验室发布...} Thought 3: 正文已拿到可以回答用户了不需要再调工具。 Final Answer: 今天 AI 新闻第一条是《国产大模型...》主要内容是……注意两个体现 ReAct 精髓的细节Action 2 的 url 不是用户给的是模型从 Observation 1 里自己读出来的——“观察→推理→新行动”正是 ReAct 能处理依赖链任务的原因Thought 3 决定“不再行动”——什么时候停下同样是推理出来的3.2 ReAct 与 tools 协议的对应关系在 OpenAI tools 协议里论文中显式的 Thought 被收进了模型内部开enable_thinking可以看到Action / Observation 则有了结构化的载体ReAct 概念论文形式tools 协议中的对应Thought推理显式文本写在 prompt 里收进模型内部每轮请求前的“想”Action行动Action: 工具名[参数]文本tool_calls数组工具名 JSON 参数Observation观察工具输出拼回 promptrole: tool消息回写进 messagesFinal AnswerFinal Answer:文本不带tool_calls的 assistant 消息也就是说runAgent 的每一次 for 循环就是 ReAct 的一次“Thought → Action → Observation”。下一节的runAgent代码就是它的工程落地。3.3 实例走查循环每轮发生了什么把这个例子放进 runAgent 循环逐轮追踪 messages 数组轮次Thought模型内部推理Action返回的 tool_callsObservation回写的 tool 消息1要正文得先有链接get_news({topic:人工智能})5 条新闻含 link2第一条的 link 可用read_url({url:https://...})网页标题 正文摘要3信息够了直接回答无 → 循环结束—这正是第二节消息数组展开后的真实内容——“查列表 → 读详情”是 ReAct 最常见的依赖链形态。3.4 从实例得到的两条工程结论Observation 的质量决定 Thought 的质量get_news返回了结构化的 title/link 字段模型才能准确提取 url如果工具返回一段含糊文本模型就容易填错参数。工具输出的设计本质是在为模型的下一步推理“供料”推理不可见 ≠ 推理不存在tools 协议隐藏了 Thought但模型每轮都在基于全部 messages 做决策。这也是为什么错误信息也要回写给模型详见第四节——它同样是 Observation是下一轮推理的输入四、手写协议multi-tool.js 核心实现4.1 工具定义声明写得越具体小模型越不容易选错工具集抽在utils/tools.js每个工具 definition给模型看的声明execute(本地实现export const getWeather { definition: { type: function, function: { name: get_weather, description: 查询指定城市的实时天气包括温度、体感温度、湿度、风力, parameters: { type: object, properties: { city: { type: string, description: 城市名称如北京、上海 }, }, required: [city], }, }, }, execute: async ({ city }) { /* fetch wttr.in返回 JSON 字符串 */ }, }; // 工具注册表整体传给模型按 name 查找执行 export const TOOLS [getWeather, getNews, getTime, readUrl]; export const TOOL_MAP Object.fromEntries(TOOLS.map((t) [t.definition.function.name, t]));本项目注册了四个工具其中read_url是依赖型工具——它的 url 参数必须来自get_news的返回结果这正是顺序执行的素材。4.2 模型请求流式 思考可视化初版用非流式一次拿到完整 JSON解析tool_calls简单可靠但页面看不到打字效果也看不到思考过程。改成流式后每轮的reasoning_content思考和content正文增量都能实时转发给页面async function callModelStream(messages, { onReasoning, onContent } {}) { const response await fetch(${BASE_URL}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.DASHSCOPE_API_KEY} }, body: JSON.stringify({ model: MODEL, messages, tools: TOOLS.map((t) t.definition), tool_choice: auto, // 模型自主决定是否调工具 stream: true, temperature: 0.3, max_tokens: 4096, // thinking 会占输出预算太小会导致 content 为空 enable_thinking: true, // 开启后才有 reasoning_content }), }); let content ; const toolCalls []; // 按 delta.index 累积的 tool_calls await parseSSEStream(response, (data) { const delta data.choices?.[0]?.delta ?? {}; if (delta.reasoning_content) onReasoning?.(delta.reasoning_content); if (delta.content) { // 工具轮的 content 常只是换行等空白副产品纯空白不转发页面避免打乱展示顺序 if (delta.content.trim() || content.trim()) onContent?.(delta.content); content delta.content; } // tool_calls 分片到达每片带 indextype/id/name 只在首片arguments 分多片 for (const tc of delta.tool_calls ?? []) { const acc (toolCalls[tc.index] ?? { id: , type: function, function: { name: , arguments: } }); if (tc.id) acc.id tc.id; if (tc.type) acc.type tc.type; if (tc.function?.name) acc.function.name tc.function.name; if (tc.function?.arguments) acc.function.arguments tc.function.arguments; } }); // 拼成与非流式同构的完整 message循环逻辑无需感知流式细节 return { role: assistant, content, ...(toolCalls.length ? { tool_calls: toolCalls } : {}) }; }三个设计要点流式下无法提前知道本轮是调工具还是最终回答只能先转发增量、流结束后看tool_calls是否累积出内容再决定循环去留。所以返回值拼成与非流式同构的 message上层循环逻辑一行不用改tool_calls分片累积是流式的核心难点每个分片带indextype/id/name只在首片出现arguments被切成多片必须按 index 逐片拼接坑 5 详述tool_choice: auto由模型自己判断要不要调工具。其他可选值none禁止调工具、required必须调一个、指定具体函数名则强制调该工具4.3 工具执行错误也要作为结果返回async function executeToolCall(call) { const tool TOOL_MAP[call.function.name]; if (!tool) return 未找到工具 ${call.function.name}; let args; try { args JSON.parse(call.function.arguments || {}); // 参数是 JSON 字符串 } catch { return 工具参数不是合法 JSON: ${call.function.arguments}; } try { return String(await tool.execute(args)); } catch (e) { return 工具执行失败: ${e.message}; } }设计原则execute 永远返回字符串失败时返回错误描述而不是抛异常。因为结果是回写给模型看的——模型读到参数不是合法 JSON就能在下一轮自行修正参数而如果抛异常中断流程模型连纠错的机会都没有。4.4 工具调用循环全文核心const MAX_TOOL_ROUNDS 5; // 循环上限防止模型反复调工具陷入死循环 async function runAgent(userMessage, { onToolCall, onToolResult, onReasoning, onContent } {}) { const messages [ { role: system, content: SYSTEM_PROMPT }, { role: user, content: userMessage }, ]; for (let round 1; round MAX_TOOL_ROUNDS; round) { const message await callModelStream(messages, { onReasoning, onContent }); // 无 tool_calls → 本轮增量就是最终回答已流式推给页面循环结束 if (!message.tool_calls?.length) { return message.content ?? ; } messages.push(message); // ★ 原样回写带 tool_calls 的 assistant 消息 // 同一轮的多个 tool_calls 彼此独立 → 并行执行 // 回调顺序调用行几乎同时发出并行启动结果行按完成先后发出如实反映并行 const results await Promise.all(message.tool_calls.map(async (call) { onToolCall?.(调用工具 ${call.function.name}(${call.function.arguments})); const result await executeToolCall(call); onToolResult?.(工具结果 ${call.function.name} → ${result}); return { callId: call.id, result }; })); // 逐个回写结果靠 tool_call_id 与调用配对 for (const { callId, result } of results) { messages.push({ role: tool, tool_call_id: callId, content: result }); } } return 工具调用超过 ${MAX_TOOL_ROUNDS} 轮未能完成任务; }循环的终止条件只有一个模型的回复里不再带tool_calls。MAX_TOOL_ROUNDS是保险丝——小模型偶尔会拿着同样的参数反复调同一个工具没有上限就会一直烧下去。五、并行 vs 顺序两种执行模式5.1 并行同一轮的多个 tool_calls问北京和上海今天天气怎么样模型会在同一次回复里返回两个tool_calls。协议语义上它们彼此独立所以本地用Promise.all并行执行const results await Promise.all(message.tool_calls.map((call) executeToolCall(call)));总耗时 ≈ 最慢的那个工具而串行是所有工具耗时之和。天气接口单次约 1~2 秒两个工具并行能省下一半等待回写结果时顺序无所谓模型靠tool_call_id配对不靠位置5.2 顺序跨轮次的工具依赖链问看看今天 AI 新闻第一条的正文模型不可能一轮完成——它必须先拿到链接。于是协议自然形成跨轮次的串行链第1轮模型返回 tool_calls[get_news(topic人工智能)] → 本地执行结果含每条新闻的 link回写 第2轮模型从上一轮结果中提取第一条的 link 返回 tool_calls[read_url(url那个链接)] → 本地执行网页内容回写 第3轮模型不再调工具整理正文给出最终回答注意这里的关键第二次的参数是模型自己从上一轮 tool 结果里读出来填的。依赖链不是代码写死的而是模型基于上下文推理出来的——这也是 system prompt 里那句先用 get_news 获取链接再用 read_url 读取的作用给小模型指路。5.3 对比总结并行执行顺序执行触发方式模型一轮返回多个 tool_calls模型多轮、每轮返回一个或多个tool_calls参数来源都来自用户问题彼此独立后一个依赖前一个的结果本地实现Promise.all同时跑天然串行上一轮结果回写后才有下一轮耗时≈ 最慢工具 N 次模型推理所有工具之和 N 次模型推理典型场景多城市天气、多关键词新闻查列表 → 读详情、搜索 → 追问谁决定模型自主决定tool_choice: auto模型基于上下文推理 prompt 引导一句话记忆并行发生在轮内顺序发生在轮间。轮内能不能并行由模型决定它一次给几个 tool_calls轮间的顺序由数据依赖决定。六、给小模型的 system prompt 设计大模型看工具声明就能用得八九不离十小模型需要更直白的指令。本项目的 prompt 只有四条短句你是一个会使用工具的中文助手。 1. **涉及天气、新闻、当前时间等实时信息必须先调用对应工具禁止凭空编造。** 2. **需要看某条新闻的正文时先用 get_news 获取链接再用 read_url 读取该链接。** 3. **拿到工具结果后用简洁的中文整理出回答。** 4. **不需要工具的问题直接回答。**经验总结短小模型上下文理解力有限指令越短遵从度越高先判断再行动第 4 条明确不需要工具就直接回答避免什么问题都去调工具显式写依赖链第 2 条直接告诉模型 get_news → read_url 的先后关系顺序执行的成功率明显提升大模型的知识又多又杂到底怎么学做这行久了被问最多的问题就一个大模型的知识又多又杂我到底先学啥翻了一堆资料看啥都像重点结果啥都没学进去。今天把我自己一路摸出来的顺序捋一遍零基础、应届生、转行的都适用。跟着走至少方向不乱、不瞎忙学完拿得出手。第一阶段10天初阶应用这一阶段就一个目标把大模型真正玩明白不是只会聊天那种而是能调教它、能用代码把大模型和业务接起来。大模型 AI 能干什么大模型是怎样获得「智能」的用好 AI 的核心心法大模型应用业务架构大模型应用技术架构代码示例向 GPT-3.5 灌入新知识提示工程的意义和核心思想Prompt 典型构成指令调优方法论思维链和思维树Prompt 攻击和防范…第二阶段30天高阶应用这阶段正式进实战目标很实在做出两个能拿出手的东西——一个私有知识库一个 agent 对话机器人。为什么要做 RAG搭建一个简单的 ChatPDF检索的基础概念什么是向量表示Embeddings向量数据库与向量检索基于向量检索的 RAG搭建 RAG 系统的扩展知识混合检索与 RAG-Fusion 简介向量模型本地部署…走完这 30 天Python 或 JavaScript 背景的基本能独立做一套 RAG 应用简历上也敢写大模型应用开发了。第三阶段约20天动手微调学到这你已经开始 “造” 模型了 —— 通过微调搞出垂直领域的专用小模型。为什么要做 RAG什么是模型什么是模型训练求解器 损失函数简介小实验2手写一个简单的神经网络并训练它什么是训练/预训练/微调/轻量化微调Transformer结构简介轻量化微调实验数据集的构建…恭喜你如果学到这里你基本可以找到一份大模型 AI相关的工作自己也能训练 GPT 了通过微调训练自己的垂直大模型能独立训练开源多模态大模型掌握更多技术方案。到此为止大概2个月的时间。你从 “只会用” 变成 “能造” 的 AI 小子。那么你还想往下探索吗想要入局AI大模型赛道、抢占行业红利的朋友微信扫描下方CSDN官方认证二维码即可100%免费领取全套学习资料第四阶段20天商业闭环部署落地技术学完落地才算数。这阶段带你搞明白大模型怎么真正跑起来、用起来。硬件选型带你了解全球大模型使用国产大模型服务搭建 OpenAI 代理热身基于阿里云 PAI 部署 Stable Diffusion在本地计算机运行大模型大模型的私有化部署基于 vLLM 部署大模型案例如何优雅地在阿里云私有部署开源大模型部署一套开源 LLM 项目内容安全互联网信息服务算法备案…想要入坑大模型的朋友给大家准备了一份2026 年最新 AI 大模型全套学习资料1、大模型系统化学习路线2、大模型学习视频教程3、大模型学习书籍电子文档4、AI大模型最新行业报告5、大模型项目实战配套源码6、2026大模型大厂面试真题7、为什么要学习大模型我国在A大模型领域面临人才短缺,数量与质量均落后于发达国家。2023年人才缺口已超百万凸显培养不足。随着AI技术飞速发展预计到2025年,这一缺口将急剧扩大至400万,严重制约我国AI产业的创新步伐。加强人才培养,优化教育体系,国际合作并进是破解困局、推动AI发展的关键。8、适合谁学传统后端 / Java / 前端开发想转型 AI 应用大学生、应届生想拿更好的 offer产品经理、运营想武装职业竞争力技术负责人想给团队落地提效学习是反人性的但回报是真金白银。技术会更新赛道会切换但只要你先动手机会就永远站在你这边。9、这些资料真的有用吗这份资料由我和鲁为民博士(北京清华大学学士和美国加州理工学院博士)共同整理现任上海殷泊信息科技CEO其创立的MoPaaS云平台获Forrester全球’强劲表现者’认证服务航天科工、国家电网等1000企业以第一作者在IEEE Transactions发表论文50篇获NASA JPL火星探测系统强化学习专利等35项中美专利。本套AI大模型课程由清华大学-加州理工双料博士、吴文俊人工智能奖得主鲁为民教授领衔研发。资料内容涵盖了从入门到进阶的各类视频教程和实战项目无论你是小白还是有些技术基础的技术人员这份资料都绝对能帮助你提升薪资待遇转行大模型岗位。想要入局AI大模型赛道、抢占行业红利的朋友微信扫描下方CSDN官方认证二维码即可100%免费领取全套学习资料好了就写到这。路线是死的人是活的卡住了随时来问我。觉得有用就点个收藏别让它进收藏夹吃灰——动起来比啥都强。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。