【Bug已解决】create_agent: model_to_tools router can return “model“ but path_map omits it -> KeyError(‘m…
【Bug已解决】create_agent: model_to_tools router can return "model" but path_map omits it -> KeyError('model')
一、现象长什么样
create_agent支持用一个model_to_tools路由器:根据当前模型/上下文,决定走哪条"模型+工具"的处理路径。路由器根据输入返回某个路由键(route key),再用path_map(路由键→处理路径的映射)找到对应逻辑。
问题在于:路由器在某些情况下会返回键"model",但path_map里根本没有"model"这个键。于是派发时path = path_map[route],route == "model"命中不了 →KeyError: 'model',create_agent直接崩。
表现:大多数情况正常(路由到path_map里有的键),但一旦输入触发了"返回 model"这个分支,就 100% 崩溃,且报错是裸KeyError,不提示"path_map 缺了 model 这条路由"。
二、背景
create_agent的设计里,model_to_tools是一个可调用对象/路由函数,签名类似:
def model_to_tools(state) -> str: # 返回路由键 ...调用方再拿返回的键去path_map查处理路径:
route = model_to_tools(state) path = path_map[route] # 若 route 不在 path_map -> KeyError路由器的"输出空间"(可能返回的键集合)和path_map的"键集合"本应完全一致。但路由器是用户/框架提供的,可能返回"model"(比如当判定"只需模型、无需工具"时),而path_map只定义了"tools"/"no_tools"等,漏了"model"。两端无单一事实来源,约定靠默契,于是 KeyError。
三、根因
根因两点:
- 路由输出空间与 path_map 不同步:路由器能返回
"model",path_map没对应项。 - 派发用裸下标:
path_map[route]直接取下标,键缺失即KeyError,无兜底、无清晰错误。
本质:把"路由键集合"和"路径映射键集合"当成两处独立维护的字典,缺单一事实来源,一端加了路由另一端忘了加映射。
四、最小可运行复现
下面缩略逻辑复现 KeyError:
def model_to_tools(state): if state.get("need_model_only"): return "model" # 路由器返回了 model return "tools" path_map = {"tools": tool_path, "no_tools": no_tool_path} # 没有 "model" route = model_to_tools({"need_model_only": True}) path = path_map[route] # KeyError: 'model'修复:path_map 补齐 "model",且派发用.get+ 清晰错误。
path_map = {"tools": ..., "no_tools": ..., "model": model_only_path} route = model_to_tools(state) path = path_map.get(route) if path is None: raise KeyError(f"route '{route}' not in path_map; keys={list(path_map)}")五、解决方案(第一层:最小直接修复)
最小修法:在create_agent装配时,校验路由器可能返回的键都存在于path_map;派发用.get并给清晰错误。
def create_agent(model_to_tools, path_map, sample_states): # 校验:路由器对样例输入返回的键都必须在 path_map for st in sample_states: route = model_to_tools(st) if route not in path_map: raise ValueError(f"router returned '{route}' but path_map lacks it") def dispatch(state): route = model_to_tools(state) path = path_map.get(route) if path is None: raise KeyError(f"route '{route}' not in path_map; have {list(path_map)}") return path(state) return dispatch这一层让缺映射在装配期就暴露,而非运行期裸 KeyError。
六、解决方案(第二层:结构化改进)
把"路由键 ↔ path_map 一致性"固化成策略对象,作为单一事实来源,明确路由输出空间必须 ⊆ path_map 键。
from dataclasses import dataclass, field from typing import Callable, Dict, List @dataclass(frozen=True) class LangChainCreateAgentRouterPolicy: """create_agent 路由/path_map 一致性策略的单一事实来源。""" path_map: Dict[str, Callable] = field(default_factory=dict) expected_routes: List[str] = field(default_factory=list) fail_closed: bool = True def validate_router(self, router: Callable, probes) -> None: for st in probes: route = router(st) if route not in self.path_map: if self.fail_closed: raise AssertionError( f"router returned '{route}' not in path_map keys {list(self.path_map)}") def dispatch(self, router, state): route = router(state) path = self.path_map.get(route) if path is None: raise KeyError(f"route '{route}' missing; keys={list(self.path_map)}") return path(state) def validate(self) -> None: if self.fail_closed and not self.path_map: raise AssertionError("path_map empty but fail_closed")create_agent用policy.validate_router+policy.dispatch,一致性集中。
七、解决方案(第三层):断言 / CI 守护
用 pytest 锁死一致性:
import pytest from policy import LangChainCreateAgentRouterPolicy as P def test_router_key_in_path_map(): p = P(path_map={"tools": lambda s: s, "model": lambda s: s}) p.validate_router(lambda s: "model", [{"x": 1}]) # 不抛 def test_missing_key_rejected(): p = P(path_map={"tools": lambda s: s}) # 无 model with pytest.raises(AssertionError): p.validate_router(lambda s: "model", [{"x": 1}]) def test_dispatch_clear_error(): p = P(path_map={"tools": lambda s: s}) with pytest.raises(KeyError) as e: p.dispatch(lambda s: "model", {}) assert "model" in str(e) def test_policy_valid(): P(path_map={"tools": lambda s: s}).validate()CI 加一条:用覆盖各路由分支的样例 state 跑validate_router,断言所有路由键都在 path_map。
八、排查清单
- create_agent 崩
KeyError: 'model'?→ 路由器返回 model 但 path_map 缺它。 - 路由输出空间和 path_map 是否同步?→ 必须 ⊆,用 policy 校验。
- 派发是否裸
path_map[route]?→ 改用.get+ 清晰错误。 - 缺映射能否装配期发现?→ validate_router 在装配时校验。
- 是否只在某些输入才崩?→ 路由到 model 分支才触发,需样例覆盖。
- 是否有"路由键⊆path_map"测试?→ 必须有。
九、小结
create_agent的model_to_tools路由器能返回键"model",但path_map漏了它,派发path_map[route]直接KeyError('model')。根因是路由输出空间与 path_map 键集合无单一事实来源、不同步。第一层装配期校验路由键都在 path_map、派发用.get给清晰错误;第二层用LangChainCreateAgentRouterPolicy把一致性固化成单一事实来源;第三层用 pytest 守护。路由派发的通用原则:路由器的输出空间必须是 path_map 键集合的子集,且派发绝不用裸下标,缺键要给可读错误。
