手机客服这个场景,我陆陆续续做了几个项目,最深的感受是:它不像通用聊天机器人那样问两句闲话就完事,而是要把“查参数、比型号、判故障、查库存、引导下单”这些具体动作串起来。前阵子我用一份手机机型数据集,把 LlamaIndex 的 RAG 问答、品牌专家路由、多智能体协作下单完整跑了一遍,从最基础的单机器人问答开始,一层一层往上加能力。这篇就按我的实际落地过程,把 LlamaIndex RAG 手机客服多智能体 Agent 教程的全流程拆给你看。
先说清楚这套东西解决了什么问题。传统客服机器人拿一个大知识库做检索,用户问“X 品牌新品和 Y 品牌旗舰哪个适合打游戏”,检索出来的内容可能是两份割裂的说明书,回答起来很别扭。而多智能体架构下,每个品牌有一个“专家助手”,路由层先判断用户想找谁,再把问题转给对应专家,最后如果需要下单,购物车、订单、售后这些执行型 Agent 再接手。整个过程是一份机型数据集贯穿起来的,从单机器人问答,到品牌专家路由,再到多智能体协作下单,我按这条线一步步演示。
1. 项目概述与整体设计思路
1.1 一个贯穿全流程的手机机型数据集
我用的数据集不是网上下载的现成知识库,而是自己整理的一份脱敏手机机型数据集。里面每款手机对应一个结构化的字段记录,包括系列名、型号、屏幕尺寸、刷新率、电池容量、充电功率、处理器型号、内存存储组合、后置摄像头参数、价格、库存状态、上市日期、售后政策等十多个字段。
为什么要强调“贯穿全流程”?因为很多 RAG 教程的数据集只是一堆纯文本,检索出来之后就只能做问答演示。而手机机型数据集天然适合做进阶玩法:它有清晰的层级,品牌下面有系列,系列下面有具体型号,型号下面挂参数和价格。这套层级结构既是 RAG 的切分依据,也是品牌专家路由的分类依据,更是多智能体下单流程里的商品对象。同一份数据,前面用来做问答检索,后面用来构造购物车条目,逻辑上是连贯的。
数据格式我用的是 Markdown 加 JSON 混合的方式。Markdown 便于人类阅读,JSON 便于程序解析。实际构建索引时,我会把每款手机单独拆成一个文档节点,节点本身包含正文,节点的元数据里存品牌、系列、价格、库存这些字段,后面做过滤检索会非常方便。
# 品牌Alpha 星曜X1 - 系列:星曜 - 型号:品牌Alpha 星曜X1 - 屏幕:6.7英寸 AMOLED,120Hz刷新率 - 电池:5000mAh,80W有线快充 - 处理器:某旗舰芯片 - 内存/存储:12GB+256GB / 16GB+512GB - 后置摄像头:5000万像素主摄 + 1200万像素超广角 - 价格:3999元 / 4499元 - 库存:现货 - 售后:支持7天无理由退货,2年质保这里说明一下,文中品牌和型号我全部做了脱敏,统一叫“品牌Alpha”“品牌Beta”,你们自己跑的时候换成真实机型数据就好,流程完全一致。
1.2 三层架构为什么这样设计
整套系统我拆成三个层次,对应标题里的三个关键词:单机器人问答、品牌专家路由、多智能体协作下单。
第一层是单机器人问答。这层不做任何花样,就是一个 LlamaIndex 的标准 RAG 链路:加载文档、切分、向量化、建索引、查询。用户问“星曜X1的电池容量是多少”,直接召回相关节点,用大模型组织成自然语言回答。这一层解决的是“信息查找”问题。
第二层是品牌专家路由。用户问“品牌Alpha 星曜X1和品牌Beta 极速Pro选哪个”,如果只有一个通用机器人,检索到的文档可能一半是 Alpha 的,一半是 Beta 的,回答质量完全看运气。路由层会先判断这是一个跨品牌对比问题,然后把问题拆解,分别交给品牌Alpha专家和品牌Beta专家去查询,再把两边的结果汇总对比。这一层解决的是“知识归属和分发”问题。
第三层是多智能体协作下单。用户问完参数对比后说“我要买品牌Alpha 星曜X1 12GB+256GB,黑色,有货吗?帮我下单”。这已经超出问答范畴,需要调用工具、读写状态、执行动作。于是用户问题被路由到下单流程,购物车 Agent 负责把商品加入购物车,订单 Agent 负责生成订单信息,售后 Agent 负责确认库存和发货政策。三个 Agent 共享一份订单状态,协同完成下单动作。这一层解决的是“从信息到行动”的问题。
这样设计的好处是每一层可以独立测试。第一层做不好,后面两层全都是空中楼阁。第二层做不好,问题会派发给错误的专家,回答再漂亮也是错的。第三层做不好,前面问得再精准也没法闭环成交。我的建议是严格按这个顺序一步一步来,不要一上来就写五个 Agent 互相调用,多半会把自己绕晕。
2. 环境准备与数据构建
2.1 安装依赖与模型接入
我用 Python 3.10 环境,虚拟环境里面直接安装 LlamaIndex 的核心包。需求文件大概长这样:
llama-index-core>=0.10.0 llama-index-llms-openai-like>=0.1.0 llama-index-embeddings-openai-like>=0.1.0 llama-index-vector-stores-chroma>=0.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( model="your-model-id", api_base=os.getenv("LLM_API_BASE"), api_key=os.getenv("LLM_API_KEY"), temperature=0.1, ) Settings.embed_model = OpenAILikeEmbedding( model="your-embedding-model-id", api_base=os.getenv("EMBED_API_BASE"), api_key=os.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设为 512,chunk_overlap设为 64。对于机型参数这种信息密度极高的文本,512 个字符大约能覆盖一款手机的核心参数,重启发音 64 是为了防止“电池容量”和“充电功率”这类参数刚好被截断到两个切片里。
如果你是第一次做 RAG,这个环节最容易踩坑的地方是无脑切分导致一款手机的参数被拆到四五个片段里,查询时可能只召回其中一段,回答出来缺胳膊少腿。我的处理办法是:每款手机的文件本身就比较短,直接以“文件”为粒度建索引,不切块,或最多切成两块。这样一段话就包含一款手机的全部参数,召回完整度远高于切得稀碎。
索引我用的是向量索引:
from llama_index.core import VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter nodes = SentenceSplitter( chunk_size=512, chunk_overlap=64, ).get_nodes_from_documents(documents) index = VectorStoreIndex(nodes) retriever = index.as_retriever(similarity_top_k=4)similarity_top_k我调成 4,因为单款手机文件的长度有限,召回太多反而会混入不相关内容。生产环境如果数据量很大、品牌很多,top_k 可以适当调大,但一定要配合元数据过滤,否则召回结果里会出现七八个品牌的机型混在一起,回答非常难组织。
3. 第一层:单机器人问答
3.1 最简 RAG 问答链路
先把最简单的那条链路跑通。LlamaIndex 的QueryEngine封装了检索和生成,核心代码只有几行:
query_engine = index.as_query_engine(similarity_top_k=4) 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(key="brand", value=brand_name)] ) retriever = index.as_retriever( similarity_top_k=4, filters=filters, ) return RetrieverQueryEngine(retriever=retriever)这样做的好处是,品牌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 12GB+256GB 三台”,主控 Agent 先调用商品咨询确认库存足够,再把商品信息传给购物车 Agent。购物车 Agent 返回购物车 ID 后,订单 Agent 拿着购物车 ID 去生成订单,同时询问用户收货地址。地址确认后,订单 Agent 再发起一个模拟支付动作。整个过程的中间状态我都实时打印出来,方便在日志里看到底卡在哪个环节:
[主控] 用户意图:购买 星曜X1 12GB+256GB x3 [商品咨询] 库存充足,单价3999元 [购物车] 已添加,cart_id=42 [订单] 订单创建成功,order_id=88,总价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], llm=Settings.llm, system_prompt="你是购物车助手,只负责购物车相关的操作。", ) order_agent = FunctionAgent.from_tools( tools=[create_order, confirm_payment], llm=Settings.llm, system_prompt="你是订单助手,负责生成订单和确认支付。", ) main_agent = AgentRunner.from_llm( agents=[cart_agent, order_agent], llm=Settings.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])有了这个,我基本能把每条日志对应的检索来源和工具调用过程完整回放,排查效率翻倍。
个人体会是,这套流程里的“三阶段”不只是演示花样,每一层解决的都是真实客服系统的核心问题。单机器人把信息问答做扎实,路由层把知识归属做清楚,多智能体让系统真正能执行动作。手机机型数据集贯穿始终,只是搭了一个足够贴近业务的脚手架。你要是想复现,建议也先选一个你自己熟悉的小领域,造一份结构化的数据集,从第一层码到第三层,别急着堆智能体数量。跑通三层之后,再回头看你的字段设计、提示词、路由逻辑,会有完全不同的一层理解。