十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

TypeScript手写通用AI Agent核心:100行实现工具调用与任务循环

TypeScript手写通用AI Agent核心:100行实现工具调用与任务循环 这次我们来看一个 TypeScript 实现的通用 AI Agent 实战项目。主题很明确不依赖 LangChain、不依赖重量级框架用 TypeScript 从零手写一个类 PI-Agent 架构的智能体核心把模型调用、工具注册、任务循环、事件回调这几个关键模块串起来核心逻辑可以控制在 100 行上下。先给结论这个方向适合三类人——想搞懂 Agent 内部原理的开发者、需要快速给团队做内部 Agent 原型的工程师、以及想用 TypeScript 统一后端和 AI 编排能力的项目组。它不涉及模型训练不需要 GPU只要你有一个 OpenAI 兼容接口或者本机跑一个 Ollama 服务就能把整套流程跑通。本文会带你完成环境准备、项目初始化、Agent 核心代码实现、功能测试、HTTP API 服务、批量任务处理、常见问题排查最后落地成一套可继续扩展的工程骨架。文章偏实操代码可以直接复制到本地跑。1. 核心能力速览能力项说明项目类型TypeScript 通用 AI Agent 教学与实战骨架架构参考类 PI-Agent 规划执行型 Agent 架构核心语言TypeScript 5.xNode.js 18运行时依赖0 个直接使用 Node fetch 调用模型接口开发依赖typescript、tsx、types/node主要功能LLM 对话、工具注册、多轮 Tool Call、事件日志、HTTP API、批量任务启动方式npm run dev / server / batch是否支持 API支持内置 HTTP JSON 接口示例是否支持批量任务支持提供文本批量任务示例GPU 要求不需要模型推理在外部模型服务或本地模型服务中完成显存占用核心编排逻辑不占用显存如需本地推理请单独评估模型服务适合场景Agent 原理学习、内部工具原型、任务批量预处理、接口集成整个项目没有魔法就是标准的 Agent 循环模型返回消息、有工具调用就执行、把结果回传给模型、直到模型给出最终答案。2. 适用场景与使用边界这个架构最擅长的场景是把“大模型 工具调用”组合成可复用的自动执行链路。例如企业内部知识问答需要查询数据库、调用内部 API 获取数据后再生成回答。批量文本处理从文件读取多个任务逐个交给 Agent 执行并输出结果。自动化运维助手把日志查询、命令执行封装成工具让 Agent 根据用户意图选择工具。作为学习项目理解 Tool Call 协议、工具注册表和循环终止条件。它不适合的场景也同样明确。生产级复杂 Agent 编排、需要状态持久化和会话恢复、需要细粒度权限审计、需要多 Agent 协同和任务链路追踪的场景不能只靠这个最小骨架需要引入消息队列、任务存储、权限服务、审计日志等基础设施。使用边界也要说清楚这个项目只负责 Agent 编排不负责模型部署、不负责数据合规。接入任何模型接口前确认你对服务方条款、数据流向、用户隐私和版权素材有明确授权。如果工具里包含本地文件读取、命令执行、网络请求等能力必须做参数白名单校验不能直接把用户输入透传给 shell 或内网接口。3. 环境准备与前置条件先准备一套最小运行环境。3.1 基础环境Node.js 18 或更高版本。核心代码使用fetchNode 18 以上内置不需要额外安装 axios。npm 或 pnpm 任一包管理器。一个可用的模型接口。推荐直接使用 OpenAI 兼容接口远程或本地均可。如果不想申请远程 API Key可以本地安装 Ollama加载一个支持工具调用的小模型然后把接口地址指向本机。3.2 模型服务准备远程接口与本地接口的差异主要在地址和模型名。项目通过环境变量隔离切换成本很低。需要准备的变量如下# 模型接口地址本地 Ollama 通常为 http://127.0.0.1:11434/v1 AGENT_API_URLhttps://api.openai.com/v1 # API Key仅本地模型时随意填写 AGENT_API_KEYsk-your-key # 模型名称本地模型用实际加载的模型名 AGENT_MODELgpt-4o-mini如果你的模型服务在别的机器把AGENT_API_URL改成对应地址即可。端口、路径、模型名都要以你实际使用的服务为准。4. TypeScript 项目初始化与目录结构新建一个项目目录例如agent-lab然后在目录内初始化 npm 项目。mkdir agent-lab cd agent-lab npm init -y安装开发依赖npm install -D typescript tsx types/node这里解释一下三个依赖的用途typescript负责类型检查和编译tsx让你可以直接运行 TS 文件不需要先编译再执行开发调试效率高很多types/node提供 Node 内置模块的类型声明。4.1 tsconfig 配置新建tsconfig.json内容如下{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, esModuleInterop: true, skipLibCheck: true, noEmit: true }, include: [src] }两个关键点module和moduleResolution使用NodeNext配合 package.json 中的type: module可以让项目以 ESM 方式运行。新版 TypeScript 对baseUrl已经有弃用提示这个配置故意不设置baseUrl避免 7.0 版本出现编译告警同时也强制你写相对路径时带上.js后缀这在 NodeNext 模式下是必要的。4.2 package.json 脚本修改package.json增加启动脚本{ name: agent-lab, version: 0.1.0, private: true, type: module, scripts: { dev: tsx src/index.ts, server: tsx src/server.ts, batch: tsx src/batch.ts, typecheck: tsc --noEmit } }4.3 目录结构agent-lab/ ├── .env.example ├── package.json ├── tsconfig.json └── src/ ├── types.ts ├── tools.ts ├── agent.ts ├── index.ts ├── server.ts └── batch.ts.env.example用来保存环境变量模板实际使用复制成.env再填充。注意这个项目没有引入 dotenv因此读取环境变量有几种方式直接在 shell 中 export或者用 Node 的--env-file参数。Node 20 以上可以直接这样运行node --env-file.env --import tsx src/index.ts如果你用的 Node 18可以在启动命令前手动 export或者安装 dotenv。下面统一按 shell 环境变量方式来演示这样最简单。5. Agent 核心代码实现这一节是整个项目的重心。我会按模块拆分代码最后汇总成一个可运行的 Agent。5.1 类型定义新建src/types.tsexport type Role system | user | assistant | tool; export interface ToolCall { id: string; type: function; function: { name: string; arguments: string; }; } export interface ChatMessage { role: Role; content: string; tool_call_id?: string; tool_calls?: ToolCall[]; } export interface ToolDef { name: string; description: string; parameters: Recordstring, unknown; execute: (args: Recordstring, unknown) Promisestring; } export interface AgentEvent { type: message | tool_start | tool_end | model_end | error; detail: Recordstring, unknown; }这几个类型定义了 Agent 的基础协议消息、工具调用、工具定义、事件。后续所有模块都依赖这套类型保证类型安全。5.2 工具注册表新建src/tools.ts定义两个简单工具一个用于加法计算一个用于获取当前时间import type { ToolDef } from ./types.js; export function createTools(): ToolDef[] { return [ { name: add, description: 把两个数字相加并返回结果, parameters: { type: object, properties: { a: { type: number, description: 第一个数字 }, b: { type: number, description: 第二个数字 } }, required: [a, b] }, async execute(args) { const a Number(args.a); const b Number(args.b); if (Number.isNaN(a) || Number.isNaN(b)) { return JSON.stringify({ error: 参数必须是数字 }); } return JSON.stringify({ result: a b }); } }, { name: get_current_time, description: 获取当前时间返回 ISO 格式字符串, parameters: { type: object, properties: {} }, async execute() { return JSON.stringify({ now: new Date().toISOString() }); } } ]; }这里的execute返回字符串是因为工具结果最终要作为tool角色的消息内容传回给模型字符串是最通用的序列化格式。生产环境建议统一返回 JSON 字符串并带error字段方便模型识别失败状态。5.3 Agent 核心循环新建src/agent.ts这是核心文件import type { AgentEvent, ChatMessage, ToolCall, ToolDef } from ./types.js; const API_URL process.env.AGENT_API_URL ?? https://api.openai.com/v1; export class Agent { private tools: Mapstring, ToolDef; private events: AgentEvent[] []; private listeners: Array(event: AgentEvent) void []; private maxIterations 8; constructor( private model: string, tools: ToolDef[] ) { this.tools new Map(tools.map((tool) [tool.name, tool])); } onEvent(listener: (event: AgentEvent) void) { this.listeners.push(listener); } private emit(type: AgentEvent[type], detail: AgentEvent[detail]) { const event: AgentEvent { type, detail }; this.events.push(event); this.listeners.forEach((listener) listener(event)); } async run(userInput: string) { const messages: ChatMessage[] [ { role: system, content: 你是一个简洁高效的中文 AI Agent需要时调用工具获取信息。 }, { role: user, content: userInput } ]; for (let i 0; i this.maxIterations; i) { const reply await this.chat(messages); this.emit(model_end, { content: reply.content, toolCount: reply.tool_calls?.length ?? 0, iteration: i }); messages.push(reply); if (!reply.tool_calls || reply.tool_calls.length 0) { return reply.content; } for (const call of reply.tool_calls) { const tool this.tools.get(call.function.name); if (!tool) { messages.push({ role: tool, tool_call_id: call.id, content: JSON.stringify({ error: 工具不存在: ${call.function.name} }) }); continue; } this.emit(tool_start, { name: call.function.name, args: call.function.arguments }); let result: string; try { const args JSON.parse(call.function.arguments) as Recordstring, unknown; result await tool.execute(args); } catch (err) { result JSON.stringify({ error: (err as Error).message }); } this.emit(tool_end, { name: call.function.name, result }); messages.push({ role: tool, tool_call_id: call.id, content: result }); } } throw new Error(Agent 超过最大迭代次数 ${this.maxIterations}); } private async chat(messages: ChatMessage[]) { const apiKey process.env.AGENT_API_KEY; if (!apiKey) { throw new Error(缺少 AGENT_API_KEY 环境变量); } const toolDefs [...this.tools.values()].map((tool) ({ type: function, function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); const response await fetch(${API_URL}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: this.model, messages, tools: toolDefs }) }); if (!response.ok) { const text await response.text(); throw new Error(LLM API 请求失败: ${response.status} ${text}); } const data (await response.json()) as { choices: Array{ message: ChatMessage }; }; return data.choices[0].message; } }核心循环的逻辑很直接把 system 和 user 消息拼成初始 messages。调用模型接口。如果模型返回tool_calls逐个执行工具把结果作为tool角色消息追加到 messages。再次调用模型直到模型返回纯文本回答超过最大迭代次数则抛错。这个循环就是 AI Agent 最核心的 ReAct 思想PI-Agent 一类架构也是在这个基础上扩展了规划器、记忆、反思等模块。当前实现没有引入额外的规划器模型本身承担了推理和规划职责这也是最简单、最容易理解的一版。5.4 入口演示新建src/index.tsimport { Agent } from ./agent.js; import { createTools } from ./tools.js; const model process.env.AGENT_MODEL ?? gpt-4o-mini; const agent new Agent(model, createTools()); agent.onEvent((event) { if (event.type tool_start) { console.log([工具调用], event.detail.name, event.detail.args); } if (event.type model_end) { console.log([模型响应 ${event.detail.iteration}], event.detail.content); } }); const question process.argv[2] ?? 今天是几号100 加 23 等于多少; console.log([用户问题], question); try { const answer await agent.run(question); console.log([Agent 最终回答]); console.log(answer); } catch (err) { console.error([Agent 执行失败], err); process.exit(1); }运行方式export AGENT_API_URLhttps://api.openai.com/v1 export AGENT_API_KEYsk-your-key export AGENT_MODELgpt-4o-mini npm run dev如果模型支持工具调用你应该能在控制台看到先调用get_current_time或add工具然后输出最终回答。如果模型返回结果不理想可以调整 system 提示词把“需要时调用工具”改成更明确的指令。6. Agent 功能测试与效果验证部署完成后建议按以下维度做一轮功能验证逐项确认是否符合预期。6.1 基础问答测试输入一个不需要工具的问题例如npm run dev -- 你好用一句话介绍你自己预期结果Agent 直接输出文本控制台不出现[工具调用]日志。这个测试验证了基础模型链路是否通畅包括 API Key、模型名、消息格式。判断成功标准返回内容为普通文本无报错无tool_calls触发。6.2 工具调用测试输入一个必须调用工具的问题例如npm run dev -- 100 加 23 等于多少预期结果控制台先出现[工具调用] add再输出最终结果。说明 Agent 正确识别了用户意图并且把工具结果回传给模型生成了最终答案。常见失败情况是模型直接凭训练知识回答不调用工具。这时候先把工具的描述写清楚例如“把两个数字相加并返回结果”并在 system 提示词中加入“涉及计算时必须调用 add 工具”。6.3 多工具连续调用测试输入一个需要多个工具协作的问题例如npm run dev -- 当前时间是什么如果当前小时大于 12就说现在是下午否则说是上午。预期结果Agent 调用get_current_time然后基于返回结果判断上午或下午。这个测试验证了多轮消息拼接是否正确工具结果是否被模型正确理解。6.4 容错测试输入一个工具不存在或参数错误的情况例如npm run dev -- 调用一个不存在的工具试试预期结果Agent 不会崩溃。如果模型试图调用不存在的工具代码会返回“工具不存在”的 tool 消息模型会根据该消息重新生成回答。6.5 事件日志验证每次运行都会触发model_end、tool_start、tool_end事件。你可以检查每次模型返回是否记录 iteration。每次工具执行是否记录参数和结果。最终回答是否正常返回。事件机制的价值在于后续接日志系统、接 UI 展示、接监控告警时只需要订阅onEvent不需要侵入 Agent 核心代码。7. Agent API 服务与批量任务核心 Agent 跑通之后下一步就是工程化提供 HTTP API 和批量任务处理能力。7.1 HTTP API 服务新建src/server.ts用 Node 内置http模块创建一个轻量服务import { createServer } from node:http; import { Agent } from ./agent.js; import { createTools } from ./tools.js; const model process.env.AGENT_MODEL ?? gpt-4o-mini; const agent new Agent(model, createTools()); const server createServer(async (req, res) { if (req.method ! POST || req.url ! /api/agent) { res.writeHead(404, { Content-Type: application/json }); res.end(JSON.stringify({ error: Not Found })); return; } let body ; for await (const chunk of req) { body chunk; } try { const parsed body ? JSON.parse(body) : {}; const question typeof parsed.question string ? parsed.question : ; if (!question) { res.writeHead(400, { Content-Type: application/json }); res.end(JSON.stringify({ error: question 不能为空 })); return; } const answer await agent.run(question); res.writeHead(200, { Content-Type: application/json; charsetutf-8 }); res.end(JSON.stringify({ answer })); } catch (err) { res.writeHead(500, { Content-Type: application/json }); res.end(JSON.stringify({ error: (err as Error).message })); } }); const port Number(process.env.PORT ?? 8787); server.listen(port, () { console.log(Agent API 已启动: http://127.0.0.1:${port}); });启动服务node --env-file.env --import tsx src/server.ts或者提前 export 环境变量后npm run server调用接口测试curl -X POST http://127.0.0.1:8787/api/agent \ -H Content-Type: application/json \ -d {question:100 加 23 等于多少}预期返回{answer:123}这个接口可以继续扩展增加会话 ID、历史消息、工具名单、超时控制、频率限制。当前版本是无状态单次调用适合原型验证生产环境建议加一层任务队列和结果缓存。7.2 批量任务处理新建src/batch.ts读取一个文本文件每行一个任务输出到outputs目录import { readFile, writeFile, mkdir } from node:fs/promises; import { Agent } from ./agent.js; import { createTools } from ./tools.js; const INPUT_FILE ./inputs.txt; const OUTPUT_DIR ./outputs; const CONCURRENCY 2; const lines (await readFile(INPUT_FILE, utf-8)) .split(\n) .map((line) line.trim()) .filter(Boolean); await mkdir(OUTPUT_DIR, { recursive: true }); const model process.env.AGENT_MODEL ?? gpt-4o-mini; const agent new Agent(model, createTools()); const results: Array{ id: number; input: string; status: string; answer: string; error?: string; } []; async function runTask(id: number, input: string) { try { const answer await agent.run(input); results.push({ id, input, status: ok, answer }); await writeFile(${OUTPUT_DIR}/result-${id}.txt, answer, utf-8); } catch (err) { results.push({ id, input, status: error, answer: , error: (err as Error).message }); await writeFile(${OUTPUT_DIR}/error-${id}.txt, (err as Error).message, utf-8); } } const pending lines.map((line, index) { const delay (index % CONCURRENCY) * 500; return new Promise((resolve) setTimeout(resolve, delay)).then(() runTask(index, line) ); }); await Promise.all(pending); console.table(results); console.log(批量任务完成共 ${results.length} 条输出目录 ${OUTPUT_DIR});准备inputs.txt今天是几号 123 乘 4 等于多少 10 加 20 加 30 等于多少运行批量任务npm run batch输出目录会生成result-0.txt、result-1.txt等文件控制台会打印每条任务的状态和结果。这里采用最简单的“延迟错峰 Promise.all”来限制并发只能保证不会瞬间把接口打爆。生产环境建议使用 p-limit、BullMQ 或者云上的消息队列来管理任务并发、重试和死信队列。7.3 失败重试建议批量任务最容易遇到的问题是网络超时和模型接口限流。建议在 Agent 的chat方法中增加重试逻辑遇到 429、5xx、超时错误时退避 1 到 3 秒重试一次单条任务最多重试 2 次。不要无限制重试避免拖垮整个任务队列。8. 资源占用与性能观察这个项目本身不跑模型推理因此不存在 GPU 显存压力重点观察的是请求耗时、token 消耗和 Node 进程内存。8.1 显存与 CPU 说明如果你把AGENT_API_URL指向本地 Ollama 服务那么模型推理进程由 Ollama 管理显存占用由 Ollama 和模型决定。你可以用nvidia-smi观察 Ollama 或 llama.cpp 进程的显存占用和这个 TypeScript Agent 进程没有直接关系。如果你的场景全部走远程模型接口那么本机只需要很低的 CPU 和内存资源核心瓶颈在模型接口的响应速度和调用频率限制。8.2 可观测指标建议在部署后重点观察以下指标单次任务耗时从用户输入到最终回答的完整时长。模型调用次数普通问题通常 1 次需要工具时 2 到 3 次。Token 消耗每次调用 messages 都会不断累积历史任务越长消耗越高。Node 进程内存批量任务时观察是否持续增长。工具执行耗时区分工具本身慢还是模型慢。可以在agent.ts中追加一个简单的耗时统计例如在run方法开头记录Date.now()结束时打印总耗时。8.3 降低消耗的手段控制历史消息数量对于长流程任务只保留最后 N 轮消息防止 token 膨胀。减少工具数量传给模型的工具越多模型选择成本越高按场景动态裁剪工具列表。使用较小模型基础问答用小模型复杂推理用大模型做模型路由。缓存重复结果相同输入在短时间内直接命中缓存。设置最大迭代次数防止模型陷入死循环当前实现默认 8 次可以根据场景调低。9. AI Agent 常见问题与排查方法问题现象可能原因排查方式解决方案启动后报缺少 AGENT_API_KEY环境变量未设置echo $AGENT_API_KEY检查环境变量export 环境变量或使用--env-file接口返回 401API Key 无效或模型服务不识别查看响应错误信息检查 Key 和模型服务要求模型不调用工具工具描述不够清晰、模型不支持工具打印请求体的 tools 字段确认模型支持 tool calling优化工具描述换支持 Tool Call 的模型Agent 无限循环模型反复调用相同工具查看事件日志确认反复调用哪个工具设置最大迭代次数检查工具参数是否合理工具参数解析失败JSON.parse 报错查看 tool_start 事件中的 args在工具执行前捕获异常并返回 error 消息NodeNext 模块找不到文件导入路径缺少.js后缀检查报错路径ESM 下导入本地文件必须带.jsTypeScript 7.0 baseUrl 弃用告警旧配置写了 baseUrl查看 tsconfig删除 baseUrl使用相对路径批量任务大面积超时并发过高触发接口限流查看任务文件和接口返回状态码降低并发数增加延时和重试消息历史过长导致请求超时多轮工具调用累积大量消息查看单次请求的 messages 长度裁剪历史消息限制迭代次数API 服务无响应服务未启动或端口被占用lsof -i:8787查看端口换端口或停止占用进程还有几个容易踩的坑值得单独说。第一工具调用后的消息必须带tool_call_id。如果没有这个字段部分模型接口会直接报错报错信息通常是“message with role tool must have a tool_call_id”。代码里已经正确拼接但你自己扩展多 Agent 或额外消息处理时要注意。第二不要把模型返回的工具执行结果直接透传给用户。工具返回的是结构化数据最终回答应该由模型基于工具结果生成而不是把 JSON 原样输出。第三不同的模型服务对tools字段的兼容程度不同。有些开源模型的工具调用格式不完全兼容 OpenAI 协议需要根据实际响应调整类型定义和解析逻辑。10. 企业级 Agent 设计最佳实践10.1 配置管理不要把 API Key 写死在代码里也不要把模型名散落在各处。统一放在环境变量并准备.env.example模板。进入生产环境后用配置中心或密钥管理服务替代环境变量。10.2 工具注册与参数校验工具是 Agent 能力的边界。每个工具必须做到入参校验不能直接信任模型传来的参数。超时控制工具执行不能无限等待。错误返回失败时要返回明确的 error 信息而不是抛异常中断整个循环。白名单策略涉及命令执行、文件读写、网络请求的工具要限制作用域。10.3 日志与事件监控当前实现的事件回调机制已经为日志系统留好了口子。生产环境建议把AgentEvent输出到结构化日志包含任务 ID、模型调用次数、工具执行耗时、token 消耗。这样出现问题时可以快速回放整个执行链路。10.4 数据安全与合规如果 Agent 会接触用户数据必须注意隐私数据脱敏后再发送给模型服务。对模型返回内容做敏感信息过滤。明确告知用户当前 Agent 是自动化生成内容不代表人工审核结果。涉及人脸、声音、版权图文素材时必须先确认授权链条完整。10.5 测试策略Agent 应用比传统后端更不稳定因为模型输出有随机性。建议维护一组回归测试用例每个用例提前写好“必须调用工具”“必须包含某个关键词”等判定规则每次修改提示词或工具后批量跑一遍避免只改一个工具结果把其他任务带崩。10.6 保持最小可运行配置项目里应该保留一套永远能跑通的最小配置最小模型、最少工具、最短提示词。任何新功能都基于这套配置扩展出现问题时能快速回退定位。11. 总结与下一步这个项目最值得尝试的地方是用极少的代码把 AI Agent 从“概念”变成“可运行的程序”。你不需要先搭一个庞大的框架只需要理解消息、工具、循环这三件事就能复刻出类 PI-Agent 的通用 Agent 架构。建议第一次上手时先验证两件事模型能不能按预期调用工具工具结果能不能正确回传给模型。这两个链路通了后面加记忆、加规划器、加多 Agent 协作都是增量开发。最容易踩的坑也集中在这两处要么模型不支持工具调用要么消息拼接时丢了tool_call_id。只要输出日志做得足够细这两个问题都能快速定位。后续可以继续扩展的方向给 Agent 加短期记忆和长期记忆支持多轮会话把工具执行改成可插拔的 Worker 模式用 Redis 做任务队列引入反思机制让 Agent 在给出最终答案前先检查自己的推理结果或者接一个前端界面把事件日志和工具调用过程可视化。这套骨架虽然简单但扩展点已经留好了接下来怎么长完全取决于你要解决的问题。
返回列表