做这个项目之前,我其实已经用 LlamaIndex 写过不少 RAG Demo,但基本都是"单问答机器人"那种,数据扔进去、检索出来、生成回答,链路很短,成就感来得快,但离真正能用到业务里还差一大截。这次这个手机客服场景不一样——标题里写得很清楚:一份手机机型数据集,要贯穿单机器人问答、品牌专家路由、多智能体协作下单三个阶段。说白了就是从"能对话"一路干到"能办事",这中间涉及的不只是代码量变多,而是架构思路要整体升级。
这篇文章我会把项目从数据设计、链路搭建、路由策略到多智能体协作的完整实现过程拆开讲,重点是每一步为什么要这么选、参数为什么这么调、哪些地方是文档里不会写但实操必踩的坑。如果你正在用 LlamaIndex 做 RAG 应用,或者刚接触多智能体 Agent 想找一个能落地的参考案例,这篇值得看完。
1. 项目拆解:一份数据集如何撑起三层复杂度
1.1 场景定位:从"能答"到"会分工"再到"能办事"
这个项目最巧妙的地方,就是没有为了炫技去堆多智能体,而是先想清楚三个递进的业务场景:
第一个场景是单机器人问答。用户问"某款手机支持无线充电吗""电池容量多少",系统需要基于手机机型数据集做检索增强生成,回答要准确、有依据。这个阶段解决的问题是"知识库能不能被有效检索和引用"。
第二个场景是品牌专家路由。用户说"我想看看 Alpha 品牌 3000 元以内的手机",系统应该判断这个问题涉及 Alpha 品牌,然后把这个请求交给专门负责 Alpha 品牌数据的专家机器人处理,而不是让一个通用机器人去检索全部数据。这个阶段解决的问题是"不同品牌的知识能不能隔离处理、精准路由"。
第三个场景是多智能体协作下单。用户说"我需要一台适合玩游戏的手机,预算 4000,帮我下单"。这句话里包含需求分析、产品推荐、价格计算、库存确认、下单执行等多个动作,单个机器人很难一口气搞定,需要多个 Agent 各司其职、协作完成。这个阶段解决的问题是"复杂任务能不能被拆解并由多个智能体协同执行"。
这三个场景如果各用各的数据集、各写各的代码,其实也能跑通,但那就失去了这个项目的真正价值。我选择用同一份手机机型数据集贯穿全部流程,原因很朴素:业务里的数据永远是同一套,不同场景只是以不同方式在使用它。单机器人问答用全量数据做召回,品牌专家路由用品牌维度做过滤,多智能体协作则是在前两者基础上叠加工具调用能力。数据不统一,后面的路由和协作就没有可信的根基。
1.2 数据集设计的统一约定
手机机型数据集是这个项目的地基。我没有用那些公开的爬虫数据,因为字段不可控,而是自己构造了一份结构化 JSON 数据集,每条记录包含以下字段:
phone_id:机型唯一标识,如A1001brand:品牌,这里用虚构品牌名 Alpha、Beta、Gamma 代替,避免商标问题model:具体型号名,如Alpha X5 Proprice:价格(元)specs:屏幕、处理器、内存、存储、电池、摄像头、充电功率features:如无线充电、NFC、防水等级、指纹识别stock:当前库存数量warranty:保修政策描述
为什么用 JSON 而不是纯文本?因为后续的路由和下单环节需要结构化字段做精确过滤和参数传递。比如路由阶段要按brand字段匹配,下单阶段要检查stock字段是否充足,如果数据是一大段自然语言文本,这些操作会非常吃力。
这里有个细节值得注意:我在每个机型描述里都保留了完整的自然语言文本描述,而不只是结构化字段。原因是 RAG 检索依赖语义相似度,用户提问"哪款手机拍照好看"并不包含"摄像头""像素"这样的显式关键词,如果只靠结构化字段,检索效果会很差。所以数据集的设计是"结构化字段 + 自然语言描述"双轨并行,结构化字段服务路由和下单,自然语言描述服务语义检索。我测试下来,这种混合结构比纯字段或纯文本都稳。
数据量方面,我构造了 90 款机型,每个品牌 30 款,覆盖 1000-8000 元价格区间。90 这个数量对于跑通整个流程足够了,再多也会拖慢索引构建和检索速度,不利于调试。
2. 基础对话落地:单机器人的 RAG 问答链路
2.1 选型与架构取舍
单机器人问答是整个项目的地基,后面的路由和 Agent 协作都是在这个基础上叠加。这个环节我踩过一个比较大的坑:一开始图省事,直接用了 LlamaIndex 的默认配置,向量索引建完就开测,结果检索效果惨不忍睹——用户问"3000 块能买到什么手机",返回的结果里混着大量 8000 元旗舰机的信息。
后来我认真分析了原因,问题出在三个地方:一是文本切分粒度太大,二是检索器没有结合结构化字段做过滤,三是指令模板没有引导模型聚焦机型参数。这三个问题其实是 RAG 落地中最常见的"三座大山",逐个解决之后效果才真正可用。
LlamaIndex 在这个场景下的优势是它把"加载-索引-检索-生成"封装得非常顺手,尤其支持多种索引类型混用。我做这个项目时用了VectorStoreIndex做主要的语义检索,配合自带的retriever和query_engine,几百行代码就能跑起来一个基础问答服务。相比从零写向量检索和上下文组装,效率高太多了。
2.2 LlamaIndex 构建检索问答的具体实现
基础问答链路其实可以拆成四步:加载数据、解析文档、构建索引、创建查询引擎。我用SimpleDirectoryReader读取 JSON 文件,然后用NodeParser做文本切分,再用VectorStoreIndex构建向量索引,最后通过as_query_engine暴露问答接口。
这里有一个我强烈建议不要略过的细节:自定义NodeParser的切分逻辑。LlamaIndex 默认按固定 chunk_size 切分,但手机机型文档的结构化特征很强,默认切分很容易把一个机型的"卖点描述"和"参数列表"切开,导致检索时上下文不完整。我推荐的最小化配置大概是这样的:
from llama_index.core.node_parser import JSONParser from llama_index.core import SimpleDirectoryReader, VectorStoreIndex documents = SimpleDirectoryReader( input_dir="./data/", required_exts=[".json"], ).load_data() parser = JSONParser() nodes = parser.get_nodes_from_documents(documents) index = VectorStoreIndex(nodes) query_engine = index.as_query_engine( similarity_top_k=4, response_mode="compact", )response_mode="compact"这点很重要。默认的response_mode会把所有检索到的文本块全部塞给大模型,回答很啰嗦而且容易跑题。compact模式会尽量压缩有用的上下文后再给模型,回答更聚焦。我在实测中对比过,回答准确率更高,而且响应时间明显缩短。
2.3 检索质量提升的关键参数调节
如果你只是按照上面的代码跑,大概率会遇到检索结果不精准的问题。我调试的时候重点盯三个参数:
similarity_top_k,这个参数控制召回几条相关片段。我建议先设 4-6 之间测试,如果回答经常漏信息就调大,如果经常塞入无关内容就调小。手机问答场景比较特殊,用户问"这款手机支持 5G 吗",答案往往只藏在某一个片段里,top_k 设得太大反而会把不相关的片段混进来干扰模型判断。
chunk_size和chunk_overlap,这两个参数决定了切分粒度。手机机型数据文本大部分在 200-500 字之间,我测试下来chunk_size=300、chunk_overlap=50效果比较好。chunk 太小容易把关键参数切断,chunk 太大检索精度下降,overlap 可以保证跨段信息不丢。
还有一个容易被忽略的是 Embedding 模型选择。LlamaIndex 默认的text-embedding-ada-002效果不错但需要付费 API,离线场景下我推荐BAAI/bge-small-zh-v1.5这类中文 Embedding 模型。手机问答场景很多描述是中文,中文 Embedding 模型的语义理解更准。切换方式是在Settings.embed_model里指定本地模型路径。
提示:
similarity_top_k和检索精度的关系不是线性的。我在这个项目里测试过,从 2 调到 6,召回率提升明显,但调到 8 以后回答开始出现轻微跑题。建议在验证集上多测几组,找到你的数据集专属的甜点值。
3. 品牌专家路由:从"一个客服"到"品牌专家组"
3.1 为什么需要路由而不是一把抓
单机器人问答跑通之后,我开始思考一个实际业务问题:如果客服系统只服务一个品牌,那一个机器人确实够了。但现实的手机客服通常要同时服务多个品牌,而不同品牌的定位、参数侧重点、卖点差异很大。让一个通用机器人回答所有品牌的问题,至少有两个明显问题:
第一,检索范围过大导致精度下降。当用户问"Alpha 品牌的旗舰机用的什么处理器",如果系统在全量 90 款机型里做语义检索,很可能把 Beta、Gamma 品牌的高端机也召回进来,因为它们同样有"旗舰""处理器"这些词,回答时模型就容易混淆品牌信息。
第二,不同品牌需要不同的回答口径。举个例子,Alpha 品牌主打性价比,推荐话术偏重"同价位配置最高";Gamma 品牌主打高端影像,推荐话术偏重"影像系统调教"。如果所有品牌共用一个系统提示词,回答风格会非常统一但失去品牌特色。
路由机制解决的就是这两个问题:先判断用户问题属于哪个品牌,然后只在该品牌的数据范围内做检索和生成。
3.2 路由方案实现:基于元数据过滤的 RouterQueryEngine
LlamaIndex 官方提供了多种路由方案,其中最常用的是RouterQueryEngine,它本质上是一个"先选择、再查询"的引擎。它内置了一个选择器,会对每个候选查询引擎打分,选中最合适的那个执行。
但光有 RouterQueryEngine 还不够,关键在于如何构造"品牌专属查询引擎"。这一步我用了 LlamaIndex 的元数据过滤能力:
from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.tools import QueryEngineTool alpha_engine = index.as_query_engine( similarity_top_k=4, filters=MetadataFilters( filters=[ ExactMatchFilter(key="brand", value="Alpha") ] ) ) beta_engine = index.as_query_engine( similarity_top_k=4, filters=MetadataFilters( filters=[ ExactMatchFilter(key="brand", value="Beta") ] ) ) router = RouterQueryEngine.from_defaults( query_engine_tools=[ QueryEngineTool.from_defaults( query_engine=alpha_engine, description="回答 Alpha 品牌手机的问题", ), QueryEngineTool.from_defaults( query_engine=beta_engine, description="回答 Beta 品牌手机的问题", ), ], select_multi=False, )用MetadataFilters配合ExactMatchFilter做品牌字段的精确过滤,是这里最核心的一步。这样每个品牌引擎在检索时就直接被限制在对应品牌的数据子集内,从源头避免了跨品牌混淆。
description字段也别小看。RouterQueryEngine 的选择器本质上是把每个引擎的 description 和用户问题一起送给大模型做意图判断,所以 description 写得越清晰,路由准确率越高。我测试时把 description 从"查询 Alpha 品牌手机"改成"回答与 Alpha 品牌手机相关的任何问题,包括参数、价格、推荐、对比"之后,路由准确率肉眼可见地提升了。
3.3 路由效果评估与兜底
路由不是配置完就完事的,一定要做效果评估。我从测试集里抽了 45 个问题,15 个问 Alpha,15 个问 Beta,15 个跨品牌或通用问题,然后逐个看路由结果和最终回答。评估下来准确率大概在九成以上,但这个数字背后有个重要的补充设计——兜底引擎。
当用户问题不涉及具体品牌,比如"4000 块以内的手机推荐一下",路由选择器会硬着头皮选一个品牌引擎,但这样回答就会偏心。我增加了第三个兜底引擎,使用全量数据,专门处理跨品牌和通用问题:
general_engine = index.as_query_engine( similarity_top_k=6, ) router = RouterQueryEngine.from_defaults( query_engine_tools=[ alpha_tool, beta_tool, QueryEngineTool.from_defaults( query_engine=general_engine, description="回答不限定品牌的通用手机问题或跨品牌对比问题", ), ] )这样一个三层路由结构就能覆盖大多数实际问答场景。品牌问题找品牌专家,通用问题找全量引擎,结构简单又不会让检索顾此失彼。
注意:路由方案还有一个变体叫
FunctionTool+Agent,原理是把每个品牌引擎封装成一个函数工具,让Agent自主决定调用哪个。这种方式更灵活,但可控性不如 RouterQueryEngine。如果你的业务路由规则很稳定,用 RouterQueryEngine 就够了;如果问题复杂到需要多轮工具组合,再上 Agent 方案。
4. 多智能体协作下单:让 Agent 真正动起来
4.1 多智能体分工设计与工具定义
到了这个阶段,项目开始真正进入"多智能体 Agent"的范畴。我的设计思路是:不再用一个超级 Agent 包办所有事,而是拆分出三个职责单一、边界清晰的智能体。
第一个是客服导购 Agent,负责理解用户需求、推荐合适机型、回答参数问题。它本质上就是前面已经做好的 RAG 问答引擎,只是被封装成了可调用工具。第二个是订单处理 Agent,负责创建订单、查询库存、生成订单详情,它不直接访问知识库,而是通过工具函数操作一个内存中的订单系统。第三个是协调者 Agent,也可以理解为调度中枢,负责理解用户意图,决定把任务交给哪个下游 Agent,并把最终结果汇总给用户。
我之所以采用这种"调度者 + 执行者"的架构,而不是把所有事情塞给一个 Agent,是因为下单流程有严格的状态依赖:必须先确定机型 -> 检查库存 -> 计算价格 -> 创建订单。如果让一个 Agent 用工具自由发挥,它可能在库存不足时仍然创建订单,或者在用户还没确认机型时就去查价格。
协调者架构让每个环节有明确的归属,Agent 之间的协作是通过"工具调用"来实现的。具体来说,协调者 Agent 手上有一组工具:recommend_phone(调用客服导购 Agent 的 RAG 引擎)、create_order(调用订单处理 Agent)、check_stock(工具函数直接查库存)。它根据用户意图选择工具,然后把多个工具调用串起来。
LlamaIndex 里实现多智能体协作有不少方式,我这个项目用的是最直观的一种:把下游能力全部封装成工具,注册给协调者 Agent。这本质上是一种"工具型多智能体"模式,虽然不如那种 Agent 之间互相通信的模式花哨,但胜在稳定、可调试、易监控。
4.2 协作流程:从咨询到下单的完整链路
以一个真实的对话流程来演示可能会更清楚。用户输入:"我想买一台适合玩游戏的手机,预算 4000 左右,帮我下单。"
协调者 Agent 的第一步是调用客服导购 Agent(推荐引擎),把用户这句话切片理解:"玩游戏"对应高刷屏幕、强处理器、好散热;"预算 4000 左右"对应价格区间过滤;"帮我下单"对应后续动作。推荐引擎返回符合条件的机型,比如 Alpha X5 Pro(3999 元,骁龙级别处理器,144Hz 高刷屏,库存 15 台)。
协调者 Agent 的第二步是调用check_stock确认该机型库存充足,再调用create_order创建订单。create_order工具接收三个参数:用户ID、机型 ID、数量。工具内部会再校验一次库存,然后生成一条订单记录,返回订单号。
第三步,协调者 Agent 把订单结果整理成自然语言响应给用户:"已为您下单 Alpha X5 Pro,订单号 A1001202401,预计 48 小时内发货。"
这个流程的关键在于:RAG 推荐结果和下单操作之间的衔接。很多教程到这里就断了,只演示"推荐"不演示"下单",但真实业务里,光有推荐没有下单动作,项目价值就少了一大半。
实现上,我会把工具函数定义为 LlamaIndex 的FunctionTool:
from llama_index.core.tools import FunctionTool def create_order(user_id: str, phone_id: str, quantity: int) -> str: # 校验库存 stock = get_stock(phone_id) if stock < quantity: return f"库存不足,当前仅剩 {stock} 台" order_id = make_order(user_id, phone_id, quantity) return f"订单创建成功,订单号 {order_id}" order_tool = FunctionTool.from_defaults( fn=create_order, description="为指定用户创建手机订单,需要用户ID、机型ID、数量" )这里有个细节:description写得越详细,Agent 正确调用工具的概率越高。我发现如果不写清参数含义,Agent 经常会把手机型号名称当机型ID传进去,导致工具内部查不到库存。加上"机型ID是数据集中 phone_id 字段,不要用型号名称"这类说明后,调用成功率会显著提升。
4.3 状态管理与错误恢复
多智能体协作最容易翻车的环节是状态管理。用户说"下单",但并没有指定具体机型,这时候如果 Agent 直接创建订单,就可能下错单。解决思路是让订单工具内部做前置条件校验,不满足条件就拒绝执行并给出明确反馈。
我在create_order里加了一个规则:必须从推荐结果中携带机型 ID,不允许凭空指定机型。如果协调者 Agent 没有先调用推荐工具就直接下单,工具会返回"未找到有效的机型推荐记录,请先选择机型"。这个设计让下单链条有清晰的依赖关系,也避免 Agent 在信息不足时瞎操作。
还有一类问题是模型幻觉导致的工具参数错误,比如机型 ID 写错、数量写负数。我的做法是在工具函数里做参数校验,非法输入直接返回错误信息而不是抛异常。大模型很擅长从错误信息里自我纠正,只要反馈明确,它下一轮往往就能修正调用参数。
我整理了个比较实用的状态流转:
- 初始状态:用户表达购机意向
- 需求分析:调用推荐引擎,获取候选机型列表
- 机型确认:确认用户选定的机型 ID 和数量
- 创建订单:校验库存后生成订单
- 完成:返回订单号
协调者 Agent 在这个状态机里担任"大脑",下游工具是"手脚"。这样一个环节一个环节走完,下单流程的稳定性和可解释性都有了保障。
5. 踩坑实录与排查技巧
5.1 高频问题速查表
做这个项目过程中我整理了一份高频问题速查表,基本都是 LlamaIndex 和 Agent 开发中常见的坑:
| 症状 | 根因 | 解决办法 |
|---|---|---|
| 检索结果出现其他品牌机型 | 没有加MetadataFilters或过滤器字段名不对 | 检查索引节点有无brand元数据,过滤器使用ExactMatchFilter |
| 路由选择器频繁选错引擎 | description描述含糊 | 重写description,明确品牌边界和可处理的问题类型 |
| Agent调用工具时参数错误 | 工具description未说明参数规则 | 在工具的description里写清参数格式、取值来源、示例 |
| 回答内容大段重复 | response_mode默认模式导致 | 切换到compact或refine模式 |
| 下单时库存为0但依然创建订单 | 业务校验放在Agent提示词里而非工具内 | 把库存校验写入工具函数内部,不满足条件直接拒绝 |
| 中文检索效果差 | Embedding模型不擅长中文 | 切换BAAI/bge-small-zh-v1.5等中文Embedding模型 |
| JSON数据加载后节点过少 | JSONParser解析逻辑不符合预期 | 自定义解析逻辑,把每个机型变成一个独立节点 |
这张表里的每一条都是我在调试过程中实际遇到过的,有一个算一个,都花了不少时间去定位。
5.2 我印象最深的三个坑
第一个坑是路由阶段元数据过滤失效。我在索引里明明设置了 brand 字段,但用了MetadataFilters就是过滤不掉其他品牌的数据。排查了一下午发现是加载 JSON 数据时,LlamaIndex 默认把 JSON 的键值对结构当成文档内容处理,并没有自动生成brand元数据字段。我后来手动在节点构造时补充了元数据,问题才解决。
第二个坑是 Agent 工具调用陷入死循环。协调者 Agent 在推荐完机型后,一直重复检查库存而不创建订单,原因是check_stock工具和create_order工具的 description 区分度不够,模型搞不清楚该用哪个。我把 description 改成更明确的"只检查库存数量,不创建订单"和"创建订单前必须已经确认过库存",模型就不再困惑了。
第三个坑是响应时间太慢。加了多智能体之后,一次完整下单流程可能需要调用多次大模型,总耗时经常超过 30 秒。优化思路有两个方向:一是把路由和推荐这类子任务用as_query_engine的缓存特性缓存结果,二是把协调者 Agent 的模型换成推理速度更快的小模型,专业任务才用大模型。实测下来,首响应时间从 30 秒降到了 10 秒以内。
提醒:多智能体项目调试时一定要保留完整的中间日志,包括路由选择结果、工具调用参数、工具返回结果。这不是可有可无的细节,而是定位问题的唯一线索。
6. 实操心得与扩展建议
整个项目做下来,我个人最深的体会是:RAG 应用从"能用"到"好用",瓶颈往往不在模型能力,而在工程设计的细致程度。同一份数据集,单机器人问答阶段,它只是检索对象;品牌路由阶段,它变成了分而治之的知识切片;多智能体协作阶段,它又成了工具调用的数据源头。数据始终没变,变的是我们处理数据、组织知识、编排任务的方式。
如果你也想复现这个项目,我的建议是严格按照"单问答 -> 路由 -> 多智能体"的顺序推进,不要一上来就跳到多智能体。没有把单机器人问答的检索质量调到满意,后面的路由和 Agent 协作会在同样的数据问题上反复踩坑,而且排查难度成倍增加。
最后再分享一个小技巧:这个项目的数据集虽然是虚构的 90 款机型,但你可以很容易地替换成真实业务的数据来做改造。手机机型数据的核心结构——品牌、型号、价格、参数、库存、自然语言描述——很多电商和客服场景的数据都能对齐到这个结构上。拿一套真实数据直接套用这个链路,很快就能验证多智能体 Agent 在你自己的业务场景里是不是真的有用。