尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于Bun+TypeScript+DeepSeek构建AI Agent:现代前端技术栈实战指南

基于Bun+TypeScript+DeepSeek构建AI Agent:现代前端技术栈实战指南 1. 项目概述为什么是Bun TypeScript DeepSeek最近在AI应用开发圈子里一个非常清晰的趋势正在形成前端和全栈开发者正在以前所未有的速度涌入AI Agent的构建领域。这背后有几个关键驱动力一是像DeepSeek这样的国产大模型在性价比和API易用性上带来了巨大优势二是JavaScript/TypeScript生态的成熟度已经足以支撑复杂的AI应用开发三是像Bun这样的现代JavaScript运行时以其极致的启动速度和一体化的工具链彻底改变了Node.js生态的开发体验。我选择这个技术栈组合是因为它精准地解决了AI Agent开发中的几个核心痛点。首先开发效率。传统的Python生态虽然AI库丰富但对于习惯了前端开发流程的工程师来说上下文切换成本高部署和工程化也更为复杂。TypeScript的静态类型检查能在开发阶段就规避大量低级错误这在处理复杂的AI提示词Prompt和结构化输出JSON Mode时尤其重要。其次性能与冷启动。AI Agent常常需要快速响应Bun的启动速度比Node.js快一个数量级这对于Serverless函数、CLI工具或需要频繁启动的批处理任务至关重要。最后成本与可控性。DeepSeek API提供了极高的性价比让个人开发者和小团队也能低成本地进行大量实验和迭代而无需担心天价的API账单。这个项目就是带你从零开始用这套“现代前端栈”构建一个具备基础能力的AI Agent。它不是一个玩具而是一个可以立即投入使用的脚手架涵盖了从环境搭建、模型调用、工具函数定义到任务编排的完整流程。无论你是想做一个自动处理邮件的助手一个分析代码仓库的Bot还是一个智能的日程规划Agent这里面的模式都可以直接复用。2. 核心概念与工具链深度解析2.1 Bun不仅仅是更快的Node.js很多人把Bun简单理解为“一个快的Node.js替代品”这大大低估了它在AI应用开发场景下的价值。Bun的核心优势在于其“一体化”设计。内置的包管理器、测试运行器和打包工具这意味着你不再需要分别安装和配置npm/yarn/pnpm、jest/vitest、webpack/vite。对于AI Agent项目我们经常需要快速安装和测试各种SDK如openai、anthropic-ai/sdk的兼容层Bun的bun install速度极快能显著缩短依赖安装时间。更重要的是Bun原生支持.env文件无需额外安装dotenv库这对于管理敏感的API密钥是开箱即用的便利。对TypeScript和JSX的原生支持你不需要ts-node或复杂的构建配置。直接运行bun run index.ts即可。这在快速原型阶段无比顺畅。在AI Agent开发中我们经常需要调整提示词模板这种即时反馈的循环能极大提升实验效率。更现代化的APIBun提供了诸如Bun.file()、Bun.sleep()等更符合现代语义的API并且在处理HTTP请求、文件I/O和子进程方面性能更好。一个典型的AI Agent可能需要读取本地文件作为上下文或者调用外部命令行工具Bun在这些方面的优化能带来实实在在的性能提升。实操心得在Mac和Linux上安装Bun非常顺畅但在Windows上建议通过WSL2来使用能获得最接近原生体验的性能和兼容性。对于AI项目稳定性和可复现性很重要建议使用bun install --frozen-lockfile来确保依赖的一致性。2.2 TypeScriptAI提示工程中的“安全带”在纯JavaScript中开发AI应用就像在冰面上高速行车——刺激但容易失控。TypeScript提供的类型安全在提示工程和结构化输出解析中是至关重要的“安全带”。结构化输出的类型保障DeepSeek等现代大模型都支持以JSON格式返回数据。我们可以用TypeScript的interface或type来严格定义期望的返回结构。例如当你要求模型分析一封邮件并提取“发件人”、“主题”和“紧急程度”时你可以预先定义好类型interface EmailAnalysis { sender: string; subject: string; isUrgent: boolean; categories: string[]; summary: string; }然后在调用API时通过response_format: { type: json_object }参数要求模型按此格式输出。TypeScript编译器能确保你后续处理数据时访问的字段是存在的、类型是正确的避免了运行时因字段名拼写错误或类型不符导致的诡异bug。提示词模板的类型化我们可以将复杂的提示词抽象成函数并用TypeScript来约束输入参数。这不仅能避免提示词中留下未替换的占位符还能让提示词的组合和复用更加清晰。function createAnalysisPrompt(context: string, instructions: string): string { return 你是一个专业的分析助手。请基于以下上下文 ${context} 遵循这些指令进行分析 ${instructions} 请确保输出为有效的JSON格式。 ; } // 调用时TypeScript会检查context和instructions是否为字符串依赖注入与配置管理AI Agent通常需要配置模型参数如temperature,max_tokens、API密钥、工具列表等。使用TypeScript我们可以创建一个强类型的配置对象并在应用启动时进行验证确保所有必要配置都已就位而不是在运行时才因配置缺失而失败。2.3 DeepSeek API高性价比的智能核心DeepSeek模型以其出色的代码能力和极高的性价比迅速获得了开发者社区的青睐。对于构建AI Agent它有几点特别吸引人强大的Function Calling工具调用能力这是构建复杂Agent的基石。Agent的核心思想是“大模型做决策外部工具做执行”。DeepSeek API支持将工具的函数签名名称、描述、参数schema传给模型模型可以根据用户请求决定调用哪个工具并生成符合该工具参数格式的JSON。这相当于给大模型装上了“手”和“脚”让它能操作外部世界。超长的上下文窗口最新的DeepSeek-V3模型支持128K的上下文长度。这意味着你可以将很长的文档、历史对话、代码库片段一次性喂给模型让它拥有更全面的“记忆”做出更准确的判断和生成。极具竞争力的定价相比其他主流模型DeepSeek的API调用成本要低得多。这使得开发者可以负担得起更大量的测试、更频繁的调用从而快速迭代Agent的能力而不必过于担心成本问题。这对于个人项目和小型创业公司尤其友好。简洁清晰的API设计DeepSeek的API与OpenAI API格式高度兼容。这意味着社区中大量为OpenAI编写的工具、库和最佳实践可以经过很小的适配甚至直接用于DeepSeek学习成本和迁移成本都很低。注意事项虽然API兼容性好但模型的行为和“性格”仍有差异。在从其他模型迁移到DeepSeek时原有的提示词Prompt可能需要进行微调以达到最佳效果。建议在关键功能上设计一些测试用例对比输出结果。3. 从零搭建你的第一个AI Agent3.1 环境初始化与项目配置首先确保你的系统已经安装了Bun。打开终端执行以下命令创建一个新的项目目录并初始化# 创建一个新项目文件夹 mkdir my-first-ai-agent cd my-first-ai-agent # 使用Bun初始化项目并安装TypeScript类型定义Bun已内置TS支持这里安装的是Node类型用于兼容一些库 bun init -y bun add -d types/node接下来安装核心依赖。我们将使用openai这个官方SDK的兼容版本因为DeepSeek API兼容OpenAI格式以及一个用于处理环境变量的库虽然Bun内置支持但使用dotenv加载更显式。bun add openai dotenv现在创建项目的基本结构my-first-ai-agent/ ├── .env # 环境变量文件切勿提交到Git ├── .gitignore ├── package.json ├── tsconfig.json # TypeScript配置文件 ├── src/ │ ├── index.ts # 应用主入口 │ ├── agent/ # Agent核心逻辑 │ │ ├── core.ts # Agent运行循环 │ │ └── types.ts # 类型定义 │ ├── tools/ # 工具函数定义 │ │ └── index.ts │ └── utils/ # 工具函数 │ └── config.ts # 配置加载 └── README.md在.env文件中填入你的DeepSeek API密钥DEEPSEEK_API_KEYyour_api_key_here DEEPSEEK_API_BASEhttps://api.deepseek.com在src/utils/config.ts中我们创建一个安全的配置加载器import dotenv from dotenv; import { z } from zod; // 我们可以用zod进行运行时验证这里先用简单检查 dotenv.config(); const envSchema { DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY, DEEPSEEK_API_BASE: process.env.DEEPSEEK_API_BASE || https://api.deepseek.com, }; // 简单的验证 if (!envSchema.DEEPSEEK_API_KEY) { throw new Error(DEEPSEEK_API_KEY is not set in .env file); } export const config envSchema;3.2 定义Agent的工具箱Tools一个没有工具的Agent只是一个聊天机器人。工具赋予了Agent行动力。我们来定义两个简单的工具一个获取当前时间一个进行简单的数学计算。在src/tools/index.ts中// 定义工具的函数签名类型 export type ToolFunction (args: any) Promisestring; // 工具1获取当前时间 export const getCurrentTime: ToolFunction async () { const now new Date(); return 当前时间是${now.toLocaleString(zh-CN)}; }; // 工具2执行数学计算这里简单用eval生产环境请用安全的数学表达式解析器如math.js export const calculate: ToolFunction async (args: { expression: string }) { try { // 警告在生产环境中直接使用eval是危险的这里仅作演示。 // 应使用沙箱或专门的数学表达式库如math.js来解析args.expression。 const result eval(args.expression); return 计算表达式 ${args.expression} 的结果是${result}; } catch (error) { return 计算失败${error instanceof Error ? error.message : 未知错误}; } }; // 工具的定义描述用于传给大模型 export const toolsDefinition [ { type: function as const, function: { name: getCurrentTime, description: 获取当前的日期和时间。, parameters: { type: object, properties: {}, // 此工具无需参数 required: [], }, }, }, { type: function as const, function: { name: calculate, description: 执行一个基本的数学计算例如\3 5 * 2\。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如 \3 5 * 2\ 或 \(10 - 4) / 2\。, }, }, required: [expression], }, }, }, ];3.3 实现Agent的核心运行循环这是Agent的大脑。它的工作流程是接收用户输入 - 调用模型并传入可用工具 - 解析模型返回的“工具调用”决策 - 执行对应工具 - 将工具结果再次传给模型 - 生成最终回答。在src/agent/core.ts中import OpenAI from openai; import { config } from ../utils/config.js; import { toolsDefinition, type ToolFunction } from ../tools/index.js; // 初始化OpenAI客户端指向DeepSeek API const client new OpenAI({ apiKey: config.DEEPSEEK_API_KEY, baseURL: config.DEEPSEEK_API_BASE, }); // 工具名称到实际函数的映射 const toolMap: Recordstring, ToolFunction { getCurrentTime: (await import(../tools/index.js)).getCurrentTime, calculate: (await import(../tools/index.js)).calculate, }; export async function runAgent(userInput: string, maxTurns 5): Promisestring { const messages: OpenAI.Chat.ChatCompletionMessageParam[] [ { role: system, content: 你是一个乐于助人的AI助手可以调用工具来帮助用户解决问题。如果你需要调用工具请严格按照要求输出。, }, { role: user, content: userInput }, ]; let currentTurn 0; while (currentTurn maxTurns) { currentTurn; // 1. 调用DeepSeek模型 const response await client.chat.completions.create({ model: deepseek-chat, // 使用DeepSeek的聊天模型 messages, tools: toolsDefinition, tool_choice: auto, // 让模型自行决定是否调用工具 }); const message response.choices[0]?.message; if (!message) { throw new Error(No response message from model); } // 将模型的响应添加到对话历史中 messages.push(message); // 2. 检查模型是否想要调用工具 const toolCalls message.tool_calls; if (toolCalls toolCalls.length 0) { // 模型决定调用一个或多个工具 for (const toolCall of toolCalls) { const toolName toolCall.function.name; let toolArgs: any; try { toolArgs JSON.parse(toolCall.function.arguments); } catch { toolArgs {}; } console.log([Agent] 决定调用工具: ${toolName}参数:, toolArgs); // 3. 执行工具 const toolFunction toolMap[toolName]; if (!toolFunction) { const errorResult 错误未知工具 ${toolName}。; messages.push({ role: tool, tool_call_id: toolCall.id, content: errorResult, }); continue; } const toolResult await toolFunction(toolArgs); // 4. 将工具执行结果返回给模型 messages.push({ role: tool, tool_call_id: toolCall.id, content: toolResult, }); console.log([Tool ${toolName}] 结果: ${toolResult}); } // 工具调用后继续循环让模型基于结果生成回复 continue; } // 5. 模型没有调用工具直接返回最终回复 if (message.content) { console.log([Agent] 最终回复: ${message.content}); return message.content; } } return 对话已达到最大轮数${maxTurns}未能完成请求。; }3.4 创建主入口并测试最后在src/index.ts中我们将一切串联起来import { runAgent } from ./agent/core.js; async function main() { console.log( 你的第一个AI Agent已启动\n); // 示例1询问时间 console.log(用户: 现在几点了); const answer1 await runAgent(现在几点了); console.log(Agent: ${answer1}\n); // 示例2进行数学计算 console.log(用户: 请帮我计算一下 (15 7) * 3 等于多少); const answer2 await runAgent(请帮我计算一下 (15 7) * 3 等于多少); console.log(Agent: ${answer2}\n); // 示例3混合请求 console.log(用户: 先告诉我现在的时间然后计算从2020年1月1日到今天过了多少天); const answer3 await runAgent(先告诉我现在的时间然后计算从2020年1月1日到今天过了多少天); console.log(Agent: ${answer3}\n); } main().catch(console.error);现在运行你的Agentbun run src/index.ts你应该能看到控制台输出Agent调用工具、获取结果并最终回复的过程。恭喜你的第一个具备“行动力”的AI Agent已经跑起来了4. 进阶构建一个实用的代码分析Agent基础框架搭好了我们来点更实用的。让我们构建一个可以分析本地TypeScript/JavaScript文件并提供改进建议的代码分析Agent。这需要新增一个工具读取文件。4.1 新增文件读取工具在src/tools/目录下创建fileTools.tsimport fs from fs/promises; import path from path; export type FileToolFunction (args: any) Promisestring; export const readFile: FileToolFunction async (args: { filePath: string }) { const { filePath } args; if (!filePath) { return 错误请提供 filePath 参数。; } try { // 解析相对路径相对于项目根目录 const absolutePath path.resolve(process.cwd(), filePath); const content await fs.readFile(absolutePath, utf-8); return 文件 ${filePath} 的内容如下\n\\\\n${content}\n\\\; } catch (error) { return 读取文件 ${filePath} 失败${error instanceof Error ? error.message : 未知错误}; } }; export const fileToolsDefinition [ { type: function as const, function: { name: readFile, description: 读取指定路径的文本文件内容。, parameters: { type: object, properties: { filePath: { type: string, description: 要读取的文件的路径可以是相对路径或绝对路径。, }, }, required: [filePath], }, }, }, ];更新src/tools/index.ts导出新的工具export * from ./fileTools.js; // ... 原有的导出同时更新src/agent/core.ts中的toolMap和工具定义引入逻辑将readFile工具加入进去。4.2 设计代码分析的系统提示词一个专业的Agent需要清晰的角色定位和指令。我们修改runAgent函数中的系统提示词或者创建一个专门的分析函数。在src/agent/下创建codeAnalyzer.tsimport { runAgent } from ./core.js; export async function analyzeCode(filePath: string): Promisestring { const systemPrompt 你是一个资深的TypeScript/JavaScript代码审查专家。你的任务是分析提供的代码并给出清晰、具体、可操作的改进建议。 请按以下结构组织你的分析报告 1. **总体概览**对代码文件的功能和结构进行简要总结。 2. **潜在问题**列出你发现的具体问题例如 - 代码风格问题命名、格式。 - 潜在的错误或边界情况处理缺失。 - 性能瓶颈或可优化的地方。 - 安全性问题如果存在。 - 类型定义不严谨或缺失。 3. **改进建议**针对每个问题提供具体的修改建议或代码示例。 4. **最佳实践**推荐适用于此代码场景的现代JavaScript/TypeScript最佳实践。 请保持专业、友好且建设性的语气。; const userPrompt 请分析以下代码文件${filePath}; // 这里我们简单复用runAgent但传入了更强的系统提示。 // 更优雅的做法是扩展runAgent使其能接受自定义的系统提示。 const messages [ { role: system, content: systemPrompt }, { role: user, content: userPrompt }, ]; // 我们需要修改runAgent以支持传入初始消息这里为了演示我们创建一个变体函数。 // 在实际项目中你应该重构runAgent使其更通用。 return await runAgent(userPrompt); // 简化处理实际应传入自定义消息 }4.3 集成与测试创建一个新的入口文件src/analyze.tsimport { analyzeCode } from ./agent/codeAnalyzer.js; async function main() { const targetFile ./src/tools/index.ts; // 分析我们刚刚写的工具文件 console.log( 开始分析文件: ${targetFile}\n); const analysisReport await analyzeCode(targetFile); console.log( 代码分析报告\n); console.log(analysisReport); } main().catch(console.error);运行这个分析器bun run src/analyze.ts你的Agent会先调用readFile工具获取指定文件的内容然后模型会基于这些代码内容和你设定的“代码审查专家”角色生成一份结构化的分析报告。你可以尝试分析不同的文件看看它能否提出有见地的建议。5. 工程化与部署考量5.1 错误处理与健壮性生产级的Agent必须有完善的错误处理。API调用重试与退避网络请求可能失败。我们需要为client.chat.completions.create添加重试逻辑并使用指数退避策略。import { sleep } from bun; async function createChatCompletionWithRetry(client: OpenAI, options: any, maxRetries 3) { let lastError; for (let i 0; i maxRetries; i) { try { return await client.chat.completions.create(options); } catch (error) { lastError error; if (i maxRetries - 1) { const delay Math.pow(2, i) * 1000 Math.random() * 1000; // 指数退避 console.warn(API调用失败${delay}ms后重试 (${i 1}/${maxRetries})...); await sleep(delay); } } } throw lastError; }工具执行超时有些工具如调用外部API可能挂起。使用Promise.race为工具执行设置超时。async function executeToolWithTimeout(toolFn: ToolFunction, args: any, timeoutMs 10000) { const timeoutPromise new Promisenever((_, reject) { setTimeout(() reject(new Error(工具执行超时 (${timeoutMs}ms))), timeoutMs); }); const toolPromise toolFn(args); return await Promise.race([toolPromise, timeoutPromise]); }输入验证与清理永远不要相信来自模型或用户的输入。在工具函数内部必须对参数进行严格的验证和类型检查。5.2 状态管理与记忆简单的单轮对话Agent很快会遇到瓶颈。一个有用的Agent需要记住对话历史短期记忆甚至拥有长期记忆。短期记忆我们已经通过messages数组实现了对话历史的管理。对于长对话需要注意上下文长度限制可能需要使用“摘要”技术将过长的历史压缩成一段摘要再喂给模型。长期记忆可以通过向量数据库如Chroma、LanceDB来实现。将对话或知识片段转换成向量存储起来当需要相关信息时进行语义搜索召回。这超出了本文的范畴但这是构建强大Agent的必经之路。5.3 部署为服务你的Agent可以部署为HTTP API、CLI工具或集成到其他应用中。HTTP API使用Bun内置的Bun.serve或框架如Hono、Elysia.js快速创建RESTful或GraphQL端点。CLI工具使用像commander或cac这样的库来解析命令行参数让用户可以通过终端与你的Agent交互。Serverless函数Bun应用可以轻松部署到Vercel、Netlify或Cloudflare Workers等Serverless平台。得益于Bun极小的冷启动体积这种部署方式性能会非常好。5.4 监控与日志记录Agent的决策过程、工具调用和模型响应对于调试和优化至关重要。结构化日志例如输出为JSON可以方便地导入到像Elasticsearch或Loki这样的日志系统中进行分析。记录每次调用的Token使用量有助于进行成本分析和优化。6. 常见问题与排查技巧实录在实际操作中你肯定会遇到各种问题。这里记录了几个我踩过的坑和解决方法。问题1模型不调用工具总是直接回答。可能原因1工具描述不清晰。模型的function.description字段至关重要。描述必须清晰、无歧义地说明工具的功能和适用场景。用自然语言写就像你在向一个聪明的实习生解释这个函数是干什么的。可能原因2用户请求与工具能力不匹配。在系统提示词中明确告诉模型“你拥有以下工具请在适当时使用它们”。也可以给用户请求“加引导”例如用户问“今天星期几”你可以让Agent先反问“你是想让我查询当前日期和时间吗我可以调用getCurrentTime工具。”但这会影响体验。排查技巧开启模型的详细日志查看它每一步的“思考”过程如果API支持。或者在调用API时暂时将tool_choice参数设置为required强制模型必须选择一个工具看看它会选哪个这有助于你理解模型是如何理解你的工具定义的。问题2工具调用参数解析失败JSON parse error。可能原因模型生成的参数不是有效的JSON字符串。这通常发生在参数比较复杂时。解决方案在解析前进行健壮性处理。使用try-catch包裹JSON.parse如果失败可以尝试用一些启发式方法清理字符串如去除首尾的json标记或多余的反引号或者直接返回错误信息给模型让它重新生成。问题3上下文长度超限。可能原因对话轮数太多或传入的文件内容太大导致总Token数超过模型限制如128K。解决方案摘要历史定期将之前的对话历史总结成一段简短的摘要替换掉冗长的原始消息。选择性记忆只保留最近N轮对话和最重要的系统指令。分块处理大文件对于代码分析不要一次性传入整个项目。可以设计工具让模型主动请求读取某个特定文件或函数。问题4Agent陷入循环或无关对话。可能原因系统提示词不够明确或者模型在多次工具调用后迷失了方向。解决方案加强系统提示词的约束。例如“你的核心目标是解决用户的问题。在调用工具获得信息后请直接基于信息给出最终答案不要询问用户下一步该怎么做除非信息确实不足以做出判断。” 同时设置maxTurns限制防止无限循环。问题5Bun运行TypeScript时找不到模块Cannot find module。可能原因路径引用错误或者tsconfig.json配置问题。解决方案确保在导入本地模块时使用完整的相对路径包括文件扩展名.js或.ts。Bun在运行TypeScript时通常期望你在导入时写明.js扩展名指向编译后的目标但Bun也能直接解析.ts。最保险的方式是在tsconfig.json中设置moduleResolution: bundler或node并保持导入语句的一致性。如果问题依旧尝试使用绝对路径别名或在package.json中设置type: module。
返回列表