人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习【免费下载链接】agent-coreopenJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力项目地址https://gitcode.com/openJiuwen/agent-core点击查看免费下载导读本文聚焦 openJiuwen agent-core 中ByModelNameAllocator按模型名分组的轮转分配器的一次关键序列化重构当团队模型池同时配置glm-5与glm-5.1这类含点号的模型名时旧实现把轮转计数器以dict[model_name, int]形式写入 session checkpoint触发了 session 持久化层expand_nested_structure的 nested-path 误解析导致全量恢复时进程崩溃。本次重构将state_dict()改为 list 序列化每条记录携带model_name与index两个字面字段同时保留对旧 dict 格式的加载兼容。读完本文你将理解崩溃的完整根因链路、新格式的设计动机、state_dict/load_state_dict的源码级实现以及如何用测试基线81 个用例验证这一兼容演进。一、问题背景多模型池中的崩溃现场用户配置了多模型同时包含glm-5与glm-5.1启动 team在 session checkpoint 持久化阶段崩溃错误如下TypeError: NoneType object does not support item assignment File openjiuwen/core/session/utils.py, line 218, in expand_nested_structure current[current_key] expand_nested_structure(value)诊断日志逐层冒泡显示了真正的原因[split_nested_path] inputglm-5.1, output[glm-5, 1] [root_to_path] nested_pathglm-5.1, current_typeint, reasoncurrent is not dict [DEBUG] root_to_path returned None: keyglm-5.1, result_keys[glm-5] [DEBUG TypeError] keyinner_indexes ... [DEBUG TypeError] keymodel_allocator_state ... [DEBUG TypeError] keyjiuwen_team_sess_... ...调用链为team session state →model_allocator_state→inner_indexes→ 处理 keyglm-5.1。二、根因剖析nested-path 误解析与 NoneType 崩溃2.1 旧实现的序列化形态ByModelNameAllocator的旧state_dict()实现把运行时的_inner_indexes: dict[str, int]直接以 dict 形式序列化其中key 就是 model_name{ inner_indexes: {glm-5: 0, glm-5.1: 1}, pool_digest: ..., }这里_inner_indexes记录的是每个模型分组当前的轮转计数游标它是 allocator 的运行时内部状态。问题在于它被直接送进了 session 持久化层。2.2 持久化层的路径语义session 持久化层的核心入口是openjiuwen/core/session/utils.py中的expand_nested_structureutils.pydef expand_nested_structure(data: Any) - Any: if isinstance(data, list) or isinstance(data, tuple): result [] for item in data: result.append(expand_nested_structure(item)) return result elif isinstance(data, dict): result {} for key, value in data.items(): current_key, current root_to_path(key, result, create_if_absentTrue) current[current_key] expand_nested_structure(value) return result ...当数据是 dict 时它对每个 key调用root_to_path(key, result, create_if_absentTrue)把 key 当作nested-path处理。而split_nested_pathutils.py对含.或[的 key 进行拆分def split_nested_path(nested_key: str) - list: ... params nested_key.split(NESTED_PATH_SPLIT) # NESTED_PATH_SPLIT . for param in params: if [ in param: ... else: final_list.append(param) return final_list模块常量定义在文件头部utils.pyNESTED_PATH_SPLIT .NESTED_PATH_LIST_SPLIT [IMMUTABLE_ATOMIC (str, int, float, bool, type(None), bytes, frozenset, complex)。于是glm-5.1被拆成[glm-5, 1]——点号被当作路径分隔符。2.3 崩溃的两条触发路径root_to_path会逐层在结果 dict 中创建中间节点create_if_absentTrue时最终把glm-5.1从字面 key 改写成嵌套结构{glm-5: {1: ...}}。崩溃取决于dict 的遍历顺序先遍历到glm-5.1先创建result[glm-5] {}随后遍历到glm-5时发现它已存在且是 dict……此时第二层再取到None后赋值即崩溃先遍历到glm-5result[glm-5]先被写成标量0IMMUTABLE_ATOMIC直接返回随后遍历到glm-5.1时root_to_path走到中间层发现current[glm-5]是int而非 dictcurrent is not dict返回(None, None)最终current[current_key]对None做 item assignment抛出TypeError: NoneType object does not support item assignmentutils.py 中root_to_path的实现会明确返回(None, None)。前者是数据损坏后者是崩溃——两条路径都不可接受。三、方案决策list 序列化模型名永不落 key 位3.1 新旧格式对比核心决策是state_dict()改为list 序列化每条 record 携带model_name与index两个字面字段load_state_dict()优先读新counterslist向后兼容读旧inner_indexesdict。# new { counters: [ {model_name: glm-5, index: 0}, {model_name: glm-5.1, index: 1}, ], pool_digest: ..., } # legacy (still loadable) { inner_indexes: {glm-5: 0, glm-5.1: 1}, pool_digest: ..., }3.2 决策要点持久化层从此永不把 model_name 放在 dict key 位置——彻底脱离key 是 path 还是字面这个误解空间。模型名在数据形态上被显式表达为 opaque value。pool_digest字段不变digest 不匹配时仍然全部归零既有不变量保持。legacy 路径只在 load 端保留——新 session 一律按 list 写。运行时内部状态self._inner_indexes: dict[str, int]不动——它在进程内不经过 session 持久化层模型名作 key 没有问题仅出口端state_dict做格式翻译。3.3 被拒绝的方案方案被拒绝的理由只修底层expand_nested_structure不动上层底层 fix 确实根除 crash 和数据损坏但上层仍把 model_name 当 dict key 用别的子系统未来再这么做也是一颗潜在地雷用 list 序列化把模型名是 opaque value的语义在数据形态上显式化比让每个 caller 都记得避坑可靠强行限制 model_name 不含./[违反用户预期——glm-5.1、claude-3.5-sonnet、gemini-1.5-pro都是真实在用的模型名不做 legacy 兼容让旧 session 的 pool_digest 不匹配兜底归零会让所有已存 session 的轮转计数器静默清零代价为零的load端 fallback 完全可以保留旧数据四、源码级实现state_dict 与 load_state_dict新实现位于 openjiuwen/agent_teams/models/allocator.py 的ByModelNameAllocator类中。4.1 state_dictlist 化的出口def state_dict(self) - dict: Snapshot per-group counters pool digest for session persistence. return { counters: [ {model_name: name, index: index} for name, index in self._inner_indexes.items() ], pool_digest: self._pool_digest, }这里把_inner_indexes.items()逐条翻译为{model_name, index}记录。docstring 明确解释了动机模型名可能含.或[如glm-5.1、claude-3.5-sonnet而 session 持久化层历史上会把 dict key 中的这类字符当作 nested-path 编码解释——Using a list keeps every model name as an opaque string value.4.2 load_state_dict新格式优先、旧格式兜底def load_state_dict(self, state: dict) - None: if state.get(pool_digest) ! self._pool_digest: self._inner_indexes {name: 0 for name in self._groups} return counters state.get(counters) if isinstance(counters, list): for record in counters: if not isinstance(record, dict): continue name record.get(model_name) if name not in self._inner_indexes: continue try: self._inner_indexes[name] int(record.get(index, 0)) except (TypeError, ValueError): self._inner_indexes[name] 0 return legacy state.get(inner_indexes) if not isinstance(legacy, dict): return for name, raw in legacy.items(): if name not in self._inner_indexes: continue try: self._inner_indexes[name] int(raw) except (TypeError, ValueError): self._inner_indexes[name] 0恢复逻辑的关键路径digest 不匹配 → 全部归零pool_digest不匹配意味着持久化计数针对的是已经变化的池布局保留旧计数会带偏新的轮转归零是唯一安全默认。新格式counterslist逐条读取model_name必须存在于当前分组name not in self._inner_indexes则跳过——容忍池布局变化index非数字时容错归零。旧格式inner_indexesdict老 session 升级后仍能正确恢复计数不会静默清零。4.3 防御性契约ModelAllocator协议allocator.py要求所有 allocator 实现allocate/state_dict/load_state_dict三个方法并明确load_state_dict的防御要求容忍缺键从 0 开始、未知键忽略、pool_digest不匹配计数器全部归零或保持默认。state_dict()的输出必须能经json.dumps/json.loadsround-trip。4.4 pool_digest 的不变量_pool_digestallocator.py对每条 entry 取(model_name, api_base_url)按池序拼接 sha1def _pool_digest(pool: list[ModelPoolEntry]) - str: h hashlib.sha1(usedforsecurityFalse) for entry in pool: h.update(entry.model_name.encode(utf-8)) h.update(b\x00) h.update(entry.api_base_url.encode(utf-8)) h.update(b\x1f) return h.hexdigest()凭证或 metadata 变更不刷 digest——它们 in-place 刷新而不使 allocator 计数失效池条目的增删 / 重排会改变 digest触发load_state_dict时计数归零。对应测试test_pool_digest_stable_under_credential_refresh验证了旋转 api_key 后计数保留这一不变量。五、验证81 个用例与新增 regression 覆盖5.1 测试基线pytest tests/unit_tests/agent_teams/models/test_allocator.py81 passed。测试文件覆盖四种内置策略RoundRobinModelAllocator、ByModelNameAllocator、RouterAllocator、IntelliRouterAllocator、build_model_allocator工厂、池条目物化、session 持久化 round-trip 与resolve_member_model位置查找。5.2 新增 regression 用例用例一带点号模型名的 round-triptest_by_model_name_state_dict_round_trips_dotted_model_names。池含glm-5/glm-5.1/claude-3.5-sonnet先断言state_dict的counters是 list再验证load_state_dict后轮转计数无误pool [ _make_named_entry(glm-5, g5a), _make_named_entry(glm-5, g5b), _make_named_entry(glm-5.1, g51a), _make_named_entry(claude-3.5-sonnet, c35), ] a ByModelNameAllocator(pool) a.allocate(model_nameglm-5) a.allocate(model_nameglm-5.1) snapshot a.state_dict() # counters must be a list so persistence never re-parses model names. assert isinstance(snapshot[counters], list) by_name {record[model_name]: record[index] for record in snapshot[counters]} assert by_name {glm-5: 1, glm-5.1: 1, claude-3.5-sonnet: 0} b ByModelNameAllocator(pool) b.load_state_dict(snapshot) assert b.allocate(model_nameglm-5).to_team_model_config().model_client_config.api_base http://g5b assert b.allocate(model_nameglm-5.1).to_team_model_config().model_client_config.api_base http://g51a用例二legacy dict 格式兼容test_by_model_name_load_state_dict_accepts_legacy_dict_format。旧inner_indexesdict 格式仍能正确恢复 counterslegacy_state {inner_indexes: {gpt-4: 1, claude: 1}, pool_digest: digest} a.load_state_dict(legacy_state) assert a.allocate(model_namegpt-4).to_team_model_config().model_client_config.api_base http://a2 assert a.allocate(model_nameclaude).to_team_model_config().model_client_config.api_base http://c25.3 既有用例迁移与新格式容错既有test_by_model_name_state_dict_resumes_per_group_rotation、test_persist_leader_config_includes_allocator_state等 round-trip 用例同步迁移到新counterslist 断言如后者断言snapshot[counters]中{gpt-4: 1, claude: 1}且pool_digest存在。test_by_model_name_load_state_dict_tolerates_malformed_input同步覆盖新格式的非法输入容忍counters非 list如not-a-list、index非数字如bogus均安全降级为计数 0。集成侧test_persist_leader_config_includes_allocator_state验证了ByModelNameAllocator经TeamAgent._persist_leader_config写入 session 后state[teams][t][model_allocator_state][counters]形状正确。六、在 session 持久化链路中的位置6.1 写入侧model_allocator_state是 session checkpoint 中 per-team 命名空间的一员。按 S_04 session-and-recovery spec 与 runtime/metadata.py结构为state[teams][team_name] { spec: ..., # TeamAgentSpec.model_dump() context: ..., # TeamRuntimeContext.model_dump() model_allocator_state: ..., # optional —— allocator.state_dict() 输出 lifecycle: running|paused, # optional db_state: pending_create|created|cleaned, pending_resume: {query: ...}, # optional checkpoints: {name: count}, # optional }写入由 recovery_manager.py 的persist_leader_config完成spec/context全量覆盖allocator.state_dict()作为model_allocator_state一并写入round 内增量走persist_allocator_state的merge_team_namespace。6.2 恢复侧冷恢复时TeamAgent.recover_from_session见 team_agent.pyallocator_state bucket.get(model_allocator_state) if allocator_state: agent.restore_allocator_state(allocator_state)restore_allocator_state在 agent_configurator.py 中实现先按当前team_spec.model_pool重建 allocatorfresh 计数器 当前pool_digest再load_state_dict(saved_state)。digest 不匹配 → 计数器归零匹配 → rotation 接续。6.3 完整生命周期spec → build_model_allocator → allocator instancein-memory │ allocate() ←─┤ │ state_dict()─┤── session checkpoint[teams][name][model_allocator_state] │ load_state_dict(saved) ←┘ ← 恢复时七、已知遗留与同类问题处理指引RoundRobinModelAllocator/RouterAllocator不受影响它们的state_dict本来就不含 dict-of-model-name。RoundRobinModelAllocator持久化{index, pool_digest}RouterAllocator/IntelliRouterAllocator无轮转计数器仅持久化{pool_digest}load_state_dict是 no-op详见 allocator.py 与 allocator.py。底层兜底已就位仓库其它地方如果再出现把用户输入字符串当 dict key 塞进 session state的代码靠下层expand_nested_structure的字面递归 fix 兜底。同类 caller 的处理范式未来如发现新的同类 caller按本 feature 同款方式改 list 序列化——把用户输入是 opaque value的语义在数据形态上显式化而不是依赖每个调用方规避./[字符。八、相关设计文档与规约F_16 feature 文档F_16_by-model-name-allocator-list-serialisation.md——本文即基于该 feature 展开S_11 models pool and allocation specS_11_models-pool-and-allocation.md——定义了ModelPoolEntry/ModelRouterConfig/IntelliRouterConfig、四种分配策略语义、ModelAllocator协议、Allocation.to_db_ref持久化身份(model_name, model_index)、state_dict/load_state_dict的 round-trip 契约以及pool_digest只覆盖结构维度(model_name, api_base_url)的不变量S_04 session and recovery specS_04_session-and-recovery.md——model_allocator_state在 checkpoint 中的存放位置与写入/恢复时机。结语ByModelNameAllocator的 list 序列化重构是一次典型的持久化边界修复问题表象在 session 层根因却在 allocator 把模型名当 dict key 的数据形态设计上。通过将出口端数据形态与运行时内部状态解耦——内部继续用dict[str, int]高效轮转出口用 list 承载 opaque model_name——既彻底消除了 nested-path 误解析的崩溃面又以零成本保留旧 session 的兼容加载最终以 81 个通过的单测把不变量钉死。这个模式用户输入字符串不进 dict key 位值得在 agent_teams 乃至全仓库的 session 状态设计中作为通用纪律沿用。赞分享人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习【免费下载链接】agent-coreopenJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力项目地址https://gitcode.com/openJiuwen/agent-core点击查看免费下载相关推荐openJiuwen agent-core 演进 checkpointing 全解析Trainer 状态快照与 Skill 经验 EvolutionStoreopenJiuwen agent core 演进 checkpointing 全解析Trainer 状态快照与 Skill 经验 EvolutionStore人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习openJiuwen Agent Core API 文档体系导读从 core、agent_teams 到 harness 与自演进框架openJiuwen Agent Core API 文档体系导读从 core、agent_teams 到 harness 与自演进框架 openJiuwen人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习CANN算子挑战赛Erf实现团队信息 团队名称 JDpan_Lemon 所属单位 南京理工大学 团队成员 潘金铎 负责赛题解读与全部代码开发、实现Host侧与Kernel侧的流程CANN文档高性能计算创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考