news 2026/9/9 1:44:52

100行TypeScript代码实现通用智能体:从工具调用到多步推理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
100行TypeScript代码实现通用智能体:从工具调用到多步推理

这次我们直接看一个很具体的问题:如何用 TypeScript 把通用智能体跑起来,并且把核心代码控制在 100 行左右。这里说的通用智能体,不是某个重量级框架,而是 Agent 最核心的闭环:模型负责理解任务、规划步骤,代码负责执行工具、回传结果,模型根据结果继续推理,直到给出最终答案。这个闭环一旦跑通,后续加记忆、加规划、加知识库都只是扩展问题。

我选择 TypeScript 来做这个示例,有几个原因:第一,它不依赖任何 Agent 框架,只依赖 Node.js 环境和 TypeScript 本身;第二,代码直接对接 OpenAI 兼容的 Chat Completions 接口,只要模型服务支持tools参数,就能接入,不管是云端模型还是本地推理服务;第三,消息结构、工具参数、返回结果都有类型约束,改起来比纯 JavaScript 清晰很多。本文会给出完整代码、运行测试流程、接口封装示例和批量任务处理思路,也会把最容易踩的坑列出来。如果你正在做智能体开发,或者想弄明白各种 Agent 框架底层是怎么工作的,这篇文章值得收藏。

1. 核心能力速览

能力项说明
项目类型TypeScript 轻量通用智能体示例
依赖环境Node.js 18+、TypeScript、支持tools参数的大模型接口
主要功能多步推理、工具调用、消息历史维护、可扩展工具集
推荐硬件调用云端模型无特殊硬件要求;使用本地模型时按模型要求配置
显存占用取决于接入的模型,本示例代码本身只占用少量内存
支持平台Windows、Linux、macOS
启动方式命令行脚本,可扩展为 HTTP API 服务
是否支持 API可以封装为 HTTP 接口,文中提供封装思路
是否支持批量任务支持,可编写批处理脚本并发执行
适合场景Agent 原理学习、原型验证、自动化任务、内部工具集成

这里要说明一点:这个项目本身是一个教学型示例,目的是把 Agent 的工具调用机制讲清楚。它不追求生产级高并发,也不内置复杂的记忆和规划模块,但核心循环和接口设计是通用的。你完全可以在它基础上继续扩展。

2. 适用场景与使用边界

这个实现适合三类人。第一类是刚开始接触智能体开发的工程师,想跳过漫长框架文档,直接看一个最小可运行样本;第二类是已经有业务系统的开发同学,需要在自己的 TypeScript 项目里嵌入一个轻量 Agent,对外提供工具调用能力;第三类是想基于本地模型做实验的玩家,只要本地推理服务支持 OpenAI 兼容接口,就可以用同样代码接入。

它不适合的场景也很明显。如果业务需要处理超长上下文、多智能体协作、复杂记忆管理、高并发生产流量,这个 100 行示例不够,应该去评估成熟的 Agent 框架或云平台能力。另外,工具调用越强大,越要注意安全边界。示例里只放了获取时间和数字加法,可以在本地放心运行;但如果要把工具扩展成执行命令、读写文件、调用外部系统,就必须做好权限校验和操作审计。涉及用户数据、版权素材、人脸声音等敏感内容时,要确保已获得合法授权,不能因为“技术能跑通”就忽略合规要求。

3. 环境准备与前置条件

首先确认本机 Node.js 版本。代码里用到原生fetch,Node.js 18 开始默认支持,所以建议使用 Node.js 18 或更高版本。打开终端检查:

node -v npm -v

如果没有安装 Node.js,先去官网下载 LTS 版本,安装完成后再次检查版本即可。

接着创建项目并安装依赖。这里需要三个基础包:typescript用于编译,tsx用于直接运行 TypeScript 文件,dotenv用于读取.env配置文件。@types/node提供 Node.js 环境类型提示。

mkdir ts-agent-demo cd ts-agent-demo npm init -y npm install typescript tsx dotenv npm install -D @types/node

然后创建一个tsconfig.json,指定编译目标。这里用ES2022是因为代码里会用到String.prototype和异步迭代等现代语法,也可以用更保守的目标,但建议直接使用 ES2022 以上。

{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["node"] } }

如果运行批量任务时用到顶层await,需要把module设置为ESNext或在tsx环境下运行。也可以用module: "CommonJS"搭配node运行编译后的 JS,但为了简单,下面所有例子都直接用tsx运行。

环境变量方面,准备一个.env文件。如果使用云端模型,需要填写 API Key;如果使用本地兼容接口,只需要把地址指向本地服务。模型名根据实际部署填写,不同模型对tools的支持程度不一样,建议选择支持 function calling 或工具调用的模型。

4. 100行代码实现通用智能体:核心代码

先理清设计思路。一个通用智能体最少需要四部分:消息结构、工具定义、模型调用函数、主循环。消息结构用来保存 system、user、assistant、tool 四种角色的消息;工具定义告诉模型有哪些函数可以调用;模型调用函数负责把消息和工具列表发给大模型;主循环负责判断模型返回的是普通文本还是工具调用,如果是工具调用就执行并把结果追加到消息历史里,然后再次调用模型,直到模型输出最终答案。

下面是完整的示例代码,保存为agent.ts

import { config } from "dotenv"; config(); const API_KEY = process.env.API_KEY ?? ""; const BASE_URL = process.env.BASE_URL ?? "https://api.openai.com/v1"; const MODEL = process.env.MODEL ?? "gpt-4o-mini"; type ToolCall = { id: string; type: "function"; function: { name: string; arguments: string }; }; type Message = { role: "system" | "user" | "assistant" | "tool"; content: string; tool_call_id?: string; tool_calls?: ToolCall[]; }; type Tool = { name: string; description: string; parameters: Record<string, unknown>; execute: (args: any) => string | Promise<string>; }; const tools: Tool[] = [ { name: "get_current_time", description: "获取当前日期和时间", parameters: { type: "object", properties: {} }, execute: () => new Date().toLocaleString(), }, { name: "add_numbers", description: "计算两个数字的和", parameters: { type: "object", properties: { a: { type: "number", description: "第一个数字" }, b: { type: "number", description: "第二个数字" }, }, required: ["a", "b"], }, execute: ({ a, b }) => String(a + b), }, ]; async function callLLM(messages: Message[]): Promise<Message> { const res = await fetch(`${BASE_URL}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${API_KEY}`, }, body: JSON.stringify({ model: MODEL, messages, tools: tools.map(({ name, description, parameters }) => ({ type: "function", function: { name, description, parameters }, })), }), }); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); return data.choices[0].message; } async function runAgent(userInput: string, maxSteps = 5): Promise<string> { const messages: Message[] = [ { role: "system", content: "你是通用智能体,可以调用工具完成任务,回答要简洁。" }, { role: "user", content: userInput }, ]; for (let step = 0; step < maxSteps; step++) { const reply = await callLLM(messages); messages.push(reply); if (!reply.tool_calls?.length) return reply.content; for (const toolCall of reply.tool_calls) { const tool = tools.find((t) => t.name === toolCall.function.name); if (!tool) { messages.push({ role: "tool", tool_call_id: toolCall.id, content: `未知工具: ${toolCall.function.name}` }); continue; } let result: string; try { const args = JSON.parse(toolCall.function.arguments || "{}"); result = String(await tool.execute(args)); } catch (err) { result = `工具执行失败: ${(err as Error).message}`; } messages.push({ role: "tool", tool_call_id: toolCall.id, content: result }); } } return "已达到最大步骤数,任务未完成。"; } async function main() { const input = process.argv.slice(2).join(" ") || "现在几点了?"; console.log(await runAgent(input)); } main().catch((err) => { console.error(err); process.exit(1); });

这段代码去掉空行和类型定义,核心循环逻辑确实在 100 行左右。重点不是代码行数,而是这个结构可以复用到很多项目里。

4.1 消息类型定义

Message类型对应 Chat Completions 接口的消息格式。role可以是systemuserassistanttool。当模型返回工具调用时,assistant消息会带上tool_calls数组;当代码执行完工具后,需要往消息列表里追加一条role: "tool"的消息,并且通过tool_call_id关联到对应的工具调用请求。这个对应关系容易出错,少了tool_call_id很多模型会直接报错。

由于接口返回的message里可能同时有contenttool_calls,这里把Message设计成可选字段。主流模型的返回结构基本一致,即使换了模型服务商,也只需要微调类型定义。

4.2 工具注册与执行

tools数组就是智能体可以使用的工具列表。每个工具包含四个字段:名字、描述、参数 JSON Schema、执行函数。描述非常关键,模型靠描述判断什么时候该调用哪个工具。参数 Schema 必须用 JSON Schema 格式,模型会参考它生成合法的参数 JSON。

示例里放了两个工具:get_current_time获取当前时间,add_numbers计算两个数字的和。execute函数可以是同步的,也可以是异步的。实际项目里,可以把execute改成任何你想让智能体执行的操作,比如查数据库、调业务接口、发消息等。但注意,工具能力越强,越要校验入参和输出,避免不可控操作。

4.3 主循环与多步推理

runAgent函数是核心。它先把 system 和 user 消息放进历史,然后进入循环。每次循环做三件事:调用模型、追加返回消息、判断是否有工具调用。如果没有工具调用,说明模型可以直接回答,返回content。如果有工具调用,就逐个执行,把工具结果追加到消息历史,然后继续下一次循环。

maxSteps参数用来限制最大推理步数。这个限制很重要,因为模型可能在复杂任务里反复调用工具,甚至陷入死循环。示例里默认是 5 步,实际使用可以按任务复杂度调整。多步推理能力是“通用智能体”的核心,它让模型不是一次生成完答案,而是可以根据工具返回结果动态调整下一步动作。

4.4 如何接入其他模型或本地推理服务

这段代码默认请求https://api.openai.com/v1,如果要用本地推理服务,只需要修改BASE_URL。比如本地部署了一个 OpenAI 兼容接口,监听在8000端口,那么.env里可以这样写:

BASE_URL=http://127.0.0.1:8000/v1 MODEL=你的本地模型名

注意,不是所有模型都支持工具调用。如果你的模型不支持tools参数,接口会忽略该字段或返回错误。建议先看模型文档,确认它支持 function calling 或 tool use 能力。从实践来看,市面上的主流模型基本都支持,但参数格式可能存在细微差异,遇到问题时优先检查接口返回的错误信息。

5. 运行测试与效果验证

先创建.env文件:

API_KEY=你的密钥 BASE_URL=https://api.openai.com/v1 MODEL=你的模型名

如果使用本地推理服务,把BASE_URL改成本地地址,并填入支持的模型名。不要真的把密钥写进代码或提交到 Git,.env要加入.gitignore

运行第一个测试,让智能体查询时间:

npx tsx agent.ts "现在几点了?"

正常结果是模型返回当前日期和时间。由于代码里没有加日志,默认只输出最终回答。如果想观察工具调用的过程,可以在main函数里打印中间消息,或者在runAgent中模型返回后加一段调试输出。

第二个测试,验证工具参数解析和计算能力:

npx tsx agent.ts "请计算 123456 + 654321"

预期结果是777777。这个用例能说明模型成功生成了add_numbers工具调用,参数被正确解析,工具结果被传回模型,最终模型基于工具结果给出答案。

第三个测试,验证多步推理。可以故意问一个需要先拿时间再判断的问题:

npx tsx agent.ts "现在是几点?如果小时数大于12,请输出下午,否则输出上午"

这会让模型先调用get_current_time,拿到结果后再根据小时数推理。判断成功标准是:最终答案正确,并且过程中发生了一次工具调用,模型没有一次性硬编答案。

常见失败原因有三个。一是 API Key 或 BASE_URL 配置错误,接口返回 401 或 404;二是模型不支持tools参数,返回 400;三是工具执行函数本身报错,比如参数解析失败。遇到这些问题,先看终端输出的异常信息,把接口返回的响应体打印出来,通常能直接定位。

6. 接口 API 与批量任务

上面的代码默认是命令行入口,适合手动测试。如果要把智能体接入到业务系统里,最直接的办法是封装成 HTTP API 服务。下面用 Node.js 原生http模块写一个极简示例,避免引入额外依赖:

import http from "http"; import { runAgent } from "./agent"; const server = http.createServer(async (req, res) => { if (req.method === "POST" && req.url === "/agent") { let body = ""; for await (const chunk of req) body += chunk; try { const { prompt } = JSON.parse(body); const answer = await runAgent(prompt); res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ answer })); } catch (err) { res.statusCode = 500; res.end(JSON.stringify({ error: (err as Error).message })); } } else { res.statusCode = 404; res.end(); } }); server.listen(3000, () => { console.log("agent service running at http://127.0.0.1:3000"); });

这里没有处理超时、并发限制、鉴权等生产级问题,但足以验证接口链路。启动服务后,可以用curl测试:

curl -X POST http://127.0.0.1:3000/agent \ -H "Content-Type: application/json" \ -d '{"prompt":"现在几点了?"}'

也可以用 Python 调用:

import requests resp = requests.post( "http://127.0.0.1:3000/agent", json={"prompt": "请计算 1 + 2"}, timeout=60 ) print(resp.json())

批量任务方面,核心思路是把一批 prompt 逐条交给runAgent,同时控制并发数量。假设有一个tasks.txt,每行一个任务:

现在几点了? 请计算 1 + 2 请计算 100 + 200

可以写一个batch.ts

import { readFile } from "fs/promises"; import { runAgent } from "./agent"; async function main() { const lines = (await readFile("tasks.txt", "utf-8")).split("\n").filter(Boolean); const concurrency = 3; let index = 0; async function worker() { while (index < lines.length) { const task = lines[index++]; try { const result = await runAgent(task); console.log(`[${task}] => ${result}`); } catch (err) { console.error(`[${task}] => 失败: ${(err as Error).message}`); } } } await Promise.all(Array.from({ length: concurrency }, worker)); } main();

批量任务最需要注意的是限流。大模型接口通常有每分钟请求次数限制,并发过高会触发 429。这里用concurrency控制同时进行的任务数,实际要根据接口限制调整。建议批量任务增加重试机制,对超时和 429 做指数退避。

7. 资源占用与性能观察

纯代码层面的资源占用非常低。开启一个 Node.js 进程,运行一个简单 Agent 任务,内存占用通常在几十 MB 级别,具体数值取决于运行时和系统环境。重点要观察的是模型接口的响应时间,因为一次任务可能需要多轮模型调用,每轮几百毫秒到几十秒都有可能。

如果接入的是云端模型,性能瓶颈主要在网络和模型推理延迟上。通过观察日志里每轮callLLM的耗时可定位瓶颈。如果接入的是本地模型,性能瓶颈可能在 GPU 显存或 CPU 算力。此时可以通过nvidia-smi或系统任务管理器观察显存占用。不同模型、不同量化方式、不同并发数量,显存占用差异很大,没有统一答案,以本机实际测试为准。

批量任务要格外注意并发对资源的影响。上面batch.ts虽然限制了并发数,但如果每个任务内部有多轮工具调用,实际同时进行的大模型请求可能超过concurrency。建议在封装批量执行器时统计“当前正在执行的 Agent 数量”,而不只是任务行数。另外,Node.js 默认堆内存有限,如果一次性读取超大tasks.txt或者积累太多消息历史,可以用node --max-old-space-size=4096提升堆内存上限。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
启动后立刻报Cannot find module依赖未安装或 tsx 不存在检查node_modules是否存在运行npm install
请求模型接口返回 401API Key 错误或未配置打印.env中读取到的值检查环境变量并确认 Key 有效
请求模型接口返回 400模型不支持tools或消息格式不对打印请求 body 和响应体换用支持工具调用的模型,或调整消息结构
模型返回文本,但没有执行工具工具描述不清晰或模型判断不需要调用查看模型返回的完整内容优化工具描述或更换提示词
工具执行时报 JSON 解析失败模型生成的参数不是合法 JSON打印toolCall.function.arguments在解析前做 try/catch 并回传错误给模型
多步循环不结束模型反复调用工具或任务过于复杂检查maxSteps是否过小增加maxSteps或给模型更明确的目标
批量任务卡住接口限流、超时未处理检查日志中是否有 429 或超时降低并发并增加重试
本地模型接口能访问但工具调用无效本地服务不支持 function calling 协议查看本地服务文档换用兼容 OpenAItools参数的服务端

排查时有一个通用技巧:把callLLM返回的原始 JSON 打印出来看。接口返回里包含了模型决定调用哪个工具、参数是什么、最终回答是什么,任何异常都能从原始结构里找到线索。不要把data.choices[0].message之外的内容忽略掉,很多错误信息在响应体的error字段里。

9. 最佳实践与使用建议

第一次运行先小参数测试。比如先用maxSteps=3,只测试一个工具,确认调用闭环保通后再扩展多个工具。这样能避免问题叠在一起很难定位。建议保留一套最小可运行配置,哪怕后续加了复杂的工具链,也能快速回退到基础版本做排查。

项目目录建议按职责分开:agent.ts放主循环和模型调用,tools.ts放工具定义,types.ts放消息和工具类型。这样后续添加新工具时不需要修改主循环代码,只要往tools数组里加一项即可。工具执行函数要统一返回字符串,方便模型消费。如果工具返回的是对象,提前JSON.stringify再回传,避免消息结构不统一。

安全性是智能体项目最容易忽略的部分。示例里的两个工具没有副作用,但真实系统里工具可能涉及文件读写、数据库操作、外部请求。建议给每个工具增加超时机制,防止模型调用一个永远不返回的工具;同时限制工具可操作的范围,比如指定可读目录、可调用的接口白名单。涉及用户隐私或版权内容时,必须先确认授权范围。

接口服务不要裸奔。如果需要把/agent接口开放给其他系统,建议在前面加一层鉴权,比如 API Token 或签名校验。批量任务要记录日志和失败重试,不能只把结果打印到控制台。日志里至少要包含任务 ID、模型名称、请求耗时、工具调用次数、最终结果,这些信息在排查问题时非常有用。

关于模型选择,建议先用一个小模型跑通流程,再根据效果换成更强模型。小模型速度快、成本低,适合调试;大模型对复杂工具调用的准确率更高,但在工具定义较多时延迟也会增加。对于生产环境,还需要关注模型版本升级对tools行为的影响,最好在发布前做一轮回归测试。

10. 总结与下一步

这个项目最值得尝试的点,是用很小的代码量把 Agent 的工具调用闭环讲清楚了。它没有隐藏逻辑,没有黑盒框架,所有消息流转都在runAgent函数里,非常适合作为智能体开发的入门模板。你先应该验证的功能是“模型返回工具调用 -> 代码执行 -> 结果回传 -> 模型最终回答”这条链路,只要这条路通了,其他功能都可以往上加。

最容易踩的坑有两个:一是模型不支持tools参数,二是消息历史里漏掉tool_call_id关联。前者会导致请求 400,后者会导致模型无法理解工具调用的上下文。只要把这两个点处理好,这个智能体就跑得起来。

后续可以扩展的方向很多:加入短期记忆模块,让 Agent 记住历史对话;接入向量数据库做 RAG,让它能回答私有知识库问题;增加多工具链和任务规划,让 Agent 自动拆解复杂任务;或者把 HTTP 接口从简易示例升级成带鉴权、限流、任务队列的生产服务。从这 100 行代码出发,一条完整的智能体开发路径已经很清晰了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 1:43:08

技能管理实战:从模糊清单到量化盘点的方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 1:40:39

AdaIN风格迁移原理与工程实践指南

简介&#xff1a;本资源是一份基于AdaIN算法的图像风格迁移实践项目&#xff0c;面向人工智能与机器学习方向的学习者、算法工程师及计算机视觉初学者&#xff0c;聚焦于如何利用深度学习高效实现艺术风格迁移这一典型CV任务。压缩包共23个文件&#xff0c;含8个Python核心脚本…

作者头像 李华
网站建设 2026/9/9 1:40:23

AI学习路线图:从大模型原理到Agent开发与模型部署

AI学习笔记我花了大半年时间整理自己学习AI的完整笔记&#xff0c;今天把它重新梳理成一份可以直接照着用的路线图。这篇文章不是什么“七天精通大模型”的速成教程&#xff0c;而是我作为AI应用开发者&#xff0c;从只会调接口到能独立完成Agent开发、模型部署、产品落地的真实…

作者头像 李华
网站建设 2026/9/9 1:38:16

NullBytes靶机通关:SQL注入与SUID提权实战记录

看了一遍又一遍&#xff0c;NullBytes 这台 VulnHub 靶机给我的感觉就是&#xff1a;麻雀虽小&#xff0c;五脏俱全。它不像 DC 系列那样动不动就要打域环境&#xff0c;也不像那些动不动堆内核漏洞的靶机让人一脸懵&#xff0c;它老老实实走的是“Web 注入 → 口令复用 → 本地…

作者头像 李华
网站建设 2026/9/9 1:38:07

基于STC89C51的双通道DHT11温湿度采集与LCD1602显示系统

简介&#xff1a;这是一套基于STC89C51单片机的双通道DHT11实时温湿度显示系统项目包&#xff0c;面向单片机初学者、电子设计与嵌入式系统爱好者&#xff0c;适合用于课程设计或毕业设计参考。项目以STC89C51为控制核心&#xff0c;通过单总线读取两路DHT11传感器数据&#xf…

作者头像 李华
网站建设 2026/9/9 1:38:02

PHP镜像克隆网站源码v4.0:整站备份与部署实战解析

简介&#xff1a;单域名PHP镜像克隆网站源码v4.0是一套以PHP开发的镜像站点程序&#xff0c;主要面向需要快速搭建单域名采集镜像站的开发者和个人站长&#xff0c;重点解决多蜘蛛抓取时IP易被限制、PC端与移动端适配成本高等问题。压缩包仅313KB&#xff0c;共58个文件&#x…

作者头像 李华