一、问题描述
RagSourceReader._search_sync (src/retrieve/pipeline.py:296-376) 每次收到一次搜索请求都 新建一个 embedder,搜索结束又 立即关掉,把模型引用也释放:
# pipeline.py:306
embedder: Embedder = self._embedder_factory()
warnings: list[str] = []
try:
...
# pipeline.py:317 进入 CollectionSession 之前
with CollectionSession(self._collection_name, read_only=True) as collection:
...
dense = self._profile.search_similar(collection, search_query, embedder) # 第一次用 embedder
...
# pipeline.py:373-376
except Exception as error:
raise SourceReaderError("原始 RAG 检索不可用") from error
finally:
embedder.close() # 每次都关
LocalEmbedder.close() (src/dao/emb/embedder.py:366-368) 实现是:
def close(self) -> None:
"""释放模型引用让 GC 回收。下次 :meth:`embed_dense` 会重新加载。"""
self._model = None # 释放引用让 GC 回收
LocalEmbedder._ensure_model (embedder.py:330-338) 实现是:
def _ensure_model(self) -> SentenceTransformer:
if self._model is None:
self._model = SentenceTransformer(self._model_name) # 30s+ 加载
return self._model
会爆的具体场景:
-
每次搜索都重新加载 sentence-transformers 模型:
-
BailianEmbedder 也受影响:
- 每次新建 embedder → 新建 HTTP client(虽然
BailianEmbedder.close 只关 client,HTTP 复用只限同实例)
- 实际影响:每次搜索多消耗一次 HTTP 握手 / keep-alive 失效
-
_search_sync 在 asyncio.to_thread 里跑:
-
finally 里 embedder.close() 隐藏原始错误:
- 如果
self._embedder_factory() 抛异常(line 306),embedder 局部变量未绑定
finally: embedder.close() 引用未绑定变量 → UnboundLocalError
- 真实的 factory 异常被
UnboundLocalError 遮盖,调试时找不到根因
对比 director.fetch 等模块的 embedder 用法 —— 其他模块要么用闭包长生命周期持有 embedder,要么显式 __init__ 时建好。_search_sync 是项目里 唯一 每次调用都重建 embedder 的热路径。
二、相关配置
三、修复建议
方案 A(最小改动):让 RagSourceReader 长生命周期持有 embedder
class RagSourceReader:
def __init__(
self,
...,
embedder_factory: Callable[[], Embedder],
...,
) -> None:
self._embedder_factory = embedder_factory
self._embedder: Embedder | None = None
...
def _get_embedder(self) -> Embedder:
if self._embedder is None:
self._embedder = self._embedder_factory()
return self._embedder
def _search_sync(self, query, options, limit):
embedder = self._get_embedder()
warnings: list[str] = []
try:
...
except Exception as error:
raise SourceReaderError("原始 RAG 检索不可用") from error
# 删掉 finally 里的 close() —— embedder 由 reader 生命周期管理
def close(self) -> None:
if self._embedder is not None:
self._embedder.close()
self._embedder = None
方案 B:修复 finally 的 UnboundLocalError
embedder: Embedder | None = None
try:
embedder = self._embedder_factory()
...
except Exception as error:
raise SourceReaderError(...) from error
finally:
if embedder is not None:
embedder.close()
方案 C(根因):改 LocalEmbedder.close() 不再释放模型
def close(self) -> None:
"""只关 HTTP client / 释放句柄;模型保留在 self._model 供下次复用。"""
# 故意不重置 _model —— LocalEmbedder 实例级缓存是设计而非泄漏
pass
推荐 方案 A + 方案 B 同时做。方案 C 单独做也有用,但 model 持有会变成"模块级全局状态",与 embedder 工厂模式冲突。
一、问题描述
RagSourceReader._search_sync(src/retrieve/pipeline.py:296-376) 每次收到一次搜索请求都 新建一个 embedder,搜索结束又 立即关掉,把模型引用也释放:LocalEmbedder.close()(src/dao/emb/embedder.py:366-368) 实现是:LocalEmbedder._ensure_model(embedder.py:330-338) 实现是:会爆的具体场景:
每次搜索都重新加载 sentence-transformers 模型:
embed_dense→_ensure_model→ 加载模型(30s+,取决于模型大小 / 磁盘 IO)close()→_model = NoneBailianEmbedder 也受影响:
BailianEmbedder.close只关 client,HTTP 复用只限同实例)_search_sync在asyncio.to_thread里跑:_embedder_factory()是同步调用(line 306)finally里embedder.close()隐藏原始错误:self._embedder_factory()抛异常(line 306),embedder局部变量未绑定finally: embedder.close()引用未绑定变量 →UnboundLocalErrorUnboundLocalError遮盖,调试时找不到根因对比
director.fetch等模块的 embedder 用法 —— 其他模块要么用闭包长生命周期持有 embedder,要么显式__init__时建好。_search_sync是项目里 唯一 每次调用都重建 embedder 的热路径。二、相关配置
src/retrieve/pipeline.py:296-376(_search_sync)src/dao/emb/embedder.py:330-338, 366-368(_ensure_model/close)LocalEmbedder._ensure_model no lock(并发竞争)——本 issue 是同源(不必要的销毁)但表现不同search()调用UnboundLocalError)三、修复建议
方案 A(最小改动):让
RagSourceReader长生命周期持有 embedder方案 B:修复
finally的 UnboundLocalError方案 C(根因):改
LocalEmbedder.close()不再释放模型推荐 方案 A + 方案 B 同时做。方案 C 单独做也有用,但 model 持有会变成"模块级全局状态",与 embedder 工厂模式冲突。