ARTICLE DETAIL

资讯详情

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

电商智能客服Agent开发实战:从架构设计到Function Calling落地

电商智能客服Agent开发实战:从架构设计到Function Calling落地 1. 需求梳理与架构设计思路1.1 为什么电商客服非要上Agent而不是继续用老式机器人做电商后端系统的同学应该都有感触传统客服机器人这几年口碑两极分化严重。说它没用吧确实能挡掉不少重复咨询说它好用吧用户问“我上礼拜买的那双鞋什么时候能到”老式机器人大概率会回复一段“亲请您提供订单号哦”的标准话术然后用户就炸了。这里面的核心痛点在于传统基于关键词匹配或意图模板的机器人本质上是一个“查表系统”。它只能识别预先定义好的问题模式一旦问题换个说法、夹带多轮上下文或者说一半突然换个话题整个对话流程就崩了。而电商客服场景恰恰是这些情况的集散地——用户不会老老实实按照你预设的意图来提问。基于Agent的智能客服核心思路是把“猜用户想干什么”这件事交给大模型而不是交给一堆if-else规则。Agent会通过大模型理解用户当前的自然语言输入再把“理解结果”映射为一系列可执行的工具调用比如查订单、查物流、改地址、算退款金额。整个过程不需要写死每一种问法而是靠模型泛化能力“见招拆招”。我这次带练的项目就是围绕这个思路搭建一个可运行的电商智能客服Agent目标不是做一个Demo级的玩具而是尽量贴近真实生产环境包含多轮上下文管理、工具调用、订单查询、商品推荐、售后处理流程等模块。对于正在学Agent开发、或者准备把大模型能力接入业务系统的同学来说这个项目能帮你把“Agent到底是什么、怎么落地”这件事彻底搞明白。1.2 整体架构分层Agent不是一个大模型而是一个调度系统很多新手容易有一个误解觉得“Agent就是ChatGPT接个API”。这是最典型的弯路。实际上一个能处理电商客服场景的Agent至少需要拆成三层来设计第一层是交互层负责接收用户消息并返回结果同时维护对话的会话ID和上下文缓存。在电商场景里用户可能会隔半天再回复“那个订单现在怎么样了”这种话如果没有会话层把历史对话存下来模型根本不知道“那个订单”是哪个。第二层是Agent核心调度层这是整个系统最关键的部位。大模型在这里充当“大脑”它做的事情是读取用户当前输入、结合对话历史判断意图、决定是否需要调用某个工具、生成工具参数、等待工具返回结果、再根据结果组织用户可直接阅读的答复。这整个流程就是Agent领域常说的ReAct范式——思考、行动、观察、再思考。第三层是工具层也就是Agent能调用的真实业务能力。在电商场景里我设计了五个核心工具订单状态查询、订单改地址、商品信息查询、售后退款申请、人工客服转接。工具层和真实的业务系统对接在这个带练项目里我用Mock服务模拟真实接口但接口签名和返回结构完全按照生产标准来设计。为什么要这样分层最大的好处是“各层可以独立替换”。你换一个大模型Agent逻辑不用动你对接真实的订单中心只需要改工具层的接口实现你想把智能客服从电商搬到本地生活服务场景新增几个工具、改一下Prompt描述就行。这种可插拔的设计才是Agent相对传统机器人在工程上的真正优势。2. 技术选型与大模型接入细节2.1 框架选择LangChain、Dify、还是自研一套轻量调度当前Agent开发的主流路线有三条我分别聊一下实际用下来的感受。第一条是LangChain这类通用Agent编排框架。LangChain的抽象层次高提供了记忆、工具调用、Chain等现成组件上手快、教程多。但它的问题是“抽象过深”你遇到一个报错经常要钻进源码里翻半天才能搞明白是哪一层出的问题。而且LangChain对Prompt的封装比较重反而限制了精细调优的空间。第二条是Dify、Coze这类低代码Agent平台。这类平台对非工程背景的同学非常友好界面点点点就能搭出一个带知识库的Agent应用内置了工作流编排、变量管理、日志追踪等能力。如果你是业务方想快速验证场景或者公司没有专职算法工程师直接从这类平台切入是性价比最高的选择。但代价是灵活度受限复杂业务状态流转、特殊工具协议接进来会很吃力。第三条也是我这次采用的方案是自研一个轻量级的Agent调度核心基于大模型API的Function Calling能力来实现工具路由。这个方案自己掌控全部逻辑代码量也不大核心调度循环两三百行就能写完但理解透彻后你再去看LangChain源码或者Dify内部的工作流机制会瞬间通透很多。做带练项目我更倾向于这种“亲手实现一遍”的模式这也是Agent开发学习路线里最值得走的一段路。模型层面这个项目我用的是Qwen系列的开源模型接口主要看中它对中文电商语境的理解能力以及Function Calling的稳定性。你换成其他主流闭源或开源模型也完全跑得通因为接入层我已经做了统一封装。2.2 工具调用Function Calling原理Agent“动手能力”的基石要让Agent不只是“聊天”而是能真正查订单、改地址依赖的关键机制就是Function Calling。简单说Function Calling就是让大模型输出一段结构化的调用请求而不是普通文本。比如用户问“订单20241215001现在到哪了”Agent内部会发生如下过程大模型会把用户这句话映射为一个结构化的JSON输出类似这样{ name: query_order_status, arguments: { order_id: 20241215001 } }这里有几个注意点。第一大模型并不会直接去调用你的函数它只负责输出“我觉得该调哪个函数、参数应该是什么”。真正去执行函数的是你的代码。第二你需要把所有工具的定义函数名、参数类型、参数说明以JSON Schema的形式传给大模型它才能“知道”有这些工具可用并在合适的时候选择调用。第三大模型生成参数时有时候会“脑补”——用户明明没提供订单号它可能会给你编一个。所以参数校验这块必须在执行前兜底。我在项目里的做法是每个工具函数都有完整的参数Schema定义其中必填参数如果缺失Agent会进入一个“追问流程”调用一个ask_for_missing_info的内部动作向用户索要必要信息。这个设计在真实项目里极其重要否则你经常看到Agent用随造出来的订单号去查数据然后堂而皇之地告诉用户“未查询到订单”。3. 核心代码实现与关键逻辑拆解3.1 工程项目结构与数据源Mock先看项目整体结构我按模块划分如下customer_service_agent/ ├── agent/ │ ├── core.py # Agent调度主循环 │ ├── memory.py # 会话记忆与上下文管理 │ ├── tools.py # 工具定义注册中心 │ └── prompts.py # 系统Prompt与工具说明模板 ├── services/ │ ├── order_service.py # 订单服务Mock │ ├── product_service.py # 商品服务Mock含向量检索 │ └── refund_service.py # 售后/退款服务Mock ├── router/ │ └── api.py # Flask/ FastAPI接口层 └── config.py # 模型配置、服务地址等订单服务这部分我模拟了一张订单表包含订单号、用户ID、商品列表、订单状态、收货地址、物流单号和时间节点。每个接口的函数签名都按真实RPC风格的入参出参设计方便你后续替换成真正的HTTP或gRPC调用def query_order(order_id: str, user_id: str) - dict: 查询订单详情与物流状态 - order_id: 用户提供的订单号 - user_id: 当前会话用户ID 返回: 订单基本信息、当前状态、最近一条物流记录 order ORDER_DB.get(order_id) if not order: return {code: 404, msg: 订单不存在, data: None} if order[user_id] ! user_id: return {code: 403, msg: 无权访问该订单, data: None} return {code: 0, msg: success, data: order}这里必须强调一句真实生产环境里订单数据权限校验永远是第一位的。Agent工具层的权限控制不能依赖Prompt必须在服务端硬校验。你可以在Prompt里写“只能查询当前用户自己的订单”但模型偶尔会选择性忘记唯一可靠的做法就是像上面代码这样每个工具函数的实现里都带上user_id校验。3.2 Agent调度主循环让“思考-行动-观察”跑起来Agent的调度主循环是整个项目的灵魂。我用Python实现了一个精简但功能完整的版本核心逻辑如下def run_agent(user_id: str, user_input: str, session: dict) - str: # 1. 追加输入到会话历史 session[messages].append({role: user, content: user_input}) # 2. 进入Agent多步执行循环 for step in range(MAX_STEPS): # 限制最大迭代步数防止死循环 # 调用大模型传入工具定义和会话历史 response llm.chat( messagessession[messages], toolsTOOL_SCHEMAS, # 所有工具定义的JSON Schema tool_choiceauto ) # 3. 判断是否需要调用工具 tool_call parse_tool_call(response) if tool_call is None: # 模型直接生成了最终回复结束循环 final_reply response.content break # 4. 执行工具调用 tool_result execute_tool(tool_call, user_id, session) # 5. 将工具结果作为观察(Observation)加入消息历史 session[messages].append({ role: tool, tool_call_id: tool_call.id, content: tool_result }) # 6. 继续循环让模型基于观察结果决定下一步 return final_reply这段代码你仔细看会发现Agent本质上就是一个“循环”。在这个循环里大模型反复做三件事看当前对话状态、决定调用哪个工具、观察工具返回结果。这就像一个人查快递先登录App看订单列表再点进详情看物流进度每一步都是基于上一步的结果来决策的。我在设计里还加了一个MAX_STEPS限制这是血泪教训。没有步骤上限的时候碰到几个工具连环报错Agent会在循环里来回转圈既浪费Token又迟迟不回复用户。设置5到8步的上限配合错误提示信息让它快速切换到人工处理是更稳妥的兜底策略。3.3 多轮对话与状态管理用户说“那个订单”时Agent怎么听懂电商客服场景里多轮对话的复杂度远高于单轮问答。用户经常说“那我不想要了怎么退”“改成明天送货可以吗”这种话里面的“那”“改一下”指代的是前几轮提到的某个订单或商品。如果Agent没有跨轮记忆能力每一轮都当成独立问题去处理就会答非所问。我在这里用了两层机制来解决问题。第一层是短期会话记忆直接把最近10轮以内的对话原文拼接到大模型的上下文里。这个方案实现简单效果也不错模型能通过原始对话找到指代对象。代价是Token消耗比较大而且对话超过10轮之后早期信息会被挤掉。第二层是会话状态变量。我在会话记忆里维护了一个结构化状态类似这样{ current_order_id: 20241215001, current_product_id: P1002, cart: {}, pending_action: refund_apply, pending_step: 1 }每轮对话结束时我会让大模型顺带输出当前会话状态的结构化更新把“用户最近在聊哪个订单”“当前卡在哪个流程步骤”这些关键信息抽出来。下一轮用户说“那我不想要了”Agent通过current_order_id就知道“那”指的是20241215001这个订单直接走退款流程不需要用户重新报订单号。这套“原始对话记忆结构化状态变量”的双层设计是我在实际项目中对比测试出来的最优解。单独靠原始对话记忆长会话会丢信息单独靠状态变量模型理解用户表达的弹性又不够。两者搭配才能真正处理电商场景里那些跳跃又口语化的真实对话。3.4 工具层实战订单查询、商品推荐、退款申请的实现细节工具层我按电商客服的高频场景做了五个当做扩展模板非常合适。这里重点拆解三个实现上最有讲究的工具。第一个是订单状态查询。这个工具看似简单其实处理逻辑很丰富。订单可能有多种状态待付款、待发货、已发货、运输中、已签收、售后中。用户问法五花八门有人问“我的快递什么时候到”有人问“发货了没有”有人问“订单到哪了”。工具本身返回原始数据比如物流轨迹数组但怎么转化成用户看得懂的话由大模型来完成。所以我在工具返回里直接给结构化JSON比如“物流轨迹[{时间, 节点, 描述}]”大模型会自己归纳成口语化的答语比如“您的包裹已到达杭州转运中心预计明天下午送达”。第二个是商品推荐。单纯的“按关键词搜商品”太简单我升级成了基于向量相似度的语义检索。先把商品库里的商品描述、属性标签做Embedding存入向量索引用户说“有没有防水的蓝牙耳机”Agent把这句话转成向量跟商品向量库做相似度计算返回TopK结果。这个方案能解决关键词匹配“差一个字就搜不到”的痛点。这里我用了一个本地向量检索服务也就几十行代码但体验差距非常明显。第三个是售后退款申请。这个工具牵扯流程状态机是五个工具里最复杂的。退款不是一步就能完成的可能包含“确认退款原因-申请发起-商家审核-用户确认”多个阶段。我在工具内部做了一个状态机同一笔退款单子在不同阶段Agent调用同一个工具时行为不一样。比如用户先说“我想退”Agent就创建一个退款申请单用户接着又说“我不退了”Agent再调用这个工具时检测到pending_step为2且用户表达撤回意向就会走“取消退款申请”的分支。这个设计让“一个工具承担整个流程”成为可能也比拆成两三个退款工具更贴近真实业务形态。4. Prompt设计、效果调试与系统集成4.1 系统Prompt把Agent的“岗位职责”一次写清楚Agent的表现上限很大程度取决于系统Prompt。这个项目的Prompt我迭代了六七版最终沉淀出一个相对可靠的结构角色定位、工作流程、行为准则、工具边界。角色定位要明确告诉大模型它是谁在什么场景下工作服务对象是谁。我写的是“你是某电商平台的智能客服助手负责为用户提供订单查询、商品咨询、售后处理等服务。回答需专业、简洁、有温度。”工作流程在这里也很重要。我会要求Agent在遇到工具调用时先确认参数齐全再执行调用最后基于工具返回结果给用户一个完整的答复。如果工具返回错误要如实说明而不是编造一个流程。行为准则部分属于避坑条款。比如我明确写了几条“不得编造订单物流信息”“不得将多个工具的结果混为一谈”“用户未明确提供订单号时必须主动询问不得猜测默认值”“回答中涉及金额、日期等关键信息必须严格按照工具返回结果输出”。工具边界是规定Agent哪些场合可以调用工具、哪些问题不需要调用。比如用户只是想闲聊感谢一下Agent不该去调订单接口用户问“你们平台有什么优惠券”如果工具集里没有这个能力Agent应该坦诚说明并转人工而不是胡编。这套Prompt写下来大约六七百字看似不长但每条规则都是我在调试中撞出来的。尤其是“必须主动询问订单号”这一条没有它的时候你会发现Agent经常用问号替代参数调接口返回一个“订单不存在”就算答完了用户体验极差。4.2 上下文裁剪与Token控制长对话下保持稳定多轮对话多了以后上下文会越来越长两个问题随之而来一是Token费用变高二是模型在超长上下文里对早期信息的注意力大幅下降。我采用三层方案来控制。第一层是滑动窗口裁剪只保留最近N轮对话原文更早的直接丢弃。第二层是关键信息压缩在每轮结束时让大模型把当前会话要点总结成一行结构化摘要比如“用户咨询订单20241215001物流状态当前状态已签收”这个摘要一直保留将来龙去脉压缩成很小的Token占用。第三层是工具结果精简比如订单查询返回一个很大的JSONAgent真正用到的可能只有状态字段和收货地址工具层会做字段裁剪不是把所有原始数据都塞进上下文。三层叠加下来一个长对话场景的Token消耗能压到原来的三成左右同时模型对关键信息的把握反而更稳定了。这个优化在没有流量压力的时候不做也没事一旦你的客服Agent要应对大量并发Token成本就会成为第一个需要正视的问题。4.3 接口层集成用SSE实现打字机效果为了把Agent能力真正接到Web端或App端我在接口层用FastAPI搭了一个会话网关最重要的一个接口是对话接口。这里我做了SSEServer-Sent Events流式输出效果就是用户看到内容一个字一个字蹦出来而不是等五六秒干等一个完整回答。SSE实现思路是后台把Agent生成文本的过程按流式模式输出前端通过EventSource或fetch流式接口逐段接收。因为大模型响应时工具调用前的“思考”阶段是不需要展示给用户的所以我在接口设计里做了一个事件类型区分工具调用事件前端可展示“正在查询您的订单...”、文本生成事件真正的回答内容、结束事件。app.post(/api/chat) async def chat(req: ChatRequest): session get_session(req.session_id) async def event_stream(): async for event in agent.stream_run(req.user_id, req.message, session): if event.type tool_call: yield fdata: {json.dumps({type: tool, name: event.name})}\n\n elif event.type text: yield fdata: {json.dumps({type: text, content: event.delta})}\n\n yield data: [DONE]\n\n return StreamingResponse(event_stream(), media_typetext/event-stream)这里给一个关键提醒SSE环境里如果你用的是StreamingResponse一定不要设置全局的响应头压迫框架自动开启GZip否则流式内容会被缓冲住前端等了半天一个字符都收不到。这个坑我在联调时踩了整整半天最后定位到是中间件缓存导致SSE不生效。4.4 效果调优为什么Agent有时答非所问怎么定位问题Agent调优和传统代码调优的思路不太一样。传统代码里Bug就是Bug原因是确定的Agent里绝大多数问题出在“模型的概率性输出”上同样的输入多次运行结果可能不同。所以定位问题时我要看的是Agent每一步的实际决策轨迹。我在项目里加了一个调试日志模块把每轮Agent的完整决策链记录下来用户输入了什么、模型判断的意图是什么、选择了哪个工具、生成参数是什么、工具返回什么、模型最终回复是什么。这段日志就是复盘调优的“黑匣子”。比如用户问“我退货的地址填哪个”Agent却去调了订单查询而不是退款信息查询那我就能快速定位到工具描述写得不够清楚导致模型理解出现了偏差于是我去改对应工具描述的措辞。实测下来有一个经验工具函数的名字和描述对Agent调用准确率的影响比Prompt里的指令描述还要大。函数名叫query_refund_address比call_refund_address_getter_info好懂得多描述里写清楚“用于查询用户退货需要寄往的收件地址”这种自然表达模型的匹配准确率会明显提升。所以调优第一步永远是检查工具定义是不是够直白。5. 高频报错与排查技巧实录5.1 工具参数“幻觉”模型生成根本不在Schema里的参数我在联调中遇到最典型的问题之一是Agent生成的工具调用参数经常“越界”比如调用query_order_status时多传了一个“order_type”参数而这个参数根本不在我定义的Schema里。如果代码里直接按参数名取参虽然不会报错但会导致后续逻辑混乱。排查思路是在execute_tool这一步做一个参数白名单校验。先取出该工具定义的Schema允许的参数集合再过滤掉多余字段。同时把“模型生成了未定义参数”这件事单独打日志出来便于追查是不是工具定义里参数描述不够清晰导致模型误以为有这个参数。还有一个更激进的做法就是在给模型传工具Schema时把JSON Schema中的additionalProperties显式设为false这样部分模型在生成时会更严格地遵守参数定义。实测对Qwen系列有效但不同模型表现有差异还是以入参校验为准。5.2 多轮上下文漂移聊着聊着Agent忘了前面的订单上下文漂移这个问题在超长会话里尤其明显。前面的订单号是20241215001聊了20轮售后流程后模型再去调查订单接口时有概率把订单号改成一个长得类似的随机数字。我的处理方案是双保险。第一道是结构化状态变量每轮更新current_order_id始终被独立保存不让它只依赖大模型的上下文注意力。第二道更狠——工具层在每次调用query_order这类接口时如果检测到参数里的order_id和当前会话状态里的current_order_id不一致会强制以会话状态里的值为准并在返回结果里附带一个“order_id_rewritten”标记提示Agent使用了修正后的参数。这种“状态变量托管关键参数”的做法比单纯要求模型“记住订单号”靠谱得多。核心思路是凡是属于关键业务参数的信息不要指望大模型全程凭记忆搬运而是通过状态变量在系统侧记账每一轮都重新注入到上下文中确保准确性。5.3 格式混乱工具正常返回但Agent答非所问有时候工具调用全流程正常数据也查出来了但Agent组织最终答复时出了问题。比如商品推荐工具返回了三条商品Agent回复时却把三个商品的价格搞混了类目属性也算错把“128GB”写成了“256GB”。这个现象的本质是结构化数据在转成大模型可理解的自然语言时语义发生了扭曲。我的解决方法是在系统Prompt里明确写入“涉及工具返回数据的具体字段值必须逐字使用不得修改或推导”。同时在工具返回内容里做好类型标注比如“价格元必填”“存储容量数值禁止转换单位”。对同一类数据字段做前后一致描述模型混淆的概率会大幅下降。如果还是持续出错最后一招是把工具返回改成只读的不可变内容在加入上下文之前先用模板生成一段自然语言描述塞进去让模型直接基于这段描述做答复而不去碰原始JSON。这样代价是丢失了一些信息维度但稳定性显著提升。5.4 常见问题速查表我把这个项目开发过程里遇到过的问题整理成了表格方便你对照排查问题现象可能原因优先排查方向Agent不调用工具直接编答案系统Prompt对工具边界描述不足检查工具描述是否清晰Prompt里补“涉及订单/物流等信息必须查询后回答”工具参数凭空多出字段模型对Schema理解不到位execute_tool层加参数白名单过滤Schema加additionalProperties: false多轮后参数错乱上下文遗忘或状态漂移增加结构化状态变量关键参数脱离对话记忆独立保存工具正常返回但回复内容错模型对结构化数据理解偏差模板将JSON转成自然语言描述后放入上下文Agent循环调用工具不结束缺少步骤上限或错误反馈不够设置MAX_STEPS工具报错时返回明确错误码和可读信息用户没给订单号Agent硬查Prompt缺少“必须追问”约束Prompt补“参数缺失时必须主动询问不得猜测”并发会话互相串数据会话状态存储作用域错误检查状态变量是否按session_id隔离确认全局变量误用5.5 生产环境部署的几个血泪提醒项目跑通之后如果要部署上线有几个生产环境层面的点需要提前思考和规划我在实际落地时都有过教训。第一是隔离性。真实生产环境里Agent实例往往是多副本部署的会话状态必须存在Redis这类外部存储里不能只放在进程内存里。部署两个副本时用户的请求第一轮打到A实例第二轮打到B实例如果状态只存在进程内存里B实例完全不认识这个用户对话直接断档。第二是超时和重试。工具调用要设置超时时间尤其是对接HTTP服务时下游慢一慢整个Agent循环都会卡住。我建议每个工具的超时控制在3秒以内超时后Agent应该把“查询超时请稍后再试”作为观察结果进入下一轮而不是干等。第三是日志链路追踪。线上环境排查问题要有从用户请求到Agent决策到工具调用的全链路日志并且打上trace_id。没有日志链路出问题只能靠用户截图那种排障体验想想都痛苦。第四是人机协作兜底。Agent做得再好也会遇到说不清的需求。项目中我把“转人工客服”设计成一个独立的工具Agent在连续两轮无法满足用户需求、或用户显式表达不满时会主动触发转人工。这个开关是我认为上线前必须加的既是对用户体验的保障也是对Agent能力的清醒认知。6. 这个项目的可扩展方向做完了电商智能客服这个基础项目之后其实可以沿着几条线继续深入拓展。我把自己觉得有价值的几个方向列出来供参考。第一个扩展方向是叠加知识库问答能力。通过RAG把电商平台关于退换货政策、优惠券规则、商品使用说明等文档接进来让Agent解决更复杂的“知识型问题”。这里需要额外引入文档切分和向量检索链路可以和现有工具调用机制并行存在。第二个方向是多Agent协作。比如增加一个“售后专员Agent”和一个“商品推荐Agent”主Agent负责接收用户请求根据场景把任务分发给对应子Agent。这种架构在处理复杂业务域时会更有优势也让Agent系统的拓展性更好。第三个方向是Agent训练层面的微调。当领域任务足够垂直、你的工具调用和话术模式相对固定时可以采集一批优质会话数据做指令微调或偏好对齐让模型在电商客服场景的表现进一步提升。第四个方向是更完善的评测体系。你可以积累一批电商客服的测试问题集覆盖常见场景、拦截场景和边界场景每次迭代Prompt或模型后自动跑一遍用通过率判断改动效果。这个评测闭环是Agent工程化不可或缺的一环。做完这个项目我自己最大的感受是Agent开发的门槛没有想象中那么高但深度远比想象中深。你说它难核心原理其实就是“循环工具调用”两件事你说它简单把一个Agent从能跑到跑得稳、跑得省、跑得可控中间牵扯的工程细节实在太多了。如果这个带练项目能帮你把第一脚迈出去那这篇折腾记录就没白写。
返回列表