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

资讯详情

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

基于Bun+TypeScript+DeepSeek构建AI Agent:从零实现ReAct智能体

基于Bun+TypeScript+DeepSeek构建AI Agent:从零实现ReAct智能体 1. 项目缘起为什么是 Bun TypeScript DeepSeek最近在社区里看到不少朋友对 AI Agent 开发跃跃欲试但往往被复杂的 Python 环境、五花八门的框架和陡峭的学习曲线劝退。作为一个常年混迹在前端和 Node.js 生态的开发者我一直在想能不能用我们更熟悉的工具链——TypeScript 和 Node.js 生态——来快速搭建一个 AI Agent 的雏形验证想法跑通流程答案是肯定的而且现在正是最好的时机。一方面像 DeepSeek 这样的模型提供了极具竞争力的 API价格亲民能力强大让个人开发者和小团队也能轻松调用顶级的大语言模型。另一方面JavaScript/TypeScript 生态的工具链正在经历一场“性能革命”Bun 的出现就是一个标志。它不仅仅是一个更快的 JavaScript 运行时其内置的包管理器、测试运行器和原生 TypeScript 支持能让我们从项目初始化到依赖安装、开发、测试的整个流程变得无比丝滑。所以这个项目的目标非常明确摒弃繁重的环境配置利用现代、高效的 TypeScript 工具链快速构建一个能与 DeepSeek API 对话、具备简单任务执行能力的 AI Agent 原型。我们不止步于调用 API 返回一句话而是要构建一个具备初步“思考-行动”循环的智能体骨架。这对于想入门 AI 应用开发但又不想完全脱离自己舒适区的 Web 开发者来说是一条非常友好的路径。2. 环境搭建用 Bun 打造极速开发体验工欲善其事必先利其器。我们选择 Bun 作为基石看中的就是它的“开箱即用”和“速度”。传统的 Node.js npm/yarn/pnpm 组合在初始化项目、安装依赖时难免需要等待而 Bun 旨在消除这些摩擦。2.1 初始化你的 TypeScript AI Agent 项目首先确保你已经安装了 Bun。如果还没安装访问 Bun 官网获取最适合你系统的安装命令通常只是一行终端指令的事。安装好后打开终端创建一个新的项目目录并进入mkdir my-ts-ai-agent cd my-ts-ai-agent接下来使用 Bun 初始化项目。Bun 的init命令会交互式地创建package.json文件但我们也可以直接指定配置bun init -y这个命令会快速生成一个基础的package.json。但为了获得更好的 TypeScript 体验我们直接使用一个更针对性的模板。实际上Bun 对 TypeScript 的支持是原生级的我们只需要安装 TypeScript 的类型定义和必要的类型声明包即可。让我们手动完善package.json{ name: my-ts-ai-agent, module: index.ts, type: module, devDependencies: { types/node: ^20.0.0, typescript: ^5.0.0 }, scripts: { start: bun run index.ts, dev: bun --watch run index.ts } }然后安装开发依赖bun add -d types/node typescript接着创建tsconfig.json文件来配置 TypeScript 编译器。对于现代 Node.js/Bun 项目推荐使用如下配置{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: bundler, lib: [ES2022], outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, allowSyntheticDefaultImports: true }, include: [src/**/*], exclude: [node_modules, dist] }同时创建项目源文件目录和入口文件mkdir src touch src/index.ts现在你的项目骨架就搭建好了。你可以尝试在src/index.ts里写一句console.log(‘Hello, AI Agent!’)然后运行bun run dev看看效果。Bun 的--watch模式会在你保存文件时自动重新运行开发体验非常流畅。注意很多人会纠结是否要安装ts-node或types/bun。对于 Bun 项目完全不需要。Bun 运行时内置了 TypeScript 编译器可以直接执行.ts文件。types/node是为了提供 Node.js API 的类型提示这在很多工具函数中会用到。2.2 安装核心依赖与 DeepSeek 对话我们的 Agent 需要大脑这里我们选择 DeepSeek 的 API。为了调用它我们需要一个 HTTP 客户端。虽然 Node.js 有内置的fetchBun 也支持但使用一个功能更完善的 SDK 或库会让代码更简洁、健壮。这里我推荐openai这个官方库因为它不仅支持 OpenAI其通用的接口设计也易于适配其他兼容 OpenAI API 格式的提供商DeepSeek 就是其中之一。使用 Bun 安装它bun add openai此外为了构建一个具备“思考-行动”能力的 Agent我们通常需要处理结构化的输出比如让模型决定下一步该调用哪个工具。虽然最新的 Chat Completions API 支持 JSON Mode但为了更精细的控制和更好的开发体验我们可以引入zod这个强大的 TypeScript 模式验证库它可以帮助我们定义和验证模型返回的数据结构。bun add zod现在我们的核心依赖就准备好了openai用于通信zod用于结构化。接下来让我们开始编写 Agent 的核心逻辑。3. 核心架构构建一个简单的 ReAct 模式 AgentAI Agent 的核心思想是让大语言模型不仅生成回答还能根据目标自主决定是否使用外部工具如搜索、计算、查询数据库来获取信息循环此过程直至完成任务。ReActReasoning Acting是其中一种经典且有效的范式。我们的第一个 Agent 将实现一个简化版的 ReAct 循环思考 - 决定是否使用工具 - 执行工具 - 将结果反馈给模型 - 最终回答。3.1 定义工具系统赋予 Agent “手脚”首先在src/tools.ts中定义我们的工具。一个工具本质上是一个函数它有明确的名称、描述、参数格式和执行逻辑。// src/tools.ts import { z } from ‘zod‘; // 定义工具的类型 export interface Tool { name: string; description: string; parameters: z.ZodSchema; // 使用 zod 定义参数模式 execute: (args: any) Promisestring; // 执行函数返回字符串结果 } // 示例工具1计算器 export const calculatorTool: Tool { name: ‘calculator‘, description: ‘执行简单的数学计算如加、减、乘、除。‘, parameters: z.object({ expression: z.string().describe(‘数学表达式例如: “3 5 * 2”‘), }), execute: async ({ expression }) { try { // 警告在实际生产中直接使用 eval 是极其危险的 // 这里仅作演示应使用安全的数学表达式解析库如 math.js const result eval(expression); return 计算结果${expression} ${result}; } catch (error) { return 计算失败${error.message}; } }, }; // 示例工具2获取当前时间 export const getCurrentTimeTool: Tool { name: ‘get_current_time‘, description: ‘获取当前的日期和时间。‘, parameters: z.object({}), // 此工具不需要参数 execute: async () { const now new Date(); return 当前时间是${now.toLocaleString(‘zh-CN‘)}; }, }; // 工具集合 export const availableTools: Tool[] [calculatorTool, getCurrentTimeTool]; // 根据工具名查找工具 export function getToolByName(name: string): Tool | undefined { return availableTools.find(tool tool.name name); }这里有几个关键点使用 Zod 定义参数这不仅能生成清晰的 JSON Schema 给模型看还能在后续验证模型返回的参数确保类型安全。工具描述至关重要description字段是模型理解工具用途的唯一依据必须清晰、准确。安全警告示例中的calculatorTool使用了eval这在任何实际项目中都是严重的安全漏洞。此处仅为演示工具的执行流程真实场景务必使用像math.js这样的安全库。3.2 实现 Agent 执行引擎驱动思考循环接下来在src/agent.ts中创建 Agent 的核心类。它将负责管理对话历史、调用模型、解析模型决策、执行工具并循环。// src/agent.ts import OpenAI from ‘openai‘; import { z } from ‘zod‘; import { availableTools, getToolByName, type Tool } from ‘./tools.js‘; // 定义模型返回的“行动”结构 const actionSchema z.object({ thought: z.string().describe(‘模型当前的思考过程‘), action: z.enum([‘final_answer‘, ‘use_tool‘]).describe(‘决定下一步行动直接回答或使用工具‘), tool_name: z.string().optional().describe(‘如果 action 是 use_tool指定工具名称‘), tool_input: z.record(z.any()).optional().describe(‘如果 action 是 use_tool指定工具输入参数‘), }); type AgentAction z.infertypeof actionSchema; export class SimpleReActAgent { private openai: OpenAI; private messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] []; constructor(apiKey: string, baseURL: string ‘https://api.deepseek.com‘) { this.openai new OpenAI({ apiKey, baseURL, // DeepSeek API 的端点 }); // 初始化系统提示词设定 Agent 的角色和行为准则 this.messages.push({ role: ‘system‘, content: 你是一个乐于助人的 AI 助手可以调用工具来帮助用户解决问题。 你可以使用的工具如下 ${availableTools.map(t - ${t.name}: ${t.description}).join(‘\n‘)} 请严格按照以下 JSON 格式回应 { “thought”: “你的推理过程”, “action”: “final_answer” 或 “use_tool”, “tool_name”: “工具名仅当 action 为 use_tool 时”, “tool_input”: {“参数名”: “值”} 仅当 action 为 use_tool 时 } 如果你已经得到足够信息可以直接给出最终答案final_answer。如果需要使用工具请确保 tool_input 的参数完全匹配工具描述。, }); } // 核心的循环执行方法 async run(userInput: string, maxIterations: number 5): Promisestring { console.log(用户: ${userInput}); this.messages.push({ role: ‘user‘, content: userInput }); for (let i 0; i maxIterations; i) { console.log(\n--- 第 ${i 1} 轮思考 ---); // 1. 调用模型获取决策 const response await this.openai.chat.completions.create({ model: ‘deepseek-chat‘, // 使用 DeepSeek 的模型 messages: this.messages, temperature: 0.1, // 低温度保证输出格式稳定 response_format: { type: ‘json_object‘ }, // 强制返回 JSON }); const content response.choices[0]?.message?.content; if (!content) { throw new Error(‘模型未返回有效内容‘); } // 2. 解析并验证模型返回的 JSON let action: AgentAction; try { const parsed JSON.parse(content); action actionSchema.parse(parsed); // Zod 验证确保结构正确 } catch (error) { console.error(‘解析模型响应失败:‘, content, error); this.messages.push({ role: ‘assistant‘, content: 我尝试响应时格式出错了${error.message}。请重新指导我。, }); continue; // 如果解析失败将错误信息反馈给模型进入下一轮 } console.log(助手思考: ${action.thought}); // 3. 根据决策执行相应操作 if (action.action ‘final_answer‘) { // 模型决定直接给出最终答案 console.log(行动: 给出最终答案); this.messages.push({ role: ‘assistant‘, content: action.thought }); return action.thought; // 返回最终答案结束循环 } else if (action.action ‘use_tool‘ action.tool_name) { // 模型决定使用工具 console.log(行动: 使用工具 [${action.tool_name}], action.tool_input); const tool getToolByName(action.tool_name); if (!tool) { const errorMsg 未知工具${action.tool_name}; this.messages.push({ role: ‘assistant‘, content: errorMsg }); console.log(工具执行错误: ${errorMsg}); continue; } // 验证工具输入参数 let toolArgs: any; try { toolArgs tool.parameters.parse(action.tool_input || {}); } catch (error) { const errorMsg 工具参数错误${error.message}; this.messages.push({ role: ‘assistant‘, content: errorMsg }); console.log(工具执行错误: ${errorMsg}); continue; } // 执行工具并获取结果 const toolResult await tool.execute(toolArgs); console.log(工具结果: ${toolResult}); // 将工具执行结果作为新的上下文信息追加到对话历史 this.messages.push({ role: ‘assistant‘, content: 我使用了工具 ${action.tool_name}结果是${toolResult}, }); // 注意这里我们没有立即返回循环继续模型将基于新信息进行下一轮思考 } } // 如果超过最大循环次数仍未得出最终答案 return ‘抱歉经过多次尝试仍未能解决问题。请尝试更清晰地描述您的问题。‘; } }这个SimpleReActAgent类是整个项目的核心它实现了以下关键机制对话历史管理this.messages数组维护了完整的对话上下文包括系统指令、用户问题、模型的历史思考和工具执行结果。结构化输出解析利用response_format: { type: ‘json_object‘ }和zod库强制并验证模型返回一个结构化的决策对象。这是实现可靠 Agent 控制流的基础。循环与终止条件通过for循环控制最大迭代次数 (maxIterations)防止陷入无限循环。当模型返回action: ‘final_answer‘时循环终止。错误处理与鲁棒性对模型返回的非 JSON、未知工具、参数错误等情况进行了捕获和处理并将错误信息反馈给模型让它有机会自我纠正。4. 实战演练让你的第一个 Agent 跑起来现在让我们把各部分组装起来并创建一个实际的交互示例。首先你需要获取 DeepSeek 的 API Key。访问 DeepSeek 平台注册账号并创建 API Key。重要提示API Key 是敏感信息切勿直接硬编码在代码中或提交到版本控制系统。务必使用环境变量管理。创建.env文件来存储密钥# .env DEEPSEEK_API_KEYyour_api_key_here然后安装dotenv包来加载环境变量bun add dotenv接下来创建主入口文件src/index.ts// src/index.ts import { config } from ‘dotenv‘; import { SimpleReActAgent } from ‘./agent.js‘; // 加载环境变量 config(); async function main() { const apiKey process.env.DEEPSEEK_API_KEY; if (!apiKey) { console.error(‘错误请在 .env 文件中设置 DEEPSEEK_API_KEY‘); process.exit(1); } // 初始化 Agent const agent new SimpleReActAgent(apiKey); // 示例对话 const questions [ ‘现在几点了‘, ‘计算一下 15 加上 27 再乘以 3 等于多少‘, ‘先告诉我时间然后计算 (100 - 58) / 7 的结果保留两位小数。‘, ]; for (const question of questions) { console.log(‘\n 新问题 ‘); try { const finalAnswer await agent.run(question); console.log(\n最终答案: ${finalAnswer}); } catch (error) { console.error(‘运行出错:‘, error); } } } main().catch(console.error);现在运行你的 Agentbun run start # 或者使用监听模式 # bun run dev你应该能在终端看到类似以下的输出清晰地展示了 Agent 内部的“思考-行动”循环 新问题 用户: 现在几点了 --- 第 1 轮思考 --- 助手思考: 用户想知道当前时间。我可以使用 get_current_time 工具来获取。 行动: 使用工具 [get_current_time] {} 工具结果: 当前时间是2024/5/27 下午3:45:20 --- 第 2 轮思考 --- 助手思考: 我已经通过工具获取了当前时间现在可以将这个信息作为最终答案告诉用户。 行动: 给出最终答案 最终答案: 当前时间是2024/5/27 下午3:45:20对于更复杂的链式问题如第三个问题Agent 会展示出多轮交互的能力先调用时间工具再调用计算器工具最后综合信息给出答案。5. 避坑指南与进阶思考第一次运行很可能不会一帆风顺。以下是我在搭建过程中遇到的一些典型问题及解决方案希望能帮你少走弯路。5.1 模型响应格式不稳定问题即使设置了response_format: { type: ‘json_object‘ }模型偶尔还是会返回非 JSON 或格式错误的文本导致JSON.parse失败。根因分析大语言模型本质上是概率生成器虽然指令遵循能力很强但并非百分之百可靠。系统提示词中对格式的描述不够强制或者温度 (temperature) 参数设置过高都可能增加输出的随机性。解决方案强化系统提示词在提示词中更严厉地强调格式要求。例如“你必须且只能返回一个有效的 JSON 对象不要有任何额外的解释、标记或代码块。”降低温度将temperature设置为一个较低的值如 0.1减少随机性使输出更确定。实现解析重试机制在代码中包裹一层重试逻辑。如果解析失败将错误信息连同原问题一起重新发送给模型要求它纠正。// 改进的解析逻辑示例 async function parseModelResponseWithRetry(content: string, maxRetries 2): PromiseAgentAction { for (let attempt 0; attempt maxRetries; attempt) { try { const parsed JSON.parse(content); return actionSchema.parse(parsed); } catch (parseError) { if (attempt maxRetries - 1) throw parseError; // 可以在这里记录日志或尝试一些简单的字符串清理如去除 markdown 代码块标记 content content.replace(/^json\s*|\s*$/g, ‘‘).trim(); } } throw new Error(‘解析失败‘); }5.2 工具参数验证失败问题模型返回的tool_input与 Zod Schema 不匹配例如参数名拼写错误、类型不对、缺少必需参数。根因分析模型对工具参数格式的理解可能出现偏差尤其是当参数结构复杂时。解决方案在提示词中提供更清晰的示例除了文字描述在系统提示词中直接给出 1-2 个完整的、格式正确的 JSON 调用示例。使用更严格的 Zod Schema充分利用 Zod 的.describe()方法为每个字段添加自然语言描述这些描述有时会被模型用于理解。提供验证失败的反馈正如我们在agent.ts中所做当验证失败时将具体的错误信息如“缺少必需字段expression”反馈给模型让它在下一次调用时修正。5.3 循环无法终止或陷入死循环问题Agent 反复调用同一个工具或者在不该调用工具的时候调用始终无法触发final_answer。根因分析可能是任务本身过于模糊模型无法从工具结果中推断出最终答案也可能是系统提示词没有清晰定义“何时任务算完成”。解决方案明确终止条件在系统提示词中更具体地说明什么情况下应该给出最终答案。例如“当你认为已经充分、准确地回答了用户的原始问题时请使用final_answer。”设置迭代上限这是必须的安全网。我们代码中的maxIterations参数就是为了防止无限循环。引入超时机制除了迭代次数还可以为整个run方法设置一个总的时间限制。优化工具结果的表现形式确保工具返回的结果是清晰、信息丰富的便于模型理解并做出下一步决策。5.4 性能与成本考量问题每次循环都是一次 API 调用复杂的任务可能导致调用次数多延迟高费用也相应增加。根因分析ReAct 模式本质上是多轮对话成本与任务复杂度成正比。优化思路任务规划在开始循环前让模型先做一个高层级的任务分解计划然后按计划执行减少不必要的“来回”思考。并行工具调用如果多个工具调用之间没有依赖关系可以尝试让模型一次性规划多个工具使用然后并行执行最后汇总结果。这需要更复杂的提示工程和输出格式设计。选择性价比更高的模型DeepSeek API 本身在成本和性能上已有优势。对于某些简单、确定性的工具调用步骤甚至可以尝试用更小、更快的模型如果可用来驱动。实现本地缓存对于重复性的查询如获取静态数据可以在工具层实现缓存机制避免重复调用外部接口。6. 从原型到产品下一步可以做什么我们这个简单的 ReAct Agent 已经具备了核心骨架但它离一个“可用”的产品还有距离。以下是一些值得投入的进阶方向你可以选择一两个深入下去6.1 集成更强大的工具目前只有计算器和时钟。真正的 Agent 威力来自于丰富的工具集。考虑集成网络搜索通过 SerpAPI 或自定义爬虫注意合规让 Agent 获取实时信息。代码执行在安全的沙箱环境中运行用户提供的代码片段并返回结果。文件操作读取、分析本地或云存储中的文档如 PDF、Word。第三方 API连接天气预报、股票、翻译、邮件发送等服务。每增加一个工具都记得用 Zod 清晰地定义其输入模式并编写详细的描述。6.2 引入记忆与持久化当前的 Agent 是“无状态”的每次运行都是全新的对话。为了实现更连贯的交互需要引入记忆机制。短期记忆对话历史我们已经用this.messages实现了但可以优化例如对长对话进行摘要防止上下文超出模型限制。长期记忆将重要的交互信息存入数据库如 SQLite、PostgreSQL或向量数据库如 Chroma、Weaviate以便在未来的对话中检索和回忆。这是实现个性化 Agent 的关键。6.3 构建更复杂的 Agent 架构多 Agent 协作创建多个具有不同专长如研究员、写手、校对员的 Agent让它们通过一个“协调员” Agent 或固定的工作流来协作完成复杂任务。分层规划与执行采用更先进的框架如 LangGraph也有 JS/TS 版本的思路将规划Plan、执行Act、观察Observe的循环图形化、结构化处理带有分支和循环的复杂任务流。集成现有框架虽然我们从零开始构建有助于理解原理但生产环境中可以考虑使用成熟的 TS/JS Agent 框架如langchain/langgraph、Vercel AI SDK的 Agent 功能等它们提供了更多开箱即用的模块和最佳实践。6.4 开发交互界面命令行演示毕竟有限。为你的 Agent 添加一个交互界面能极大提升体验Web API使用 Express.js、Hono 或 Next.js 快速搭建一个 RESTful API接收用户查询并返回 Agent 的执行结果。聊天界面利用 Next.js 或类似框架配合 Vercel AI SDK快速构建一个类似于 ChatGPT 的流式聊天界面实时展示 Agent 的思考过程和工具调用。集成到现有应用将 Agent 作为后端服务为你现有的笔记软件、项目管理工具或内部系统添加智能辅助功能。通过这个从 Bun 初始化到 DeepSeek API 集成的完整旅程我们不仅得到了一个可以运行的 AI Agent 原型更重要的是我们理解了其核心的工作机制提示词工程、结构化输出解析、工具抽象与执行、以及循环控制逻辑。这套模式是构建更复杂智能应用的基础。接下来就基于这个骨架用你熟悉的 TypeScript 生态和工具去创造更有趣、更有用的 AI 智能体吧。
返回列表