LlamaIndex RAG多智能体手机客服实战:从知识问答到协作下单全流程 手机客服这个场景我陆陆续续做了几个项目最深的感受是它不像通用聊天机器人那样问两句闲话就完事而是要把“查参数、比型号、判故障、查库存、引导下单”这些具体动作串起来。前阵子我用一份手机机型数据集把 LlamaIndex 的 RAG 问答、品牌专家路由、多智能体协作下单完整跑了一遍从最基础的单机器人问答开始一层一层往上加能力。这篇就按我的实际落地过程把 LlamaIndex RAG 手机客服多智能体 Agent 教程的全流程拆给你看。先说清楚这套东西解决了什么问题。传统客服机器人拿一个大知识库做检索用户问“X 品牌新品和 Y 品牌旗舰哪个适合打游戏”检索出来的内容可能是两份割裂的说明书回答起来很别扭。而多智能体架构下每个品牌有一个“专家助手”路由层先判断用户想找谁再把问题转给对应专家最后如果需要下单购物车、订单、售后这些执行型 Agent 再接手。整个过程是一份机型数据集贯穿起来的从单机器人问答到品牌专家路由再到多智能体协作下单我按这条线一步步演示。1. 项目概述与整体设计思路1.1 一个贯穿全流程的手机机型数据集我用的数据集不是网上下载的现成知识库而是自己整理的一份脱敏手机机型数据集。里面每款手机对应一个结构化的字段记录包括系列名、型号、屏幕尺寸、刷新率、电池容量、充电功率、处理器型号、内存存储组合、后置摄像头参数、价格、库存状态、上市日期、售后政策等十多个字段。为什么要强调“贯穿全流程”因为很多 RAG 教程的数据集只是一堆纯文本检索出来之后就只能做问答演示。而手机机型数据集天然适合做进阶玩法它有清晰的层级品牌下面有系列系列下面有具体型号型号下面挂参数和价格。这套层级结构既是 RAG 的切分依据也是品牌专家路由的分类依据更是多智能体下单流程里的商品对象。同一份数据前面用来做问答检索后面用来构造购物车条目逻辑上是连贯的。数据格式我用的是 Markdown 加 JSON 混合的方式。Markdown 便于人类阅读JSON 便于程序解析。实际构建索引时我会把每款手机单独拆成一个文档节点节点本身包含正文节点的元数据里存品牌、系列、价格、库存这些字段后面做过滤检索会非常方便。# 品牌Alpha 星曜X1 - 系列星曜 - 型号品牌Alpha 星曜X1 - 屏幕6.7英寸 AMOLED120Hz刷新率 - 电池5000mAh80W有线快充 - 处理器某旗舰芯片 - 内存/存储12GB256GB / 16GB512GB - 后置摄像头5000万像素主摄 1200万像素超广角 - 价格3999元 / 4499元 - 库存现货 - 售后支持7天无理由退货2年质保这里说明一下文中品牌和型号我全部做了脱敏统一叫“品牌Alpha”“品牌Beta”你们自己跑的时候换成真实机型数据就好流程完全一致。1.2 三层架构为什么这样设计整套系统我拆成三个层次对应标题里的三个关键词单机器人问答、品牌专家路由、多智能体协作下单。第一层是单机器人问答。这层不做任何花样就是一个 LlamaIndex 的标准 RAG 链路加载文档、切分、向量化、建索引、查询。用户问“星曜X1的电池容量是多少”直接召回相关节点用大模型组织成自然语言回答。这一层解决的是“信息查找”问题。第二层是品牌专家路由。用户问“品牌Alpha 星曜X1和品牌Beta 极速Pro选哪个”如果只有一个通用机器人检索到的文档可能一半是 Alpha 的一半是 Beta 的回答质量完全看运气。路由层会先判断这是一个跨品牌对比问题然后把问题拆解分别交给品牌Alpha专家和品牌Beta专家去查询再把两边的结果汇总对比。这一层解决的是“知识归属和分发”问题。第三层是多智能体协作下单。用户问完参数对比后说“我要买品牌Alpha 星曜X1 12GB256GB黑色有货吗帮我下单”。这已经超出问答范畴需要调用工具、读写状态、执行动作。于是用户问题被路由到下单流程购物车 Agent 负责把商品加入购物车订单 Agent 负责生成订单信息售后 Agent 负责确认库存和发货政策。三个 Agent 共享一份订单状态协同完成下单动作。这一层解决的是“从信息到行动”的问题。这样设计的好处是每一层可以独立测试。第一层做不好后面两层全都是空中楼阁。第二层做不好问题会派发给错误的专家回答再漂亮也是错的。第三层做不好前面问得再精准也没法闭环成交。我的建议是严格按这个顺序一步一步来不要一上来就写五个 Agent 互相调用多半会把自己绕晕。2. 环境准备与数据构建2.1 安装依赖与模型接入我用 Python 3.10 环境虚拟环境里面直接安装 LlamaIndex 的核心包。需求文件大概长这样llama-index-core0.10.0 llama-index-llms-openai-like0.1.0 llama-index-embeddings-openai-like0.1.0 llama-index-vector-stores-chroma0.1.0 python-dotenv这里我特意标注openai-like是因为很多开源模型服务都实现了兼容接口环境变量里配好LLM_API_BASE和LLM_API_KEY就能用模型服务商本身不影响 LlamaIndex 的代码逻辑。如果你的环境接的是其他模型只要把Settings.llm和Settings.embed_model替换成对应的实现即可。from llama_index.core import Settings Settings.llm OpenAILike( modelyour-model-id, api_baseos.getenv(LLM_API_BASE), api_keyos.getenv(LLM_API_KEY), temperature0.1, ) Settings.embed_model OpenAILikeEmbedding( modelyour-embedding-model-id, api_baseos.getenv(EMBED_API_BASE), api_keyos.getenv(EMBED_API_KEY), )我实际测试的时候踩过一个坑temperature设置太高回答里会出现编造的参数值。客服问答场景建议把温度压到 0.1 以下甚至 0。检索到的内容是什么就回答什么不要让模型自由发挥。2.2 构造手机机型数据集数据文件的目录结构我是按品牌分的每个品牌一个子目录子目录里每款手机一个 Markdown 文件data/ brand_alpha/ star_x1.md star_x1_pro.md brand_beta/ speed_pro.md speed_pro_max.md目录结构本身也是一种元数据加载时可以自动关联到品牌。不要低估这种“文件系统即分类”的做法后面的品牌路由会省很多事。每款手机的 Markdown 文件里我除了正文还在顶部加了 YAML front matter 做元数据标注--- brand: brand_alpha series: star name: 星曜X1 price: 3999 stock: available ---LlamaIndex 的SimpleDirectoryReader不会自动识别 YAML front matter所以我会写一段自定义加载逻辑解析这些字段放进Document的metadata里。这样一来后面做元数据过滤检索就非常方便比如“只查品牌Alpha的现货机型”可以直接在检索时过滤掉品牌字段以外的节点。2.3 文档切分与索引构建切分策略我用的是固定大小切分chunk_size设为 512chunk_overlap设为 64。对于机型参数这种信息密度极高的文本512 个字符大约能覆盖一款手机的核心参数重启发音 64 是为了防止“电池容量”和“充电功率”这类参数刚好被截断到两个切片里。如果你是第一次做 RAG这个环节最容易踩坑的地方是无脑切分导致一款手机的参数被拆到四五个片段里查询时可能只召回其中一段回答出来缺胳膊少腿。我的处理办法是每款手机的文件本身就比较短直接以“文件”为粒度建索引不切块或最多切成两块。这样一段话就包含一款手机的全部参数召回完整度远高于切得稀碎。索引我用的是向量索引from llama_index.core import VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter nodes SentenceSplitter( chunk_size512, chunk_overlap64, ).get_nodes_from_documents(documents) index VectorStoreIndex(nodes) retriever index.as_retriever(similarity_top_k4)similarity_top_k我调成 4因为单款手机文件的长度有限召回太多反而会混入不相关内容。生产环境如果数据量很大、品牌很多top_k 可以适当调大但一定要配合元数据过滤否则召回结果里会出现七八个品牌的机型混在一起回答非常难组织。3. 第一层单机器人问答3.1 最简 RAG 问答链路先把最简单的那条链路跑通。LlamaIndex 的QueryEngine封装了检索和生成核心代码只有几行query_engine index.as_query_engine(similarity_top_k4) response query_engine.query(品牌Alpha 星曜X1的电池容量是多少) print(response)跑通之后你会得到一句自然语言回答参考的依据来自检索到的向量节点。这一步没什么魔法本质上是把问题向量化跟索引里的节点算相似度召回最相关的几个节点再塞给大模型生成回答。我习惯在调试阶段把检索到的参考来源也打印出来确认模型确实是用正确依据回答的for node in response.source_nodes: print(node.node.metadata) print(node.node.get_text()[:100])这一步很多初学者会跳过直接看最终回答。如果最终回答错了不去看召回来源就永远定位不了问题。先确认“有没有检索到正确的文档”再确认“检索到了但回答错了”后者往往是提示词问题前者是索引或切分问题。3.2 提示词与检索参数调优单机器人的提示词我写了很长一版后来才意识到客服场景不需要花哨。核心就三件事只能基于检索内容回答不要编造参数如果检索内容不够就明确说“我不确定请转人工”多轮对话中不要重复追问已经给出的信息。from llama_index.core.prompts import PromptTemplate custom_prompt PromptTemplate( 你是一个手机客服助手。请基于以下资料回答用户问题。 如果资料中没有相关答案直接说资料中没有找到相关信息。\n 资料:\n{context_str}\n\n 用户问题:\n{query_str}\n\n 回答: )检索参数里最值得调的是similarity_top_k和相关性阈值。有些问题比如“星曜X1和小米对标机型哪个好”如果 top_k 太小可能只召回星曜X1的文档回答缺少对比信息。如果 top_k 太大又可能把无关品牌召回进来。我最后定的是先跑一遍真实用户问题观察召回来源再反过来调 top_k。3.3 单机器人的边界在哪里我把单机器人做完了跑了一批测试问题表现不错但明显感觉到边界。第一用户问“品牌Alpha 星曜X1 和 品牌Beta 极速Pro 哪个更值得买”单机器人虽然能把两边的参数都召回但回答还是倾向于“罗列参数”因为模型拿到的是一堆碎片化文档没有一个统一的对比结构。第二用户问“帮我下单”单机器人完全没有执行能力它只能回答“我们支持在线下单请到页面操作”没法真正把订单给创建出来。第三如果用户继续追问“我刚问的星曜X1是哪个颜色”单机器人很难在上下文中记住上一轮聊到哪个具体型号。这些问题都指向同一个方向需要把“一个大而全的问答机器人”拆成“多个有分工的智能体”。单机器人是本项目的地基但千万不要把它当成终点。你在上面花的时间不会白费因为后面每层智能体本质上还是在调用类似能力的检索脚本。4. 第二层品牌专家路由4.1 路由是“分诊台”而不是“二次检索”路由这个概念听起来高级但落地时经常被做成“二次检索”先把用户问题拿去搜一遍看命中了哪个品牌就把问题派给那个品牌。这做法在简单场景下能跑但遇到“品牌Alpha和品牌Beta哪个更适合游戏”这种跨品牌问题就废了。我更推荐把路由层做成“意图和品牌的双重分诊”。先判断用户问的是售前咨询、参数对比、价格库存、售后问题哪一类再判断主要涉及哪个品牌或者是否涉及多个品牌。只有明确涉及单一品牌才直接派给专家 Agent涉及多品牌就拆成多个子问题分别派发最后做汇总。这一步的代码实现其实不复杂。我写了一个路由函数基于小模型分类再用规则兜底def route_to_brand(question: str) - list[str]: # 规则兜底命中品牌关键词直接返回 brand_alpha_hits [星曜, Alpha] brand_beta_hits [极速, Beta] if any(k in question for k in brand_alpha_hits): return [brand_alpha] if any(k in question for k in brand_beta_hits): return [brand_beta] # 未命中关键词走LLM分类 prompt f判断以下问题涉及哪些品牌{question}。只回答品牌代号列表。 resp llm.complete(prompt) return parse_brands(resp.text)关键词兜底很关键。实际测试里LLM 偶尔会把“星曜X1”理解成某种配件而不是手机型号关键词匹配能保证主流品牌一定不会漏。反过来全靠关键词也容易漏掉“我昨天看的那款高刷屏手机”这种模糊指代所以必须保留 LLM 分类。4.2 品牌专家 Agent 的封装每个品牌专家 Agent 本质上是一个“索引 提示词 工具”的集合体。我直接复用第一层做好的单品牌索引给每个品牌单独建一份from llama_index.core.indices.query.query_engine import RetrieverQueryEngine alpha_engine create_brand_engine(brand_alpha) beta_engine create_brand_engine(brand_beta)create_brand_engine里边建了一个带元数据过滤的检索器def create_brand_engine(brand_name: str): filters MetadataFilters( filters[ExactMatchFilter(keybrand, valuebrand_name)] ) retriever index.as_retriever( similarity_top_k4, filtersfilters, ) return RetrieverQueryEngine(retrieverretriever)这样做的好处是品牌Alpha专家回答问题时检索器根本不会看到品牌Beta的文档所有相关度计算只在 Alpha 的范围内进行答案的专业度明显提升。更完整的做法是把每个专家包成一个 Agent让它具备调用自身工具的能力。比如品牌Alpha专家内部可以调用“查现货”“查价格”“查售后政策”这几个工具。这一步我用的是 LlamaIndex 的FunctionAgent或自定义 ReAct Agent每个 Agent 有自己的系统提示词和工具集合。核心是让专家 Agent 只关心自己品牌域内的事情不要跨域“表演”。4.3 路由准确性的兜底策略路由层做得再精细也一定会遇到新问题用户提到的品牌名有错别字或者用“某水果品牌”这种说法LLM 分类也可能翻车。我加了两道保险。第一道路由到专家后专家 Agent 如果发现检索结果为空或相关性过低允许它返回“该问题不在我的知识范围内”路由层拿到这个信号后回退到全量索引再查一遍。第二道我保留了一个“通用坐席 Agent”专门接收无法明确归类的模糊问题。这个 Agent 没有品牌元数据过滤类似第一层单机器人但回答会更谨慎倾向于引导用户补充信息。route_resp classify_question(question) if len(route_resp.brands) 0: return general_agent.chat(question) if len(route_resp.brands) 1: return expert_engines[route_resp.brands[0]].query(question) if len(route_resp.brands) 1: sub_results [] for brand in route_resp.brands: resp expert_engines[brand].query(f从{品牌}角度回答{question}) sub_results.append(resp) return merge_compare_answer(sub_results, question)这个兜底策略在真实项目里非常重要它保证了系统的“体面”宁可让通用坐席说“我不确定”也不要让路由层硬把一个品牌问题派给另一个品牌答得牛头不对马嘴。5. 第三层多智能体协作下单5.1 从回答问题到执行任务前两层解决的是“把话说对”第三层要解决“把事做对”。下单一句话听起来简单但真正拆开看涉及好几道独立动作确认商品和规格、检查库存、计算价格、创建订单、填写收货信息、发起支付。如果用一个 Agent 把所有动作全部塞进去工具调用会异常臃肿一个环节出错就得整个重来。多智能体协作的思路是让不同 Agent 各负责一个环节通过一个状态容器互相协作。LlamaIndex 里做多智能体的方式有很多种我这套用的是任务委派模式也就是一个大流程 Agent 负责拆解任务再把子任务交给对应的小 Agent。我把下单流程拆成三个 Agent商品咨询 Agent负责查规格、价格、库存解决“能不能买”的问题。购物车 Agent负责把选定商品加入购物车维护购物车清单。订单 Agent负责根据购物车生成订单确认金额和收货信息。这里有一个容易搞混的地方有些人管“商品咨询 Agent”叫“客服 Agent”但它其实和第二层的品牌专家 Agent 职责重叠。我的配对方式是品牌专家 Agent 回答提问一旦用户表达购买意向就把会话转交给下单流程。下单流程里先用商品咨询功能校验库存和价格再让购物车 Agent 和订单 Agent 接手。5.2 购物车、订单、售后三个 Agent 的协作细节每个 Agent 需要暴露一组可调用函数而不是让它自由发挥。购物车 Agent 至少要有这几个工具def add_to_cart(item_name: str, spec: str, quantity: int) - dict: return cart_service.add(...) def view_cart() - dict: return cart_service.list()订单 Agent 的工具def create_order(cart_id: str, address: dict) - dict: return order_service.create(cart_id, address) def confirm_payment(order_id: str) - dict: return order_service.pay(order_id)这些函数的输入和输出最好全是 JSON字段严格固定不要返回自然语言。Agent 的模型输出一旦是自然语言再接工具函数时很容易解析失败。我在项目里把所有工具函数的结果用 Pydantic 模型校验输出格式不合规直接报错避免脏数据往下游传。协作的过程大致是用户说“我要买星曜X1 12GB256GB 三台”主控 Agent 先调用商品咨询确认库存足够再把商品信息传给购物车 Agent。购物车 Agent 返回购物车 ID 后订单 Agent 拿着购物车 ID 去生成订单同时询问用户收货地址。地址确认后订单 Agent 再发起一个模拟支付动作。整个过程的中间状态我都实时打印出来方便在日志里看到底卡在哪个环节[主控] 用户意图购买 星曜X1 12GB256GB x3 [商品咨询] 库存充足单价3999元 [购物车] 已添加cart_id42 [订单] 订单创建成功order_id88总价11997元 [订单] 等待用户确认收货地址和支付这里“等待用户确认”非常重要。在真实电商场景里直接调支付接口是很危险的行为所以我特意让订单 Agent 在支付前停下来要求用户明确回复“确认支付”。这是个典型的护栏设计。5.3 用状态容器串联协作这套 Agent 协作机制底层靠一个共享状态字典把各个环节串起来state { query: , selected_item: None, cart: None, address: None, order: None, }每个 Agent 执行完自己的动作后更新 state主控 Agent 读取 state 决定下一步执行哪个 Agent。这比让 Agent 之间互相传递对象更方便排查因为任意时刻你都能看到整个订单流走到了哪一步也便于在出错时直接修改状态重跑。我用 LlamaIndex 的 AgentRunner 来做这一步。它的核心逻辑是给主控 Agent 注册三个子 Agent 作为工具主控根据用户输入决定调用哪个子 Agent。代码示意from llama_index.core.agent import FunctionAgent, AgentRunner cart_agent FunctionAgent.from_tools( tools[add_to_cart, view_cart], llmSettings.llm, system_prompt你是购物车助手只负责购物车相关的操作。, ) order_agent FunctionAgent.from_tools( tools[create_order, confirm_payment], llmSettings.llm, system_prompt你是订单助手负责生成订单和确认支付。, ) main_agent AgentRunner.from_llm( agents[cart_agent, order_agent], llmSettings.llm, system_prompt你是手机客服主管负责拆解用户需求把任务交给购物车或订单助手。, )主控 Agent 不是“多了一个 OpenAI Agent”而是把购物车和订单两个子 Agent 的调用能力暴露给模型模型在对话过程中自行决策调谁。把模型当成调度中枢好处是面对没见过的表达方式也能灵活应对坏处是会出现调度错误把“查物流”误派给购物车助手。所以我的提示词里明确写了不确定的任务不要让子 Agent 处理必须回到主控重新确认。5.4 多智能体协作的安全护栏说到安全护栏这部分新手最容易忽略。我建议至少做四个层面的限制。第一支付动作必须人工确认。我在临码里故意不让模型直接触发真实支付而是把支付动作模拟成一个“待确认”状态只有用户回复“确认支付”后订单 Agent 才能执行后续动作。第二跨 Agent 传递的数据要校验。购物车传给订单的 cart_id、价格、数量必须经过 Pydantic 校验类型对不上就终止流程。第三每个子 Agent 的能力要窄。购物车 Agent 不要给它挂“查天气”这种无关工具也不要给它“删除全部订单”的权限否则模型在多步推理时容易误调危险工具。第四整个流程要有超时和重试机制。某个 Agent 长时间不返回主控要能主动撤销并重试避免一次卡死整个会话。我把这四条作为一条铁律写进了项目的 README 里。做智能体项目模型能力越强越需要护栏不然一个“帮我取消所有订单”的误操作就可能造成线上事故。6. 常见问题与调试心得6.1 检索结果不相关这是我的项目里出现频率最高的问题。表现在用户问星曜X1的屏幕参数检索回来的却是另一个系列的文档。排查步骤我固定为三步。第一步看召回来源。打印response.source_nodes的元数据确认召回的节点品牌和型号对不对。第二步看你存的向量是什么。如果是用统一 model 对长短文本混合做向量化短的型号名称和长的参数描述计算相似度时经常不理想我会把型号名、系列名这一类的字段也拼到正文文本里再向量化让“星曜X1”这个关键词本身能在向量空间里被检索到。第三步看切分粒度。前面说过机型参数文件我建议整体作为节点不要切碎。我实测下来切碎之后“电池容量”和“充电功率”经常被分到两个块里回答时模型只能看到一半信息很容易漏答。6.2 路由分错品牌路由分错品牌的表现很隐蔽用户问“品牌Alpha 星曜X1”路由却判断成品牌Beta。我发现大多数分错都是因为用户问题里同时出现了通用词比如“高性能手机”“拍照手机”模型无法判断具体品牌就猜了一个。我的解决办法是在路由提示词里加入白名单校验逻辑。如果用户问题里没有出现白名单里的品牌关键词且明确涉及购买意向那就默认走通用坐席而不是乱猜。宁可让用户明确一下品牌也不要给出一个挂羊头卖狗肉的答案。另外关键词兜底花费了我大量时间。一定要把每个品牌的系列名、营销词、重点型号存成一个字典路由时先查字典再问模型这样品牌的准确率能稳定在 90%以上剩下的靠通用坐席兜底。6.3 Agent 容易“幻觉”库存和价格第三层我遇到的最头疼的问题是Agent 在回答“有货吗”“多少钱”时有时会不查工具直接编一个价格。原因是大模型的对话惯性让它顺着用户的话往下说工具调用被跳过了。解决办法有两个。一是把工具返回结果强制插入到上下文里而不是让模型“决定是否要看”。我在商品咨询 Agent 里做了一个规则涉及库存或价格的问题必须调用查询工具工具返回后直接把结果写进提示词的资料区模型只能基于这个结果回答。二是在提示词里限制“如果你没有调用查询工具就不能回答价格和库存问题”。这两条双管齐下幻觉问题基本被消灭了。6.4 上下文长度与 token 成本把所有检索结果和工具返回都塞进上下文多轮下来上下文很容易越长越大。品牌专家 Agent 还要拆成多轮对话token 成本直接飙升。我的个人经验是把“长期记忆”和“工作记忆”分开。长期记忆存用户偏好、历史购买记录只在需要时检索相关片段工作记忆就是当前订单流程的状态机每次只保留必要字段不要整段对话历史全部传给模型。多智能体协作时子 Agent 之间的中间过程不要全部透传给最终模型只汇总关键结论。这两招能让 token 消耗降一半以上。如果预算紧张还可以给检索和路由用更便宜的模型只有最终生成回答时用贵模型。我从第二层开始就做了模型分层路由分类用小模型品牌专家回答用大模型实际效果没打折成本省了很多。6.5 LlamaIndex 版本兼容性与调试技巧我做这个项目时 LlamaIndex 的 API 还在快速迭代期间踩了不少兼容性坑最典型的是QueryEngine、FunctionAgent、AgentRunner这些类的引入路径在不同版本之间变化很大。我从 0.9 升到 0.10 的时候有一大段导入代码直接报错。后来养成一个习惯所有引入都尽量从稳定的llama_index.core走agent 相关的从llama_index.core.agent走第三方集成单独抽出来放一个文件里方便以后适配。还有一个调试技巧非常有用在 LlamaIndex 里把日志打开加上回调用CallbackManager记录每一次检索和工具调用的耗时和token数。这样定位问题就不用靠猜测直接看日志里哪一步耗时最长、哪一次调用返回了空结果。from llama_index.core.callbacks import CallbackManager, LlamaDebugHandler debug_handler LlamaDebugHandler() Settings.callback_manager CallbackManager([debug_handler])有了这个我基本能把每条日志对应的检索来源和工具调用过程完整回放排查效率翻倍。个人体会是这套流程里的“三阶段”不只是演示花样每一层解决的都是真实客服系统的核心问题。单机器人把信息问答做扎实路由层把知识归属做清楚多智能体让系统真正能执行动作。手机机型数据集贯穿始终只是搭了一个足够贴近业务的脚手架。你要是想复现建议也先选一个你自己熟悉的小领域造一份结构化的数据集从第一层码到第三层别急着堆智能体数量。跑通三层之后再回头看你的字段设计、提示词、路由逻辑会有完全不同的一层理解。