TR-001: TFNeuralModel / Neural deepcopy 循环引用¶
| 字段 | 值 |
|---|---|
| 编号 | TR-001 |
| 标题 | TFNeuralModel / Neural deepcopy 循环引用 |
| 状态 | 已修复 |
| 日期 | 2026-02-12 |
| 影响版本 | <= 0.3.3b1 |
问题现象¶
在对包含 BaseTool(或其他 TFNeuralModel 子类)的容器执行 copy.deepcopy() 或 Pydantic 的 model_copy(deep=True) 时,会出现以下三种异常之一:
| 异常类型 | 触发条件 | 表现 |
|---|---|---|
RecursionError |
默认递归深度 | maximum recursion depth exceeded |
TypeError: cannot pickle 'socket' object |
Neural 持有网络相关资源时 | deepcopy 尝试复制不可序列化对象 |
TypeError: cannot pickle 'module' object |
Neural 持有模块引用时 | deepcopy 尝试复制 Python 模块 |
已确认的触发点:
dc_chains.py:60—chain_ctx.model_copy(deep=True)- TFRobotServer
dc_chain.py的split_func—context.model_copy(deep=True)
两者都通过 ChainContext.tools: list[BaseTool] 触发。
根因分析¶
循环引用结构¶
TFNeuralModel 与 Neural 之间存在双向引用:
TFNeuralModel Neural
┌──────────────────┐ ┌──────────────────┐
│ │ _neural │ │
│ _neural: Neural ├─────────────►│ _tf_models: │
│ │ │ WeakSet[ │
│ │◄─────────────┤ TFNeuralModel│
│ │ (弱引用) │ ] │
└──────────────────┘ └──────────────────┘
deepcopy 递归路径¶
copy.deepcopy(ChainContext)
└─ deepcopy(tools: list[BaseTool])
└─ deepcopy(BaseTool) # TFNeuralModel 子类
└─ deepcopy(BaseTool._neural) # Neural 实例
└─ deepcopy(Neural._tf_models) # WeakSet
└─ deepcopy(BaseTool) # 回到起点 → 无限递归
└─ deepcopy(BaseTool._neural)
└─ ... # RecursionError
weakref.WeakSet 在被 deepcopy 时会迭代其全部成员并逐一深拷贝,因此无法自动打断循环。
影响范围¶
所有 TFNeuralModel 的子类均受影响:
| 子类 | 模块 |
|---|---|
BaseTool |
tfrobot.drive.tool.base |
BaseChain |
tfrobot.brain.chain.base |
Chains |
tfrobot.brain.chain.chain_structures.base |
BaseLLM |
tfrobot.brain.chain.llms.base |
BaseBrain |
tfrobot.brain.base |
BaseDrive |
tfrobot.drive.base |
BaseMemory |
tfrobot.brain.memory.base |
修复方案¶
方案:TFNeuralModel + Neural 实现 __deepcopy__ / __copy__ 返回 self¶
在 TFNeuralModel 和 Neural 类上分别添加 __copy__ 和 __deepcopy__ 方法,直接返回 self。
# tfrobot/schema/meta/tf_neural_model.py
class TFNeuralModel(TFBaseModel):
...
def __copy__(self) -> "TFNeuralModel":
return self
def __deepcopy__(self, memo: dict[int, Any]) -> "TFNeuralModel":
memo[id(self)] = self
return self
# tfrobot/neural/base.py
class Neural(TFBaseModel):
...
def __copy__(self) -> "Neural":
return self
def __deepcopy__(self, memo: dict[int, Any]) -> "Neural":
memo[id(self)] = self
return self
为什么返回 self 是安全的¶
-
_prepare_chain_context已使用浅拷贝:base.py:550中copy.copy(tools)只拷贝列表容器,工具对象本身保持引用共享。这是有意设计——注释明确写道"浅拷贝,避免循环引用"。 -
深拷贝 Neural 组件在架构上不成立:深拷贝的副本不会注册到 Neural 信号系统中,成为"幽灵"实例——没有信号连接、没有事件处理能力,违反了组件必须通过 Neural 注册才能工作的架构不变量。
-
代码库中没有故意 deepcopy TFNeuralModel 的场景:所有触发 deepcopy 的代码都是对容器(如
ChainContext)做深拷贝时意外递归到组件。
为什么不在 ChainContext 层修复¶
| 考量 | 说明 |
|---|---|
| 覆盖不完整 | 只保护 ChainContext 一个容器,其他持有 TFNeuralModel 的容器仍有风险 |
| 维护负担 | ChainContext 的 __deepcopy__ 需要在每次字段变更时同步维护 |
| 不必要 | TFNeuralModel 层修复后,ChainContext.model_copy(deep=True) 自动正常工作 |
验证¶
修复后的行为:
import copy
neural = Neural(name="test")
tool = StubTool()
neural.register(tool)
# 不再抛出 RecursionError
copied = copy.deepcopy(tool)
assert copied is tool # 返回同一实例
# ChainContext.model_copy(deep=True) 正常工作
ctx = ChainContext(tools=[tool], ...)
ctx_copy = ctx.model_copy(deep=True)
assert ctx_copy.tools[0] is tool # tools 内对象共享引用
assert ctx_copy.tools is not ctx.tools # tools 列表本身是新的