news 2026/10/16 12:03:38

AngusRepo 缓存架构设计与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AngusRepo 缓存架构设计与实践


制品仓库的读请求看起来像一次静态文件下载,真正决定延迟和正确性的,往往是文件打开之前的那几步判断。客户端在拿到 JAR、镜像层或 deb 之前,仓库要先确认目标是否存在、是否仍在线、当前身份能否读取、这个坐标有没有被隔离,然后才决定返回索引文档还是文件本身。这些判断如果每次都回到数据库,上传和下载都会被元数据查询拖住;如果把判断结果在内存里放得太久,一次「改为私有」或一次覆盖发布,就会在后续请求上继续生效。

AngusRepo 把缓存做成一套有边界的一致性机制。共享层只加速体积小、可失效、重启后仍希望预热的事实;正确性来自事务提交之后的失效,存活时间只用来减少重复解析。下面先给出结论和用语,再按数据放置、读路径、版本号、失效传播和文件语义展开。

导读

问题。制品仓库的读请求依赖一串判断,缓存如果只用来加速文件,权限、覆盖写和多进程上的内存会各自漂移。

做法。共享缓存只放仓库快照和聚合仓库的成员列表;鉴权结论、隔离门禁、派生文档和代理未命中留在本进程。仓库行上的四个版本号标出哪一类事实变了。单节点在事务提交后删除本进程缓存;多实例在同样的删除之外,再给每个其他存活实例一只信箱,并用版本号探测兜底。共享层使用平台已有的进程内缓存和数据库中的缓存行,失效消息走持久化工作队列。要共享的数据很少,要传播的动作是「请删除本进程里的相关缓存」,因此不另引入 Redis。

承诺。事务提交之后,处理这次写的进程在下一次请求就能看到新状态;事务回滚时,缓存和版本号都停在提交前。覆盖写让读者看到完整的旧文件或完整的新文件。多实例把多份内存拉齐,它不把单节点变成高可用部署。

本文写给设计或评审制品仓库、缓存一致性和多进程部署的工程师。后文会用到托管、代理、聚合三种仓库;默认数值集中在「默认约定」,一条「改为私有」的时间线放在可见性一节。

文中的几个词

仓库分三种,后文按这个差别讨论文件。

词在本文中的意思
托管仓库文件由本仓库保存,是这份制品的权威副本
代理仓库按需向上游取得制品,并在本地保留一份副本
聚合仓库按成员顺序选择第一个能返回的仓库,自己不复制成员文件

缓存和失效用下面这些词。

词在本文中的意思
仓库快照一份不可变的仓库描述,供在线检查和公开性判断使用,不含上游口令
隔离门禁某个路径上的制品是否存在、是否被隔离,以及原因和可信度
派生文档由仓库内容生成的协议响应,例如 Maven 元数据、npm 包文档、索引页,以及体积不大的镜像清单
新鲜度记录代理仓库对本地副本的描述,包括校验和、上游标识、下次校验时间,以及坐标是否可变
版本号仓库行上只增不减的四个整数,分别对应元信息、权限、内容、成员
工作队列一条消息只会被一个消费者取走并确认,不会自动复制给所有进程
信箱多实例时每个进程专属的一条队列,只由该进程消费
兜底探测多实例在信箱之外,按间隔读取本进程缓存过的仓库版本号,发现落后就删除过期内存
成员版本摘要把各成员的内容版本压成定长摘要,放进聚合索引的键里,避免键超长

1. 什么进入缓存

共享缓存的值是字符串,键最长 256 个字符,进程内默认大约容纳 1 万条。JAR、镜像层、Helm 包和安装包放不进这块空间,强行放入还会在淘汰时挤掉真正需要长期命中的仓库资料。文件已经在对象存储里,热文件再交给操作系统的页缓存即可。托管仓库中的文件就是这份制品;代理仓库中的文件是上游的一份本地副本,能否直接返回由新鲜度记录决定。进程内不保留第二份文件字节。

适合进入共享缓存的,是体积小、条目少、重启之后仍希望预热的事实:仓库快照,以及聚合仓库的成员列表。快照包含标识、租户、名称、格式、类型、在线状态、是否公开、上游地址、是否允许缓存上游,以及四个版本号。上游账号、口令和令牌留在快照之外。需要回源时按仓库标识临时读取凭证,用完即丢,进程里不会长期持有密钥,快照也不会在后续请求中被原地改写。

条目多、或者结果依赖当前用户的数据,各自留在本进程内存里,容量互不影响。

区域留在本机的原因正确性如何维持
鉴权结论按用户和权限展开,数量远多于仓库默认保留 30 秒。仓库自身的公开性和访问规则另有失效路径
隔离门禁按路径展开,只保留是否存在、是否隔离、原因和可信度默认 60 秒;内容版本变化后,旧键不再被读取
派生文档协议生成的元数据和索引单条上限 256KB,约 5 万条,合计约 64MB;超出的响应当次生成
代理未命中记住上游确认不存在的路径,避免反复回源约 10 万条,存活时间跟随仓库配置

仓库快照需要在进程重启后从缓存行恢复,所以放在共享缓存里。鉴权结论和索引正文又多又短命,放进同一块有限空间,只会把快照挤出去。两层缓存并存,是因为它们的共享范围和淘汰代价不同。

2. 读请求如何穿过这些层

一次下载或索引读取按固定顺序进行:先取仓库快照,再判断仓库是否在线,然后计算公开读或访问规则,接着做隔离门禁,最后才生成派生文档或打开文件。文件或文档的命中发生在这些判断之后。私有仓库的文件可以留在磁盘上,权限在每次请求时计算,路径本身不携带授权,系统也不签发长期有效的下载地址。

图 2 读请求先完成判断,再返回文档或文件。

公开仓库的读取对匿名和已登录用户都允许,结论直接来自快照,不必写入鉴权缓存。其余请求先区分匿名与已登录,再依次看令牌、管理员、仓库创建者、部署令牌和访问规则。只有访问规则算出来的允许或拒绝进入本机鉴权区。创建者、管理员和部署令牌是否放行,跟这次请求的身份一起判断,不固化进仓库快照,因此身份变化不会被一份旧快照拖住。

聚合仓库是否可读,看聚合仓库自己的快照。选定某个成员的文件之后,仍执行该成员制品的隔离门禁,这样已隔离的制品不会借组仓库的入口被读出去。成员仓库自己的登录规则不再对终端用户重算。用户面对的是组仓库这一个入口,若每一层成员都再拦截一次,同一次拉取会被重复拒绝,而组仓库的权限模型也会变得难以解释。

不存在的仓库名不进入缓存。随机探测若也能占住一条「不存在」记录,有限的快照容量会被无意义的名字填满。快照默认保留 10 分钟,这段时间只为减少重复解析。仓库下线、改为私有或访问规则变化是否立刻可见,取决于失效传播,而不是这段存活时间。

3. 用四个版本号描述失效范围

仓库行上维护四个只增不减的版本号,与业务更新在同一个事务里前进。一次变更只推动它真正影响的那一个或几个数,读路径据此知道哪些缓存还可以用。

版本何时前进
元信息改名、状态、上游地址、格式或类型,以及其他与权限无关的设置
权限公开性变化、访问规则的增删改、删除仓库
内容本仓发布、覆盖、删除、隔离或可信度变化;代理仓库回源写入或刷新
成员聚合仓库的成员增删或调整顺序

公开性变化时,元信息版本和权限版本一起前进:快照里的公开标志变了,按旧规则算出的结论也要失效。发布制品只推动内容版本,访问规则的变化只推动权限版本。持续上传时,数据库增加的是一次整数更新,仓库设置不必每次重新装载。这个划分把「设置变了」和「内容变了」拆开,避免一次发布把整份快照打掉。

本进程为每个仓库记下当前生效的四个数。拼门禁、派生文档和成员列表的键时,用的就是这四个数。取值只有一条规则:快照里记下的数,和最近一次失效动作写下的数,取较大的一边。

可以用内容版本看这条规则怎么工作。仓库 R 在数据库里的内容版本是 12,本进程的视图也是 12,门禁键因此带着 12。一次发布提交成功后,库里的数变成 13,写进程随即把视图写成 13。此后的读取使用带着 13 的键,带着 12 的旧结论即使还留在内存里,也不会再被读到。其他进程若仍停在 12,信箱到达或兜底探测读到 13 时,同样把视图抬到 13。即便一份尚未刷新的快照里仍写着 12,拼键也以 13 为准。发布只抬高内容版本,不会为了这次上传把整份快照删掉重读。

图 3 同一次发布前后,拼键用的内容版本。

共享缓存不提供按前缀删除,所以上面采用换键,而不是按路径把旧条目扫掉。成员列表把成员版本放进键里,旧键留到自己的存活时间结束。快照的键里不含这些版本号,元信息或权限变化时要显式删掉快照。门禁、派生文档和成员列表靠换键失效,快照靠显式删除,两种手段合在一起才覆盖全部区域。

索引类响应跟随仓库的内容版本。同一仓库任意一次成功发布,都会让旧的 Maven 元数据或同类索引不再命中。若以后观测到重建成本过高,可以把键收窄到具体坐标,那是在同一套版本模型上的优化,失效方式保持不变。

聚合仓库合并出来的索引还要反映成员。它的键包含组的成员版本,以及成员版本摘要。任一成员发布,或成员顺序调整,摘要随之变化,旧的合并结果不再被读取。组仓库合并的是索引视图,成员文件仍留在各自的对象存储里。

4. 单节点直接失效,多实例用信箱传播

单节点

部署方式显式配置,默认是单节点。此时缓存只存在于这一个进程。写事务修改业务数据并推动版本号,事务成功提交之后,再删除本进程里受影响的缓存。提交失败时版本号停在原处,内存保持原样,下一次请求读到的仍是提交前的状态。单节点不为了确认失效再读一次版本号,也不启动队列消费。

一次删除要覆盖这次变更影响到的全部区域。

变更提交后删掉什么
改名旧名和新名上的快照
公开性或访问规则快照,以及该仓库的鉴权结论
发布、覆盖、删除或隔离变化抬高本机内容版本,使该路径的门禁和该仓库的派生文档失效
聚合仓库的成员变化成员列表和组仓库快照
代理路径重新落盘,或上游确认删除该路径的未命中记录。可变路径能否直接返回,仍看新鲜度记录

共享缓存中的快照在删除时同时去掉内存和缓存行。若只清内存,进程重启会把旧快照从缓存行装回来,一次「改为私有」就会在重启后复现。本机删除如果失败,该仓库的快照立即作废,下一次读取直接查库并记录错误,正确性不寄托在那 10 分钟的存活时间上。

信箱

多个进程各有一份内存,失效必须到达每一份。平台队列是工作队列:一条消息被一个消费者取走并确认之后,其他进程看不到它;按某个键分区,也只是让同一个消费者按顺序处理,不会把消息复制给所有进程。因此所有实例共用一个主题,或者按仓库标识把消息固定送到某一台机器,都到不了其余进程上的那份缓存。

多实例改为给每个存活实例一只信箱,也就是一个只由该实例消费的主题。实例启动时使用自己的编号建立这只信箱,编号在集群内唯一;容器环境使用 Pod 名,因为节点主机名可能重复。

图 4 一次提交如何到达其他进程的内存。

在线名单由心跳维护,默认每 10 秒刷新一次,超过 30 秒没有心跳就视为离线。发送时只给最近仍有心跳、并且不是自己的实例写信。消息和业务数据在同一个事务里提交,业务回滚时消息一并消失。本进程的内存仍在提交成功之后再删,这样提交前的并发读不会把旧数据装回刚刚清掉的缓存。

一次事务里,对同一个仓库、同一个目标实例只发一条消息,把本次的变更范围和相关路径合并进去。批量发布不会按文件数乘以实例数写满消息表。消息体只描述要删除什么,包含租户、仓库、名称变化、影响的版本类别和可选路径,不携带文件字节和上游口令。收到消息的实例执行与单节点相同的本地删除,并把消息里的版本号写进本机视图。正常情况下,其他实例能感知的延迟大约是一轮轮询加上处理时间,轮询间隔是 200 毫秒。

新进程启动时内存是空的,从数据库和共享缓存行加载当前事实,不回放历史信箱。心跳停止超过 30 秒的实例不再接收新消息;它信箱里尚未消费的记录由仓库侧的清理任务删除。队列对已完成消息的清理,清不掉没有消费者的记录,所以这份清扫放在仓库自己的任务里。消费失败会拒绝并短暂重试,超过次数进入死信。死信不自动重放,也不负责把缓存修好,兜底探测在间隔内把状态拉回。同一条消息执行多次也是安全的,因为删除和抬高版本号都可以重复做。

心跳与探测

队列正常时,失效不靠轮询数据库发现。另有一层兜底:对本进程里确实缓存过的仓库,默认每 5 秒读取那四个版本号。库里的数更大,就执行与收到消息相同的本地删除。这一读只覆盖热点仓库上的几个整数,不解析设置,也不查询访问规则。消息丢失、实例当时没能进入心跳名单,或消费停滞时,过期内存的停留时间不超过这个探测间隔。单节点不做这层读取。四个版本号仍然维护,以后改成多实例时不必补历史数据。

心跳是否成功,决定这个进程走信箱还是只走探测。

状态本进程如何得知其他写入
心跳正常以自己的信箱为主,5 秒探测只作兜底
心跳写不进数据库停止宣称自己在线,改为只按探测间隔读版本号
超过 30 秒没有心跳其他实例不再给它发新消息;未消费记录由仓库清理任务删除
消息进入死信不靠重放修复缓存,探测在 5 秒间隔内拉回
进程刚刚启动内存为空,加载当前事实,不回放历史信箱
多实例但没有唯一编号拒绝启动,避免两台进程共用一只信箱

用户组和部门关系属于统一身份系统,不在这次仓库事务里。两种部署都把按访问规则算出的允许或拒绝限制在默认 30 秒以内。仓库自己的公开标志和访问规则走提交后的直接删除或信箱,不依赖这 30 秒。若以后要求组成员变化立刻生效,由身份系统通知仓库提高相关仓库的权限版本,再走同一套失效。

5. 三种仓库如何对待文件

三种仓库的文件都不进缓存。差别在副本放在哪、何时重新确认,以及失效推动哪个版本号。

仓库文件放在哪何时重新确认失效走哪个版本号
托管对象存储里的权威副本原子替换完成后,新打开的请求读新文件内容
代理上游副本落在本地不可变坐标有本地文件就直接返回;可变坐标默认 60 秒后条件回源内容
聚合不另存一份,按成员顺序现选每次按当前顺序选择,不记住路径属于哪个成员成员;合并索引用成员版本摘要

托管仓库

覆盖写把新内容先写入同目录的临时文件,校验完整之后再原子改名。临时文件必须和目标在同一文件系统上,否则改名会退化为复制,并发读者仍可能看到写到一半的内容。已经打开旧文件的下载可以读完旧内容;改名之后新打开的请求读到新文件。同一时刻,一个读者看到的要么是完整的旧文件,要么是完整的新文件。

成功提交后内容版本前进。写进程抬高本机内容版本,并丢掉该路径的门禁和本仓派生文档;多实例同时把这个内容变更送进其他实例的信箱。事务失败时删除临时文件,版本号不变,消息不发出,内存也不改。

代理仓库

新鲜度记录描述本地副本,包括校验和、大小、落盘时间、上游的校验标识和修改时间、下次需要重新校验的时间,以及这份坐标是不可变还是可变。正式版本且坐标不可变时,本地有就直接返回。快照版本、Maven 元数据、latest、镜像标签和各种索引,默认在 60 秒后带条件请求回源。上游回答未修改,只把下次校验时间往后推;上游返回新内容,按托管仓库同样的原子替换更新本地文件,并让该代理仓库的内容版本前进;上游确认不存在,删除本地副本,并写入有容量上限的未命中记录。仓库关闭缓存时不落盘。

上游出现服务错误或超时时,若本地仍有副本,则返回这份副本,在响应和日志里标明正在沿用本地副本,并且不把下次校验时间往后推。这样上游恢复后的下一次请求仍会重新校验,而不是把一次故障延长成一段新的免校验时间。

聚合仓库

聚合仓库不保存成员文件的第二份副本。每次按成员列表的当前顺序,选择第一个能够返回的成员,并且不长期记住「这个路径属于哪个成员」。高优先级成员后来才发布该制品时,若仍返回早先选中的低优先级成员,客户端会稳定地拿到过时的那一份。成员列表的失效走成员版本,合并索引的失效走成员版本摘要。

6. 权限和内容各自何时可见

一次变更何时被下一次请求看见,取决于它推动的是哪个版本,以及请求落在写进程还是其他进程。

变更处理写的进程其他进程(多实例)
公开性或访问规则提交后的下一次请求约一个轮询间隔;消息丢失时不超过探测间隔
下线或维护打开文件前拒绝约一个轮询间隔;消息丢失时不超过探测间隔
覆盖发布或解除隔离新的内容版本立即用于本进程的键信箱到达后同样抬高内容版本
身份系统中的组成员变化不超过鉴权缓存的 30 秒不超过鉴权缓存的 30 秒

把仓库从公开改为私有,可以把上表走成一条时间线。设提交时刻为 T0,权限版本从 7 变为 8。

时刻发生的事
T0同一次事务更新公开标志,元信息版本和权限版本一起前进,并为每个其他存活实例写入一条信箱消息
T0 之后,写进程的下一次读本进程的快照和该仓库鉴权结论已经删除。匿名请求不再放行,已登录用户按访问规则重算
大约 T0 + 200 毫秒其他实例取到自己的信箱,做同样的删除
信箱没有送达其他实例最迟在约 5 秒的探测里发现权限版本变大,删除旧快照和旧的鉴权结论
同期只是用户组变化不在这次事务里。仓库规则未改时,原允许结论最多再保留 30 秒

私有改为公开、仓库下线或进入维护,走与 T0 相同的路径,并在打开文件之前就拒绝。收回某个用户的读取权也推动权限版本,写进程在提交后丢掉快照和鉴权结论,其他进程仍由信箱或探测跟上。

7. 失败时仍然成立的承诺

可见性的边界是事务已经提交。在此之前,缓存和版本号都停留在提交前;在此之后,写进程的删除和其他进程的信箱才开始生效。

情况结果
业务事务回滚版本号没有增加,多实例的消息也没有提交,内存保持原样
提交成功,但进程在删除本机缓存之前退出单节点重启后内存为空,随后从数据库加载。多实例的消息已经和业务一起提交,其他实例仍会删掉自己的内存
共享缓存行暂时不可写快照退化为只留在本进程内存。本机删除仍然能纠正它,重启后的预热变弱
派生文档超过单条大小上限不进入缓存,当次生成并返回
清空全部缓存行操作保持关闭。那张表上还可能有其他使用方的数据,失效按仓库和区域进行

观测按区域分开记录:命中、回源、信箱延迟、兜底探测纠正了多少次,以及代理回源时未修改、已更新、沿用旧副本各有多少。共享缓存自带的汇总统计把所有键混在一起,不能单独用来判断仓库缓存是否正确。

8. 默认约定

这些数值是一致性边界,不是调优起点。改短存活时间只会增加读取,改长存活时间不会让公开性或覆盖写更正确,那两件事走的是提交后的删除和版本号。

约定默认它约束什么
部署模式单节点提交后只删除本进程缓存,不启动队列
快照存活10 分钟减少重复解析,不承担权限和下线的正确性
鉴权结论30 秒,约 10 万条访问规则算出的允许或拒绝
隔离门禁60 秒,约 10 万条内容版本变化后旧键不再被读取
派生文档单条 256KB,约 5 万条,合计约 64MB超出则当次生成,不进入缓存
代理未命中约 10 万条,时间随仓库配置记住上游确认不存在的路径
可变坐标再校验60 秒到期后带条件请求回源
共享缓存容量键最长 256 字符,内存约 1 万条只放快照和成员列表
信箱轮询200 毫秒仅多实例;其他进程的正常可见延迟
心跳10 秒刷新在线名单
视为离线30 秒无心跳不再给该实例发新消息
兜底探测5 秒仅多实例,且只探测本进程缓存过的仓库

9. 配置偏离时会看到什么

上面的时间线成立,有一个前提:部署模式和真实的进程数一致,多实例的编号唯一,心跳能够写入,删除快照时内存和缓存行一起去掉。偏离这些前提时,系统仍按设计运行,只是可见性和设计者的预期会分开。

偏离会看到什么
多个进程仍配置成单节点其他进程收不到这次删除,只能等到各自的存活时间。这是配置错误,不是失效算法的误差
多实例没有唯一实例编号进程拒绝启动。若两台进程共用一只信箱,消息只会被其中一台取走
心跳写失败却仍当作在线其他实例可能不再把消息送给它,它自己的旧权限会留下。设计上此时停止宣称在线,改为按 5 秒读版本号
删除快照时只清内存重启后旧快照从缓存行装回,公开性变化会复现。删除必须同时去掉缓存行
代理的可变路径不按新鲜度回源本地副本会一直是旧字节。到期后应带条件请求回源;上游故障时可以沿用副本,但不把下次校验时间往后推

10. 边界

共享层停在平台已有的进程内缓存、数据库缓存行和持久化队列上。多实例要解决的是每一台都删掉自己的内存,按实例划分的信箱加上版本号兜底可以闭合这件事,队列继续保持工作队列的取走语义。

文件字节、鉴权结论、未命中记录和大段协议响应留在本进程或对象存储,不写入共享缓存和消息。单节点不在每个请求上读取版本号。不存在的仓库名不占用快照容量。身份系统的用户组变更保持 30 秒的上限,本方案不另做一条同步链路。内容去重解决的是相同字节在磁盘上占多份,覆盖写解决的是这个路径现在指向哪一份完整内容,两者继续分开:去重存储保持原样,路径上的替换采用原子改名。

对运行仓库的人,默认的单节点意味着:提交成功后的下一次请求能看到新的仓库状态、公开性和新上传的字节,回滚则两者都不变。需要多个进程共享同一套数据时,打开多实例模式并给出唯一的实例编号。其他进程在一轮轮询左右跟上,消费停住时由版本号探测在数秒内纠正。文件层保证读者看到的是完整的旧包或完整的新包,代理层对可变坐标重新向上游确认,对不可变坐标继续直接复用本地副本。

原文地址:anguskit.com

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 19:09:07

Java时间轮机制:高效定时任务管理实践

1. 时间轮机制概述时间轮(Time Wheel)是一种高效管理定时任务的环形数据结构,最早由George Varghese和Tony Lauck在1996年提出。它通过模拟机械表盘的运转方式,将时间划分为多个槽位(slot),每个…

作者头像 李华
网站建设 2026/10/16 19:09:07

Java美妆电商系统开发指南与毕业设计实践

1. 项目背景与核心价值这个Java美妆购物平台系统是一个典型的电商类毕业设计项目,特别适合计算机相关专业的学生作为毕业设计选题。我见过太多学生在毕业设计阶段苦苦寻找合适的项目,这个选题之所以值得推荐,是因为它涵盖了电商系统的主要功能…

作者头像 李华
网站建设 2026/10/16 19:09:07

双极性四线步进电机微步控制原理与闭环实现

简介:本资源是一份面向嵌入式开发者与自动化控制初学者的双极性四线步进电机驱动技术详解文档,系统梳理了从基础到进阶的四种核心控制方式:单4拍(整步)、双4拍(整步)、8拍(半步&…

作者头像 李华
网站建设 2026/10/16 19:09:10

Elastic-Job-Lite作业分片策略源码解析与选型实战

1. 从一个真实困惑说起:为什么我的定时任务总在单机上打转刚接触分布式定时任务那会儿,我遇到过一个很典型的问题:明明部署了三台机器,任务配置也写的是分片模式,可每次跑起来,日志里只有一台机器在干活&am…

作者头像 李华
网站建设 2026/10/16 19:09:11

智能体AI从语言模型到物理世界的范式转移与工程实践

1. 从语言模型到智能体:一个正在发生的范式转移过去两年,我一直在跟踪和实操各类语言模型应用,从最简单的提示词工程到复杂的多智能体协作系统,踩过的坑和收获的经验都不少。今天想聊的这个话题,是我认为目前AI领域最值…

作者头像 李华
网站建设 2026/10/16 19:09:13

Robo-Dopamine:用通用奖励模型为机器人操作任务构建稠密奖励信号

1. 从“奖励模型”到“机器人多巴胺”:Robo-Dopamine 系列到底在解决什么问题第一次看到“Robo-Dopamine”这个名字,我脑子里蹦出来的不是生物学,而是一个很具体的工程困境:机器人操作任务里,奖励信号太稀疏了。做过机…

作者头像 李华