从零构建AI Agent:基于Node.js与Function Calling的股价查询智能体实践

发布时间:2026/8/9 20:08:42
从零构建AI Agent:基于Node.js与Function Calling的股价查询智能体实践 1. 项目概述从概念到实践的AI Agent构建之旅最近和不少同行交流发现大家一提到AI Agent都觉得它是个“黑盒子”——听起来很酷但具体怎么从零开始搭一个尤其是让它能真正干点实事比如查个股价、订个餐就有点无从下手了。我自己也是从一堆论文和开源项目里摸爬滚打过来踩过不少坑。今天我就想抛开那些高大上的术语用最接地气的方式和你一起拆解一个AI Agent的核心骨架并手把手实现一个能联网查询实时股价的智能体。我们用的技术栈会很务实Node.js作为运行环境结合当下主流的大语言模型LLM接口。你会发现所谓智能体其核心就是三要素的有机组合一个会思考的“大脑”LLM、一双能操作的“手”Tools、以及一个负责记事的“小本本”记忆体。搞懂这三样东西如何协同工作你就能举一反三构建出解决各种实际问题的智能助手。2. AI Agent核心概念深度拆解在开始写代码之前我们必须把地基打牢。很多人一上来就找框架、抄代码结果遇到问题根本不知道从何调试。理解核心概念是为了让你在开发时心里有张清晰的地图。2.1 大脑大语言模型LLM的角色与局限LLM是AI Agent的“大脑”负责理解、推理和决策。你可以把它想象成一个博学但“四肢不勤”的顾问。它读过海量文本知识渊博能和你流畅对话分析问题头头是道。但是它有几个关键局限知识截止性它的知识来自训练数据对于训练截止日期之后的新事件比如今天某支股票的最新价格、非公开数据或实时信息一无所知。缺乏行动能力它无法直接操作外部系统比如调用一个API、查询数据库、点击一个按钮。它只能“说”不能“做”。可能产生“幻觉”当被问到不确定的事情时它可能会编造一个看似合理但完全错误的答案。正因为这些局限我们才需要为它配备“工具”。LLM在智能体中的核心任务是根据我们的指令比如“帮我查一下苹果公司的股价”和当前的对话上下文决定下一步该做什么是直接回答还是需要调用某个工具来获取信息。2.2 双手工具Tools的定义与设计范式Tools就是AI Agent的“双手”是它感知和影响外部世界的唯一途径。一个Tool本质上是一个函数它封装了一个特定的能力比如“搜索网络”、“查询数据库”、“发送邮件”。LLM通过调用这些Tool来弥补自身的不足。设计一个良好的Tool有几个关键原则功能单一且明确一个Tool只做一件事并且要把这件事描述清楚。例如“get_current_stock_price”就比“handle_finance_request”要清晰得多。描述清晰你需要用自然语言向LLM清晰地描述这个Tool是干什么的、需要什么输入参数、会返回什么。这就像是给LLM一份工具说明书。安全可控Tool可能执行具有副作用的操作如发邮件、写数据库。必须在设计时就考虑权限控制和输入验证防止LLM被诱导执行危险操作。目前让LLM使用Tools主要有两种主流范式Function Calling函数调用这是由OpenAI等厂商推广的标准。你在请求LLM时同时传入一组你定义好的函数Tools的描述。LLM在分析用户问题后如果认为需要调用某个Tool它不会直接执行而是返回一个结构化的JSON告诉你它想调用哪个函数以及传入什么参数。执行权完全掌握在你的代码手里。这是目前最安全、最推荐的方式。ReActReason Act这是一种通过提示工程Prompt Engineering实现的模式。你会在给LLM的提示词中规定一个固定的输出格式比如“Thought: 思考过程... Action: 工具名... Action Input: 工具参数...”。LLM会按照这个格式输出你的程序再解析这个输出执行对应的Tool。这种方式更灵活但对提示词设计的要求高且输出格式不稳定。在我们的股价查询Agent中将采用更稳定可靠的Function Calling方式。2.3 记忆体短期记忆与长期记忆的协同记忆体是AI Agent的“小本本”负责存储对话的历史和上下文。没有记忆每次对话都是全新的开始Agent就无法进行多轮连贯的交流也无法从历史中学习。记忆通常分为两类短期记忆Conversation Buffer也叫对话缓存。它保存当前会话窗口内的所有消息用户输入、AI回复、工具调用及结果。当上下文长度超过LLM的限制时就需要对这部分记忆进行裁剪或总结。这是实现多轮对话的基础。长期记忆Vector Store当需要让Agent记住大量、超出上下文长度的信息时比如公司知识库、用户个人偏好就需要长期记忆。通常的做法是将文本信息转换成向量Embedding存入向量数据库。当需要相关信息时通过语义搜索相似度匹配从向量库中召回相关内容再注入到短期记忆中供LLM参考。对于查股价这种简单任务我们暂时不需要复杂的长期记忆一个高效的短期记忆管理就足够了。2.4 智能体运行的核心循环推理、行动、观察理解了三大件我们来看它们是如何动起来的。一个典型的AI Agent运行遵循一个“推理-行动-观察”循环ReAct Loop的抽象化推理LLM根据用户输入和当前的记忆历史对话思考当前的目标和状态。它会判断用户的问题我能直接回答吗如果不能我需要使用哪个工具需要提供什么参数行动如果LLM决定要使用工具它就会通过Function Calling输出一个工具调用请求。你的程序接收到这个请求后安全地执行对应的工具函数。观察工具执行完毕后会返回一个结果可能是数据也可能是成功/失败的状态。这个结果会被添加到对话记忆中形成一条新的上下文信息“用户使用了XX工具得到了YY结果”。循环LLM基于包含了工具执行结果的新记忆再次进行推理。它可能会说“根据查到的股价我现在可以回答用户了”然后生成最终回复也可能发现还需要调用另一个工具于是进入下一个循环。这个循环会一直持续直到LLM认为已经收集到足够的信息可以给出最终答案为止。整个过程中记忆体保证了信息的连贯传递。3. 实战构建一个股价查询AI Agent理论说得再多不如动手一行。接下来我们用Node.js一步步实现这个能查股价的智能体。我选择Node.js是因为其异步特性非常适合处理LLM API调用和网络请求生态也丰富。3.1 开发环境搭建与核心依赖选择首先确保你的系统已经安装了Node.js建议版本18或以上和npm。然后我们初始化项目并安装核心依赖。mkdir stock-price-agent cd stock-price-agent npm init -y我们将使用以下几个关键库openai用于调用OpenAI的Chat Completions API它原生支持Function Calling。如果你使用其他LLM如Anthropic Claude、国内大模型需要换用对应的SDK但概念相通。axios一个流行的HTTP客户端用于我们自定义的股价查询工具函数去调用金融数据API。dotenv管理环境变量安全地存储你的API密钥。安装它们npm install openai axios dotenv接下来创建项目的基本结构stock-price-agent/ ├── .env # 存储API密钥不要提交到Git ├── .gitignore # 忽略node_modules和.env ├── package.json ├── index.js # Agent主循环逻辑 └── tools/ # 工具函数目录 └── stockTool.js # 股价查询工具在.env文件中填入你的OpenAI API Key和一个金融数据API的Key后面会说明OPENAI_API_KEYsk-your-openai-key-here FINANCIAL_DATA_API_KEYyour-financial-data-key注意.env文件务必添加到.gitignore中避免密钥泄露。这是安全开发的第一步。3.2 核心工具函数股价查询的实现一个工具的本质就是一个函数。我们在tools/stockTool.js中定义它。// tools/stockTool.js const axios require(axios); /** * 获取指定股票代码的实时股价 * param {string} symbol - 股票代码例如AAPL (苹果), 000001.SS (上证指数) * returns {Promisestring} - 返回股价信息或错误信息 */ async function getCurrentStockPrice(symbol) { // 注意这里需要一个真实的金融数据API // 示例使用一个假设的API端点实际中你可以用Alpha Vantage、Yahoo Finance非官方等 const apiKey process.env.FINANCIAL_DATA_API_KEY; const url https://api.example-finance.com/quote?symbol${symbol}apikey${apiKey}; try { const response await axios.get(url); const data response.data; // 根据实际API返回结构解析数据 // 假设返回格式为 { symbol: AAPL, price: 175.25, change: 1.5 } if (data data.price) { return 股票 ${data.symbol} 的当前价格为 $${data.price}今日变动 ${data.change 0 ? : }${data.change}。; } else { return 未能获取到股票代码 ${symbol} 的有效价格信息。; } } catch (error) { console.error(调用股价API失败:, error.message); return 查询股价时出现错误${error.message}。请检查股票代码是否正确或稍后再试。; } } // 关键定义工具的“说明书”用于Function Calling const toolDefinition { type: function, function: { name: getCurrentStockPrice, // 函数名与上面一致 description: 根据股票代码例如AAPL, 000001.SS获取该股票的实时最新价格和涨跌幅。, // 给LLM看的描述 parameters: { type: object, properties: { symbol: { type: string, description: 股票的交易代码例如AAPL苹果美股000001.SS上证指数。, }, }, required: [symbol], // 必须的参数 additionalProperties: false, }, }, }; module.exports { getCurrentStockPrice, toolDefinition, };实操心得API选择免费的金融数据API通常有调用频率限制。对于个人项目Alpha Vantage的免费层足够用。你也可以用yahoo-finance2这类封装好的npm库非官方但稳定性和合规性需自行评估。错误处理工具函数内部必须有完善的错误处理try-catch。永远不要假设API调用一定会成功。将错误信息以友好的方式返回给LLM它才能更好地向用户解释。描述的重要性toolDefinition中的description和parameters.description至关重要。LLM完全依赖这些文字描述来理解何时以及如何使用这个工具。描述要具体、无歧义。3.3 智能体主循环集成LLM与工具调用这是最核心的部分我们在index.js中实现Agent的大脑和循环逻辑。// index.js require(dotenv).config(); const OpenAI require(openai); const { getCurrentStockPrice, toolDefinition } require(./tools/stockTool); // 初始化OpenAI客户端 const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // 简单的对话记忆存储短期记忆 let conversationHistory []; /** * 主函数运行AI Agent * param {string} userInput - 用户输入 */ async function runAgent(userInput) { // 1. 将用户输入加入历史 conversationHistory.push({ role: user, content: userInput }); // 2. 准备发送给LLM的消息包括历史对话和工具定义 const messagesForLLM [ { role: system, content: 你是一个专业的股票查询助手。你可以使用工具来获取实时股价。如果用户问的股票你不确定代码可以询问澄清。回答要简洁专业。, }, ...conversationHistory, // 注入所有历史对话 ]; // 3. 第一次调用LLM让它决定是直接回答还是调用工具 const firstResponse await openai.chat.completions.create({ model: gpt-4o-mini, // 或 gpt-3.5-turbo后者成本更低 messages: messagesForLLM, tools: [toolDefinition], // 关键告诉LLM有哪些工具可用 tool_choice: auto, // 让LLM自行决定是否调用工具 }); const responseMessage firstResponse.choices[0].message; console.log(LLM初始回复:, JSON.stringify(responseMessage, null, 2)); // 4. 将LLM的回复也加入历史包含可能的工具调用请求 conversationHistory.push(responseMessage); // 5. 检查LLM是否想要调用工具 const toolCalls responseMessage.tool_calls; if (toolCalls) { // 6. 处理每一个工具调用理论上一次可能调用多个 for (const toolCall of toolCalls) { const functionName toolCall.function.name; const functionArgs JSON.parse(toolCall.function.arguments); console.log(检测到工具调用: ${functionName}参数:, functionArgs); let toolResponse ; // 根据工具名执行对应的函数 if (functionName getCurrentStockPrice) { toolResponse await getCurrentStockPrice(functionArgs.symbol); } else { toolResponse 错误未知的工具调用 ${functionName}; } console.log(工具执行结果: ${toolResponse}); // 7. 将工具执行结果作为一条新消息加入历史告知LLM conversationHistory.push({ role: tool, tool_call_id: toolCall.id, // 必须对应之前的tool_call id content: toolResponse, }); } // 8. 第二次调用LLM让它基于工具执行结果生成最终回答 const secondResponse await openai.chat.completions.create({ model: gpt-4o-mini, messages: [ { role: system, content: 你是一个专业的股票查询助手。, }, ...conversationHistory, // 此时历史已经包含了工具执行结果 ], // 这次不需要再传递tools除非可能触发链式调用 }); const finalMessage secondResponse.choices[0].message; conversationHistory.push(finalMessage); console.log(\n 最终回复 ); console.log(finalMessage.content); return finalMessage.content; } else { // LLM没有调用工具直接回复 console.log(\n 直接回复 ); console.log(responseMessage.content); return responseMessage.content; } } // 模拟一次对话 (async () { try { const answer await runAgent(苹果公司现在的股价是多少); // 可以继续 runAgent(那特斯拉的呢) 测试多轮对话记忆 } catch (error) { console.error(Agent运行出错:, error); } })();代码逻辑解析记忆管理我们用conversationHistory数组简单模拟了短期记忆。每次交互的user、assistant和tool消息都被按顺序存入。工具调用触发在第一次调用chat.completions.create时我们通过tools参数将工具定义传给LLM。LLM分析整个对话历史后如果判断需要查股价它会在回复的tool_calls字段里返回调用信息。安全执行我们的代码解析tool_calls并在自己的服务器上执行对应的getCurrentStockPrice函数。我们绝不会让LLM直接执行代码这是Function Calling模式的安全基石。结果反馈工具执行后我们将结果以role: tool的消息格式并附上对应的tool_call_id添加回对话历史。这相当于告诉LLM“你刚才让我查的工具结果是这样的”。最终生成LLM看到工具返回的结果后结合所有上下文生成面向用户的、自然流畅的最终答案。3.4 运行测试与效果验证现在让我们运行这个Agent。在终端执行node index.js你应该会看到类似以下的输出流这清晰地展示了Agent的思考和工作过程LLM初始回复: { role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: getCurrentStockPrice, arguments: {\symbol\: \AAPL\} } } ] } 检测到工具调用: getCurrentStockPrice参数: { symbol: AAPL } 工具执行结果: 股票 AAPL 的当前价格为 $175.25今日变动 1.5。 最终回复 根据实时数据苹果公司AAPL的股票当前价格为 **$175.25**今日上涨 **1.5**。成功的关键标志LLM的第一次回复content为null但包含了tool_calls这说明它成功决定要调用工具。工具函数被正确触发并返回了格式化的股价信息。LLM在收到工具结果后生成了整合信息的、人性化的最终回复。你可以尝试不同的问法比如“特斯拉的股票咋样了”或“AAPL和MSFT今天股价对比一下”观察Agent如何解析你的意图并可能发起多次工具调用。4. 深入优化与高级特性探讨一个基础的Agent跑起来后我们会发现很多可以优化和深入的点。这才是从“能用”到“好用”的关键。4.1 记忆管理的优化策略我们上面用的conversationHistory数组会无限增长而所有LLM都有上下文长度限制如GPT-4是128K但成本高。我们需要策略来管理记忆滑动窗口只保留最近N条对话。简单粗暴但会丢失早期的重要信息。总结压缩当对话轮数增多时主动调用LLM对之前的对话历史进行总结将冗长的历史压缩成一段摘要然后用“摘要近期对话”作为新的记忆。这能极大地节省Token并保留核心信息。向量记忆长期记忆对于需要记忆大量静态知识如产品手册、个人资料的场景可以将这些知识文本转换成向量存入如Chroma、Pinecone这类向量数据库。当用户提问时先进行向量相似度搜索把相关片段作为上下文注入给LLM。实操心得对于大多数对话式Agent实现一个“总结压缩”策略性价比最高。你可以在conversationHistory长度超过某个阈值比如10轮后触发一个总结任务。4.2 复杂工作流的编排LangGraph与自定义状态机我们的例子是简单的“用户问 - LLM决定 - 调用一个工具 - 回复”。但现实任务可能更复杂比如 “帮我查一下苹果的股价如果高于180美元就发邮件提醒我否则在日历上创建一个下周再看的事件。”这涉及到多个工具的连续调用和条件判断。此时就需要工作流编排。有两个主流方向使用框架像LangChain的LangGraph、微软的AutoGen、或Dify的工作流提供了可视化或代码化的方式来编排多个Agent或工具的执行顺序和条件分支。自定义状态机对于逻辑明确的任务你可以自己用代码实现一个状态机。定义好任务的不同状态如“等待查询股价”、“判断价格”、“发送邮件”、“创建日历事件”让LLM或规则引擎驱动状态转移。选择建议如果你的业务逻辑非常固定且复杂自定义状态机更可控。如果是探索性的、需要大量LLM进行决策的复杂任务LangGraph这类框架更合适。4.3 工具生态的扩展与实践股价查询只是一个工具。一个强大的Agent背后是一个丰富的工具生态。你可以考虑添加网络搜索让Agent能获取最新新闻和知识。可以使用Serper API、Exa AI等。代码执行在安全沙箱中执行数学计算或数据分析如使用math.js或连接Jupyter内核。数据库操作封装安全的查询函数让Agent能查询用户订单、产品信息等。软件操作通过封装API让Agent能操作其他软件如发送Slack消息、创建Notion页面、管理Trello看板。安全警告每增加一个工具就增加了一个攻击面。必须为每个工具设计严格的输入验证和权限控制。例如数据库操作工具绝不能接受任意的SQL字符串而应该只有几个预定义的、参数化的查询函数。4.4 提示工程与系统指令的打磨系统提示词system角色的content是Agent的“人格”和“行为准则”设定器。它的好坏直接影响Agent的表现。明确角色你是一个专业的股票查询助手。规定能力范围你只能使用提供的工具来获取实时信息。对于不知道或无法查询的信息要诚实告知。设定输出格式回答时先给出核心数据再简要分析。价格请用加粗标出。安全与合规要求你不得提供投资建议。所有数据仅供参考。你需要像产品经理一样不断根据测试结果调整和优化这段提示词。一个技巧是将复杂的指令放在系统提示词的开头因为LLM对开头的内容关注度更高。5. 常见问题与故障排查实录在实际开发和调试中你肯定会遇到各种问题。这里记录了几个最典型的坑和解决方案。5.1 LLM不调用工具检查工具描述与提示词问题你明明定义了工具但LLM总是尝试直接回答而不触发工具调用。排查步骤检查工具描述toolDefinition里的description是否清晰、无歧义是否准确描述了工具的功能和适用场景用更直接的语言试试比如“获取实时股价”强调“实时”。检查系统提示词你的system提示词是否明确指示LLM去使用工具可以加入强引导如“当用户询问股价、股票价格等金融实时数据时你必须使用getCurrentStockPrice工具来获取信息。”检查用户问题用户的问题是否足够明确有时LLM会因为问题模糊而选择直接泛泛而谈。让用户的问题更具体如“用工具查一下AAPL的股价”。启用调试打印出你发送给LLM的完整消息列表messagesForLLM确认工具定义确实被包含在内。5.2 工具调用参数错误强化参数描述与示例问题LLM调用了工具但传入的参数格式不对或值错误比如把公司名“Apple”而不是股票代码“AAPL”传给了symbol。排查步骤细化参数描述在parameters.properties.symbol.description里提供更详细的说明和示例。例如“股票的交易代码必须是标准代码。示例苹果公司对应AAPL腾讯控股对应0700.HK上证指数对应000001.SS。”在系统提示词中补充在系统提示词里再次强调“所有股票查询必须使用标准的股票交易代码而不是公司名称。”增加预处理在工具函数内部可以增加简单的参数校验和清洗逻辑。例如如果传入的是“apple”可以尝试映射到“AAPL”或者直接返回错误要求用户提供代码。5.3 多轮对话中记忆混乱实现记忆压缩与修剪问题对话进行到10轮以后Agent可能开始胡言乱语或者忘记了很早之前的关键信息。解决方案立即方案滑动窗口只保留最近10条消息。简单实现conversationHistory conversationHistory.slice(-20);假设每条交互产生2条消息。进阶方案总结压缩实现一个summarizeConversation函数定期调用LLM来总结之前的对话。例如async function summarizeHistory(longHistory) { const summaryPrompt 请将以下对话历史简洁地总结成一段摘要保留关于股票代码、价格和用户核心意图的信息\n${longHistory}; // 调用LLM生成摘要 // 返回摘要文本 }然后用[系统提示 历史摘要 最近3轮对话]作为新的上下文。终极方案向量记忆为每一段重要的对话或信息生成向量嵌入存入向量库。在每次需要时进行语义检索动态注入最相关的记忆片段。这适合知识库型的长期记忆。5.4 性能与成本优化考量问题每次交互都可能多次调用LLM思考、生成Token消耗大响应慢成本高。优化策略模型选型在保证效果的前提下使用更小、更快的模型。例如用gpt-4o-mini代替gpt-4用claude-3-haiku代替claude-3-opus。在工具调用逻辑清晰的任务中小模型的表现往往足够好。减少上下文长度这就是上面记忆优化的核心目的。更短的上下文意味着更低的Token消耗和更快的处理速度。缓存对于相同或相似的查询结果进行缓存。例如股价数据可以缓存1分钟在一分钟内相同的查询直接返回缓存结果避免重复调用金融API和LLM。异步与流式响应对于耗时的工具调用如网络搜索可以考虑使用流式响应Streaming先返回“正在查询…”的提示让用户体验更好。构建AI Agent不是一个一蹴而就的过程而是一个“设计-实现-测试-迭代”的循环。从最简单的单个工具开始逐步增加复杂性并持续观察和优化它的行为。这个能查股价的智能体已经包含了最核心的骨架。当你掌握了大脑LLM、双手Tools和记事本记忆体这三者如何连接和协作你就拥有了打造各种自动化智能助手的能力。接下来试着为它增加新闻搜索、价格提醒甚至简单的趋势分析工具看看它能进化成什么样吧。

相关新闻