
最近经常有开发者在社区里问同一个问题Agent 框架这么多我到底该选哪一个LangChain 抽象复杂CrewAI 概念多自己从零写又怕掉进无底洞。其实换个角度想如果只保留一个 Agent 最核心的骨架它到底需要多少行代码我的判断是在 TypeScript 里100 行左右足够了。这不是一句口号而是本文要完整演示的事实。本文会带你从一个最朴素的 ReAct 循环出发用 TypeScript 实现一个真正“通用”的智能体核心它能注册任意工具、能在多步任务中反复调用工具、能在失败后继续尝试也能接上任意一个 OpenAI 兼容的大模型接口。读完这篇文章你会明白 Agent 的本质不是某种神秘系统而是“LLM 决策 工具执行 上下文维护”三件事的循环以及这个循环在生产环境里为什么会变得复杂。更重要的是我不会只讲概念。完整源码、本地可跑的模拟演示、真实大模型接入示例、常见问题排查表都会给出。无论你是被框架劝退的 TypeScript 开发者还是想给 Node.js 项目引入 Agent 能力但怕引入重依赖的技术负责人这篇文章应该都能帮你把“通用智能体”这件事看透一层。1. 这篇文章真正要解决的问题先说一个观察。很多人第一次接触 LangChain 等 Agent 框架时会陷入一种“配置大于实现”的困境业务工具还没写就已经在理解 Chain、Graph、Memory、Callback、Retriever、Tool Spec 这些抽象。框架确实成熟但心智负担也是真实的。那本文要解决的问题是什么答案不是否定框架而是告诉你框架底下的“最小必要结构”有多简单。我把它拆成三件事来回答。第一理解 Agent 最小的执行单元。一个通用智能体表面上看能干很多事但去掉所有包装之后它的运行机制就是“思考 → 行动 → 观察 → 再思考”的循环也就是学术界常说的 ReAct 模式。把这个循环吃透你再看任何框架都不会晕因为你已经知道它最核心的东西其实就是那几行代码。第二给你一个可以修改的 TypeScript 骨架。100 行代码不是玩具是一个完整可运行的 Agent。它能注册工具、调用工具、把工具结果放回上下文、最终输出回答。你可以直接搬进项目也可以在这个基础上加日志、加限流、加上下文压缩所有扩展点都是清楚的。第三讲清楚边界。这类自研轻量 Agent 适合原型验证、内部工具、中小规模任务如果你的场景是生产级多智能体协作、复杂状态机、大规模并发那选成熟框架更明智。文章末尾我会给一个判断标准帮你在“自研”和“上框架”之间做选择而不是无脑吹自研。2. 基础概念通用智能体到底在做什么2.1 什么是通用智能体通用智能体Agent不是一个固定的算法而是一种程序架构。你可以把它理解成一个“会使用工具的对话系统”。原始大模型只能基于训练数据回答一旦遇到“当前时间”“本地文件”“数据库里的订单”这类没有进过训练集的信息它就只能靠猜。Agent 做的事是给大模型一套“调用外部工具”的协议模型不会直接访问系统而是输出一个结构化的意图由代码去真正执行再把执行结果喂给模型继续推理。这里有一个容易被误解的点Agent 的“智能”来自大模型而不是来自代码。代码做的只是把决策权交给大模型、把执行结果送回去。反过来说如果你的大模型本身推理能力弱写再漂亮的 Agent 框架也救不回来。所以本文的核心代码关注的是“协作流程”而不是“如何训练模型”。2.2 ReAct 循环思考、行动、观察ReAct 是 Reasoning Acting 的缩写。它的思路非常朴素每一轮大模型输出一个行动通常是调用哪个工具、传什么参数程序执行这个行动然后把观察结果作为新消息放回上下文让大模型继续思考。整个过程循环直到大模型认为任务完成。用比喻说Agent 就像一位会用办公软件的新同事。大模型是那个“会思考但看不见电脑”的大脑工具是“Excel、日历、浏览器”这些手脚ReAct 循环则是大脑和手脚之间的协作流程想一下要做什么动手执行看一下结果再决定下一步。而你要写的 100 行代码就是在搭建这个协作流程并不是在帮同事学会思考。2.3 工具调用协议为什么重要要让循环跑起来必须定义大模型和代码之间的交流格式。本文采用非常简单的协议大模型每次输出一个 JSON 对象包含 action 和 args 两个字段。action 是工具名args 是传给工具的参数当任务完成时action 固定为 finishargs 里带着最终回答。{action: calculator, args: {expression: 23 * 4}}{action: finish, args: {answer: 结果是 92}}这种“自己定义协议”的做法看起来简单却是 Agent 设计里最值得花心思的地方。因为真实项目中工具名、参数类型、错误返回格式只要有一条对不上整个循环就会卡住。TypeScript 在这里的优势非常明显工具的参数可以用类型声明约束编译器能帮你在集成阶段就发现错误而不是等运行到第 3 轮循环才暴露。这也是本文坚持用 TypeScript 而不是 JavaScript 的原因TypeScript 与 JavaScript 的语法几乎相同但多出来的类型系统正好踩在 Agent 工具治理的痛点上。2.4 为什么选择 TypeScript选择 TypeScript 不只是因为类型。Node.js 18 自带 fetch接大模型接口不需要额外 HTTP 库TypeScript 编译后的代码可以运行在 Node、Deno、Bun也能在前端项目里复用 Agent 逻辑npm 生态几乎覆盖所有你想调用的服务。对团队来说如果前端本来就是 TS 栈那么把 Agent 能力直接放在共享代码里比单独维护一个 Python 服务轻量得多。3. 环境准备与前置条件在写代码之前先把环境准备好。本文示例基于 Node.js 和 TypeScript不会引入任何 Agent 框架依赖。3.1 安装 Node.js 与 TypeScript建议使用 Node.js 18 及以上版本。后面接真实大模型时我会直接使用 Node 18 内置的全局 fetch不需要安装 axios 或 node-fetch。node -v npm -v创建项目目录并初始化mkdir agent-demo cd agent-demo npm init -y npm install typescript tsx types/node --save-devtsx 是一个可以直接运行 TypeScript 的开发工具避免先编译再运行适合本文的演示场景。安装完成后可以验证 TypeScript 版本npx tsc --version本文示例基于 TypeScript 5.x 编写。虽然 TypeScript 后续版本一直在调整模块解析策略例如旧的 baseUrl 配置在未来主版本中会逐步弃用但只要跟随官方提示修改 tsconfig 即可不影响本文的核心代码。3.2 tsconfig 配置创建 tsconfig.json使用现代模块解析配置{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: Bundler, strict: true, esModuleInterop: true, skipLibCheck: true, noEmit: true, types: [node] }, include: [src] }这里最需要注意的是 strict: true。类型严格模式虽然会让开发时多一些类型标注但对 Agent 这种“多处传递参数”的代码严格模式能挡住大量低级错误。noEmit 是因为我们直接用 tsx 运行不需要先编译成 JS。3.3 VSCode 推荐设置如果你使用 VSCode建议使用项目本地安装的 TypeScript 版本。打开任意 .ts 文件后点击 VSCode 右下角的 TypeScript 版本号选择“使用工作区版本”。这样能保证编辑器使用的语言服务与命令行一致避免出现命令行编译正常、编辑器却报错的差异。日常开发中开启 TypeScript 的“建议”诊断和 JSON 格式校验对调试大模型返回的 JSON 会有帮助。但需要说明的是本文使用 npm 的 tsx 运行方式所以即使不在 VSCode 里做任何特殊配置命令行也可以完整跑通所有示例。4. 核心流程拆解开始写代码之前先拆解 Agent 的核心流程。理解了这五步后面看代码就会非常快。4.1 第一步定义消息结构Agent 与大模型的对话本质上是一个消息数组。每条消息有 role 和 content 两个字段。role 有三种system 表示系统指令user 表示用户输入assistant 表示大模型输出。为了让大模型看到工具执行结果我会把工具结果也作为一条新的 system 消息塞进历史里。这不是唯一的做法但在轻量实现中最简单直接。4.2 第二步注册工具所有工具被放进一个 Map以工具名为 key。这样 Agent 运行时能根据大模型输出的 action 快速找到对应工具。每个工具描述里附带“这个工具是干什么的”“参数是什么”这些描述会被拼进系统提示词帮助大模型在多个工具之间做选择。工具的执行函数统一返回字符串这是为了简化上下文拼接让你不用在历史中处理复杂对象。4.3 第三步构建系统提示词系统提示词是整个 Agent 的“老板”。它写清楚当前任务、可用工具列表、输出格式要求。大模型的输出稳定性很大程度依赖这一段写得有多清楚。我在提示词里加入了两条硬性要求回答必须输出 JSON任务完成时 action 必须是 finish。这看起来普通却是避免大模型“自由发挥”的关键。提示词生成逻辑可以单独维护方便后续按照不同模型调整措辞。4.4 第四步主循环主循环是 Agent 的心脏。每一轮做四件事调用大模型拿到输出把输出追加到历史用 JSON.parse 解析输出如果不是 finish就执行对应工具并把结果追加到历史。循环终止条件有两个大模型输出 action 为 finish或者达到最大迭代次数。最大迭代次数的存在是为了防止模型反复调用同一个工具或陷入死循环。4.5 第五步容错与重试真实场景里大模型经常输出不规范 JSON或者工具名拼错。在轻量实现中我用 try-catch 捕获 JSON 解析失败并往历史里塞一条“JSON 解析失败请重新输出”让大模型带着警告重试。虽然简单但这种“反馈-重试”机制正是 Agent 容错能力的最小体现。生产环境中可以再扩展出格式校验、重试次数限制、人工干预入口等能力。5. 100 行代码实现完整源码新建 src/agent.ts核心实现如下// 文件路径src/agent.ts export type Message { role: user | assistant | system; content: string; }; export type Tool { name: string; description: string; parameters: string[]; execute: (args: Recordstring, unknown) Promisestring; }; export type AgentOptions { tools: Tool[]; llm: (messages: Message[]) Promisestring; maxIterations?: number; }; export class Agent { private tools: Mapstring, Tool; private llm: (messages: Message[]) Promisestring; private maxIterations: number; private history: Message[]; constructor(options: AgentOptions) { this.tools new Map(options.tools.map((t) [t.name, t])); this.llm options.llm; this.maxIterations options.maxIterations ?? 10; this.history []; } private buildSystemPrompt(task: string): string { const toolList Array.from(this.tools.values()) .map((t) - ${t.name}: ${t.description}。参数: ${t.parameters.join(, )}) .join(\n); return [ 你是一个通用智能体请根据用户任务、历史消息和工具观察结果决定下一步动作。, 当前任务: ${task}, 可用工具:, toolList, 你的回答必须是一个 JSON 对象: {action:工具名,args:{...}}。, 当任务完成后输出: {action:finish,args:{answer:最终总结}}。, 如果工具执行结果异常请调整参数后重试不要编造结果。, ].join(\n); } private async executeTool(action: string, args: Recordstring, unknown): Promisestring { const tool this.tools.get(action); if (!tool) return 错误: 工具 ${action} 不存在; try { return await tool.execute(args ?? {}); } catch (err) { return 错误: 工具执行异常 ${err instanceof Error ? err.message : String(err)}; } } private async observe(parsed: { action: string; args?: Recordstring, unknown }): Promisestring { if (parsed.action finish) { return (parsed.args?.answer as string) ?? 任务完成; } return await this.executeTool(parsed.action, parsed.args ?? {}); } async run(task: string): Promisestring { this.history.length 0; this.history.push({ role: system, content: this.buildSystemPrompt(task) }); this.history.push({ role: user, content: task }); for (let step 0; step this.maxIterations; step) { const response (await this.llm(this.history)).trim(); this.history.push({ role: assistant, content: response }); let parsed: { action?: string; args?: Recordstring, unknown } | null null; try { parsed JSON.parse(response); } catch { this.history.push({ role: system, content: JSON 解析失败请严格按照要求输出 JSON 对象。 }); continue; } if (!parsed || typeof parsed ! object || !parsed.action) { this.history.push({ role: system, content: 输出中缺少 action 字段请重新生成。 }); continue; } const observation await this.observe(parsed as { action: string; args?: Recordstring, unknown }); if (parsed.action finish) return observation; this.history.push({ role: system, content: 工具结果: ${observation} }); } return 达到最大迭代次数任务未完成。; } }这段代码的核心在 run 方法。每一轮先把大模型回复推进历史再解析 JSON。解析失败就反馈一条警告消息继续下一轮解析成功且 action 为 finish 就直接返回答案其他情况执行工具把“工具结果: xxx”作为 system 消息塞回历史让大模型下一轮能看到执行结果。有几个设计点值得展开说明。第一llm 是一个函数而不是某个具体 SDK 的绑定。这是整个实现最重要的抽象。你可以在不修改 Agent 代码的前提下把 llm 换成 OpenAI、Claude、DeepSeek甚至本地模型服务。这也意味着这个骨架可以直接用于测试不同模型的工具调用能力非常适合做模型选型对比。第二工具执行结果统一转成字符串。这样做的好处是 Agent 历史里的所有内容都能作为普通文本传给大模型不需要单独处理二进制、对象等复杂类型。缺点是丢失了结构化信息但如果只是传递执行结果字符串足够。第三关于行数。去掉空行和注释这段核心代码大约 100 行。它不是刻意压缩代码而是刚好覆盖了“工具抽象 提示词生成 ReAct 循环 容错重试”四个必要部分。多一个功能行数就可能翻倍少一个功能循环又跑不稳。6. 运行示例与效果验证代码写完了需要真的能跑起来。我准备了两个演示先用模拟 LLM 验证循环逻辑不需要 API Key再接真实大模型。6.1 用模拟 LLM 跑通循环新建 src/demo-simulated.ts// 文件路径src/demo-simulated.ts import { Agent, Message, Tool } from ./agent; const tools: Tool[] [ { name: calculator, description: 计算数学表达式, parameters: [expression, 数学表达式例如 23 * 4], execute: async (args) { const expr String(args.expression ?? 0); const result Function(use strict; return (${expr});)(); return String(result); }, }, { name: getTime, description: 获取当前日期和时间, parameters: [], execute: async () new Date().toLocaleString(zh-CN), }, ]; const simulatedLLM async (messages: Message[]): Promisestring { const assistantRounds messages.filter((m) m.role assistant).length; if (assistantRounds 0) { return JSON.stringify({ action: calculator, args: { expression: 23 * 4 } }); } if (assistantRounds 1) { return JSON.stringify({ action: getTime, args: {} }); } const lastSystem [...messages] .reverse() .find((m) m.role system m.content.startsWith(工具结果)); return JSON.stringify({ action: finish, args: { answer: 计算结果是 92最新获取到的时间信息为${lastSystem?.content ?? 未知}, }, }); }; async function main() { const agent new Agent({ tools, llm: simulatedLLM, maxIterations: 5, }); const answer await agent.run(帮我计算 23 * 4然后告诉我当前时间); console.log(智能体最终回答:, answer); } main().catch(console.error);这里的 simulatedLLM 是一个模拟大脑第一次调用返回计算工具第二次返回时间工具第三次从历史里取到工具结果后输出最终回答。它的作用是让你在没有模型 API 的情况下也能看到 Agent 的完整运行轨迹。需要提醒的是计算结果使用 Function 构造器只是为了演示精简。它存在代码注入风险只能用于本地测试不要直接暴露在服务端对外输入上。后面最佳实践部分会讲替换方案。运行npx tsx src/demo-simulated.ts预期输出类似智能体最终回答: 计算结果是 92最新获取到的时间信息为工具结果: 2025/5/25 14:30:00这里“工具结果:”前缀会出现在回答里是因为模拟 LLM 直接取了历史里的字符串行为像模型在“读”工具输出。真实场景下模型会组织更自然的语言不必担心这个前缀。6.2 接入真实 LLM模拟能验证循环但要让 Agent 真正“通用”需要接一个能根据任务自行决策的大模型。下面是一个 OpenAI 兼容接口的适配器你只需要设置环境变量即可使用// 文件路径src/demo-openai.ts import { Agent, Message, Tool } from ./agent; const tools: Tool[] [ { name: calculator, description: 计算数学表达式, parameters: [expression, 数学表达式例如 23 * 4], execute: async (args) { const expr String(args.expression ?? 0); return String(Function(use strict; return (${expr});)()); }, }, { name: getTime, description: 获取当前日期和时间, parameters: [], execute: async () new Date().toLocaleString(zh-CN), }, ]; async function callOpenAICompatible(messages: Message[]): Promisestring { const resp await fetch(https://api.openai.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.OPENAI_API_KEY ?? }, }, body: JSON.stringify({ model: gpt-4o-mini, messages, temperature: 0, }), }); if (!resp.ok) { throw new Error(LLM 请求失败: ${resp.status} ${await resp.text()}); } const data await resp.json(); return data.choices[0].message.content as string; } async function main() { const agent new Agent({ tools, llm: callOpenAICompatible, maxIterations: 5, }); const answer await agent.run(帮我计算 23 * 4然后告诉我当前时间并总结这两件事的结果); console.log(智能体最终回答:, answer); } main().catch(console.error);运行前设置环境变量export OPENAI_API_KEY你的密钥 npx tsx src/demo-openai.ts如果你的模型服务商提供 OpenAI 兼容接口改掉请求地址和模型名即可。如果接口支持 JSON Mode可以在请求体里加上 response_format: { type: json_object }提升大模型输出 JSON 的稳定性。实际使用哪种模型请以你当前项目和可用服务为准。6.3 如何判断运行是否成功判断标准有两条。第一看控制台是否出现“智能体最终回答”。出现说明循环正常走到 finish没有触达最大迭代次数。第二看任务内容是否被正确执行。如果任务里要求计算最终回答里应该出现计算结果如果要求查时间回答里应该出现时间。若模型输出了 JSON 但工具没有生效一般是工具名或参数名与提示词里描述不一致需要回到 buildSystemPrompt 的格式要求检查。如果运行失败优先看终端里的错误信息。fetch 报 401 或 429是密钥或限流问题报 400通常是请求体格式问题比如历史消息里出现了模型不接受的 role 顺序或字段格式。7. 常见问题与排查思路问题现象可能原因排查方式解决方案大模型返回内容无法 JSON 解析模型没有严格遵守输出格式或上下文过长干扰指令打印原始 response查看是否混入解释文字提高 temperature 为 0或使用支持 JSON Mode 的模型Agent 一直重复调用同一个工具工具结果信息不足模型无法判断下一步检查上一轮工具结果是否完整进入历史让工具返回更明确的成功/失败信息工具名或参数名对不上提示词中的工具描述与代码里 Map key 不一致打印 buildSystemPrompt 生成的提示词统一命名参数格式写清楚达到最大迭代次数但任务未完成任务步骤太多或循环陷入死局增大 maxIterations并观察每轮输出人工分析路由考虑换更强模型TypeError: fetch is not definedNode 版本低于 18node -v 查看版本升级 Node 或安装 node-fetchTS 编译报模块解析错误moduleResolution 配置与项目结构不匹配检查 tsconfig 与 package.json统一使用 Bundler 配置或调整导入路径模型请求报错 400历史消息里 system 位于 user 之后部分 API 不接受查看服务商 API 文档将工具观察结果改为 user 消息或忽略部分历史计算器工具执行报语法错误表达式包含非法字符打印 args.expression 原始值在生产环境换成安全表达式解析库8. 最佳实践与工程建议跑通 100 行代码只完成了第一步。如果这个 Agent 要进入真实项目下面这些建议值得认真考虑。8.1 工具治理优先于代码行数Agent 的价值上限由工具质量决定而不是由循环代码决定。每个工具应该职责单一、返回信息明确、参数命名统一。建议在工具描述里写清楚这个工具在什么场景下使用、参数的单位和格式、失败时会返回什么。千万不要把整个业务系统的逻辑塞进一个巨型工具里模型会很难判断什么时候该调用它。8.2 安全边界必须从第一天想清楚工具执行层是 Agent 系统的风险集中点。计算器工具如果用 Function 或 eval会允许模型执行任意表达式文件工具如果允许相对路径可能访问到不该访问的目录网络工具如果放开请求地址可能被诱导访问内网。生产环境的原则是工具执行需要最小权限输入需要严格校验涉及外部系统变更的操作必须增加人工确认环节。对应到本文计算器工具只是演示生产环境应该换成安全的表达式解析库或者接入专门的数学计算服务。8.3 上下文管理决定长期稳定性Agent 每执行一轮历史消息就变长一些。10 轮以内问题不大但如果任务超过 20 轮上下文可能越来越长推理质量和响应速度都会下降。轻量方案是限制工具结果长度进阶方案是定期对早期历史做摘要或者只保留最近 N 轮消息。这些都可以在 run 方法的 history.push 位置统一处理不影响工具层逻辑。上下文压缩策略需要结合你的模型上下文窗口大小来设计。8.4 日志与可观测性生产环境必须能重放 Agent 的每一步。建议至少记录每次大模型返回的原始内容、解析结果、调用的工具名和参数、工具执行耗时、上下文消息数。一旦线上出现任务失败你可以按任务 ID 把整段历史导出复现问题。如果你把 Agent 暴露成 HTTP 服务还需要在入口处做超时控制和并发控制防止慢模型请求拖垮整个服务。8.5 什么时候该用框架什么时候该自研这是很多人最终会问的问题。我的判断是如果业务只需要“单 Agent 几个工具”、任务步骤在 5 到 10 轮以内自研轻量 Agent 完全够用而且更容易调试、依赖更少。如果业务涉及多 Agent 协作、复杂状态机、持久化记忆、多人共用的 Agent 平台那么成熟框架能帮你省下很多治理成本。不存在绝对的好坏关键看你要解决的问题有多大。9. 总结与后续学习方向这篇文章把“通用智能体”从抽象概念拉到了一个具体的 TypeScript 实现。你需要记住的核心结论有三条。第一通用智能体的最小执行单元是 ReAct 循环思考、行动、观察反复进行直到任务完成。100 行代码可以覆盖这个循环的全部必要环节。第二类型与协议是 Agent 工程的关键。TypeScript 的类型系统能让工具边界在编译期可见而“大模型输出 JSON → 代码解析执行 → 结果回填上下文”这套协议是否稳定直接决定 Agent 能不能跑稳。第三框架存在的意义是解决治理问题不是解决循环问题。先理解循环再决定要不要用框架技术选型会清醒很多。下一步你可以做三件事把模拟 LLM 换成你常用的模型服务观察它在真实任务上的工具调用能力增加一两个项目里真正需要的工具比如查数据库、调内部 API再尝试给 Agent 增加简单的上下文压缩和日志记录。当你把这三件事做完再回头审视 LangChain 之类的框架你会发现自己已经能读懂它们每一个抽象想解决的问题了。如果这篇文章对你有帮助建议收藏备用。也欢迎在评论区聊一聊你在自己的项目里最想给 Agent 接的第一个工具是什么。