跳转到内容

3.5 路由模式总结与最佳实践

经过前面四节的深入探讨,我们从条件边的基本用法讲到多层路由、LLM 驱动的动态决策,再到测试与调试方法,已经建立了一套完整的路由知识体系。这一节作为第3章的收尾,我们会把所有内容串联起来,提炼出一些通用的设计模式和最佳实践原则,帮助你面对复杂的路由场景时能够快速做出正确的架构决策。

路由决策的三个维度

在设计一个路由系统时,不管具体场景如何,你都需要在三个维度上做权衡:决策依据(基于什么信息做路由)、决策时机(什么时候做路由)、决策粒度(路由到多细的粒度)。理解这三个维度有助于你系统化地思考路由设计,而不是凭直觉堆砌 if-else。

决策依据指的是路由函数用来做判断的信息来源。最简单的是基于状态中的某个字段值(比如 priority == "high"),稍复杂一点是基于多个字段的组合判断(比如 priority == "high" and category == "billing"),再高级是基于语义理解(用 LLM 分析消息内容)。决策依据的选择应该与业务逻辑的复杂度匹配——如果业务规则清晰明确、用几个字段就能区分,那规则路由就足够了;如果需要理解用户意图、处理模糊表达,那就需要 LLM 路由。

python
# 维度1: 决策依据
def rule_based_router(state):  # 基于字段值
    return "vip" if state["tier"] == "enterprise" else "standard"

def composite_rule_router(state):  # 基于多字段组合
    if state["tier"] == "enterprise" and state["value"] > 10000:
        return "priority"
    if state["tier"] == "vip" or state["is_returning"]:
        return "fast"
    return "standard"

def llm_router(state):  # 基于语义理解
    decision = llm_router_chain.invoke({"content": state["message"]})
    return decision.queue

决策时机指的是在图的哪个位置做路由决策。最常见的是在节点执行之后立即路由(node → conditional_edge → next_node),但也可以在图的入口处就根据初始输入做路由(START → conditional_edge → branch1/branch2/...),或者在多个节点串联后才做综合路由(node1 → node2 → node3 → conditional_edge → ...)。决策时机的选择会影响图的结构和状态的设计——入口路由通常意味着各分支是相互独立的、不需要共享中间状态;节点后路由意味着分支之间可能有共享的前置处理逻辑;综合路由则意味着各分支共享了前面的所有计算结果。

python
# 维度2: 决策时机
# 入口路由: 各分支独立
graph.add_conditional_edges(START, initial_classifier, {
    "technical": technical_branch,
    "business": business_branch,
    "support": support_branch
})

# 节点后路由: 共享前置逻辑
graph.add_edge(START, "common_preprocess")
graph.add_edge("common_preprocess", "classify")
graph.add_conditional_edges("classify", router, {...})

# 综合路由: 共享多步计算
graph.add_edge(START, "step1")
graph.add_edge("step1", "step2")
graph.add_edge("step2", "step3")
graph.add_conditional_edges("step3", final_router, {...})

决策粒度指的是路由的目标有多细。最粗粒度是路由到几个大的处理分支(比如技术/业务/支持),中等粒度是路由到具体的处理节点(比如 API 支持/数据库支持/前端支持),最细粒度是路由到具体的处理函数或参数配置(比如路由到 handle_api_support 节点并传入 priority="high" 参数)。粒度的选择取决于后续处理逻辑的差异程度——如果各分支的处理逻辑差异很大,那粗粒度路由就够了;如果各分支只是参数不同,那细粒度路由可以减少节点数量。

python
# 维度3: 决策粒度
# 粗粒度: 路由到大的处理分支
graph.add_conditional_edges("classify", router, {
    "technical": "technical_branch_subgraph",
    "business": "business_branch_subgraph",
    "support": "support_branch_subgraph"
})

# 中等粒度: 路由到具体节点
graph.add_conditional_edges("classify", router, {
    "api_support": "handle_api_support",
    "db_support": "handle_db_support",
    "frontend_support": "handle_frontend_support"
})

# 细粒度: 路由到节点+参数
def route_with_params(state):
    return ("handle_support", {"priority": "high"})

graph.add_conditional_edges("classify", route_with_params, {
    ("handle_support", {"priority": "high"}): "handle_support_high",
    ("handle_support", {"priority": "normal"}): "handle_support_normal"
})

这三个维度不是独立的——它们之间会相互影响。比如如果你选择 LLM 作为决策依据,那决策粒度就不能太细(LLM 从 20 个选项中选择的准确率会显著下降);如果你选择入口路由作为决策时机,那决策粒度通常会比较粗(因为各分支是独立的)。理解这些权衡关系能帮你做出更合理的设计决策。

常见路由模式速查表

基于前面几节的内容,我们可以总结出几种最常用的路由模式,每种模式都有其适用场景和优缺点。这个速查表可以作为你设计路由系统时的参考:

模式适用场景优点缺点代码复杂度
单层规则路由规则清晰、字段明确的简单分类简单高效、易于理解、性能好无法处理语义、规则维护成本高
链式多层路由多维度决策、优先级明确的场景逻辑清晰、易于扩展、可追踪节点数量多、状态传递复杂
LLM 单轮路由需要语义理解、模糊判断的场景理解能力强、适应性好成本高、速度慢、输出不稳定
LLM 多轮路由选项很多(>10)的分类场景准确率高、可解释性好成本高、延迟大中高
混合路由高频规则+低频语义的混合场景性能和能力的平衡逻辑复杂、需要维护两套系统
循环+条件路由需要重试、迭代优化的场景自动化程度高、容错性好可能死循环、调试困难中高

单层规则路由是最简单也最常用的模式,适用于那些可以用几个明确的规则就区分开来的场景。比如根据用户等级(普通/VIP/企业)路由到不同的处理队列、根据订单金额(小额/中额/大额)选择不同的物流方式等。这种模式的优点是性能极好(路由函数只是几个 if-elif 判断,微秒级)、完全可预测(相同的输入永远得到相同的输出)、易于测试(纯函数,无外部依赖)。缺点是无法处理需要语义理解的情况,而且当规则变得复杂时维护成本会急剧上升。

链式多层路由适用于需要考虑多个维度、且各维度有明确优先级的场景。比如贷款审批:先看基本条件(收入/评分/工龄),通过后再看风控指标(DTI/贷款额),最后决定利率。这种模式通过把大决策拆解成多个小决策,每个小决策只关注一个维度,保持了逻辑的清晰性。优点是易于理解和扩展(新增一个维度就是新增一层路由)、可追踪性很好(每一步的决策结果都记录在状态中)。缺点是节点数量会线性增长,状态传递需要仔细设计。

LLM 单轮路由适用于需要理解用户意图、处理模糊表达的场景。比如客服消息分类、用户查询意图识别等。这种模式的核心优势是 LLM 的语义理解能力——它能识别出"系统太烂了"是投诉而不是技术问题,能区分"价格太贵"和"收费不合理"都是 billing 相关的。缺点是每次调用需要几百毫秒时间和一定的 token 费用,而且 LLM 的输出具有不确定性(可能返回不在预定义列表中的键值)。通过结构化输出可以缓解输出不稳定的问题。

LLM 多轮路由适用于选项很多(超过 10 个)的分类场景。如果让 LLM 一次性从 20 个选项中选择,准确率会显著下降。更好的做法是先做粗粒度分类(5-6 个大类),然后在每个大类内部做细粒度分类(每个大类 3-4 个子类)。这种模式虽然增加了 LLM 调用次数,但能显著提高准确率。缺点是成本和延迟都更高,适合那些对准确率要求极高且可以接受一定延迟的场景。

混合路由是实际生产中最常见的模式——先用规则路由处理那些明显、高频的输入(比如包含"密码"的消息直接路由到账户管理),只有规则无法确定的情况下才调用 LLM。这种模式在性能和能力之间取得了很好的平衡,大部分请求走快速规则路由,只有少数复杂的请求才走昂贵的 LLM 路由。缺点是需要维护两套路由系统,逻辑复杂度较高。

循环+条件路由适用于需要自动重试、迭代优化的场景。比如代码自动修复、数据清洗流水线等——不断尝试修复问题,每次修复后重新检查,直到所有问题都被修复或者达到最大尝试次数。这种模式的核心是在循环体内部使用条件边来决定是继续循环还是退出。优点是自动化程度高、容错性好。缺点是可能陷入死循环(如果退出条件永远不被满足),调试也比较困难。

路由设计的反模式

知道"怎么做"很重要,但知道"不要怎么做"同样重要。下面列出几种常见的路由设计反模式,了解它们能帮你避免踩坑。

反模式一:巨型路由函数。把所有的路由逻辑都塞进一个函数里,这个函数可能有几百行代码,包含几十个 if-elif 分支。这种反模式的问题在于:难以测试(每个分支都需要构造复杂的输入)、难以维护(修改一个分支可能影响其他分支)、难以理解(没有人能一眼看清楚这个函数到底在做什么)。正确的做法是把路由逻辑拆分成多个小的路由函数,或者用链式路由模式。

python
# ❌ 反模式: 巨型路由函数
def massive_router(state):
    if state["tier"] == "enterprise":
        if state["value"] > 10000:
            if state["region"] == "international":
                if state["urgency"] == "critical":
                    return "priority_intl_critical"
                # ... 更多嵌套
        # ... 更多分支
    elif state["tier"] == "vip":
        # ... 又是几十行
    # ... 总共几百行

# ✅ 正确: 拆分成多个小路由函数
def tier_router(state): return state["tier"]
def value_router(state): return "high" if state["value"] > 10000 else "normal"
# 然后用链式路由把它们串联起来

反模式二:路由函数中有副作用。路由函数应该是纯函数——只根据输入状态返回路由键,不应该有其他副作用(比如修改数据库、发送网络请求、写入日志文件等)。如果在路由函数中引入副作用,会导致路由行为变得不可预测(比如路由到某个节点时数据库恰好挂了)、难以测试(需要 mock 外部依赖)、难以调试(无法复现路由失败的场景)。正确的做法是把所有副作用都放到节点函数中,路由函数只做判断。

python
# ❌ 反模式: 路由函数中有副作用
def router_with_side_effects(state):
    if state["is_suspicious"]:
        log_to_audit_system(state)  # 副作用!
        send_alert_email(state)      # 副作用!
        return "investigate"
    return "normal"

# ✅ 正确: 副作用放到节点函数中
def router(state):
    return "investigate" if state["is_suspicious"] else "normal"

def investigate_node(state):
    log_to_audit_system(state)
    send_alert_email(state)
    return {"status": "under_investigation"}

反模式三:路由键硬编码。在路由函数中直接返回字符串字面量,在 path_map 中也用字符串字面量,两者之间没有统一的约束。这很容易导致拼写错误——路由函数返回 "handle_success" 但 path_map 中写的是 "success_handle",这种 bug 在编译期无法发现,只能在运行时抛出 ValueError。正确的做法是用枚举或常量来统一管理路由键,让 IDE 和类型检查器在编码阶段就能发现问题。

python
# ❌ 反模式: 路由键硬编码
def router(state):
    return "handle_success"  # 拼写可能错误

graph.add_conditional_edges("node", router, {
    "success_handle": next_node  # 拼写不一致!
})

# ✅ 正确: 用枚举统一管理
class Routes(str, Enum):
    HANDLE_SUCCESS = "handle_success"
    HANDLE_FAILURE = "handle_failure"

def router(state):
    return Routes.HANDLE_SUCCESS.value

graph.add_conditional_edges("node", router, {
    Routes.HANDLE_SUCCESS.value: success_node,
    Routes.HANDLE_FAILURE.value: failure_node
})

反模式四:过度使用 LLM 路由。有些开发者觉得 LLM 路由很强大,就把所有路由决策都交给 LLM,哪怕那些完全可以用规则解决的简单场景。这会导致不必要的成本增加和性能下降。正确的做法是先用规则路由处理那些明显、高频的输入,只有规则无法确定的情况下才调用 LLM。比如"密码重置"这种明确的关键词匹配,用规则路由几微秒就能完成,完全没有必要用 LLM。

python
# ❌ 反模式: 过度使用 LLM 路由
def everything_with_llm(state):
    return llm_router_chain.invoke({"content": state["message"]})

# ✅ 正确: 混合路由
def hybrid_router(state):
    msg = state["message"].lower()
    if "密码" in msg or "登录" in msg:
        return "account_management"  # 规则路由
    if "账单" in msg or "支付" in msg:
        return "billing"  # 规则路由
    return llm_router(state)  # 只有规则无法确定时才用 LLM

反模式五:路由爆炸。试图为所有可能的输入组合都创建一个独立的路由分支,导致 path_map 中有几十甚至上百个条目。这种反模式通常发生在那些有很多维度、每个维度又有多个取值的场景中。比如用户有 3 个等级、请求有 5 种类型、数据有 4 种敏感度,理论上有 3×5×4=60 种组合,如果为每种组合都创建一个分支,图的结构会变得极其复杂且难以维护。正确的做法是用优先级排序+短路求值,不要试图穷举所有组合。

python
# ❌ 反模式: 路由爆炸
graph.add_conditional_edges("node", router, {
    "enterprise_technical_public": node1,
    "enterprise_technical_confidential": node2,
    "enterprise_technical_secret": node3,
    # ... 还有 57 个分支
})

# ✅ 正确: 优先级排序+短路求值
def smart_router(state):
    if state["sensitivity"] == "top_secret":
        return "security_review"  # 最高优先级,立即返回
    if state["tier"] == "enterprise":
        return "vip_channel"
    # ... 少量规则就能覆盖所有场景

性能与成本的权衡矩阵

路由系统的性能和成本是两个需要持续关注的指标。不同路由模式的性能和成本差异很大,你需要根据具体场景选择合适的平衡点。下面是一个简单的权衡矩阵:

路由模式单次延迟单次成本吞吐量适用场景
规则路由<1ms~$0极高高并发、低延迟要求
LLM 单轮路由200-500ms~$0.001中等需要语义理解
LLM 多轮路由500-1500ms~$0.003较低高准确率要求
混合路由1-500ms~$0-$0.001性能和能力平衡

从矩阵可以看出,规则路由在性能和成本上都有绝对优势,但能力有限;LLM 路由能力强但代价高;混合路由是实际生产中最常用的折中方案。在设计路由系统时,你应该先回答几个问题:这个系统需要处理多大的并发量?对延迟的要求有多高?路由决策的准确率有多重要?预算是多少?根据这些问题的答案来选择合适的路由模式。

一个实用的优化策略是动态路由选择——根据输入的复杂度动态决定用规则路由还是 LLM 路由。比如先用一个轻量级的规则路由尝试分类,如果置信度很高就直接使用规则结果;如果置信度低或者规则无法处理,再调用 LLM。这样大部分请求走快速规则路由,只有少数复杂请求才走昂贵的 LLM 路由。

python
def adaptive_router(state):
    msg = state["message"].lower()

    rule_match = None
    rule_confidence = 0.0

    if "密码" in msg:
        rule_match = "account_management"
        rule_confidence = 0.95
    elif "账单" in msg:
        rule_match = "billing"
        rule_confidence = 0.90
    elif "bug" in msg or "error" in msg:
        rule_match = "technical_support"
        rule_confidence = 0.85

    if rule_match and rule_confidence >= 0.85:
        return rule_match

    return llm_router(state)

总结:路由设计的黄金法则

经过这一章的深入探讨,我们可以总结出几条路由设计的黄金法则。这些法则不是教条,而是在大量实践经验中提炼出来的指导原则,能帮助你在面对复杂的路由场景时做出正确的决策。

法则一:路由函数应该是纯函数。它只根据输入状态返回路由键,不应该有副作用。这保证了路由行为是可预测的、可测试的、可复现的。如果需要在路由决策前后做额外的操作(比如记录日志、发送监控指标),把这些操作放到节点函数中,而不是塞进路由函数里。

法则二:路由键应该用枚举或常量统一管理。避免在路由函数和 path_map 中分别使用字符串字面量,这很容易导致拼写不一致的错误。用枚举类型可以让你在编码阶段就发现问题,而且 IDE 的自动补全功能也能提高开发效率。

法则三:路由逻辑应该保持简洁。如果一个路由函数超过 30 行,或者包含超过 5 层的 if-elif 嵌套,就应该考虑拆分——要么拆成多个小的路由函数用链式路由串联,要么把部分逻辑提取到前置节点中。简洁的路由函数更容易理解、测试和维护。

法则四:优先用规则路由,必要时用 LLM 路由。规则路由在性能、成本、可预测性上都有绝对优势,只有在规则无法满足需求(需要语义理解、处理模糊表达)时才引入 LLM。混合路由是实际生产中最常用的模式——规则处理高频简单场景,LLM 处理低频复杂场景。

法则五:为每个路由函数编写完整的测试用例。至少包括三类测试:正常路径(每个分支至少一个)、边界值(刚好在阈值上的值)、异常输入(缺失字段、类型错误、空值)。路由函数是整个图中最适合做单元测试的部分,投入测试的回报率很高。

法则六:用 stream 模式和 LangSmith 追踪路由过程。当路由出现问题时,不要盲目猜测,用 stream_mode="values"stream_mode="updates" 来观察每一步的状态变化,或者在 LangSmith 中查看完整的执行 trace。这些工具能帮你快速定位问题所在。

遵循这些法则,你就能构建出既强大又健壮的路由系统——它能够处理复杂的业务逻辑,同时保持代码的清晰和可维护性。路由是 LangGraph 中最核心的概念之一,掌握好它,你就掌握了构建复杂工作流编排系统的关键能力。

基于 MIT 许可发布