1. 项目概述为什么是Bun TypeScript DeepSeek最近在AI应用开发圈子里一个非常清晰的趋势正在形成越来越多的开发者开始用TypeScript来构建AI Agent。这背后有几个很实在的原因。TypeScript的静态类型检查能在开发早期就帮你规避掉大量低级错误尤其是在处理复杂的AI API响应和嵌套数据结构时一个明确的interface或type定义能省去无数调试时间。而Node.js生态虽然成熟但在启动速度和工具链一体化方面新秀Bun展现出了惊人的潜力。它的极速启动和内置的测试运行器、包管理器、打包工具让“从零到一”的体验异常流畅。与此同时大模型的选择也在向“高性价比”和“强编程能力”倾斜。DeepSeek系列模型特别是其代码生成版本凭借出色的逻辑推理能力和极具竞争力的API价格成为了许多个人开发者和初创团队构建AI Agent的首选。它不像一些通用大模型那样“话痨”或回避代码问题而是能精准理解开发意图生成可用的、符合上下文的代码片段。所以这个组合——“Bun”作为高性能的TypeScript运行时“TypeScript”作为保证代码质量的语言“DeepSeek”作为核心的AI大脑——就构成了一个非常现代且高效的AI Agent开发技术栈。它不是为了炫技而是实实在在地解决开发效率、运行性能和成本控制这三个核心痛点。接下来我会带你从零开始用这套技术栈构建一个能理解你指令、并执行简单任务的AI Agent。2. 环境准备与项目初始化2.1 Bun的安装与优势解析首先我们需要安装Bun。Bun不仅仅是一个JavaScript运行时它更是一个一体化的工具包包含了包管理器、打包器、任务运行器和测试运行器。对于AI Agent项目来说快速迭代和测试至关重要Bun的快速启动能极大提升开发体验。安装BunmacOS/Linux打开你的终端执行以下命令。这里使用的是curl安装方式也是最推荐的方式之一。curl -fsSL https://bun.sh/install | bash安装完成后重启你的终端或者运行source ~/.zshrc或~/.bashrc来让环境变量生效。验证安装是否成功bun --version如果看到版本号输出说明安装成功。相比于传统的Node.js npm/yarn/pnpm组合Bun的优势在于极速安装Bun的包管理器用Zig编写安装依赖的速度极快对于需要大量外部库的AI项目来说这是巨大的时间节省。内置工具链你不需要单独配置ts-node、nodemon、jest、webpack等一堆工具。Bun内置了对TypeScript、JSX的支持以及测试、打包和文件监视功能。高性能Bun的运行时性能在多数场景下优于Node.js这对于需要频繁调用AI API并处理响应的Agent来说能带来更低的延迟。2.2 创建并初始化TypeScript项目接下来我们创建一个全新的项目目录并初始化。我将项目命名为my-first-ai-agent你可以根据喜好修改。mkdir my-first-ai-agent cd my-first-ai-agent bun initBun的init命令会以交互式的方式引导你创建项目。按照提示操作项目名、入口文件等使用默认值即可。关键的一步是当询问是否使用TypeScript时一定要选择“Yes”。初始化完成后你会得到一个基本的项目结构其中package.json和tsconfig.json已经为你配置好了。Bun生成的tsconfig.json通常已经包含了适合现代TypeScript项目的合理配置。现在安装我们项目最核心的依赖用于调用DeepSeek API的SDK。这里我们选择社区维护良好、类型定义完善的openai包是的DeepSeek的API与OpenAI API兼容。bun add openai同时安装TypeScript类型定义作为开发依赖虽然Bun内置了TypeScript但一些库的类型需要单独安装bun add -d types/node注意openai库是调用兼容OpenAI API格式的各种大模型服务的通用选择。DeepSeek的API端点虽然不同但请求和响应的数据格式与OpenAI高度兼容这使得我们可以复用这个强大且生态完善的SDK只需在初始化时传入我们自己的baseURL和apiKey即可。这是目前最稳定、类型支持最好的方式。2.3 获取DeepSeek API密钥要调用DeepSeek的模型你需要一个API密钥。访问DeepSeek的官方平台通常是平台官网。注册并登录你的账户。在控制台或个人信息部分找到“API密钥”或“应用密钥”管理页面。创建一个新的API密钥并妥善保存。它通常是一串以sk-开头的长字符串。安全须知永远不要将你的API密钥直接硬编码在代码中更不要提交到Git等版本控制系统。接下来我们会使用环境变量来管理它。在项目根目录创建一个名为.env的文件touch .env在.env文件中填入你的密钥DEEPSEEK_API_KEY你的_DeepSeek_API_密钥_放在这里同时我们需要一个包来读取环境变量。安装dotenvbun add dotenv现在你的项目基础环境已经搭建完毕。package.json应该看起来类似这样{ name: my-first-ai-agent, module: index.ts, type: module, devDependencies: { types/node: ^20.0.0, typescript: ^5.0.0 }, dependencies: { dotenv: ^16.4.5, openai: ^4.40.0 } }3. 核心架构设计构建一个简单的任务执行Agent3.1 AI Agent的基本构成与设计思路在开始写代码之前我们先厘清要构建的AI Agent是什么。一个最简单的AI Agent通常包含以下几个核心部分大脑LLM负责理解用户输入、进行逻辑推理和决策。这就是DeepSeek模型扮演的角色。记忆Memory用于存储对话历史或上下文信息使Agent能进行多轮连贯的对话。我们初版先从简单的短期记忆即保留本次会话的消息列表开始。工具Tools赋予Agent执行具体操作的能力比如调用一个函数来获取天气、计算数学公式、读写文件等。这是Agent从“聊天”走向“执行”的关键。执行器Executor负责协调大脑、记忆和工具管理整个对话和工作流程。我们第一个Agent的目标是让它能够理解用户关于“计算”和“文件”的简单指令并调用相应的工具函数来完成任务。例如用户说“计算一下15的平方根”Agent应该识别出这是一个计算请求调用数学计算工具并返回结果。3.2 初始化DeepSeek客户端与消息系统首先我们在index.ts中建立与DeepSeek服务的连接并构建最基础的消息处理循环。import OpenAI from openai; import * as dotenv from dotenv; // 加载环境变量 dotenv.config(); // 初始化OpenAI兼容客户端指向DeepSeek的API端点 const client new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, // 从.env文件读取 baseURL: https://api.deepseek.com, // DeepSeek的API基础地址 }); // 定义消息类型遵循OpenAI的ChatCompletionMessage格式 type Message { role: system | user | assistant | tool; content: string; name?: string; // 可选工具调用时使用 tool_call_id?: string; // 可选工具调用ID }; // 初始化消息历史从系统提示开始 const messages: Message[] [ { role: system, content: 你是一个乐于助人的AI助手擅长数学计算和简单的文件操作。当用户需要计算或进行文件操作时你会调用相应的工具来完成任务。请用中文回复。, }, ];代码解析我们使用dotenv在代码最开始加载.env文件中的变量这样process.env.DEEPSEEK_API_KEY就能获取到密钥。baseURL被设置为DeepSeek的官方API地址。这是与使用标准OpenAI服务唯一的配置区别。我们定义了一个Message类型来规范消息结构。role字段很重要system用于设定Agent的角色和指令user是用户输入assistant是AI的回复tool则是工具执行后的返回结果。初始的messages数组包含了一条system消息它设定了Agent的初始人设和能力范围。一个清晰、具体的system prompt是引导Agent行为的关键。3.3 定义与实现Agent的工具Tools工具是Agent能力的延伸。我们将实现两个基础工具一个数学计算器和一个文件阅读器。// 定义工具的类型。OpenAI SDK期望工具以特定格式描述。 const tools: OpenAI.ChatCompletionTool[] [ { type: function, function: { name: calculate, description: 执行一个数学计算。支持加()、减(-)、乘(*)、除(/)、乘方(^)和平方根(sqrt)等基本运算。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如: “3 5”, “10 / 2”, “sqrt(16)”, “2 ^ 8”。, }, }, required: [expression], additionalProperties: false, }, }, }, { type: function, function: { name: readFile, description: 读取指定路径的文本文件内容。, parameters: { type: object, properties: { filePath: { type: string, description: 要读取的文件的相对或绝对路径。, }, }, required: [filePath], additionalProperties: false, }, }, }, ]; // 实现工具对应的实际函数 const toolImplementations: Recordstring, Function { calculate: ({ expression }: { expression: string }): string { console.log([工具调用] 正在计算表达式: ${expression}); try { // 安全警告在实际生产环境中直接使用eval是极度危险的 // 这里仅用于演示。更安全的做法是使用数学表达式解析库如math.js // 替换常见的数学符号 let safeExpr expression.replace(/\^/g, **).replace(/sqrt\(/g, Math.sqrt(); // 非常简单的安全过滤生产环境需要更严格的沙箱 if (/[a-zA-Z\\]/.test(safeExpr.replace(/Math\.sqrt/g, ))) { throw new Error(表达式包含潜在的不安全字符); } const result eval(safeExpr); return 计算结果为: ${result}; } catch (error) { return 计算失败: ${error.message}; } }, readFile: async ({ filePath }: { filePath: string }): Promisestring { console.log([工具调用] 正在读取文件: ${filePath}); try { // Bun内置了高性能的File I/O API const file Bun.file(filePath); if (!(await file.exists())) { return 文件不存在: ${filePath}; } const content await file.text(); return 文件“${filePath}”的内容如下\n---\n${content}\n---; } catch (error) { return 读取文件失败: ${error.message}; } }, };关键点与避坑指南工具描述至关重要description和parameters的描述必须清晰、准确。大模型DeepSeek正是根据这些描述来决定是否以及如何调用工具的。模糊的描述会导致错误的工具调用。参数定义要严谨required字段指明了哪些参数是必须的。additionalProperties: false可以防止模型传入未定义的参数增加调用的可控性。calculate工具的安全警告这是本教程最需要警惕的一点。在演示代码中我们使用了eval()来执行数学表达式这在实际的、暴露给外部用户的系统中是极其危险的因为它允许执行任意JavaScript代码。这里仅为了演示流程的简洁。在真实项目中你必须使用安全的数学表达式求值库例如math.js或者自己实现一个严格的解析器。Bun的文件API我们利用了Bun运行时内置的Bun.file()API来读取文件它返回一个BunFile对象提供了text()、json()等异步方法来获取内容。这比Node.js的fs.readFile在Bun环境下更原生、更高效。4. 实现Agent的执行循环与流式响应4.1 构建主对话循环函数有了工具定义和实现我们需要一个核心函数来驱动整个对话。这个函数负责接收用户输入将历史消息和工具定义发送给DeepSeek处理模型可能返回的工具调用请求执行工具并将结果再次发送给模型以获得最终的自然语言回复。async function runAgentConversation(userInput: string): Promisevoid { // 1. 将用户输入加入消息历史 messages.push({ role: user, content: userInput }); console.log(\n[用户] ${userInput}); // 2. 循环处理因为一次对话可能涉及多次“模型思考-工具调用-返回结果”的循环 while (true) { // 3. 调用DeepSeek Chat Completions API const response await client.chat.completions.create({ model: deepseek-chat, // 使用DeepSeek的聊天模型 messages: messages, tools: tools, // 告诉模型可用的工具 tool_choice: auto, // 让模型自行决定是否调用工具 stream: true, // 启用流式输出提升用户体验 }); let fullAssistantContent ; let toolCalls: OpenAI.ChatCompletionMessageToolCall[] []; // 4. 处理流式响应 for await (const chunk of response) { const choice chunk.choices[0]; if (!choice) continue; const delta choice.delta; // 收集模型生成的自然语言内容 if (delta.content) { process.stdout.write(delta.content); // 逐字打印实现打字机效果 fullAssistantContent delta.content; } // 收集模型决定的工具调用请求 if (delta.tool_calls) { // 初始化或追加tool_calls信息 if (!toolCalls[delta.tool_calls.index]) { toolCalls[delta.tool_calls.index] { id: , type: function, function: { name: , arguments: }, }; } const toolCall toolCalls[delta.tool_calls.index]; if (delta.tool_calls.id) toolCall.id delta.tool_calls.id; if (delta.tool_calls.function?.name) { toolCall.function.name delta.tool_calls.function.name; } if (delta.tool_calls.function?.arguments) { toolCall.function.arguments delta.tool_calls.function.arguments; } } } console.log(); // 流式输出完后换行 // 5. 将模型的回复无论是普通内容还是工具调用指令存入历史 if (fullAssistantContent) { messages.push({ role: assistant, content: fullAssistantContent }); } if (toolCalls.length 0) { // 如果有工具调用需要添加一条特殊的assistant消息其中包含tool_calls messages.push({ role: assistant, content: null, // 当有tool_calls时content通常为null tool_calls: toolCalls, }); } // 6. 如果没有工具调用说明对话结束跳出循环 if (toolCalls.length 0) { break; } // 7. 执行模型请求的工具调用 for (const toolCall of toolCalls) { const functionName toolCall.function.name; const functionArgs JSON.parse(toolCall.function.arguments); console.log(\n[Agent决定调用工具] ${functionName}(${JSON.stringify(functionArgs)})); // 查找并执行工具函数 const toolFunction toolImplementations[functionName]; if (!toolFunction) { console.error(未知工具: ${functionName}); continue; } let toolResult: string; try { // 执行工具 toolResult await Promise.resolve(toolFunction(functionArgs)); } catch (error) { toolResult 工具执行出错: ${error.message}; } console.log([工具执行结果] ${toolResult}); // 8. 将工具执行结果作为一条“tool”角色的消息添加回对话历史 // 这是关键步骤让模型知道工具执行的结果以便它生成最终回复给用户。 messages.push({ role: tool, tool_call_id: toolCall.id, // 必须与请求的tool_call id对应 content: toolResult, }); } // 工具执行结果添加后进入下一轮while循环模型将基于新历史生成回复 } }流程深度解析 这个runAgentConversation函数是Agent的“心脏”。它实现了一个完整的“思考-行动”循环添加用户输入将用户问题加入上下文。调用模型携带完整的对话历史和工具定义请求DeepSeek。tool_choice: auto让模型自主决策。流式处理通过stream: true和for await...of循环我们实现了回复的逐字输出体验更好。同时我们需要在流中耐心地拼接content和tool_calls这两个可能并行产生的数据。保存模型响应将模型生成的文本或工具调用指令保存到messages中。注意当模型决定调用工具时它的content通常为null而tool_calls字段包含了调用详情。判断循环如果没有工具调用循环结束本次对话完成。如果有则进入执行阶段。执行工具解析每个工具调用的名称和参数从toolImplementations中找到对应的函数并执行。反馈结果将工具执行结果以role: tool的消息格式附上对应的tool_call_id添加回messages。这个id的对应关系是API设计的关键确保了结果能准确关联到之前的请求。再次循环添加工具结果后while循环再次开始模型会接收到包含工具执行结果的新上下文并据此生成面向用户的最终回答。4.2 创建交互式命令行界面CLI为了让我们的Agent能真正互动起来我们创建一个简单的命令行交互界面。// 引入readline模块用于在控制台进行行读取 import * as readline from readline/promises; import { stdin as input, stdout as output } from process; async function main() { console.log( 你的第一个TypeScript AI Agent已启动); console.log(系统提示我可以帮你进行数学计算和读取文本文件。); console.log(输入“退出”或“quit”来结束对话。\n); const rl readline.createInterface({ input, output }); try { while (true) { const userInput await rl.question( 你: ); const trimmedInput userInput.trim(); if (trimmedInput.toLowerCase() 退出 || trimmedInput.toLowerCase() quit) { console.log(再见); break; } if (trimmedInput.length 0) { continue; } // 调用核心对话函数 await runAgentConversation(trimmedInput); console.log(); // 每次对话后空一行 } } finally { rl.close(); } } // 启动程序 main().catch(console.error);现在你的index.ts文件已经完整了。运行它来体验你的第一个AI Agentbun run index.ts你应该会看到提示符然后可以尝试输入“计算一下123乘以456等于多少”“读取当前目录下的README.md文件。”“先计算圆的面积半径是5然后把这个结果保存到一个临时文件里再读出来给我看。”这个复杂指令会触发多轮工具调用和思考5. 项目优化、调试与扩展方向5.1 错误处理与健壮性增强上面的基础版本缺乏足够的错误处理。让我们增强几个关键点1. API调用错误处理async function runAgentConversation(userInput: string): Promisevoid { messages.push({ role: user, content: userInput }); console.log(\n[用户] ${userInput}); while (true) { let response; try { response await client.chat.completions.create({ model: deepseek-chat, messages: messages, tools: tools, tool_choice: auto, stream: true, temperature: 0.7, // 新增控制创造性越低越确定 max_tokens: 1500, // 新增限制单次回复长度 }); } catch (apiError) { console.error(\n[错误] 调用DeepSeek API失败:, apiError.message); // 可以选择将错误信息反馈给用户或加入一个系统错误消息到历史 messages.push({ role: system, content: 系统提示上一次API调用失败原因${apiError.message}。请忽略上一条用户输入或尝试重新表述。, }); break; // 或 continue取决于你想如何处理 } // ... 后续处理不变 } }2. 工具调用参数验证在toolImplementations的每个函数开头增加参数校验。const toolImplementations: Recordstring, Function { calculate: ({ expression }: { expression: string }): string { // 参数校验 if (typeof expression ! string || expression.trim().length 0) { return 错误计算表达式不能为空。; } // ... 原有计算逻辑 }, readFile: async ({ filePath }: { filePath: string }): Promisestring { // 参数校验 if (!filePath || typeof filePath ! string) { return 错误文件路径无效。; } // 简单的路径安全校验防止目录遍历攻击 if (filePath.includes(..) || filePath.startsWith(/)) { return 错误出于安全考虑仅支持读取当前工作目录下的相对路径文件。; } // ... 原有文件读取逻辑 }, };5.2 添加更多实用工具与技能一个强大的Agent需要丰富的工具集。我们可以轻松扩展1. 获取当前时间/日期// 在tools数组中添加 { type: function, function: { name: getCurrentTime, description: 获取当前的日期和时间信息。, parameters: { type: object, properties: {}, additionalProperties: false }, }, } // 在toolImplementations中添加 const toolImplementations: Recordstring, Function { // ... 已有工具 getCurrentTime: (): string { const now new Date(); return 当前时间是${now.toLocaleString(zh-CN)}; }, };2. 网络请求获取天气首先安装一个HTTP客户端比如ofetchBun推荐或axios。bun add ofetch然后添加工具import { ofetch } from ofetch; // 在tools数组中添加 { type: function, function: { name: getWeather, description: 获取指定城市的当前天气情况。需要城市名称作为参数。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如“北京”、“Shanghai”。 }, }, required: [city], additionalProperties: false, }, }, } // 在toolImplementations中添加 const toolImplementations: Recordstring, Function { // ... 已有工具 getWeather: async ({ city }: { city: string }): Promisestring { try { // 使用一个免费的天气API示例实际使用时请替换为可靠的API并处理密钥 const apiKey process.env.WEATHER_API_KEY; // 假设你在.env配置了 const url https://api.openweathermap.org/data/2.5/weather?q${encodeURIComponent(city)}appid${apiKey}unitsmetric; const data await ofetch(url); const weather data.weather[0].description; const temp data.main.temp; return 城市 ${city} 的天气${weather}温度 ${temp}°C。; } catch (error) { return 获取天气失败${error.message}; } }, };5.3 性能优化与高级配置1. 消息历史管理防止token超限大模型API有上下文长度限制。长时间对话后messages数组会越来越大。我们需要一个策略来修剪历史。const MAX_HISTORY_MESSAGES 20; // 保留最近N轮对话 function manageMessageHistory(messages: Message[]): Message[] { // 总是保留system消息 const systemMessage messages.find(m m.role system); const otherMessages messages.filter(m m.role ! system); // 如果消息太多从最旧的用户/助手消息开始删除但尽量保留最近几轮 if (otherMessages.length MAX_HISTORY_MESSAGES) { const messagesToKeep otherMessages.slice(-MAX_HISTORY_MESSAGES); return systemMessage ? [systemMessage, ...messagesToKeep] : messagesToKeep; } return messages; } // 在runAgentConversation函数的while循环开始前或每次添加新消息后调用 // messages manageMessageHistory(messages);2. 并行工具调用目前的代码是顺序执行工具。如果模型请求多个独立工具可以并行执行以提升速度。// 替换原来的for循环 const toolExecutionPromises toolCalls.map(async (toolCall) { // ... 执行工具的逻辑 return { toolCallId: toolCall.id, result: toolResult }; }); const executionResults await Promise.allSettled(toolExecutionPromises); // 然后按顺序将结果添加回messages顺序可能影响模型理解但通常问题不大 executionResults.forEach((result) { if (result.status fulfilled) { const { toolCallId, result: toolResult } result.value; messages.push({ role: tool, tool_call_id: toolCallId, content: toolResult, }); } else { // 处理失败的工具执行 console.error(工具执行失败:, result.reason); } });5.4 常见问题排查与调试技巧问题1模型不调用工具总是直接回答。检查工具描述确保description清晰说明了工具的用途和适用场景。模型是根据描述做决策的。调整系统提示在system消息中更明确地指示模型使用工具例如“当你需要计算或操作文件时必须调用相应的工具函数。”检查参数定义确保parameters的properties定义正确特别是type和description。使用更具体的用户查询有时用户问题太模糊模型认为不需要工具。尝试“请用计算工具帮我算一下(1527)*3的值”。问题2工具调用参数解析错误JSON.parse失败。模型生成问题有时模型生成的参数不是完美JSON。可以在JSON.parse外加上try-catch失败时给模型一个友好的错误反馈。参数描述不清检查工具parameters的description确保模型理解每个参数应该是什么格式。问题3流式响应中tool_calls收集不完整。网络流中断流式响应对网络稳定性要求高。考虑增加重试逻辑或先使用非流式(stream: false)进行调试确保基础逻辑正确。拼接逻辑错误仔细检查流式处理中tool_calls的拼接代码确保index、id、name、arguments都被正确累加。问题4Bun运行时报错找不到模块。清除缓存运行bun install --force重新安装依赖。检查TypeScript配置确保tsconfig.json中的module设置为ESNext或NodeNexttarget设置为ES2022或更高以兼容Bun的ES模块系统。调试建议开启详细日志在关键步骤如发送API请求前、收到响应后、执行工具前打印messages数组的内容观察对话上下文的演变。模拟测试可以暂时写死一个包含tool_calls的响应对象绕过真实的API调用来单独测试你的工具执行和结果反馈逻辑是否正确。使用DeepSeek的调试模式如果平台提供查看API请求和响应的原始数据确认格式是否符合预期。通过以上步骤你已经拥有了一个功能相对完整、可扩展性强的TypeScript AI Agent雏形。从环境搭建、核心逻辑实现到优化调试这个项目涵盖了构建一个实用AI Agent的主要环节。你可以在此基础上继续添加更复杂的工具如数据库操作、调用其他API、实现长期记忆向量数据库、或者构建一个Web界面让它从一个命令行玩具成长为一个真正的AI应用。