十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Function Calling实战:Web开发者如何打造多步交互AI Agent

Function Calling实战:Web开发者如何打造多步交互AI Agent 一个很真实的现状不少Web开发者对AI Agent的印象是“很酷但不知道从哪下手”看到LangChain、AutoGPT、MetaGPT这类名字就觉得门槛高、玩不动。但如果你已经有Web API开发经验其实有一条特别适合你的路——Function Calling。这条路能让你在不太懂复杂概念的情况下用最熟悉的接口思维直接做出能跟业务系统深度互动的AI Agent。这篇文章我来拆解一个完整的实战案例如何基于Function Calling把一次普通的“一问一答”变成“问-查-校验-答-再问-再查”的多步交互。整个过程我会从提示词怎么写、工具定义怎么设计、循环逻辑怎么控制讲起到调试心得、常见坑位排查全部梳理成可以直接复现的方案。适合有过接口开发经验、想快速上手AI Agent能力的Web开发者哪怕你之前完全没接触过Agent概念只要会用JSON、会调HTTP接口这套思路就能落地。1. 整体设计思路为什么Function Calling对Web开发者最友好1.1 从接口思维到工具调用的迁移先说一个可能让你觉得熟悉的事你平时写一个下单接口流程大概是什么接收参数、校验身份、查库存、扣库存、返回订单号和支付链接。这套流程的本质是“多次数据查询和操作的有序编排”。而一个AI Agent处理复杂任务时干的也是类似的事——只不过“接收参数”这一步变成了从用户自然语言里提取信息“校验身份”变成了大模型判断用户意图和参数是否合法“查库存”变成了调用你现有的业务接口。Function Calling函数调用就是连接大模型和你现有业务系统的那根线。它允许你把业务能力以“结构化工具”的形式暴露给大模型。大模型不会直接执行你的代码它会先“理解”用户的请求然后返回一个结构化的“调用指令”告诉你“请用订单查询工具参数是order_id10086你需要从数据库里拿数据”。你拿到这个指令后自己调用代码再把结果喂回给大模型让它继续组织语言回答用户。这套机制最妙的地方在于完全不需要做模型微调、不需要跑向量数据库、不需要搭建复杂的Agent编排框架。你的Web开发经验直接无缝迁移过来——本质上你就是给一个“很聪明但不懂业务的外包员工”写API文档让它学会在什么时候、用什么参数去调你的API。1.2 单次调用与多步交互的核心差异最早玩GPT应用的时候大家普遍的做法是用户输入问题 → 拼接一个超长的提示词 → 让模型直接返回答案。这叫做单次调用Single-turn。对于“帮我写一封邮件”这类知识推断类任务单次调用完全够用。但一旦涉及真实业务单次调用就露馅了。比如用户说“帮我查一下昨天订单OD123456的退款进度顺便看看那个买家的历史订单”。如果你只靠一次调用模型没有数据库连接权限怎么回答它只能瞎编。真实业务场景需要“多步交互Multi-turn Interaction”第一轮模型识别出需要查询订单决定调用get_order_status工具并提取订单号第二轮你的程序执行完查询把返回结果传给模型模型发现退款状态是“处理中”但买家历史订单还没查于是再调用第二个工具get_user_orders第三轮拿到历史订单后模型综合两部分数据生成最终回复。整个过程可能需要3-5轮甚至更多次模型调用但每一轮对Web开发者来说都是熟悉的“请求-响应”模式。1.3 提示词优化的真正价值我把这块单拎出来讲是因为很多开发者把重心放在“怎么写系统提示词”上但实际调试时发现真正影响成功率的是工具描述提示词Tool Description。系统提示词是管“你是谁、你要怎么说话”的工具描述是管“大模型在什么条件下、用什么参数调用什么函数”的。多步交互是否能正确进行更多取决于后者。一个模糊的工具描述会导致模型把“订单号”和“退货单号”搞混或者把一次应该走两步的操作压成一步错招。所以本文的提示词优化实战会同时覆盖两处系统提示词负责约束多步交互的策略和表达工具定义提示词负责约束每一步调用的准确率。两者配合才能让Agent在多轮对话中不跑偏。2. 工具定义与提示词结构先把地基夯实2.1 用OpenAI格式定义你的第一个工具目前各大模型厂商的Function Calling格式大同小异最通用的是OpenAI风格的JSON Schema描述。定义一个查询订单的工具大致结构如下{ type: function, function: { name: get_order_status, description: 根据订单ID查询订单的当前状态、商品明细和退款进度。当用户提到订单、单号、物流、退款且提供了订单号时必须使用此工具。, parameters: { type: object, properties: { order_id: { type: string, description: 用户提供的订单号例如OD123456。必须从原始对话中精确提取不要编造。 }, include_items: { type: boolean, description: 是否需要返回订单内的商品明细。默认false。仅当用户明确询问商品清单、买了什么时设为true。 } }, required: [order_id] } } }注意几个关键点。description字段写得越具体越好这里不只是在给开发人员看的文档更是在给大模型“上课”。你要明确写出“什么场景下必须调用”“参数从哪里提取”“什么情况下不调用”。我见过很多翻车案例就是description写得特别泛比如“查询订单”结果模型在用户问“我买了什么东西”时也不知道该不该调用最后给了一段凭空编的订单信息。参数的description同样重要。你要主动告诉模型参数的格式规则和提取来源。比如订单号的格式是“OD6位数字”模型在提取时就会更有把握。如果用户只在上一轮对话里提过订单号这一轮没重复模型通常也能从上下文里带过来但你最好还是通过系统提示词明确允许它这么做。2.2 多步交互中系统提示词的写法工具定义只是第一步接下来是系统提示词。这部分负责定义Agent在多步交互中的“工作方式”和“沟通策略”。举个例子我用的系统提示词骨架如下你是电商客服助手小派。你的任务是通过调用工具帮助用户查询和解决订单相关问题。 工作方式 1. 当用户的问题涉及订单、信息查询时先判断是否需要调用工具。 2. 如果缺少必要信息例如订单号不要瞎猜先向用户询问缺少的信息。 3. 如果一次需要多个数据才能完整回答分步调用工具不要试图一步到位。 4. 每次拿到工具返回结果后先核对结果是否完整再组织语言回复用户。 5. 如果工具返回的结果为空或异常如实告知“暂时查不到”不要用推测内容充当事实。 回复要求 - 语气自然、简洁像真人客服。 - 遇到需要等待查询时说“稍等我查一下”。 - 结果中包含金额、状态、时间时原样输出不自行修改。这套提示词核心是明确“链路规则”先判定、再询问、再分步调用、再核对。特别是第3条“分步调用不要一步到位”这个很关键。有些场景确实可以一个工具搞定但有些场景会因为信息依赖问题必须分步例如“先查订单 → 拿到用户ID → 再查该用户的其他订单”强制分步能减少模型为了“省事”而强行编造参数的冲动。2.3 参数提取的约束技巧多步交互中一个高频翻车点是参数提取错误。用户说“帮我查一下上个月的退款”系统里其实需要的是退款申请单号模型可能提取出订单号或者干脆用了对话里出现的另一个数字。应对办法是在参数的description里加上更严格的约束refund_id: { type: string, description: 退款单号格式为RF8位数字。只能从用户消息中提取并且必须与用户明确提及的退款单号概念匹配。如果用户只提供了订单号而工具需要退款单号不要自行转换先调用订单查询工具获取关联的退款单号。 }这种写法等于是在告诉模型“这条路走不通的时候换一条路”模型会更有策略性地选择工具链而不是硬用一个错误参数。这个思路在多工具场景下非常实用。3. 多步交互实操从请求到循环的完整实现3.1 基础架构与依赖选型为了贴近Web开发者习惯我这里用一个Node.js的Express应用做演示配OpenAI SDK跑Function Calling。技术选型上不需要引入LangChain那种重量级框架裸的SDK足够帮你理解原理。装好依赖之后核心代码就两大块callModel(prompt, tools, toolResults)和executeTool(name, arguments)。npm install openai express另外准备一个模拟的业务服务层用来扮演“真实数据库查询”的角色。这类代码在你的正式项目中通常是已有的查询Service直接复用即可。整个流程的结构如下前端通过HTTP接口把用户输入传给后端。后端组装消息数组system user。调用大模型接口传入工具定义。判断返回是否包含tool_calls。如果有逐条执行工具把结果以role: tool的消息形式追加进消息数组。带着完整上下文再次调用模型。循环直到模型返回纯文本回复。这个循环过程就是Agent推理的核心也是“多步交互”的落点。3.2 主循环代码详解直接看代码。这里我把关键逻辑全部揉进了一个class里方便理解整体节奏。// agent.js import OpenAI from openai; const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export class AgentRunner { constructor(model gpt-4o-mini) { this.model model; this.messages []; } setSystemPrompt(prompt) { this.messages.push({ role: system, content: prompt }); } addUserMessage(content) { this.messages.push({ role: user, content }); } async run(tools) { const MAX_ITERATIONS 6; // 防止死循环 for (let i 0; i MAX_ITERATIONS; i) { const response await openai.chat.completions.create({ model: this.model, messages: this.messages, tools: tools, tool_choice: auto, }); const message response.choices[0].message; this.messages.push(message); if (!message.tool_calls) { // 没有工具调用说明模型已经准备好返回最终回复 return message.content; } // 执行本轮所有工具调用注意可能同时有多个 for (const toolCall of message.tool_calls) { const toolName toolCall.function.name; const args JSON.parse(toolCall.function.arguments); console.log([Agent] 调用工具: ${toolName}, 参数:, args); const result await executeTool(toolName, args); this.messages.push({ role: tool, tool_call_id: toolCall.id, content: JSON.stringify(result), }); } } return 抱歉处理步骤过多请换个更具体的说法重新提问。; } }这个循环的逻辑其实和你爬虫里的“翻页循环”、批处理里的“任务队列循环”非常相似。模型的每次返回都有两种可能一种是准备回答用户那直接返回文本另一种是主动要求调用工具那你的代码就老实执行工具并把结果回传。这样循环往复直到模型觉得信息够了开始生成最终回答。关于MAX_ITERATIONS我设置的是6。这个值建议根据业务复杂程度调节。简单业务3轮足够复杂流程8轮也正常。设置上限是为了防止模型一直在调用工具但一直得不到有效信息形成死循环。日志中的console.log请务必保留排查问题时能省你大量时间。3.3 工具执行层把你的业务接口变成Agent的“手脚”executeTool函数本质是一个路由分发器根据模型的工具名调用对应的业务函数。示例// tools.js const orderService require(./services/orderService); async function executeTool(toolName, args) { switch (toolName) { case get_order_status: return await orderService.getOrderStatus(args.order_id, args.include_items); case get_user_orders: return await orderService.getOrdersByUserId(args.user_id); case create_refund_request: return await orderService.createRefundRequest(args.order_id, args.reason); default: return { error: 未知工具 }; } }这里有一个很多新手忽略的问题返回给模型的数据要做精简压缩。比如get_order_status查询的结果可能包含一个非常长的商品描述字段、内部备注、数据库创建时间等这些信息模型用不上却会占用大量上下文窗口。我在真实项目中会先把查询结果映射map成精简结构function compactOrderStatus(data) { return { order_id: data.orderId, status: data.status, // pending / paid / shipped / refunding / closed paid_amount: data.payAmount, refund_status: data.refund?.status || null, items: (data.items || []).map(item ({ name: item.name, quantity: item.quantity, price: item.price })), updated_at: data.updatedAt }; }多步交互的稳定性很大程度上靠“每轮喂给模型的token质量”。你把无用字段越早过滤掉模型被干扰的可能性就越低后续步骤的准确率也随之提升。这一步也是提示词优化中容易被忽视的一环——给大模型的数据和给用户看的数据不应该是同一个结构。3.4 一个多步交互的完整走通示例假设用户提问“你好帮我查一下订单OD123456的退款进度另外最近一周我是不是还有别的订单在退款”第一轮调用模型返回tool_calls内容是get_order_status(order_idOD123456, include_itemsfalse)。程序执行得到订单状态为refunding退款状态为processing同时取到关联的user_idU888。结果回传发起第二轮模型调用。第二轮调用模型看到退款还在处理中但用户还问了“别的订单在退款”于是调用get_user_orders(user_idU888, date_from2026-01-01)。程序返回该用户最近一周的3笔订单其中一笔也在退款中。结果回传发起第三轮模型调用。第三轮调用模型综合两轮数据生成回复“订单OD123456的退款已提交目前处理中预计1-2天到账。另外你近期还有一笔订单OD999888也在退款流程中进度为待仓库确认。其他订单状态正常。”整个步骤里模型被调用3次工具执行了2次第3轮未触发工具。实际日志中你能清楚看到这种“提问-工具-思考-工具-回答”的链路。这就叫多步交互并不神秘本质就是把思考过程拆成多轮接口调用。4. 提示词调优在多步交互中“稳住”模型4.1 系统提示词的分层优化策略我在前期测试时踩过一个大坑把所有规则一股脑塞进系统提示词结果模型在长对话中总是“忘事儿”。后来我把提示词拆成三层效果立刻有了提升。第一层角色设定层。一句话说明你是谁、在什么场景下服务。第二层任务流程层。分步骤描述遇到请求时要怎么处理这是多步交互的核心策略区。第三层输出格式层。规定回答的语气、格式、注意事项。拆开之后模型在长上下文中的指令遵循率明显提升。因为大模型对提示词不同位置的注意力权重不同业务逻辑放在前面、回答格式放在后面它更容易把“先思考后回复”的顺序走对。这里给一个可以直接用来抄作业的分层示例角色你是电商平台智能客服助手负责处理订单查询、退款咨询和售后问题。 流程 1. 收到用户消息先检查是否与订单、退款、物流相关。 2. 若相关判断现有消息内容是否足够调用工具。若缺少参数如没有订单号必须反问用户补充。 3. 需要多个工具结果时先执行信息收集型工具如查询订单再根据结果执行操作型工具如发起退款。 4. 每次工具返回后把结果与用户原始诉求比对看是否还需其他信息。 5. 所有信息收齐后再组织最终回复。 回复风格 - 简洁自然避免书面腔。 - 金额、状态必须引用工具返回的真实值。 - 若工具未返回数据不要脑补直接说“暂时没有查到相关信息”。4.2 工具定义提示词优化的比赛输了当心描述歧义工具描述写得好不好直接决定多步交互的成败。举个失败案例。我之前定义了一个change_order_address修改收货地址的工具description写的是“修改订单地址”。结果用户说“我买完发现地址写错了”模型就直接调用这个工具但此时它既没有校验订单状态是否允许修改也没有确认新地址是否完整。这一下子就出大事了。后来我把description改了根据订单ID和新的收货地址修改订单的收货信息。 仅在订单状态为【待发货paid】时允许修改。 调用前必须核实用户提供了完整的省市区详细地址。 如果订单已发货不要调用此工具而是引导用户联系快递拦截。加了“调用前必须核实”这类约束之后模型在多步交互中会主动向用户索要缺失信息而不是急吼吼地直呼工具。这实际上就是把业务规则通过提示词语言“注入”给了模型弥补了Function Calling纯参数校验的不足。4.3 上下文管理与防遗忘技巧多步交互到第4轮、第5轮时早期的“订单号”“用户名”等信息可能淹没在长对话里。模型并不是真的“记忆力”不好而是注意力被新内容分散了。我的解决办法有两个。第一个办法关键信息摘要Key Info Extraction。每轮工具结果返回后除了原始结果额外在content字段最前面加一行摘要格式如下[摘要] 订单OD123456状态退款处理中用户IDU888关联退款单号RF12345678。 [完整结果] {...}这样做等于帮模型画了重点。模型在后续轮次里即使把原始结果忘了也能从摘要里快速找回关键参数。第二个办法系统提示词里固定全局常量。把一些高频使用的规则固定写在系统提示词里比如“如果用户没有提供订单号不要猜测直接询问”。这能让模型在多轮交互中不至于犯低级错误。4.4 并行工具调用与依赖决策多步交互中另一个值得聊的点是并行parallel工具调用。OpenAI等模型现在支持一次返回多个tool_calls意味着模型认为可以“同时”查询多个互不依赖的数据。比如用户问“订单OD123456的物流和退款进度”模型可能一次性返回tool_calls: [ { name: get_order_logistics, arguments: { order_id: OD123456 } }, { name: get_refund_status, arguments: { order_id: OD123456 } } ]这种情况下你的循环里需要用Promise.all并发执行然后把两个结果按tool_call_id对应回传。代码里那个for循环可以改成const executionResults await Promise.all( message.tool_calls.map(tc executeTool(tc.function.name, JSON.parse(tc.function.arguments))) ); executionResults.forEach((result, idx) { this.messages.push({ role: tool, tool_call_id: message.tool_calls[idx].id, content: JSON.stringify(result), }); });但要注意不是所有场景都适合并行。比如“先查订单拿到用户ID再查询用户详情”这种有依赖关系的步骤只能串行。你可以通过工具描述里写“此工具需要先用XX工具获取用户ID后才能调用”来引导模型决策。这就是多步交互中“哪一步先、哪一步后、能不能一起做”的编排柔术。5. 常见问题与排查实录5.1 模型总是反复调用同一个工具停不下来这是多步交互里最头疼的死循环问题。现象是MAX_ITERATIONS用完后模型还在调工具日志里显示调用的是同一个工具、同样的参数。常见原因有两个工具返回结果没有变化模型想再试一次。返回结果里塞了太多无关字段导致模型每次都判定“还没拿到正确答案”。排查方法是把工具返回的content原样打出来检查确认它是否包含了回答用户问题所需的信息。如果确实缺失就要调整业务查询逻辑如果信息齐全但模型不停止多半是工具描述中缺少“已包含XX字段无需再次查询”的终止条件。你可以在返回内容的末尾加一句[提示] 本结果已经包含订单的最新状态和退款进度。如果用户只是询问这两项可以直接回答。这种“元提示”对模型的停止判断有奇效。5.2 模型从用户消息里提取错了参数比如用户说“查一下我的上笔订单”模型把消息里出现的某个日期当成了order_id。问题多半出在参数描述不够严格。我给参数的description加上了“从哪里取、从哪里不能取”的约束order_id: 订单号格式为OD6位数字。该值只能从用户明确描述为订单号的文本中提取。如果用户没有提供订单号不要从对话中猜测调用本工具前先反问用户。同时在系统提示词的流程部分明确写了“缺少必备参数时反问补充”。双管齐下之后参数提取错误率大幅下降。5.3 工具返回结果正常但最终回答驴唇不对马嘴这个情况一般出现在多步交互的后半段模型拿到了两个工具的结果却把二者搞混了。比如用户查订单A的物流又查了订单B的退款模型回答时把A和B的信息交叉缝合了。我的解决办法是把每条工具结果都带上非常明确的“标签头”[订单OD123456的结果开始] ... [订单OD123456的结果结束]并对每条结果的第一行做核心摘要。实践下来回答错位的问题能减少一大半。如果还不行就降低单轮Tool结果的数据量只保留跟当前问题最相关的字段。5.4 上下文太长了怎么办多步交互中如果每轮都把完整结果塞回去token消耗会越来越大甚至撞上模型上下文上限。这时的策略是只保留最近2轮的工具原始结果更早的转成摘要后存放在tool消息里。实现上也不复杂工具消息的content字段里我一般用一个rawsummary的结构{ summary: 用户U888近一周有3笔订单1笔退款中。, raw: { ... } }当对话轮次超过一定数量程序自动把更早的raw清掉只保留summary。这样既不影响模型对整体链路理解又大幅节省了token。这个策略在真实高并发场景下非常实用直接关系着成本。5.5 调试技巧善用日志与复盘多步交互模型的时间复杂度高问题复现率也不稳定。强烈建议在生产环境把每次完整链路system提示词、工具定义、每一轮返回、工具结果、最终回复全部落日志。排查问题时直接把日志中tool_calls的JSON片段贴给模型看往往立刻能找到是哪一个环节描述不清导致了错误分派。我自己的调试习惯是先跑通“单用户单问题”的最小链路再逐步叠加“多轮追问”“同主题多参数”“跨主题跳跃”等场景。每一步都观察模型是否选对了工具、参数是否正确、下一步是否需要额外查询。这样迭代出来的Agent稳定性远高于一上来就堆很多业务流程的方案。6. 几点实际心得Function Calling这套方案我实际用下来最大的感受是它把AI Agent从“玄学”变回了“工程”。Web开发者不再需要重新学习一套难懂的Agent框架只要会用JSON定义工具、会写业务函数、会跑一个循环就能把大模型无缝接入自己的系统。做好提示词优化尤其是工具描述与系统提示词的分层配合比换更强的模型更能提升Agent表现。很多时候模型“不够聪明”并不是模型不行而是它手里的“说明文档”写得太差。你把这些说明文档按业务规则、参数约束、终止条件、回复风格拆清楚模型的多步交互能力才会真正释放出来。至于未来趋势多步交互一定是Agent落地的必修课。不管是做客服机器人、流程自动化、还是个人知识助理只要你的Agent需要跟真实业务数据打交道Function Calling和多步提示词优化就一定是绕不开的地基。建议你从本文的案例出发先跑通一个最小的多步交互链路再逐步扩展。代码在手剩下的就是大胆折腾。
返回列表