最近在做 AI 应用落地时,一个很深的体会是:真正难的不是调用大模型 API,而是怎么让模型在真实任务里稳定地“干活”。网上很多 Agent 教程要么贴了一堆概念图,要么只给出 Python 代码。对于 TypeScript 技术栈的同学来说,想找一个能直接跑起来的 Agent 最小框架并不容易。
这篇文章就用 TypeScript 手写一个通用智能体,核心逻辑控制在 100 行左右,完整呈现企业级 Agent 的主干架构。读完你不仅能跑通代码,还能理解 Agent 设计里最重要的模块划分和职责边界。代码全部放在一个最小项目中,可以复制到本地直接运行。
1. 为什么用 TypeScript 写 AI Agent
1.1 AI Agent 到底是什么
很多人把 Agent 理解成“调一次大模型接口,拿到回答”,这是不完整的。AI Agent 的核心特征是目标驱动和自主决策:你给它一个目标,它能自己拆解任务、选择工具、执行操作、观察结果,再决定下一步怎么做,直到完成目标。
举个最简单的例子:
- 普通 API 调用:用户问“北京天气怎么样”,程序直接把问题发给模型,模型返回一段话。
- Agent 方式:模型先判断需要调用天气查询工具,然后程序执行工具,把工具返回的数据回传给模型,模型基于真实天气数据生成最终回答。
区别在于“工具调用”和“多轮循环”。Agent 不是一个单次接口调用,而是一个循环决策系统。大模型在这个循环里充当“大脑”,工具系统充当“手脚”,消息列表充当“工作记忆”。
1.2 为什么选择 TypeScript
目前社区里 Agent 框架确实以 Python 为主,但 TypeScript 在工程化方面有不可替代的优势。
第一,类型安全。Agent 涉及大量的数据结构:消息、工具定义、参数校验、工具返回值。用 TypeScript 写,编译阶段就能发现字段名错误,而不是等运行时才报错。第二,生态复用。Node.js 环境下可以直接使用数据库驱动、Redis 客户端、消息队列 SDK,前端团队也能无缝参与开发和调试。第三,部署成本低。TypeScript 编译后的产物可以打包成 Serverless 函数、Docker 镜像,或者直接跑在 Node 服务里。
如果你的团队本身就是 React/Node 技术栈,用 TypeScript 写 Agent 意味着不需要引入一套全新的 Python 工程体系,学习成本和维护成本都更低。
1.3 类 PI-Agent 架构带来了什么启发
标题里的“PI-Agent”并不是某个固定开源项目的名称,而是业界对 Agent 分层组织方式的一种通俗叫法。大家参考的其实是同一套核心思路:让大模型在一个受控循环中自主决策,而不是一次性生成答案。
类 PI-Agent 架构给开发者最大的启发,是把 Agent 拆成几个边界清晰的部分:
- 模型调用层:负责和大模型 API 通信。
- 工具系统:负责声明、注册、执行外部能力。
- 记忆模块:负责保存对话历史、工具结果、关键中间状态。
- 主循环:负责指挥模型“决策—执行—观察—再决策”。
这种拆分方式的好处是:每一层都能独立扩展。你可以替换底层模型,可以新增工具,可以调整记忆策略,而不会把整个系统搅成一团。下面我们就按这个思路,从零搭建一个 TypeScript 版本的 Agent 骨架。
2. 环境准备与 TypeScript 项目初始化
2.1 开发环境要求
本文示例所需的运行环境如下:
| 依赖 | 版本建议 | 说明 |
|---|---|---|
| Node.js | 18+ | 需要原生 fetch,Node 18 之前需要额外安装 polyfill |
| npm / yarn / pnpm | 任意 | 本文以 npm 为例 |
| TypeScript | 5.x | 使用现代模块配置 |
| 大模型 API | OpenAI 兼容接口 | 例如 OpenAI、DeepSeek、通义千问等兼容服务 |
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。如果你本地 Node 版本较低,建议先升级到 18 以上,否则后面运行时会遇到 fetch 未定义的报错。
2.2 初始化项目与安装依赖
先创建一个空目录,然后初始化 npm 项目:
mkdir typescript-agent-demo cd typescript-agent-demo npm init -y接着安装开发依赖:
npm install -D typescript tsx @types/node这里用到了两个工具:
typescript:TypeScript 编译器。tsx:一个基于 esbuild 的 TypeScript 运行工具,可以直接运行.ts文件,省去先编译再运行的步骤,非常适合开发调试。@types/node:提供 Node.js 的类型定义,否则process.env等内置 API 会报类型错误。
如果安装时报 peer dependencies 冲突,可以检查一下 npm 版本,或者使用npm install -D指定版本安装。
2.3 tsconfig.json 配置
在项目根目录创建tsconfig.json,内容如下:
{ "compilerOptions": { "target": "ES2022", "module": "CommonJS", "moduleResolution": "Node", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": false }, "include": ["src"] }重点说明几个配置项:
target: ES2022:保证可以使用较新的 JavaScript 语法,比如async/await、for...of、Record类型。module: CommonJS:Node.js 服务端最常用的模块规范。strict: true:开启严格类型检查,这是 TypeScript 的核心价值所在。esModuleInterop: true:方便在 CommonJS 环境下使用默认导入写法。
这里额外提醒一个细节:新版 TypeScript 中baseUrl选项已经开始弃用,并计划在未来版本停止支持。如果你的项目里还在使用baseUrl,建议改为paths配合相对路径的方式,避免升级 TypeScript 后出现编译警告。
3. Agent 核心架构模块拆解
3.1 模型调用层(LLM Adapter)
模型调用层是整个 Agent 的大门,负责屏蔽不同厂商 API 的差异。无论是 OpenAI、DeepSeek 还是其他兼容服务,它们的请求格式和响应格式都不完全一样。如果没有这一层抽象,Agent 主循环里会到处散落着fetch调用和响应解析逻辑,后期想换模型会非常痛苦。
一个设计良好的 LLM Adapter 需要暴露两个核心能力:
- 接收一组消息,返回模型生成的内容。
- 支持工具调用:把当前可用的工具列表传给模型,模型返回“要调用哪些工具、参数是什么”。
在 TypeScript 里,这可以抽象成一个通用接口,不同厂商提供不同实现。本文示例采用 OpenAI 兼容接口实现,因为它目前是事实上的行业标准,很多厂商都提供了兼容端点。
3.2 工具注册与执行层(Tool Registry)
工具系统是 Agent 的“手脚”。模型本身不能查询天气、不能读写数据库、不能调用内部 API,但这些能力都可以通过工具暴露给模型。
在实现上,工具系统至少要负责三件事:
- 声明:告诉模型这个工具叫什么、是干什么的、需要哪些参数。
- 注册:把工具放到一个可管理的注册表里,统一管理。
- 执行:根据模型返回的工具名和参数,真正调用对应的函数,并返回结构化结果。
设计时要注意:模型传入的参数是不可信的,工具执行前必须做参数校验。比如模型传了一个city: null,代码里不能直接报错,而是要返回一个结构化的错误结果,让模型知道“参数不对,请调整后重试”。
3.3 记忆与上下文管理(Message Buffer)
Agent 的记忆本质上就是聊天消息列表。每次循环都会向消息列表追加新内容:用户输入、模型的工具调用意图、工具执行结果、模型的最终回答。
在 TypeScript 实现中,消息需要区分几种角色:
system:系统提示词,定义 Agent 的角色和行为边界。user:用户的输入。assistant:模型的输出。tool:工具执行后的结果。
上下文管理有两个实际问题需要处理。一是消息量膨胀:工具调用轮次越多,消息越长,最终会超过模型上下文窗口。二是工具结果格式:工具返回的可能是一个大型对象,直接塞进消息里会浪费 token,需要做摘要或截断。
本文的最小实现聚焦前者,使用一个简单的消息数组。生产环境则需要引入滑动窗口、摘要压缩、向量检索等机制。
3.4 任务规划与主循环(Agent Loop)
主循环是 Agent 的灵魂,所有模块在这里被串联起来。循环的逻辑非常直观:
- 把消息列表和工具列表发给模型。
- 判断模型返回结果:如果需要调用工具,就执行工具并把结果追加到消息列表,然后进入下一轮循环;如果返回的是最终回答,就结束循环。
- 如果超过最大迭代次数,强制终止,避免模型陷入死循环。
这个循环看似简单,却是很多 Agent 框架的基石。它把“大模型自主决策”和“程序确定性执行”结合在了一起:模型负责判断“做什么”,程序负责“怎么做”以及“结果反馈”。
4. 用 TypeScript 实现通用智能体核心骨架
4.1 项目结构规划
代码虽然不多,但我们按模块拆分,方便后续扩展。完整项目结构如下:
typescript-agent-demo/ ├── package.json ├── tsconfig.json └── src/ ├── index.ts # 入口,组装工具和 Agent ├── types.ts # 类型定义 ├── tools.ts # 工具注册表 ├── llm.ts # LLM 适配器 └── agent.ts # Agent 主循环这样的分层结构在企业项目中很重要。如果所有代码都堆在index.ts里,也许 100 行能跑通,但后续加一个工具、换一个模型、调整记忆策略,都会互相影响。
4.2 类型定义:先想清楚消息模型
先创建src/types.ts,定义整个项目的基础类型:
export type Role = 'system' | 'user' | 'assistant' | 'tool'; export interface ToolCallParam { id: string; name: string; arguments: Record<string, unknown>; } export interface Message { role: Role; content: string; toolCalls?: ToolCallParam[]; toolCallId?: string; name?: string; } export interface ToolDefinition { name: string; description: string; parameters: Record<string, unknown>; } export interface ToolResult { success: boolean; data?: unknown; error?: string; } export interface Tool { definition: ToolDefinition; execute(args: Record<string, unknown>): Promise<ToolResult>; } export interface AgentResponse { content?: string; toolCalls?: ToolCallParam[]; }这里面有几个字段需要重点解释。
Message.toolCalls表示模型在某一轮次里决定调用的工具列表,只有当role为assistant时才需要存在。Message.toolCallId用于关联工具调用和工具结果:模型发出多个工具调用,工具系统执行后,每个结果必须对应到具体的那一次调用,否则模型无法区分哪个结果属于哪次调用。name在role为tool时使用,表示是哪个工具返回的结果。
parameters使用 JSON Schema 风格的结构,虽然不是完整规范,但已经足够模型理解参数格式。大多数兼容 OpenAI 格式的接口都支持这种描述结构。
4.3 实现工具注册表
创建src/tools.ts,实现工具注册、列表、调用的完整闭环:
import { Tool, ToolResult } from './types'; export class ToolRegistry { private tools = new Map<string, Tool>(); register(tool: Tool): void { this.tools.set(tool.definition.name, tool); } list(): Tool[] { return Array.from(this.tools.values()); } async call(name: string, args: Record<string, unknown>): Promise<ToolResult> { const tool = this.tools.get(name); if (!tool) { return { success: false, error: `未找到工具: ${name}` }; } try { return await tool.execute(args); } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } }这个类虽然短,但体现了几个重要的工程决策:
- 使用
Map存储工具,注册时以工具名为 key,避免同名工具重复注册。 list()返回的是当前所有工具,Agent 主循环每次都会把最新工具列表传给模型。call()内部做了异常兜底,任何工具执行抛错都会转成ToolResult返回,而不是直接中断整个 Agent 流程。这一点很关键,因为 Agent 的容错逻辑依赖“错误也要变成模型能理解的反馈”。
如果工具执行失败后直接抛出异常,Agent 主循环就感知不到失败原因,模型也没法根据原因调整策略。把错误包装成{ success: false, error: '...' },模型读到后就知道刚才的调用出了问题。
4.4 实现 LLM 适配器
创建src/llm.ts,以 OpenAI 兼容接口为例实现模型调用:
import { AgentResponse, Message, Tool } from './types'; interface LLMConfig { baseUrl: string; apiKey: string; model: string; } interface OpenAIResponse { choices?: Array<{ message?: { content?: string | null; tool_calls?: Array<{ id: string; type: string; function: { name: string; arguments: string; }; }>; }; }>; } export class OpenAICompatibleAdapter { constructor(private config: LLMConfig) {} async chat(messages: Message[], tools: Tool[]): Promise<AgentResponse> { const url = `${this.config.baseUrl.replace(/\/$/, '')}/chat/completions`; const body: Record<string, unknown> = { model: this.config.model, messages: messages.map((m) => { if (m.role === 'tool') { return { role: 'tool', tool_call_id: m.toolCallId, content: m.content, }; } const msg: Record<string, unknown> = { role: m.role, content: m.content, }; if (m.toolCalls && m.toolCalls.length > 0) { msg.tool_calls = m.toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments), }, })); } return msg; }), }; if (tools.length > 0) { body.tools = tools.map((tool) => ({ type: 'function', function: { name: tool.definition.name, description: tool.definition.description, parameters: tool.definition.parameters, }, })); } const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}`, }, body: JSON.stringify(body), }); if (!response.ok) { const text = await response.text(); throw new Error(`LLM 接口调用失败: ${response.status} ${text}`); } const data = (await response.json()) as OpenAIResponse; const choice = data.choices?.[0]; if (!choice) { return {}; } const toolCalls = choice.message?.tool_calls?.map((tc) => ({ id: tc.id, name: tc.function?.name ?? '', arguments: safeParse(tc.function?.arguments), })); return { content: choice.message?.content ?? '', toolCalls, }; } } function safeParse(json: string | undefined): Record<string, unknown> { if (!json) return {}; try { return JSON.parse(json) as Record<string, unknown>; } catch { return {}; } }这个适配器做了几件核心事情。
第一,消息格式转换。OpenAI 接口对tool消息有特殊要求:必须带上tool_call_id,否则接口会报错。我们内部统一的Message结构需要在发送前转换成厂商要求的格式。
第二,工具声明格式。OpenAI 要求工具以 JSON Schema 形式放在tools字段里,本文的ToolDefinition.parameters正好可以直接透传。
第三,响应容忍度。模型接口偶尔会出现tool_calls缺失或arguments不是合法 JSON 的情况。代码里用safeParse做了兜底,解析失败时返回空对象,避免因为一个非法 JSON 直接崩溃。
第四,错误处理。HTTP 非 2xx 状态码会直接抛出异常,由上一层的 Agent 主循环决定如何处理。
各厂商的兼容接口在请求体上会有细微差异。如果你接入的是 DeepSeek、通义千问等平台,建议先查看对应文档确认字段命名,核心的 adapter 结构通常不需要大改。
4.5 实现 Agent 主循环
创建src/agent.ts,这是整个 Agent 的核心逻辑:
import { OpenAICompatibleAdapter } from './llm'; import { ToolRegistry } from './tools'; import { Message } from './types'; interface AgentOptions { systemPrompt: string; maxIterations?: number; } export class Agent { private messages: Message[] = []; private maxIterations: number; constructor( private llm: OpenAICompatibleAdapter, private tools: ToolRegistry, private options: AgentOptions ) { this.maxIterations = options.maxIterations ?? 5; this.messages.push({ role: 'system', content: options.systemPrompt }); } async run(userInput: string): Promise<string> { this.messages.push({ role: 'user', content: userInput }); for (let i = 1; i <= this.maxIterations; i++) { console.log(`[Agent] 第 ${i} 轮决策开始`); const response = await this.llm.chat(this.messages, this.tools.list()); if (response.toolCalls && response.toolCalls.length > 0) { this.messages.push({ role: 'assistant', content: response.content ?? '', toolCalls: response.toolCalls, }); for (const toolCall of response.toolCalls) { console.log(`[Agent] 调用工具: ${toolCall.name}`, toolCall.arguments); const result = await this.tools.call(toolCall.name, toolCall.arguments); console.log(`[Agent] 工具结果:`, result); this.messages.push({ role: 'tool', content: JSON.stringify(result), toolCallId: toolCall.id, }); } continue; } if (response.content) { this.messages.push({ role: 'assistant', content: response.content }); return response.content; } throw new Error(`第 ${i} 轮迭代未产生有效输出`); } throw new Error(`超过最大迭代次数 ${this.maxIterations},任务未完成`); } }主循环的逻辑只有十几行,却把 Agent 的核心机制完整实现了。
maxIterations是安全阀。模型在工具调用过程中偶尔会陷入“反复调用同一个工具、参数不变”的死循环。如果没有最大迭代次数保护,会一直消耗 token 直到超时。工程实践中一般设置 5 到 10 次,具体根据任务复杂度调整。
还有一个容易被忽略的点:assistant消息保存了content和toolCalls。即使内容为空,这条assistant消息也必须追加到历史中,否则模型会丢失“自己已经做出过哪些决策”的信息,导致重复发起相同的工具调用。
工具执行结果用JSON.stringify序列化后保存为字符串。这样做的好处是消息结构简单,坏处是大对象会占用大量 token。本文示例保持了最小实现,生产环境可以根据工具类型对结果做摘要。
4.6 组装工具与运行入口
创建src/index.ts,把前面所有模块组装起来:
import { Agent } from './agent'; import { OpenAICompatibleAdapter } from './llm'; import { ToolRegistry } from './tools'; import { Tool } from './types'; async function main() { // 定义天气查询工具 const weatherTool: Tool = { definition: { name: 'get_weather', description: '查询指定城市的当前天气', parameters: { type: 'object', properties: { city: { type: 'string', description: '城市名称' }, }, required: ['city'], }, }, async execute(args) { const city = String(args.city ?? ''); if (!city) { return { success: false, error: '缺少 city 参数' }; } // 演示数据,生产环境请接入真实天气服务 return { success: true, data: { city, weather: '晴', temperature: 26 }, }; }, }; // 定义计算器工具 const calculatorTool: Tool = { definition: { name: 'calculator', description: '执行简单的四则运算', parameters: { type: 'object', properties: { expression: { type: 'string', description: '数学表达式,例如 25 * 4 + 16' }, }, required: ['expression'], }, }, async execute(args) { const expr = String(args.expression ?? ''); // 注意:生产环境请使用 mathjs 等表达式解析库,不要直接使用 Function const result = Function(`'use strict'; return (${expr})`)(); return { success: true, data: { expression: expr, result } }; }, }; // 注册工具 const tools = new ToolRegistry(); tools.register(weatherTool); tools.register(calculatorTool); // 初始化 LLM const llm = new OpenAICompatibleAdapter({ baseUrl: process.env.LLM_BASE_URL ?? 'https://api.openai.com/v1', apiKey: process.env.LLM_API_KEY ?? '', model: process.env.LLM_MODEL ?? 'gpt-4o-mini', }); // 构建 Agent const agent = new Agent(llm, tools, { systemPrompt: '你是一个通用智能体。请根据用户问题选择合适的工具,并在得到工具结果后给出最终回答。', maxIterations: 5, }); // 运行 const answer = await agent.run('北京今天天气怎么样?顺便帮我算一下 25 * 4 + 16'); console.log('\n=== 最终回答 ==='); console.log(answer); } main().catch((err) => { console.error(err); process.exit(1); });从工具箱到 Agent,一共不到 100 行核心逻辑。读者可以在这个基础上任意扩展新工具,只需要实现Tool接口并注册即可,不需要改 Agent 主循环。
5. 运行验证与效果演示
5.1 配置模型环境变量
在项目根目录创建.env文件(注意不要提交到代码仓库),内容如下:
LLM_BASE_URL=https://api.openai.com/v1 LLM_API_KEY=你的_API_Key LLM_MODEL=gpt-4o-mini如果使用国内兼容平台,把LLM_BASE_URL换成平台提供的 base URL 即可。API Key 也可以通过环境变量设置,但要确保密钥不暴露在前端代码中。
5.2 编译与启动
开发调试阶段,直接使用tsx运行:
npx tsx src/index.ts也可以把启动命令写入package.json的 scripts 中:
{ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "start": "node dist/index.js" } }之后再运行:
npm run dev如果要部署到生产环境,先执行npm run build编译到dist目录,再用node dist/index.js启动。
5.3 预期执行流程
假设模型决定调用两个工具,运行日志大致如下:
[Agent] 第 1 轮决策开始 [Agent] 调用工具: get_weather { city: '北京' } [Agent] 工具结果: { success: true, data: { city: '北京', weather: '晴', temperature: 26 } } [Agent] 调用工具: calculator { expression: '25 * 4 + 16' } [Agent] 工具结果: { success: true, data: { expression: '25 * 4 + 16', result: 116 } } [Agent] 第 2 轮决策开始 === 最终回答 === 北京今天天气晴,气温 26 摄氏度。25 * 4 + 16 的计算结果是 116。注意,模型不一定总是在第一轮就选择调用所有工具,也可能先查天气再计算,这取决于模型本身和系统提示词的引导。只要 Agent 循环正常结束并返回最终回答,就说明整个架构已经跑通了。
6. 常见问题与排查思路
在实际运行过程中,最常见的问题集中在网络、类型和环境变量三个方面。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
fetch is not defined | Node.js 版本低于 18 | 升级 Node.js 到 18+ |
403 Forbidden或401 Unauthorized | API Key 错误或缺失 | 检查LLM_API_KEY环境变量 |
baseUrl弃用警告 | TypeScript 高版本对旧配置提示 | 改用paths并移除baseUrl |
| 模型始终不调用工具 | 工具描述不清晰或模型能力不足 | 优化工具 description,使用支持 function calling 的模型 |
| 模型重复调用同一个工具 | 缺少历史消息回填 | 检查assistant消息是否正确追加追问 |
| 上下文超长 | 工具结果和消息过多 | 压缩工具结果、限制消息轮次 |
这里重点说一下模型不调用工具的问题。很多人遇到“Agent 就是不用工具”的情况,第一反应是换模型,其实更常见的原因是工具描述写得太模糊。比如description: '天气工具'和description: '查询指定城市的当前天气,参数 city 为城市名称',后者的调用成功率会高很多。此外,系统提示词里应明确说明“你需要调用工具来完成任务”,而不是让模型自由发挥。
另一个常见问题是工具参数解析失败。模型返回的arguments偶尔会是格式不完整的 JSON 字符串,safeParse兜底后返回空对象,但工具拿到空对象就会报参数缺失。遇到这种情况,可以在提示词里强调“严格按照 JSON 格式输出参数”,同时在工具执行端做好默认值处理。
7. 企业级 Agent 设计最佳实践
7.1 工具执行的安全边界
工具系统是 Agent 与外部世界交互的入口,也是风险最高的地方。
首先,不要把未经校验的模型输出直接拼接到命令行、SQL 或代码片段中。本文示例里的calculator工具使用了Function执行表达式,这是高风险写法,生产环境必须替换为mathjs这类成熟的表达式解析库。任何工具如果需要执行远程命令或访问数据库,都必须先做参数白名单校验、加上权限限制,并开启审计日志。
其次,为工具设置最小权限。一个查询天气的工具不应该拥有访问订单数据库的权限。Agent 能调用的工具集合越小,攻击面就越小。有些企业级系统还会为不同的 Agent 分配不同的工具集,做到“工具级最小化”和“模型级最小化”。
7.2 可观测性与运行日志
Agent 的决策过程不是一次调用,而是一个多轮循环。线上排查问题时,如果看不到“模型为什么这么决策”,排错会非常困难。因此必须把关键信息记录下来:
- 每一轮的系统提示词、用户输入、模型输出。
- 模型请求了哪些工具、传入了什么参数。
- 工具执行结果、耗时、是否成功。
- 最终回答内容和总轮数。
生产环境建议把这些日志接入链路追踪系统,给每轮 Agent 运行分配一个 traceId,方便按任务维度检索。如果你用的是云厂商的日志服务,也可以把console.log替换为结构化 JSON 日志,实现更精细的检索。
7.3 超时、重试与降级策略
大模型接口的延迟和稳定性通常比普通 HTTP 服务更不可控。企业级 Agent 至少需要考虑以下策略:
- 超时控制:给
fetch设置合理的超时时间,比如 30 秒,避免模型接口卡死导致整个 Agent 进程挂起。 - 重试机制:对于 429 限流、5xx 等瞬时错误,可以指数退避重试 2 到 3 次。
- 降级方案:如果主模型不可用,可以切换到备用模型。如果 Agent 循环超限,应该返回一个友好的兜底回答,而不是把异常直接抛给用户。
在代码层面,建议把重试逻辑放在 LLM Adapter 内部,而不是散落在 Agent 主循环里,这样主循环只关注决策流程,不需要关心网络细节。
7.4 从单 Agent 走向多 Agent 协作
本文实现的是单 Agent 最小架构,但企业级场景往往需要多个 Agent 分工协作。常见做法是设置一个“主管 Agent”负责拆解目标,再把子任务分配给不同的“执行 Agent”,最后汇总结果。
多 Agent 协作在架构上并不是推翻单 Agent,而是在单 Agent 基础上增加两层能力:
- 任务队列:子任务需要排队、调度、状态管理。
- 结果汇总:多个子 Agent 执行完后,需要合并结果并做冲突处理。
如果你的系统复杂度还没到多 Agent 的程度,建议先把单 Agent 的稳定性做扎实:工具调用可靠、上下文管理合理、调度日志完整。这些能力在多 Agent 阶段同样不可省略。
8. 总结与下一步学习路线
这篇文章的核心内容可以浓缩成一句话:Agent 的本质是一个“决策—执行—反馈”的循环,TypeScript 完全可以实现一套结构清晰的企业级 Agent 骨架。
你掌握了以下几个关键点:
- Agent 基础概念与核心模块划分。
- TypeScript 项目初始化和类型设计。
- 工具注册表、LLM 适配器、主循环的实现思路。
- 常见运行问题的排查方法。
- 企业级 Agent 在安全、可观测性、稳定性方面的最佳实践。
如果你打算继续深入,可以按照下面的路线逐步进阶:
- 为 Agent 增加持久化记忆,把关键历史写入 Redis 或 SQLite。
- 接入 RAG 能力,让 Agent 能检索私有知识库。
- 实现流式输出,把模型生成过程实时推送给前端。
- 引入多 Agent 编排框架,让不同 Agent 协作完成复杂任务。
- 建立 Agent 评测集,用自动化测试评估工具调用准确率和任务完成率。
先跑通这个最小框架,再逐步加记忆、加 RAG、加多 Agent 协作。动手试一下,比看十篇架构图都有用。遇到问题可以在评论区交流。