TypeScript 手写通用 Agent 框架:100 行核心代码实现工具调用与循环决策
最近 TypeScript 和 AI Agent 的组合热度一直很高。这次我们来看一个适合实战的选题用 TypeScript 手搓一个通用 Agent 框架原型用大约 100 行核心代码复刻类 PI-Agent 的架构思路把 Agent 设计里的核心概念一次性讲清楚。这类项目的重点不是模型本身有多强而是你能不能自己实现一套 大模型 工具调用 记忆管理 循环决策 的骨架代码。理解了这套骨架后面读 LangChain、读 OpenAI Function Calling、读各类企业级 Agent 框架都会轻松很多。我之前在评论区看到不少同学问AI Agent 到底怎么从零搭建Agent 组成 到底是什么学习路线怎么规划这篇文章就直接用 TypeScript 代码回答这些问题。本文会带大家完成以下内容搭建一个最小的 TypeScript 项目、定义 LLM Provider 接口、实现工具注册与调用、实现 Agent 主循环也就是通常说的 ReAct 循环最后跑通一个可用的问答 工具调用示例。同时会补充批量任务、API 接口扩展、Token 与性能观察、常见问题排查。适合的人群是有一定 TypeScript 基础、想理解 Agent 底层原理、想在业务里快速集成 Agent 能力的开发者。整个项目不需要 GPU不需要装 CUDA不涉及本地大模型部署只要电脑能装 Node.js 就能跑。它能让你看到 Agent 的完整运作过程也方便你在此基础上继续加功能。1. 核心能力速览先给一张速览表把你最关心的信息列清楚。能力项说明项目类型TypeScript 编写的通用 Agent 框架原型核心功能多轮对话、工具注册、工具调用、LLM 接入抽象、上下文记忆管理编程语言TypeScriptNode.js 环境硬件要求无需 GPU能运行 Node.js 即可额外依赖建议使用 pnpm 或 npm 管理依赖LLM 支持可接入任意 OpenAI 兼容接口也可以替换为本地模型服务启动方式npm scripts 启动命令行交互或脚本调用是否支持 API可通过 Express 或 Fastify 包装为 HTTP API是否支持批量任务可以核心 Agent 类是无状态可复用的循环调用即可适合场景教学演示、企业内部工具集成、Agent 原型验证从这张表能看出来这个项目不依赖特定硬件核心价值在代码框架本身。你可以把它当作一个 Agent 引擎的最小实现在上面继续叠加自己的业务能力。2. 适用场景与使用边界先说适合谁。如果你是 Agent 初学者这个项目能帮你理解 AI Agent 组成。很多人第一次看 Agent 框架时会看到一堆抽象概念Planner、Executor、Memory、Tool、Callback。但在最简单的实现里Agent 本质上就干三件事接收用户输入、决定是否需要工具、把工具结果反馈给模型继续生成。用 TypeScript 手写一遍这些概念会变得非常具体。如果你是业务开发者这个项目可以作为企业内部工具调用的最小骨架。比如你的系统里已经有内部 API想通过自然语言让模型调用这些 API就可以用这套代码快速做验证。你只需要把工具函数替换成真实的业务接口调用再配置好 LLM 的 API Key 即可。如果你在做 Node.js 服务端开发这个项目还能展示如何把 Agent 封装成 HTTP API便于前端或其他服务调用。再说使用边界。这个项目是一个框架原型不是生产级 Agent 平台。它默认没有做用户权限管理、没有做接口限流、没有做并发队列、没有持久化存储也没有对模型输出的严格校验。也就是说你把它用在演示和验证阶段没有问题但直接上生产还需要补很多工程细节。后面的章节我会给出补全建议。安全合规方面需要特别注意如果你接入的是 OpenAI 等外部大模型 API用户输入会发送到第三方服务涉及敏感数据时要做好脱敏或使用私有化部署模型。如果 Agent 要调用企业内部接口必须对模型可以触发的工具做白名单控制避免让模型自由调用高风险操作比如删除数据、修改权限、转账支付等。工具执行前建议加入人工确认环节尤其是涉及写操作的时候。3. 环境准备与前置条件在开始写代码之前先检查一下本机环境。这个项目只需要 Node.js 和 TypeScript如果你之前装过 Node.js 18 以上版本基本可以跳过很多麻烦。完整的准备清单如下。操作系统Windows、macOS、Linux 都可以。Node.js推荐 18.0.0 以上版本20.x 或 22.x 更好。可以用node -v检查。包管理器推荐 pnpm当然 npm 也可以。TypeScript可以通过pnpm add -D typescript安装到本地。LLM API Key准备一个 OpenAI 兼容接口的 API Key。如果你想完全本地化测试也可以使用 Ollama 或其他本地模型服务只要它们暴露兼容接口。现在开始创建项目。mkdir ts-agent cd ts-agent pnpm init然后安装依赖。这里只需要 TypeScript 和tsx用于直接运行 TS 文件如果你要接口调用还需要openai要包 HTTP API 还需要express或fastify。pnpm add -D typescript tsx pnpm add openai express初始化 TypeScript 配置npx tsc --init如果你使用的是较新的 TypeScript 版本可能会在tsconfig.json里看到baseUrl被标记为废弃的提示。这个提示不影响功能TS 6.x 中baseUrl仍可运行只是官方建议后续配置逐步迁移到paths的相对解析方式。我们这个小项目用不到baseUrl保持默认即可。建议在package.json里加上两个脚本方便后续启动{ scripts: { dev: tsx src/index.ts, build: tsc } }到这里环境准备就完成了。4. Agent 核心架构设计在动手写代码之前先想清楚 Agent 的核心架构。网上搜 AI Agent 完整架构 能看到很多复杂图形但拆开来看最小可用的 Agent 通常包含五个部分。第一是 LLM Provider。它负责与大模型对话接收消息数组返回文本。这一层要做成接口这样你既可以用 OpenAI也可以切换成国内大模型甚至本地模型。第二是 Tool。工具是 Agent 能力的延伸。模型本身不能查数据库、不能调用内部接口、不能执行计算但通过工具注册Agent 就能完成这些动作。工具通常包含名字、描述、参数定义和执行函数。第三是 Memory。在简单实现里Memory 就是消息数组。每轮对话产生的用户消息、模型回复、工具调用结果都追加到消息数组里作为下一轮的上下文。更复杂的 Agent 会做摘要压缩、向量检索但最小实现不需要。第四是 Agent Loop也就是常说的 ReAct 循环。流程是接收用户输入追加到消息数组调用 LLM看模型的输出是普通回答还是工具调用 JSON如果是工具调用执行工具把结果写入消息数组回到上一步继续调用 LLM如果是普通回答直接返回给用户。循环要设置最大迭代次数防止模型陷入死循环。第五是 Prompt 模板。Agent 和普通 Chat 的区别就在于系统提示词里描述了工具列表和输出格式。模型需要知道有哪些工具可以用什么时候需要用以及工具调用的 JSON 格式是什么样的。这五个部分是一个企业级 Agent 体系的最小闭环。理解了它们再看 LangChain 这类框架时你会发现很多概念都是基于此扩展出来的比如增加 Memory 抽象、增加 Agent Executor、增加 Callback、增加多 Agent 协作。下面我们用代码实现这个最小闭环。5. 100 行核心代码实现第一部分LLM Provider 接口。所有大模型能力都收敛到这个接口后面。// src/llm.ts export interface ChatMessage { role: system | user | assistant | tool; content: string; name?: string; } export interface LLMProvider { chat(messages: ChatMessage[]): Promisestring; }第二部分工具定义。这里使用一个通用接口兼容各种工具函数。// src/tool.ts export interface AgentTool { name: string; description: string; parameters: Recordstring, unknown; execute(args: Recordstring, unknown): Promisestring; }第三部分Agent 主类。这是整个项目的核心代码控制在 100 行左右。// src/agent.ts import { ChatMessage, LLMProvider } from ./llm; import { AgentTool } from ./tool; export class Agent { private messages: ChatMessage[] []; constructor( private llm: LLMProvider, private tools: AgentTool[] [], private systemPrompt You are a helpful assistant., private maxIterations 5 ) {} private buildSystemPrompt(): string { const toolDescriptions this.tools .map((t) - ${t.name}: ${t.description} (参数: ${JSON.stringify(t.parameters)})) .join(\n); return ${this.systemPrompt} 可调用工具: ${toolDescriptions || - 无} 当需要使用工具时必须严格输出以下 JSON 格式不要包含其他内容: {tool: 工具名, args: {}} 不需要调用工具时直接输出普通文本回答。; } async run(userInput: string): Promisestring { this.messages.push({ role: user, content: userInput }); for (let i 0; i this.maxIterations; i) { const response await this.llm.chat([ { role: system, content: this.buildSystemPrompt() }, ...this.messages, ]); const parsed this.parseToolCall(response); if (!parsed) { this.messages.push({ role: assistant, content: response }); return response; } this.messages.push({ role: assistant, content: response }); const tool this.tools.find((t) t.name parsed.tool); if (!tool) { this.messages.push({ role: tool, name: parsed.tool, content: 工具 ${parsed.tool} 不存在请尝试其他工具, }); continue; } const result await tool.execute(parsed.args); this.messages.push({ role: tool, name: tool.name, content: result, }); } return 已达到最大迭代次数请简化问题或检查工具输出。; } private parseToolCall(text: string): { tool: string; args: Recordstring, unknown } | null { const match text.match(/\{[\s\S]*\}/); if (!match) return null; try { const parsed JSON.parse(match[0]); if (parsed parsed.tool) { return { tool: parsed.tool, args: parsed.args || {} }; } } catch { // 输出不是合法 JSON按普通回答处理 } return null; } }第四部分OpenAI Provider 实现。这里使用 OpenAI 官方的 Node SDK只需要传入 API Key 和模型名。// src/openai-provider.ts import OpenAI from openai; import { ChatMessage, LLMProvider } from ./llm; export class OpenAIProvider implements LLMProvider { private client: OpenAI; constructor(apiKey: string, private model gpt-4o-mini) { this.client new OpenAI({ apiKey }); } async chat(messages: ChatMessage[]): Promisestring { const completion await this.client.chat.completions.create({ model: this.model, messages: messages.map((m) ({ role: m.role, content: m.content, })), }); return completion.choices[0].message.content ?? ; } }第五部分入口文件。注册两个示例工具比如查时间和计算器然后开始交互。// src/index.ts import { Agent } from ./agent; import { OpenAIProvider } from ./openai-provider; import { AgentTool } from ./tool; const apiKey process.env.OPENAI_API_KEY || ; const llm new OpenAIProvider(apiKey, gpt-4o-mini); const tools: AgentTool[] [ { name: get_current_time, description: 获取当前时间, parameters: {}, execute: async () { return new Date().toLocaleString(); }, }, { name: calculator, description: 执行数学计算传入表达式即可, parameters: { expression: string }, execute: async (args) { const expression String(args.expression || ); try { // 注意真实环境建议使用安全的表达式解析库 const result Function(use strict; return (${expression}))(); return String(result); } catch (e) { return 计算失败: ${e}; } }, }, ]; const agent new Agent(llm, tools, 你是一个会调用工具的智能助手。); const userInput process.argv[2] || 现在几点了; const answer await agent.run(userInput); console.log(answer);启动方式很简单OPENAI_API_KEYsk-xxx pnpm dev 现在几点了如果你用的是 Windows PowerShell设置环境变量可以这样$env:OPENAI_API_KEYsk-xxx pnpm dev 现在几点了如果一切正常你会看到模型输出类似这样它会识别到需要调用get_current_time这个工具Agent 循环执行工具拿到时间结果再把结果组织成自然语言回答给你。这里要注意一个细节虽然 Agent 代码核心只有 100 行左右但它已经具备了一个生产级 Agent 框架的完整链路。后续你要加日志、加 token 统计、加记忆持久化都是在这些接口上扩展。6. 功能测试与效果验证写完代码后建议按下面的顺序做功能验证。从基础对话开始逐个确认每个环节是否正常。6.1 基础问答测试先用一个不需要工具的问题测试链路是否走通。pnpm dev 用一句话介绍你自己预期结果是控制台直接输出模型的回答不会触发任何工具调用。判断标准输出内容正常无报错Agent 没有进入工具调用分支。这个测试看起来简单但能帮你排查 API Key、网络连通性、参数传递等问题。6.2 工具调用测试然后测试工具触发。pnpm dev 现在几点了预期结果是先看到模型输出一个 JSON 工具调用然后 Agent 执行get_current_time工具最后模型根据工具结果生成自然语言回答。如果模型没有触发工具调用可能的原因是系统提示词不清晰、模型版本能力较弱或者工具描述与问题匹配度不高。这里有个调优技巧给工具写描述时尽量写清楚 什么时候该用。比如get_current_time的描述可以改成 当用户询问当前时间、日期时使用这样模型更容易做出正确决策。6.3 多轮对话与记忆测试Agent 类是同一个实例消息数组会随对话历史累积。要实现多轮对话可以在入口写一个循环持续读取用户输入。// src/chat-loop.ts import readline from readline; import { Agent } from ./agent; import { OpenAIProvider } from ./openai-provider; import { AgentTool } from ./tool; import { getCurrentTimeTool, calculatorTool } from ./tools; const rl readline.createInterface({ input: process.stdin, output: process.stdout, }); const llm new OpenAIProvider(process.env.OPENAI_API_KEY || ); const agent new Agent(llm, [getCurrentTimeTool, calculatorTool]); async function loop() { rl.question(你: , async (input) { if (input exit) { rl.close(); return; } const answer await agent.run(input); console.log(Agent:, answer); loop(); }); } loop();测试时可以这样验证记忆是否生效输入 记住我的名字叫张三。输入 我叫什么名字如果 Agent 能正确回答 张三说明多轮上下文累积正常。在实际开发中记忆不能无限增长。当消息数组超过一定长度后需要做截断或摘要。否则随着对话轮次增加Token 消耗会直线上升最终超出模型上下文窗口。6.4 最大迭代次数测试为了验证 Agent 不会死循环可以设计一个输出不规范的工具。比如让模型在一个工具调用之后再次输出工具调用并一直循环。实际使用时maxIterations默认值 5 足够大多数场景使用。你可以故意注册一个返回结果仍然触发工具的测试工具观察 Agent 是否会在 5 轮之后终止并返回提示。如果确实终止了说明循环保护正常。6.5 失败场景测试测试一个不存在的问题场景故意让模型说出一个未注册的工具名比如 调用 abc_tool 帮我做某事。由于模型很可能会拒绝执行真正要测试的是当工具名不存在时Agent 是否会把错误信息回传给模型并让模型尝试其他方案。预期结果是 Agent 给模型返回 工具 abc_tool 不存在然后模型可能主动道歉或换一种回答方式。这个环节能确认错误回传链路是否完整。7. 接口 API 与批量任务命令行交互只是最基础的演示。实际使用中你通常需要把 Agent 包装成 HTTP API或者用脚本执行批量任务。这一节分别说明。7.1 HTTP API 封装使用 Express 快速封装一个/api/agent接口。// src/server.ts import express from express; import { Agent } from ./agent; import { OpenAIProvider } from ./openai-provider; import { getCurrentTimeTool, calculatorTool } from ./tools; const app express(); app.use(express.json()); const llm new OpenAIProvider(process.env.OPENAI_API_KEY || ); const agent new Agent(llm, [getCurrentTimeTool, calculatorTool]); app.post(/api/agent, async (req, res) { const input req.body.input; if (!input || typeof input ! string) { res.status(400).json({ error: input is required }); return; } const answer await agent.run(input); res.json({ answer }); }); app.listen(3000, () { console.log(Agent server running on http://localhost:3000); });调用示例curl -X POST http://localhost:3000/api/agent \ -H Content-Type: application/json \ -d {input: 现在几点了}Python 调用示例import requests url http://localhost:3000/api/agent payload {input: 现在几点了} response requests.post(url, jsonpayload, timeout60) print(response.json())包成 HTTP API 之后前端可以接、脚本可以调企业内部服务之间也能通过这个接口复用 Agent 能力。7.2 批量任务批量任务的核心思路是为每个任务创建独立的 Agent 实例或者共享一个 LLM Provider 但重置消息数组。因为 Agent 类的消息数组是实例属性新实例天然隔离上下文适合批量处理。这里给一个批量处理的简单示例// src/batch.ts import { Agent } from ./agent; import { OpenAIProvider } from ./openai-provider; import { getCurrentTimeTool, calculatorTool } from ./tools; const llm new OpenAIProvider(process.env.OPENAI_API_KEY || ); const tasks [ 帮我查一下今天是几号, 计算 12345 * 6789, 写一句话介绍量子计算, ]; async function runBatch() { for (const task of tasks) { const agent new Agent(llm, [getCurrentTimeTool, calculatorTool], 你是一个会调用工具的智能助手。); const result await agent.run(task); console.log(任务: ${task}); console.log(结果: ${result}); console.log(---); } } runBatch();批量任务的注意点每个任务使用独立 Agent 实例避免上下文污染。记录每个任务的 Token 消耗和耗时方便后续统计分析。如果任务量大要控制并发数避免触发上游 API 限流。可以用 p-limit 这类库实现并发控制。失败任务要单独记录并支持重试。7.3 请求参数扩展如果多个任务需要不同的系统提示词可以在创建 Agent 时传入不同的systemPrompt。这比在用户输入里反复强调规则更稳定。const agentA new Agent(llm, tools, 你是一个擅长写代码的助手。); const agentB new Agent(llm, tools, 你是一个擅长总结文档的助手。);这样的设计天然支持在不同业务场景下复用同一套 Agent 引擎。8. 资源占用与性能观察这个项目不涉及 GPU但依然有性能指标需要关注。第一是 Token 消耗。每轮工具调用都会带来至少一次 LLM 请求消息数组越长每次请求的 Token 数越多。比如一个工具调用的完整流程通常会有三次请求第一次模型决定调工具第二次工具把结果发回模型第三次模型生成最终回答。如果一次循环内有多个工具调用Token 消耗会翻倍增长。观察 Token 消耗最简单的方法是在 OpenAIProvider 里打印每次请求的 usage 字段// openai-provider.ts 中增加打印 const usage completion.usage; if (usage) { console.log(prompt_tokens${usage.prompt_tokens} completion_tokens${usage.completion_tokens} total${usage.total_tokens}); }第二是响应延迟。延迟主要取决于 LLM API 的响应速度、工具本身的执行时间、以及循环次数。如果你发现 Agent 响应很慢优先检查是否出现了不必要的工具调用是否在循环里重复调用了同一个工具。第三是上下文长度。系统提示词里的工具描述会占用不少 Token。工具数量越多描述越长每次请求的固定开销越大。如果工具超过 10 个建议只放当前场景需要的工具不要把全量工具都丢给模型。第四是内存占用。TypeScript 实现的 Agent 本身内存占用很低主要瓶颈在消息数组和工具结果。如果工具返回超长文本注意不要无脑写入消息数组可以做截断。优化建议工具描述要精简减少系统提示词长度。工具结果只保留关键信息必要时截断。多轮对话中旧消息可以定期做摘要压缩。批量任务要控制并发避免上游 API 限流。这些优化不需要改架构只需要在接口实现层增加策略即可。9. 常见问题与排查方法直接给排查表格方便遇到问题时快速定位。问题现象可能原因排查方式解决方案启动后提示找不到模块依赖未安装或 Node 版本过低执行 pnpm install检查 node -v安装依赖升级 Node.js 到 18API 返回 401API Key 错误或未设置检查环境变量检查 Key 是否有效重新配置 OPENAI_API_KEY模型一直不调用工具提示词不够清晰或模型版本弱单独测试模型对工具描述的响应优化工具描述加 当用户询问...时使用工具调用格式解析失败模型输出不是合法 JSON打印原始模型输出优化提示词强制输出 JSONAgent 死循环工具结果反复触发同一工具限制 maxIterations打印每次循环日志增加最大轮数保护工具结果标注已有最终答案响应很慢工具调用次数多或 LLM 响应慢打印每轮耗时精简工具减少循环次数使用更快的小模型API 服务内存持续增长每个请求都创建 Agent 且未清理观察 Heap Used 变化每次请求使用新 Agent请求结束及时释放引用TypeScript 编译报 baseUrl 废弃使用 TS 6.x 且配置了 baseUrl查看编译日志移除 baseUrl改用相对路径或 project references在实际测试中最常遇到的是提示词问题。模型不调工具时先不要怀疑代码要怀疑模型有没有理解工具描述。可以单独打印buildSystemPrompt()的输出拼到模型对话里看一眼效果。还有一个容易被忽略的问题Function构造器执行表达式。如果你把calculator工具直接用Function执行用户提供的表达式在真实环境存在安全风险。演示可以用生产环境建议使用mathjs这类安全的表达式解析库并禁止执行任意代码。10. 最佳实践与使用建议把这套代码用于实际项目时建议按下面的工程化思路补充。10.1 保持 Agent 接口稳定把LLMProvider和AgentTool定义为项目内部的核心接口所有功能模块都依赖接口而不是具体实现。这样换模型、加工具时不会改动 Agent 主循环。10.2 工具要收敛为受控清单不要把所有业务函数直接注册为工具。工具是模型可以主动调用的能力注册范围越大风险面越大。对内部系统来说建议只注册只读查询类工具和低风险工具。高风险操作要做权限校验和人工确认。10.3 打日志与可观测性Agent 的循环过程要打详细日志// 在 agent.ts 的循环里增加日志 console.log([Agent] 第 ${i 1} 轮模型输出: ${response}); console.log([Agent] 解析到工具调用: ${JSON.stringify(parsed)});这些日志在排查问题时非常关键。在线环境可以接结构化日志记录每轮的消息、工具名、耗时、Token 消耗。10.4 记忆管理要早做最小实现用的是无限增长的消息数组。业务中建议设置上下文窗口长度超出后把最早的消息摘要化或者直接丢弃。也可以按会话维度做持久化方便用户下次继续对话。10.5 API 服务要加访问控制如果你把 Agent 暴露为 HTTP API至少需要做到限制内网访问、加 API Key 认证、增加请求频率限制、记录调用日志。否则非法调用会消耗大量 Token。10.6 合规与授权提醒接入真实大模型 API 或本地模型时要关注模型输出内容的合规性。Agent 调用企业内外部工具时确保有合法的数据访问授权。涉及用户个人信息、商业敏感数据时提前评估数据出境和存储要求。不要用这个框架生成违规内容也不要在生产环境中放任模型自主执行高风险操作。10.7 先跑通再扩展第一版代码保持最小可用跑通一个工具、一个模型、一个接口。之后再逐步加 LangChain 式的高级功能向量记忆、多 Agent 协作、事件回调、流式输出。好处是每个阶段都有可运行的结果排查问题范围也小。11. 总结与下一步这个 TypeScript 手搓 Agent 项目最值得尝试的地方是它让你真正理解了一个 Agent 的运作机制。100 行核心代码跑通了 用户输入 - 模型决策 - 工具调用 - 结果回填 - 最终回答 的完整链路。无论你之前是否接触过 LangChain、AutoGPT、MetaGPT 或者 PI-Agent 这类项目理解了这套最小骨架以后再去看它们的源码和架构图会觉得清晰很多。建议你先验证三件事一是基础问答链路二是工具调用是否命中三是多轮对话记忆是否正常。最容易踩的坑是模型不按预定的 JSON 格式输出以及工具描述写得不够明确。前者靠优化提示词和解析兜底解决后者靠把工具描述改具体解决。下一步可以考虑这几个扩展方向给 Agent 增加流式输出让用户体验更接近打字机效果接入向量数据库做长期记忆引入多个 Agent 角色让它们分别负责规划、执行和审校增加可视化界面把每一轮模型决策过程显示出来。TypeScript 在 Agent 领域的一个明显优势是静态类型让工具参数、消息结构在编译期就能发现问题前后端还能共用一套类型定义这套代码完全可以继续长成企业内部实用的 Agent 底座。建议先收藏然后照着跑一遍核心示例再根据你自己的业务场景去扩展工具列表和提示词。

相关新闻

最新新闻

日新闻

周新闻

月新闻