Day 34 :Routing(路由)—— 让 Agent 学会“分流”
欢迎来到第三十四天!在构建复杂的 AI 应用时,我们常常需要处理多种类型的用户请求:闲聊、知识问答、调用外部 API、查询数据库等。如果用一个 Agent 处理所有请求,不仅工具列表会变得庞大,模型也容易选错工具。今天我们将学习Routing(路由)技术:先判断用户意图,再将请求分发给专门的子链或子 Agent。这就像医院的分诊台——先判断你该去哪个科室,再由专科医生处理。
一、今日学习目标
- 理解 Routing 的核心思想:将复杂问题分解为多个专门的处理器,通过意图分类决定路由目标。
- 掌握两种常见的路由实现方式:
- 基于 LLM 的路由:用模型判断用户意图并输出路由标签。
- 基于语义相似度的路由:用 Embedding 匹配预定义意图。
- 学会使用 LangChain 的
RunnableBranch和自定义路由链。 - 构建一个“多功能助手”:根据用户输入,分别路由到闲聊、数学计算、天气查询三条不同的处理链。
- 了解 Routing 在多智能体系统中的作用,为后续多 Agent 协作打下基础。
二、详细实现步骤
步骤 1:理解 Routing 的架构
一个典型的路由系统包含三个部分:
- 意图分类器(Router):接收用户输入,输出一个路由标签(如
"chat"、"math"、"weather")。 - 路由分发器(Dispatcher):根据标签将请求转发给对应的处理链。
- 子处理器(Handler):每个处理链专门负责一类任务,可以是简单的 LLM 链、RAG 链或完整的 Agent。
用户输入 → 意图分类器 → 路由标签 → 分发器 → 子处理器 → 输出
步骤 2:环境准备
确保已安装langchain、langchain-openai。新建routing_demo.py,导入所需模块:
importosfromdotenvimportload_dotenvfromlangchain_openaiimportChatOpenAIfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.runnablesimportRunnableBranch,RunnablePassthrough,RunnableLambdafromlangchain_core.messagesimportHumanMessage load_dotenv()llm=ChatOpenAI(model="deepseek-chat",api_key=os.getenv("DEEPSEEK_API_KEY"),base_url="https://api.deepseek.com",temperature=0.1)步骤 3:构建意图分类器
意图分类器本质上是一个 LLM 链,输入用户问题,输出预定义的类别标签。我们定义三个类别:chat(闲聊)、math(数学计算)、weather(天气查询)。
router_prompt=ChatPromptTemplate.from_messages([("system","""你是一个意图分类器。请根据用户输入判断其意图,并只输出以下类别之一: - chat:闲聊、问候、与数学和天气无关的问题 - math:数学计算、数学问题 - weather:天气查询 只输出类别名称,不要输出任何其他文字。"""),("human","{input}")])router_chain=router_prompt|llm|StrOutputParser()# 测试print(router_chain.invoke({"input":"你好,今天过得怎么样?"}))# 期望输出 chatprint(router_chain.invoke({"input":"计算 25 * 4 等于多少?"}))# 期望输出 mathprint(router_chain.invoke({"input":"北京今天天气怎么样?"}))# 期望输出 weather如果输出不稳定(例如带有多余文字),可以加入 Few-shot 示例或降低温度。
步骤 4:构建三个子处理器
每个子处理器是一个独立的链,专门处理一类请求。
4.1 闲聊处理器
chat_prompt=ChatPromptTemplate.from_messages([("system","你是一个友好的聊天助手,请用轻松愉快的语气回复用户。"),("human","{input}")])chat_chain=chat_prompt|llm|StrOutputParser()4.2 数学处理器
数学处理器可以是一个简单的 LLM 链,也可以是一个带计算器工具的 Agent。这里为了简洁,我们用一个带 CoT 的链。
math_prompt=ChatPromptTemplate.from_messages([("system","你是一个数学专家。请一步步计算,最后给出答案。"),("human","{input}")])math_chain=math_prompt|llm|StrOutputParser()4.3 天气处理器
天气处理器需要一个工具来获取天气信息。我们定义一个简单的模拟函数,并用 LLM 链调用它。
defget_weather(city:str)->str:weather_data={"北京":"晴,26°C,湿度 40%","上海":"多云,28°C,湿度 65%","广州":"小雨,30°C,湿度 80%"}returnweather_data.get(city,f"未找到{city}的天气信息")weather_prompt=ChatPromptTemplate.from_messages([("system","你是一个天气助手。用户会询问天气,你需要从以下天气数据中提取信息回答。\n天气数据:{weather_data}"),("human","{input}")])defweather_handler(input_dict):user_input=input_dict["input"]# 简单提取城市名(实际项目中可以用 NER 或让 LLM 提取)city="北京"# 默认forcin["北京","上海","广州"]:ifcinuser_input:city=cbreakweather_data=get_weather(city)returnweather_prompt|llm|StrOutputParser()weather_chain=RunnableLambda(weather_handler)步骤 5:使用 RunnableBranch 实现路由分发
RunnableBranch是 LangChain 提供的路由组件,它根据条件选择不同的分支。
defroute_by_intent(input_dict):"""根据意图返回对应的链"""intent=router_chain.invoke({"input":input_dict["input"]})intent=intent.strip().lower()print(f"[路由] 检测到意图:{intent}")if"math"inintent:returnmath_chainelif"weather"inintent:returnweather_chainelse:returnchat_chain# 构建完整路由链full_chain=RunnableLambda(route_by_intent)# 测试print("\n===== 测试路由 =====")forqueryin["你好呀","计算 (12+7)*3","上海天气怎么样?"]:print(f"\n用户:{query}")result=full_chain.invoke({"input":query})print(f"回答:{result}")运行后,你会看到每个请求被正确路由到对应的处理器。
步骤 6:使用 RunnableBranch 的另一种写法
如果你希望使用更声明式的方式,可以用RunnableBranch:
branch=RunnableBranch((lambdax:"math"inrouter_chain.invoke({"input":x["input"]}).lower(),math_chain),(lambdax:"weather"inrouter_chain.invoke({"input":x["input"]}).lower(),weather_chain),chat_chain# 默认分支)# 测试result=branch.invoke({"input":"计算 5*6"})print(result)但注意:这种方式会对每个条件都调用一次路由器,效率较低。推荐使用步骤 5 的自定义函数方式,只调用一次路由器。
步骤 7:基于语义相似度的路由(备选方案)
除了 LLM 分类,还可以用 Embedding 计算用户输入与预定义意图描述的相似度,选择最接近的意图。这种方法更快,但灵活性稍差。
fromlangchain_openaiimportOpenAIEmbeddingsimportnumpyasnp embeddings=OpenAIEmbeddings(model="embedding-2",api_key=os.getenv("ZHIPU_API_KEY"),base_url="https://open.bigmodel.cn/api/paas/v4")# 预定义意图描述intent_descriptions={"chat":"闲聊、问候、日常对话","math":"数学计算、算术、数学问题","weather":"天气查询、气温、下雨"}# 预先计算意图向量intent_vectors={k:embeddings.embed_query(v)fork,vinintent_descriptions.items()}defcosine_similarity(v1,v2):v1,v2=np.array(v1),np.array(v2)returnfloat(np.dot(v1,v2)/(np.linalg.norm(v1)*np.linalg.norm(v2)))defroute_by_similarity(user_input:str)->str:query_vec=embeddings.embed_query(user_input)best_intent=Nonebest_sim=-1forintent,vecinintent_vectors.items():sim=cosine_similarity(query_vec,vec)ifsim>best_sim:best_sim=sim best_intent=intentreturnbest_intent# 测试print(route_by_similarity("今天天气如何"))# 期望 weatherprint(route_by_similarity("3 加 5 等于几"))# 期望 math这种方法的优点是不需要调用 LLM,速度快、成本低;缺点是无法处理复杂的意图边界,且需要维护意图描述。
步骤 8:整合与测试
将路由链封装成一个完整的应用,测试多个不同意图的输入,观察路由是否正确。
defmulti_function_assistant(user_input:str)->str:intent=router_chain.invoke({"input":user_input}).strip().lower()print(f"[意图]{intent}")if"math"inintent:returnmath_chain.invoke({"input":user_input})elif"weather"inintent:returnweather_chain.invoke({"input":user_input})else:returnchat_chain.invoke({"input":user_input})# 测试test_queries=["你好,你叫什么名字?","请帮我计算 100 / 4 + 20","广州今天热不热?","讲个笑话吧","北京天气"]forqintest_queries:print(f"\n用户:{q}")print(f"助手:{multi_function_assistant(q)}")三、常见问题与调试
Q1:意图分类器输出不稳定,有时返回“chat”,有时返回“闲聊”。
→ 在 System Prompt 中明确要求“只输出类别名称,不要输出其他文字”,并给出确切的标签列表。加入 Few-shot 示例可以进一步提高稳定性。如果仍然不稳定,可以在代码中用关键词匹配做二次校验。
Q2:用户的问题涉及多个意图,例如“北京天气怎么样?顺便算一下 25*4”。
→ 简单的路由只能选择一个分支。要处理多意图,可以先做意图拆分(用 LLM 将问题分解为多个子问题),然后分别路由。这属于更高级的 Agent 规划能力,我们后续会涉及。
Q3:天气处理器中,城市名提取太简单,无法处理“今天首都的天气”。
→ 可以改用 LLM 提取城市名,或者使用专门的 NER 工具。在天气处理器中加入一个 LLM 调用,从用户输入中提取城市名。
Q4:路由器的 LLM 调用增加了延迟和成本。
→ 对于简单场景,可以使用语义相似度路由(步骤 7)替代 LLM 路由。或者使用更小、更快的模型做分类。也可以缓存路由结果,对于相同输入直接返回。
Q5:如何扩展到更多意图?
→ 只需在路由器的 System Prompt 中增加类别,并添加对应的子处理器。当意图数量很多时,可以考虑分层路由:先粗分类(如“技术类”、“生活类”),再细分类。
Q6:路由和 Agent 的关系是什么?
→ 路由可以看作一种“预决策”,决定用哪个 Agent 或链来处理。在多智能体系统中,路由常用于将任务分发给不同角色的 Agent(如“客服 Agent”、“技术 Agent”)。这为后续的 AutoGen/CrewAI 协作奠定了基础。
四、今日总结与作业
今天你完成了:
- ✅ 理解了 Routing 的核心思想和架构。
- ✅ 使用 LLM 构建了意图分类器,并实现了三条子处理器链。
- ✅ 掌握了
RunnableBranch和自定义路由函数的用法。 - ✅ 了解了基于语义相似度的路由方案。
- ✅ 构建了一个多功能助手,能根据意图自动分流。
今日作业(必做):
- 为多功能助手增加第四个意图
translate(翻译),要求用户输入“把‘你好’翻译成英文”时路由到翻译链。翻译链可以使用简单的 LLM 提示词实现。 - 修改意图分类器的 Prompt,加入 Few-shot 示例(每个类别给一个例子),测试分类准确率是否提高。
- 实现一个基于语义相似度的路由器,使用智谱 Embedding 计算相似度,并与 LLM 路由器的结果进行对比(测试 10 个不同输入,记录两者的一致性)。
- (思考题)在构建一个企业级客服系统时,你如何设计路由层次?请画出路由流程图(可以用文字描述),并说明每个路由节点的判断依据。
明日预告:我们将进入第三阶段的最后一天,进行项目复盘——完成一个完整的带记忆的客服 Agent。这个项目将整合 RAG、记忆管理、工具调用和路由技术,是你简历上的又一个亮点。
有任何问题欢迎随时提问!