第 26 篇
工程技术
为什么索引任务需要独立 Worker
结合实际开发项目,讨论为什么索引任务需要独立 Worker,说明设计理由、关键源码、验证方法及适用边界。
本文目录 · 7 节
本文属于 Memory Center:AI 编程上下文与记忆系统工程实践,第 26 篇,位于“可靠后台任务”模块。案例背景统一采用实际开发项目;复现步骤使用脱敏教学样例,不代表特定生产事故。
从实际问题出发
解析大项目和请求嵌入模型可能持续很久。如果在普通请求线程里等待全部完成,客户端连接、超时与服务响应都会受影响。后台任务将发起动作和实际计算分开。
用任务标识连接两个阶段
发起请求完成参数与范围检查后返回持久任务标识,随后查询状态。Worker 在隔离工作目录构建结果,协调器负责调度与发布。进程隔离增加启动与通信成本,但提供了回收和资源限制的控制点。

图中展示的是本篇关注的职责和顺序。正常流程之外,还要结合下面的源码与实验检查失败分支;它不是对某次线上调用的运行时追踪。
进程独立不等于没有共享状态
Worker 的结果仍然要通过受控发布条件进入当前索引。任务取消、项目配置变化和服务退出都可能使结果失效。不能让计算进程自行认定成功就直接覆盖当前索引。
关键源码与阅读方法
源码基线为 Memory Center 6e90553(文档标注 5.0.0b24)。定位:memory_center/platform/jobs.py,函数 run_index,起始行 232。
以下为源码节选,省略后续分支;不能作为独立脚本执行。
def run_index(data_dir, database_url, workspace, job_id, limits=None, file_workers=0):
"""子进程内装配旧事实引擎;派生 YAML 位于新服务 generation,绝不读源码旧配置。
``file_workers`` 是父进程按并发项目数切分后的文件级 worker 数
(0 表示项目内串行)。池在本进程生命周期内常驻,只服务本次索引。
"""
from memory_center.core.foundation.models import ServiceConfig, WorkspaceConfig
from memory_center.core.indexing.reindex import reindex_workspace
from memory_center.core.storage.catalog import CatalogStore
from memory_center.core.storage.workspace import WorkspaceStore
if os.name == 'posix' and multiprocessing.current_process().name != 'MainProcess':
os.setsid()
limits = limits or IndexLimits.from_env()
data_dir = Path(data_dir)
db = Database(data_dir, database_url)
store, knowledge = PlatformStore(db), Knowledge(PlatformStore(db), data_dir)
catalog = PostgresCatalog(data_dir, database_url) if database_url else CatalogStore(data_dir)
report = JobProgress(store, job_id)
pool = WorkPool(file_workers, kind='parse')
# 解析缓存落在**工作区级**目录,不在 generation 内:generation 会被 prune 清理,
# 而缓存必须跨代际存活,「只改了几个文件的再次索引」才能跳过其余文件的解析。
cache_root = data_dir / 'workspaces' / workspace['workspace_id'] / 'parse-cache'
# 向量缓存同样放在工作区目录下、跨代际存活:热跑时块内容不变,命中的块直接复用
# 上一代算出的向量,一次模型请求都不发。
embedding_cache_root = data_dir / 'workspaces' / workspace['workspace_id'] / 'embedding-cache'
try:
root = Path(workspace['root'])
if root.resolve() != root or not root.is_dir():
raise CatalogError('ROOT_CHANGED', '源码目录已移动,请暂停后重新登记路径')
chunks = []
source_digests: dict[str, str] = {}
_apply_memory_limit(limits.memory_limit_bytes)
# 默认完整扫描;仅当超过软上限且未显式批准时才拒绝,且不发布任何索引。
report('scanning')
阅读时先找输入由谁提供,再区分纯计算、读取和写入;遇到异常或提前返回,追问调用方会得到什么,以及已有状态是否仍然有效。上面的代码只证明展示范围内的实现,跨模块行为应继续查看调用处和相应测试。
用一个小实验核对设计
建议在独立临时目录和隔离数据中操作,使用虚构的接口、文档与项目名称。
- 向隔离服务提交索引后立即查询状态。
- 在索引运行时执行普通查询。
- 记录主服务是否仍可响应。
**要核对的结果:**任务标识应可持续查询,高成本计算不应阻塞所有管理与查询入口。
可把观察记录为下表;没有执行的项写“未执行”,不能用设计预期冒充实测结果。
| 核对项 | 应保留的证据 |
|---|---|
| 使用条件 | 源码版本、配置、输入与项目范围 |
| 正常行为 | 实际返回、来源位置或状态变化 |
| 关键失败分支 | 错误码、可见结果与原状态是否保留 |
| 结论范围 | 已覆盖、未覆盖和需要进一步验证的条件 |
适用边界与继续阅读
本篇以静态源码与项目设计为依据,给出可执行的核对方法,未据此宣称已经完成新的性能或生产环境测试。不同版本的入口、默认值和底层实现可能变化,应先核对当前合同,再把方法用于自己的实际开发项目。
理解这一节后,可以回到专题总导航,沿同一模块继续阅读;顺序学习时使用专栏内的上一篇和下一篇。涉及具体技术选择,应连同本篇的代价与限制一起采用。