LangChain RunnableBranch 怎样路由输入
来源:17golang原创
时间:2026-11-06 17:15:37 160浏览 收藏
我第一次把一个问答入口拆成“技术问题”和“账单问题”时,最容易写成一串 if/else。这样能跑,但路由判断、处理逻辑和后续模型链很快搅在一起。LangChain 的 RunnableBranch 更适合这个场景:把“条件 + Runnable”组成有顺序的分支,输入到来后按顺序判断,执行第一个匹配的处理 Runnable;全部不匹配时交给最后的默认 Runnable。
RunnableBranch 的关键不是“自动理解输入”,而是把已经得到的判断条件组合成可继续串接的 Runnable。条件顺序要从窄到宽,所有分支最好返回同一结构,并始终准备一个明确的默认分支。
官方地址:https://reference.langchain.com/python/langchain-core/runnables/branch/RunnableBranch
先约定输入和输出契约
我现在会先把路由输入定成一个小字典,而不是让每个条件函数自行猜测字符串。下面的示例约定 route 表示已经确定的路由标签,text 保存用户原话;每个分支最终都返回 route、answer 和 source_text,这样后面接格式化器、日志或模型链时不需要再判断分支类型。

如果输入来自上游解析器,建议让解析器负责补齐 route,让 RunnableBranch 只负责选择。这样做的好处是:路由选择可以单独记录,条件函数也不会偷偷触发网络请求或模型调用。
用条件与处理 Runnable 组成分支
RunnableBranch 的构造参数由若干个二元组和最后一个默认 Runnable 组成。二元组左侧是条件,右侧是处理逻辑;普通 Python 可调用对象会被 LangChain 转成 Runnable。示例里我使用确定性字段,方便把“路由器是否选对”与模型输出质量分开。
from typing import Any
from langchain_core.runnables import RunnableBranch
def is_technical(payload: dict[str, Any]) -> bool:
# 条件只读取稳定字段,不在判断阶段调用外部服务。
return payload.get("route") == "technical"
def is_billing(payload: dict[str, Any]) -> bool:
# 账单分支与技术分支使用同一份输入契约。
return payload.get("route") == "billing"
def handle_technical(payload: dict[str, Any]) -> dict[str, str]:
# 分支只负责产生统一结构,后续链可以继续消费 answer。
return {
"route": "technical",
"answer": f"技术队列已接收:{payload['text']}",
"source_text": payload["text"],
}
def handle_billing(payload: dict[str, Any]) -> dict[str, str]:
# 账单结果保持与技术结果相同的字段,便于统一格式化。
return {
"route": "billing",
"answer": f"账单队列已接收:{payload['text']}",
"source_text": payload["text"],
}
def handle_default(payload: dict[str, Any]) -> dict[str, str]:
# 未识别的 route 不抛出含糊结果,而是明确交给人工或通用队列。
return {
"route": "default",
"answer": "暂时无法识别路由,请补充问题类型。",
"source_text": payload.get("text", ""),
}
router = RunnableBranch(
# 条件按这里的顺序依次判断。
(is_technical, handle_technical),
(is_billing, handle_billing),
handle_default, # 最后一个参数是默认分支,不是条件二元组。
)
result = router.invoke({"route": "technical", "text": "接口返回 429 怎么处理"})
print(result["route"], result["answer"])
这里的要点有三个:第一,构造器至少需要一个条件分支和一个默认分支;第二,默认处理函数放在最后,不能写成带条件的二元组;第三,invoke 的输入会传给被选中的 Runnable,而不会自动改造成另一种数据结构。
利用从上到下的首次匹配
我第一次把多个“包含关键词”的条件放在一起时,踩过一个很隐蔽的坑:宽条件写在前面,后面的精确条件永远到不了。RunnableBranch 的语义是从前往后求值,哪个条件第一次返回真,就执行对应分支并停止继续选择;没有条件返回真,才进入 default。

因此,条件排序可以按“最具体的规则在前、最宽泛的规则在后”来安排。例如同一个输入既带有 technical 标签,又满足“文本非空”的通用条件时,技术条件必须先出现。更稳妥的写法是让条件只看一个已经规范化的 route 字段,减少重叠。
from langchain_core.runnables import RunnableBranch
def has_route(payload: dict[str, str]) -> bool:
# 只把存在明确路由的输入交给专用分支。
return bool(payload.get("route"))
def handle_known(payload: dict[str, str]) -> dict[str, str]:
# 保留原始 route,方便审计实际选择。
return {"route": payload["route"], "answer": "进入专用处理链"}
def handle_any_text(payload: dict[str, str]) -> dict[str, str]:
# 这是较宽的兜底规则,应放在专用规则之后。
return {"route": "text", "answer": "进入通用文本处理链"}
ordered_router = RunnableBranch(
(has_route, handle_known),
handle_any_text,
)
这段代码并不负责判断“哪个 route 合法”,它只演示顺序。生产环境里可以在上游做枚举校验;如果未知标签仍然允许进入系统,就把它显式标记为 unknown,不要让空值静默落到一个看起来成功的分支。
把路由器接入后续链
RunnableBranch 的价值在于它仍然是 Runnable,所以可以使用管道操作符继续组合。实际项目里我通常把分支输出规范成字典,再接一个统一的格式化函数。无论前面命中哪个分支,后面的链都只处理相同字段。
from langchain_core.runnables import RunnableLambda
def format_result(result: dict[str, str]) -> str:
# 统一出口只依赖契约字段,不需要知道前面命中了哪条分支。
return f"[{result['route']}] {result['answer']}"
final_chain = router | RunnableLambda(format_result)
print(final_chain.invoke({"route": "billing", "text": "发票抬头怎么修改"}))
如果某个分支后面要接提示模板或聊天模型,也建议先在分支内部把输入映射成模板需要的字段,再让每个分支返回一致的结果类型。不要让一个分支返回字符串、另一个分支返回字典,除非后续链明确处理这两种类型。
处理默认分支与工程边界
默认分支不是可有可无的装饰。它决定了输入不满足任何条件时的行为,也给日志和人工接管留下稳定入口。我的做法是让默认分支返回可识别的 route=default,并在外层记录原始文本,而不是直接返回空字符串。
还要注意三个边界:
- 条件应尽量轻:条件里做纯字段判断最容易复现;如果必须异步判断,官方 API 支持异步条件,但仍应把超时、错误和成本放在路由设计中考虑。
- 不要把 RunnableBranch 当成状态机:它负责一次输入的分支选择,不负责保存跨请求状态、重试策略或人工审批。
- 统一输出比省几行代码更重要:多个分支共用下游链时,先定义输出字段,能显著降低后续拼接和排错成本。
最后可以用下面这张表快速检查设计:
| 检查项 | 建议 |
|---|---|
| 条件顺序 | 精确规则在前,宽泛规则在后 |
| 默认分支 | 始终给未知输入一个可识别结果 |
| 输入契约 | 让所有条件和 Runnable 读取同一组字段 |
| 输出契约 | 分支返回相同的字段形状 |
| 后续组合 | 用 | 接统一格式化、模型或解析步骤 |
相关问题
RunnableBranch 会执行多个匹配分支吗?
不会。条件按顺序判断,首个返回真的条件对应的 Runnable 被选中;后面的条件不会继续参与这次选择。
最后一个参数为什么不是二元组?
最后一个参数就是默认 Runnable。它接收原始输入,在前面的所有条件都不匹配时执行。
多个分支可以返回不同类型吗?
可以,但接统一下游链时会增加类型判断。更实用的做法是让分支返回统一字段,或者在分支末尾统一转换。
什么时候应该改用更完整的编排方案?
当需求包含跨多步状态、循环、人工介入、持久化或复杂重试时,RunnableBranch 只适合其中的局部路由,应该把状态和流程交给更合适的编排层。
对我来说,RunnableBranch 最有价值的地方不是替代所有条件逻辑,而是把“选择哪条 Runnable 链”变成一个可以继续组合、记录和复用的组件。先固定输入输出契约,再按具体到宽泛排列条件,最后用默认分支兜底,路由代码就会比散落的 if/else 更容易维护。
-
346 收藏
-
235 收藏
-
387 收藏
-
447 收藏
-
360 收藏
-
111 收藏
-
408 收藏
-
259 收藏
-
440 收藏
-
426 收藏
-
177 收藏
-
123 收藏
-
218 收藏
-
435 收藏
-
456 收藏
-
157 收藏
-
153 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习