![]()
單一方式來過濾、排序與分頁任何清單。
find()貫穿本文的範例是一個圖書館目錄。一本書會儲存以下內容:
// src/book/book.schema.ts
@Schema({ timestamps: true })
export class Book {
@Prop() title: string;
@Prop({ type: Types.ObjectId, ref: "Author" }) author: Types.ObjectId;
@Prop() publishedAt: Date;
@Prop() copies: number; // copies on the shelf
@Prop() available: boolean;
@Prop() acquisitionPrice: number; // what it cost us: internal, never published
}
Enter fullscreen mode
Exit fullscreen mode
作者的名字不在這裡:它存在於 authors 集合中,位於該參照的另一端。而使用目錄的畫面是一張帶有搜尋框、逐欄過濾器與分頁的表格。
提供資料的端點只寫一次,並透過累積方式成長。它一開始回傳固定排序的一頁資料,等到表格擁有所有過濾器時,它就變成這樣:
// src/book/book.controller.ts
@Controller("books")
export class BookController {
constructor(
@InjectModel(Book.name) private readonly model: Model<BookDocument>,
) {}
@Get()
async getAll(
@Query("title") title?: string,
@Query("available") available?: string,
@Query("minCopies") minCopies?: string,
@Query("sortBy") sortBy?: string,
@Query("page") page?: string,
) {
const filter: FilterQuery<BookDocument> = {};
if (title) {
filter.title = { $regex: title, $options: "i" };
}
if (available) {
filter.available = available === "true";
}
if (minCopies) {
filter.copies = { $gte: Number(minCopies) };
}
const current = Number(page ?? 1);
const [items, total] = await Promise.all([
this.model
.find(filter)
.sort({ [sortBy ?? "createdAt"]: -1 })
.skip((current - 1) * 20)
.limit(20),
this.model.countDocuments(filter),
]);
return { items: items, total: total, page: current };
}
}
Enter fullscreen mode
Exit fullscreen mode
該方法內有一些正確的決策:總數來自與項目相同的過濾器,因此分頁不會自相矛盾,而且兩個查詢是平行執行的。像這樣撰寫的清單可以穩定運行多年而不發生事故,其行為並非本文打算修正的內容。
值得衡量的是最終寫在檔案外面的東西。端點的簽名與呼叫它所需的 URL 共同構成一份合約:
GET /books?title=dune&available=true&minCopies=3&sortBy=publishedAt&page=2
Enter fullscreen mode
Exit fullscreen mode
這份合約沒有在任何地方宣告,卻已經在正式環境中使用:當有人在工單中分享該 URL 或將其寫入匯入腳本的那一刻,查詢字串中的五個名稱就有了儲存庫外部的消費者。而它所使用的詞彙並非目錄的詞彙,而是集合的詞彙:sortBy=publishedAt 完全依照資料庫的稱呼來命名文件欄位,minCopies 也鎖定了未出現在名稱中的運算子,而讀到 title=dune 的人無法判斷它是在尋找精確比對還是部分比對,因為這只寫在 if 裡面。
本文所建構的是 Criteria 模式:一個描述清單的物件——什麼被過濾、如何排序、哪一頁——從客戶端旅行到儲存庫,並被翻譯兩次,在每個邊界各一次。在分析之前先看看結果是值得的,因為接下來的一切都是為了證明為什麼是這種形狀而不是另一種。
同樣的表格,呼叫同樣的端點,會像這樣被請求:
GET /books
?filters[0][field]=title&filters[0][operator]=CONTAINS&filters[0][value][0]=dune
&filters[1][field]=authorName&filters[1][operator]=EQUAL&filters[1][value][0]=Herbert
&order[by]=publishedAt&order[type]=DESC
&page=2&pageSize=20
Enter fullscreen mode
Exit fullscreen mode
回應會帶有頁面以及繪製分頁器所需的資訊:
{ "items": [], "totalItems": 143, "totalPages": 8, "pageSize": 20 }
Enter fullscreen mode
Exit fullscreen mode
而控制器內不再有任何欄位名稱:
// src/book/infrastructure/nest/book.controller.ts
@Get()
async getAll(
@Query() request: CriteriaRequest,
): Promise<PaginationResponse<BookResponse>> {
const useCase = new GetAllBooks(this.repository, new BookCriteriaRequestMapper());
return await useCase.execute({ request: request });
}
Enter fullscreen mode
Exit fullscreen mode
將兩個 URL 並排在一起時,不用閱讀伺服器就能看出四個差異:
CONTAINS 會隨請求一起傳送,因此讀到 URL 的人知道 dune 是在尋找部分比對。在之前的版本中,這活在一個 if 裡面。authorName 不存在於任何文件中——作者在另一個集合中——但仍然可以像任何其他欄位一樣被過濾與排序。這些都不是免費的:達到這個目標每個實體需要四個檔案,外加每個資料庫引擎一個轉換器,而且有些專案並不值得這麼做。本文剩下的部分將說明為什麼是這種形狀、它的成本以及何時不值得。
端點與呼叫它的人被綁在一起的地方有五個,而且它們並非同一種類。前四個透過閱讀檔案就能看見;第五個只有在第二個清單出現時才會變得可見。
1. 運算子活在方法主體內。 title 是用 $regex 解析,而 minCopies 是用 $gte,但兩個名稱都沒有說出來,這表示過濾器的行為可以在不碰簽名的情況下改變:把那個 $regex 改成精確比對不會破壞任何編譯,唯一的訊號是回應開始帶回更少的列。
2. 參數名稱就是欄位名稱。 sortBy=publishedAt 能運作是因為該字串被直接傳給 .sort()。在 schema 中重新命名屬性會留下兩條出路:破壞已經在流通的 URL,或是在控制器內保留一張從舊名稱到新名稱的別名表——這正是該模式最終會正式化的翻譯對映,只是寫得太晚而且只針對移動的那個欄位。
3. 簽名隨著欄位乘以運算子而成長。 minCopies 涵蓋了 copies 其中一種可能的比較;最大值是另一個參數,而精確範圍是第三個。端點累積的不是每個欄位一個參數,而是每個有人想對某欄位提出的問題一個參數。
4. 可以被過濾的東西沒有寫在任何地方:它是 if 的殘餘。 要知道端點接受什麼,你必須閱讀整個方法並記住分支。在排序方面甚至沒有分支可讀,因為 sortBy 直接進入 .sort():文件中的任何路徑都是有效的排序,包括清單不會回傳的那些欄位。
5. 格式對這個端點來說是私有的。 下一個清單——作者、借閱、複本——會從頭開始再次決定一切:頁面是用 page 還是 offset 請求,排序是用 sortBy 加上 order 還是單一的 sort=-publishedAt,布林值是用 true、1 還是單純參數的存在來傳遞。在客戶端,每個畫面都寫自己的序列化器,而且沒有一個與前一個足夠相似到可以共用。
前四個是耦合的麻煩事:它們活在一個檔案內,透過編輯該檔案來修正,而修正它們的成本不取決於你等了多久。第五個是不同種類。它不活在任何檔案中,而是活在撰寫端點的人與消費它的人之間的協議中,而且它不會隨著欄位數量成長:它隨著清單數量乘以客戶端數量而成長。
只有單一清單、四個固定過濾器與一個畫面呼叫它時,這五個都沒有可觀察到的成本,而上面的方法是對問題的適當解答。它們在三個條件出現時變得可衡量,而且這三個條件往往一起出現:清單不再只有一個,客戶端不再只有一個,以及過濾器不再固定,因為使用者從表格標頭組合它們。
在查詢字串中傳遞的名稱是文件欄位的名稱,而已發佈的 URL 沒有版本也沒有棄用:只要有人繼續使用它,它就存在。當 publishedAt 變成 firstPublishedAt 的那一天,編譯器或測試都不會說任何話,壞掉的是已經在儲存庫外部流通的連結。然而成本不是在重新命名時支付:它支付在你不會重新命名這個事實,因為既然沒有辦法知道誰用舊名稱呼叫,遷移就被延後,而不再描述它所儲存內容的名稱就保留下來。
簽名累積每個可以對欄位提出的問題一個參數,而關於日期或數字的有用問題有好幾個;再乘以清單的數量,因為每個清單都從頭開始重複這個練習。效果體現在成長的方向:參數進來但不出去,因為移除 minCopies 需要證明沒有人呼叫它,而這個證明無法針對沒有在任何地方宣告的合約產生。該方法最終成為曾經呼叫過它的每個畫面的總和,包括那些已經不存在的。
sortBy 以文字到達並直接進入 .sort(),因此你可以排序的欄位清單不是由端點決定:它是由 schema 決定。而對欄位排序是一種讀取它的方式——用 sortBy=acquisitionPrice 與幾頁資料,你可以重建整個目錄中採購價格的相對順序,而回應從未回傳任何單一價格。二階效果是控制所在之處:該表面是透過編輯schema而變寬,而不是控制器,因此明天加入供應商利潤的人是在用一個沒有碰觸任何人會查看的檔案的 diff 來擴大 API 暴露的範圍。
這三種成本共享一個根源:客戶端可能要求的東西沒有以資料形式存在於任何地方,而是散落在方法簽名、幾個 if 的主體以及每個畫面組裝其 URL 的方式中。
Criteria 模式幾乎總是以相同的論點被引入:它避免了儲存庫方法的爆炸。在 CodelyTV 的表述中(這是西班牙語世界中該模式的參考),如果你必須依多個欄位過濾,「我們最終可能會有一個儲存庫,每個要過濾的欄位一個方法,加上可能存在的任何排列組合」——而 criteria 在尊重開放/封閉原則的同時解決了它。
它所描述的問題是真的,推理也是正確的。值得衡量的是它的大小。一個真實的儲存庫不會累積排列組合,它累積的是實際需要的那些方法:findByTitle、findByAuthorAndAvailable,以及不多不少的其他。成長不是組合式的,而是等於畫面的數量,而介面中四或五個相似的方法讀起來尷尬但修正起來便宜——它們正是上面清單中四個局部且可逆的點之一。
這個論點也沒有觸及某件事。方法爆炸完全在後端內部被解決:一個由 use case 手動組裝的 criteria,使用 new BookCriteria({ filters: [...] }),就已經移除了它,而為了做到這點,你不需要 DTO、不需要驗證、不需要公開欄位清單,也不需要在 URL 中傳遞運算子。一個只以此為理由的實作只會停在後端邊緣之前,而那正是本文問題開始的地方。另一個流傳的論點——這樣你就可以更換資料庫——在這裡更薄弱,因為 criteria 的轉換器正是遷移中便宜的部分;TypeORM 章節會展示它,但它是作為可驗證的後果而不是動機。
注意:可攜性論點確實有一個被認真捍衛的地方,那就是 Repository 模式,而我在那裡詳細衡量了它:NestJS 中的 Repository 模式——一個碰巧住在資料庫中的集合。如果你對那個討論感興趣,它全都在那裡;對接下來的内容來說,知道它不是 criteria 所支付的就足夠了。
取代它的問題是整個合約有哪些可用:不是為了避免介面中重複的方法,而是讓客戶端知道它可以要求什麼,而伺服器知道它接受什麼。在那裡,生態系提供的比通常被承認的還多。
nestjs-paginate 是開箱即用走得最遠的選擇,而且值得毫無保留地這麼說。目錄清單,在 15.0.1 版完整呈現:
// src/book/book.controller.ts
@Get()
async getAll(@Paginate() query: PaginateQuery): Promise<Paginated<Book>> {
return paginate(query, this.repository, {
relations: ["author"],
sortableColumns: ["title", "publishedAt", "copies"],
searchableColumns: ["title", "author.name"],
filterableColumns: {
available: [FilterOperator.EQ],
copies: [FilterOperator.GTE, FilterOperator.LTE],
"author.name": [FilterOperator.ILIKE],
},
defaultSortBy: [["publishedAt", "DESC"]],
defaultLimit: 20,
maxLimit: 100,
});
}
Enter fullscreen mode
Exit fullscreen mode
GET /books?filter.available=$eq:true&filter.copies=$gte:3&sortBy=publishedAt:DESC&page=2&limit=20
Enter fullscreen mode
Exit fullscreen mode
sortableColumns 是必填欄位,不是可選的,而 filterableColumns 宣告每個欄位接受哪些運算子,出自一個包含十一個運算子的目錄——$eq、$gte、$in、$btw、$ilike、$null、$contains 及其同伴——加上 $not 後綴與 $all / $any / $none 量化器。maxLimit 設定頁面上限,searchableColumns 提供全文搜尋,有游標分頁,而且 filterExpressionMaxComplexity 限制過濾器表達式可擁有的節點數量,以免有人用巢狀 filter= 把伺服器搞垮。也要注意 "author.name" 能運作:名稱可以跨越關聯,因此即使是本文用作嚴苛測試的案例也被涵蓋了。成本 2 消失了——一個參數——成本 3 也消失了:沒有宣告的東西不會進來。
它沒有解決的是詞彙,而其他一切都由此而來。接受的名稱是 Column<T> 型別,這是實體的屬性路徑,因此 URL 中的 publishedAt 在類別中也是 publishedAt,成本 1 依然未被觸及。而 paginate() 接受 TypeORM 的 Repository<T> 或 SelectQueryBuilder<T> 並回傳 TypeORM 實體:合約很優秀,但它與 ORM 密不可分,因此對使用 Mongoose 或 Prisma 的人來說它不存在,而且 use case 無法在不匯入 TypeORM 的情況下表達它。
GraphQL 透過另一條路徑解決整個問題——客戶端宣告它想要什麼,而 schema 就是合約:
query {
books(where: { available: true }, orderBy: { publishedAt: DESC }, first: 20) {
title
author {
name
}
}
}
Enter fullscreen mode
Exit fullscreen mode
三種成本一次消失。代價不是函式庫,而是傳輸:HTTP 快取、授權、速率限制與可觀測性全都移到其他地方,這讓它成為一個架構決策,而不是加到清單上的一層。
將整個 req.query 傳給 find() 在零行程式碼中解決了端點的成長:
@Get()
async getAll(@Query() query: FilterQuery<BookDocument>) {
return this.model.find(query);
}
Enter fullscreen mode
Exit fullscreen mode
而它把另外兩種成本加劇到極致,因為公開詞彙變成引擎的全部:?acquisitionPrice[$gt]=0 會依一個沒有人決定要暴露的欄位過濾,而且端點的表面不再有任何人能說出的限制。
Specification 與 Query Object,這些經典模式,解決了領域內部條件的組合——以草圖形式,repository.match(new Available().and(new PublishedAfter(2020)))。這是一個真實的問題,也是前一節討論的問題,但兩者都沒有提到 HTTP 傳輸或驗證到達的內容,而在這裡這佔了一半的工作。
| 成本 1 · URL 與 schema 耦合 | 成本 2 · 端點成長 | 成本 3 · 意外表面 | |
|---|---|---|---|
nestjs-paginate |
未處理:公開名稱就是實體屬性 | 已解決:單一參數 |
已解決:sortableColumns 是必填 |
| GraphQL | 已解決:schema 就是合約 | 已解決 | 已解決 |
ORM 的 where 傳入 find()
|
加劇它 | 已解決,沒有限制 | 加劇它:表面是整個引擎 |
| Specification / Query Object | 未處理 | 已解決 在後端內部 | 未處理 |
注意:表格沒有衡量人機工程或生產環境中第一個端點的時間,而且
nestjs-paginate在兩者都大幅領先。它也沒有衡量在任何其他之前決定的條件:底下坐的是哪個持久化引擎。於 2026 年 8 月針對nestjs-paginate15.0.1 與 TypeORM 1.1.0 檢查;這些 API 會在主要版本中改變。
差距就在那裡。對任何不在 TypeORM 上的人來說,這些都不可用,而對在它上面的人來說,合約最終是以 ORM 的詞彙表達。缺少的是一個既不命名欄位也不命名函式庫的清單描述。
Fowler 用一行定義了 Query Object——「一個代表資料庫查詢的物件」——並將其發展為一個直譯器:一個能夠將自己轉成 SQL 的物件結構。兩種表述都朝向引擎。本文的表述則是反過來看:
Criteria 不是在 URL 中旅行的查詢:它是清單的描述——什麼被過濾、如何排序、哪一頁——用領域的詞彙撰寫,並被翻譯兩次,在每個邊界各一次。
這句話決定了三個部分,而這三個部分佔據了文章剩下的部分。
每個實體一個公開欄位 enum。 合約的詞彙以資料形式存在於一個檔案中,而不是幾個 if 的殘餘。這是你決定客戶端可以命名什麼的地方,而這個決定不再取決於 schema 碰巧包含的東西:這是對成本 1 與 3 的直接回答。
一個沒有依賴的領域 criteria。 描述清單的物件不匯入 NestJS、不匯入驅動程式、也不匯入 ORM。Use case 建構它並交給儲存庫,而不知道背後是什麼,這允許同一個清單可以從 Mongo、從 Postgres 或從測試中的記憶體替身提供。
兩個彼此一無所知的翻譯。 第一個將 HTTP 請求轉成 criteria 並活在應用層;第二個將 criteria 轉成引擎的查詢並活在基礎設施中。兩者都不知道對方存在,而這就是接下來兩節要測試的特性:更換引擎只碰第二個,而改變客戶端可以要求的東西只碰第一個。
這是整個檔案,不是摘錄:
// src/shared/domain/criteria/criteria.ts
type Props = {
filters?: CriteriaFilter[];
order?: CriteriaOrder | null;
page?: number | null;
pageSize?: number | null;
search?: string | null;
};
export abstract class Criteria<T extends string> {
private _filters: CriteriaFilter[];
private _order: CriteriaOrder | null;
private _page: number | null;
private _pageSize: number | null;
private _search: string | null;
constructor({ filters, order, page, pageSize, search }: Props = {}) {
this._filters = filters ?? [];
this._order = order ?? null;
this._page = page ?? null;
this._pageSize = pageSize ?? null;
this._search = search ?? null;
}
get filters() {
return this._filters;
}
get order() {
return this._order;
}
get page() {
return this._page;
}
get pageSize() {
return this._pageSize;
}
get search() {
return this._search;
}
// Replaces the whole list: this is what the mapper does with the request's filters.
setFilters(v: CriteriaFilter[]) {
this._filters = v;
}
// Accumulates: what the server imposes is added and the request cannot drop it.
addFilters(v: CriteriaFilter[]) {
this._filters = [...this._filters, ...v];
}
find(field: T): CriteriaFilter[] {
return this._filters.filter((f) => f.field === field);
}
}
Enter fullscreen mode
Exit fullscreen mode
這個檔案重要的是它不包含的東西:沒有裝飾器、沒有 NestJS 匯入、沒有資料庫驅動程式匯入。這個特性是可檢查的——在專案依賴未安裝的情況下它可以編譯——而其他一切都依賴於它:同一個物件可以在 use case 中建構、旅行到 Mongo 儲存庫,也可以在測試期間旅行到記憶體替身。
三個細節比看起來更有份量。T extends string 參數是將每個 criteria 綁定到其欄位清單的東西,因此如果名稱不在實體的 enum 中,criteria.find("subtitle") 就不會編譯。find 回傳清單而不是單一過濾器,因為一個欄位可以攜帶兩個——publishedAt 在一個日期之後且在另一個日期之前是一個區間——而翻譯者需要同時擁有兩者。setFilters 與 addFilters 的區分存在是因為它們是兩種不同的情況:客戶端的過濾器取代清單,而伺服器強加的那些則累積,而且用不同的名稱時,差異在 use case 中是可見的,而不是必須被記住。
過濾器是一個欄位、一個運算子與一些值。基礎類別固定前兩個,並將第三個留給每種型別:
// src/shared/domain/criteria/criteria-filter.ts
export abstract class CriteriaFilter {
readonly field: string;
readonly operator: CriteriaFilterOperator;
constructor({ field, operator }: CriteriaFilterProps) {
this.field = field;
this.operator = operator;
}
abstract hasValues(): boolean;
}
Enter fullscreen mode
Exit fullscreen mode
// src/shared/domain/criteria/criteria-number-filter.ts
export class CriteriaNumberFilter extends CriteriaFilter {
readonly values: number[];
constructor(props: CriteriaFilterProps & { values: number[] }) {
super(props);
this.values = props.values;
}
numbers(): number[] {
return this.values;
}
// A filter with no values must not restrict the query.
hasValues(): boolean {
return this.numbers().length > 0;
}
}
Enter fullscreen mode
Exit fullscreen mode
有一種常見的替代方案,值得說明為什麼它比較差:儲存 values: unknown[] 並搭配一個判別欄位 type: "string" | "number" | "date" | "boolean"。使用那種形狀,轉換器會對 type 做 switch,而編譯器不會檢查它讀取的值是否符合所在的分支,因此每個 case 都需要 as number[] 斷言。使用每個型別一個類別,filter instanceof CriteriaNumberFilter 會縮小型別,而 filter.numbers() 已經回傳 number[]。差異在基礎設施轉換器中被收取,那是模式中一個長長的 switch,涵蓋運算子,也是最容易無聲出錯的地方。
這個檔案是客戶端可以命名的整個表面:
// src/book/domain/criteria/book-criteria-field.ts
export enum BookCriteriaField {
ID = "id",
TITLE = "title",
AUTHOR_NAME = "authorName",
PUBLISHED_AT = "publishedAt",
COPIES = "copies",
AVAILABLE = "available",
}
Enter fullscreen mode
Exit fullscreen mode
// src/book/domain/criteria/book-criteria.ts
export class BookCriteria extends Criteria<BookCriteriaField> {}
Enter fullscreen mode
Exit fullscreen mode
acquisitionPrice 不在裡面,而這個缺席就是對成本 3 的完整回答:API 的表面不再由 schema 決定。明天將供應商利潤加入文件的人不會擴大任何東西,因為從 URL 命名它將意味著編輯這個檔案,而這正是有人在審核時會尋找它的地方。
另一方面,authorName 在裡面,而它不是文件欄位。這個 enum 是清單的詞彙,而不是 schema 的詞彙——而這就是成本 1 被解決的地方,因為公開名稱不再與欄位名稱綁定,重新命名屬性變成內部變更。authorName 活在另一個集合中是轉換器的問題,而嚴苛測試會在後面處理它。
具體的 criteria 只有一行,因為它所有的型別都來自 enum:從這裡開始,任何在這六個名稱之外的 find 都不會編譯。
這是模式中唯一帶有裝飾器的檔案,而這種集中是故意的:它是所有你無法控制的東西通過的邊界。
// src/shared/application/dto/criteria-request.ts
export class CriteriaFilterRequest {
@IsString()
@IsNotEmpty()
field: string;
@IsEnum(CriteriaFilterOperator)
operator: CriteriaFilterOperator;
// qs returns a string when the query carries `value=x` once, and an array when it
// carries indexes (`value[0]=x`); normalised so it does not depend on how many
// values the client happened to send.
@IsDefined()
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
@IsArray()
@IsString({ each: true })
value: string[];
}
export class CriteriaRequest {
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CriteriaFilterRequest)
filters?: CriteriaFilterRequest[];
@IsOptional()
@ValidateNested()
@Type(() => CriteriaOrderRequest)
order?: CriteriaOrderRequest;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
pageSize?: number;
@IsOptional()
@IsString()
search?: string;
}
Enter fullscreen mode
Exit fullscreen mode
兩個在應用程式啟動時的設定決定這是否運作,而兩個都會無聲失敗:
// src/main.ts
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Express 5 parses the query with `simple`, which is querystring.parse and does not
// nest: `order[by]` would arrive as a key literally called "order[by]".
app.set("query parser", "extended");
// Without `transform`, the DTO's @Type() decorators are not applied and `page` is
// still a string.
app.useGlobalPipes(new ValidationPipe({ transform: true }));
Enter fullscreen mode
Exit fullscreen mode
第一個是版本 5 中的變更:在 Express 5.2.1 的 lib/application.js 中,預設設定是 this.set('query parser', 'simple'),而 extended 是插入 qs 的東西。沒有它,filters[0][field]=title 不會以巢狀物件到達,而驗證會因為不是客戶端的錯而拒絕整個請求。
這是兩個翻譯中的第一個。它將 DTO 轉成 criteria,並沿途做了三個決定:
// src/shared/application/criteria/criteria-request-mapper.ts
export type CriteriaFilterOption<T extends string> = {
field: T;
type: CriteriaFilterType;
};
export abstract class CriteriaRequestMapper<T extends string> {
abstract options(): CriteriaFilterOption<T>[];
execute({ criteria, request }: Props<T>): Criteria<T> {
if (request.search !== undefined) {
criteria.setSearch(this.mapSearch(request.search));
}
// Pagination is always set, whether the request carries it or not: a criteria
// with no pageSize translates into a query with no limit.
criteria.setPage(this.mapPage(request.page));
criteria.setPageSize(this.mapPageSize(request.pageSize));
if (request.order !== undefined) {
criteria.setOrder(
new CriteriaOrder({
orderBy: request.order.by,
orderType: request.order.type,
}),
);
}
if (request.filters !== undefined) {
const options = this.options();
const result: CriteriaFilter[] = [];
for (const filter of request.filters) {
const mapped = this.mapFilter(filter, options);
if (mapped !== null) {
result.push(mapped);
}
}
criteria.setFilters(result);
}
return criteria;
}
// The ceiling is what stops an absurd pageSize from ending up an unbounded find().
private mapPageSize(value: number | undefined): number {
if (value === undefined || !Number.isFinite(value)) {
return DEFAULT_PAGE_SIZE;
}
return Math.min(Math.max(Math.trunc(value), 1), MAX_PAGE_SIZE);
}
// What is not in options() is not filtered: there is no field to apply it to.
private mapFilter(
filter: CriteriaFilterRequest,
options: CriteriaFilterOption<T>[],
): CriteriaFilter | null {
const option = options.find((o) => o.field === filter.field);
if (option === undefined) {
return null;
}
const props = { field: option.field, operator: filter.operator };
switch (option.type) {
case CriteriaFilterType.STRING:
return new CriteriaStringFilter({ ...props, values: filter.value });
case CriteriaFilterType.NUMBER:
return new CriteriaNumberFilter({
...props,
values: this.mapNumbers(filter.value),
});
// ...dates and booleans, the same way
}
}
}
Enter fullscreen mode
Exit fullscreen mode
而實體的那一個宣告清單,這是每個清單唯一必須撰寫的部分:
// src/book/application/criteria/book-criteria-request-mapper.ts
export class BookCriteriaRequestMapper extends CriteriaRequestMapper<BookCriteriaField> {
options(): CriteriaFilterOption<BookCriteriaField>[] {
return [
{ field: BookCriteriaField.ID, type: CriteriaFilterType.STRING },
{ field: BookCriteriaField.TITLE, type: CriteriaFilterType.STRING },
{ field: BookCriteriaField.AUTHOR_NAME, type: CriteriaFilterType.STRING },
{ field: BookCriteriaField.PUBLISHED_AT, type: CriteriaFilterType.DATE },
{ field: BookCriteriaField.COPIES, type: CriteriaFilterType.NUMBER },
{ field: BookCriteriaField.AVAILABLE, type: CriteriaFilterType.BOOLEAN },
];
}
}
Enter fullscreen mode
Exit fullscreen mode
第一個決定是在 options() 中宣告的型別就是轉換文字的東西。查詢字串沒有型別,因此 copies 的 "3" 會在這裡變成數字 3,一次,而不是在每個引擎轉換器中各自做一次。第二個是頁面上限:總是設定 page 與 pageSize,以 MAX_PAGE_SIZE 為上限,就是阻止客戶端把清單變成集合傾印的東西。
第三個值得帶著它的成本陳述。對未宣告欄位的過濾器會被無聲丟棄,它不會回傳 400,這表示客戶端的一個打字錯誤會產生未過濾的清單而不是一個可見的錯誤——這更難除錯。選擇這種方式的原因是幾個月前儲存的 URL 在某個欄位停止可過濾時,仍然會回傳某個合理的東西,而不是壞掉。nestjs-paginate 採取相反的決定並提供 throwOnInvalidFilter;兩者都有道理,而沒有選擇是沒有道理的。
注意:編譯器不會檢查
options()是否涵蓋整個 enum。將其宣告為Record<BookCriteriaField, CriteriaFilterType>而不是清單會強制這點,但代價是失去表格形狀。目前的樣子,一個沒有型別的 enum 欄位是一個只有在實際過濾時才會注意到的疏忽。
這裡也是模式的成本,與好處在同一個地方:每個清單你必須撰寫四個檔案——enum、單行 criteria、帶有 options() 的對映器,以及稍後出現的基礎設施對映——而之前只有五個 @Query()。你換來的是這四個可以在一分鐘內讀完,並且說出關於端點接受什麼的全部真相。
領域儲存庫暴露單一的列出方法:
// src/book/domain/repository/book.repository.ts
export interface BookRepository {
pagination(criteria
![]()
[Submitted on 9 Jun 2025 (v1), last revised 19 Aug 2026 (this version, v4)]
Abstract:蛋白質生成模型在蛋白質設計方面展現了顯著的潛力,但其成功率仍受限於依賴人工整理的序列-結構資料集,以及監督式目標與實際設計目標之間的錯位。我們提出 ProteinZero,這是一個用於反向摺疊模型的線上強化學習框架,能夠實現可擴展、自動化且持續的自我改進,並提供計算效率高的回饋。ProteinZero 採用結合來自 ESMFold 的結構引導與新型自我衍生的 ddG 預測器的獎勵管線,在避免物理基礎方法高昂成本的同時提供穩定的多目標訊號。為了確保線上 RL 的穩健性,我們進一步引入一種新型的嵌入層級多樣性正則化器,可減輕模式崩潰並促進具功能意義的序列變異。在平衡多獎勵優化、來自參考模型的 KL 散度以及多樣性正則化的一般 RL 表述中,ProteinZero 在可設計性、穩定性、回復率與多樣性等方面均取得穩健的改進。在 CATH-4.3 基準測試中,它持續優於包含 ProteinMPNN、ESM-IF 與 InstructPLM 在內的現有最先進基準,將設計失敗率降低 36-48%,並在多樣摺疊中達到超過 90% 的成功率。重要的是,完整的 RL 運行可在單一 8 張 GPU 節點上於三天內執行完畢,包括獎勵計算與資料生成。這些結果顯示,高效的線上 RL 微調可透過讓蛋白質生成模型從自身輸出中持續演化並在無標記資料下優化多重設計目標,來補充監督式預訓練,為探索廣大的蛋白質設計空間開啟新的可能性。完整原始碼與模型檢查點將於發表後釋出。
From: Jiajun Fan [view email]
[v1]
Mon, 9 Jun 2025 06:08:59 UTC (4,052 KB)
[v2]
Tue, 10 Jun 2025 18:30:51 UTC (4,052 KB)
[v3]
Mon, 2 Mar 2026 05:31:24 UTC (11,066 KB)
[v4]
Wed, 19 Aug 2026 22:42:25 UTC (11,067 KB)
![]()
格式:稽核檢查清單 — 放在眼前逐項驗證。
版本: 1.1 (2026-08-21)
適用範圍: 任何基於 LLM 的助理與代理,具有長期記憶(對話、情節、語義、向量、圖譜、多模態),不限技術堆疊與平台。
稽核遵循一個通用的記憶循環。每個檢查清單項目對應此模型中的一個節點或邊緣。
INPUT (user, files, web, images, other agents, external models)
→ INGESTION (validation, meaning extraction, importance scoring)
→ STORAGE (messages, episodes, concepts, vectors, graphs, caches)
→ RETRIEVAL (relevance, freshness, importance, boundaries)
→ ASSEMBLY (formatting, compression, injection, budgets)
→ MODEL / LLM
→ OUTPUT (responses, actions: files, search, memory calls)
→ FEEDBACK (output returns to INGESTION) ← loop is closed
Enter fullscreen mode
Exit fullscreen mode
關鍵特性是閉環:任何進入記憶的內容都會回到模型,並能自我複製。大多數嚴重的記憶體故障是邊緣故障,而非節點故障。
yes / partial / no / n/a。no 和 partial,記錄證據:檔案與行號、SQL 查詢與結果、傾印、提示快照、記錄項目。沒有證據的斷言不視為已驗證。return False 不可接受。[CRIT]
%、_)與元字元在 LIKE/regex 記憶體搜尋中已跳脫。[IMP]
症狀 → 可能缺陷類別。用於完整檢查前的快速導航。
| 症狀 | 可能缺陷 | 參見章節 |
|---|---|---|
| 模型「分析」自己的報告/回應 | 回饋循環 | F |
| 技術文字、日誌、程式碼出現在「記憶」中 | 輸入驗證 / 評分 | A, D |
| 相同資料在上下文中重複 | 寫入冪等性 | B |
| 重建後儲存量大幅成長 | 非冪等索引 | C |
| 資料存在於儲存體中,但檢索結果為空 | 寫入/讀取鍵不符 | 0, D |
| 「有時正常」、「重啟後才有效」 | 隱藏儲存、RAM 狀態、快取 | 0, C |
| 資料庫「乾淨」但問題持續 | 地圖外存在儲存體 | 0 |
| 舊的不相關內容排擠新的 | 靜態重要性 vs 相關性 | D |
| 模型突然「失去」所有長期記憶 | 非決定性持久鍵 | D |
| 回應帶有不相關主題/代理的痕跡 | 隔離邊界突破 | G |
| 簡短隨意查詢得到空上下文 | 積極的檢索閘控 | D |
| 回應內容「斷在半個字」 | 組裝時盲目截斷 | E |
| 被發現的秘密出現在回應中 | 驗證前洩漏至記憶 | A |
| 負載下卡住,「資料庫已鎖定」 | 存取並行 | H |
| 代理「猜測」而非「記得」 | 嵌入模型不符 / 檢索路徑 | 0, D |
完成完整檢查後填寫:
Audit date: ______
System under audit: ______
System map completed: yes / no (if no — audit is not complete)
| Section | items | yes | partial | no | n/a | failed [CRIT] |
|---------|-------|-----|---------|----|-----|---------------|
| 0. Map | 7 | | | | | |
| A. Input validation | 7 | | | | | |
| B. Write integrity | 6 | | | | | |
| C. Growth | 7 | | | | | |
| D. Retrieval | 8 | | | | | |
| E. Assembly | 11 | | | | | |
| F. Feedback loop | 6 | | | | | |
| G. Isolation | 7 | | | | | |
| H. Concurrency | 6 | | | | | |
| I. Observability | 8 | | | | | |
| J. Change management | 6 | | | | | |
Verdict:
- DOES NOT PASS: any [CRIT] failure — list: ______
- PASSES WITH CAVEATS: [CRIT] clean, [IMP] failures present: ______
- CONFORMS: no [CRIT]/[IMP] failures, only [REC] notes
Evidence (each failure → file/query/dump): ______
Priority remediation order: ______
Enter fullscreen mode
Exit fullscreen mode
判決規則:
此標準是透過統整多年實際助理系統的運作經驗、事件與事後檢討而來,這些系統具備多層記憶:自我污染循環、非決定性鍵造成的無聲失憶、含有秘密的外部內容對長期儲存的污染、重複索引造成的容量倍增、嵌入模型語言不符導致的檢索失敗、同時變更造成的退化、重啟後人格特質遺失,以及將模型輸出與使用者輸入一同儲存造成的回饋循環。每個項目皆有對應類別的真實事件作為後盾;沒有事件基礎的項目標記為 [REC]。
作者:Aleksandr Kossarev, Jõgeva, Estonia
標籤:#ai #architecture #memory #standard
![]()
外帶作業:公平地對 CognoDB 與其他幾個圖資料庫進行基準測試。公平規則很直接——每個資料庫都只能使用相同的極小資源上限:0.5 vCPU、256MB RAM,沒有例外。
說起來容易。有兩個平台沒能撐過去。
Memgraph 在匯入過程中不斷被 Linux OOM reaper 殺掉。一開始猜是缺少索引。錯了,檢查過、修正了,還是掛掉。第二次猜是儲存模式。Memgraph 的分析模式會跳過預寫日誌以加速匯入,聽起來很有希望。結果它也不支援基準測試所需的唯一性約束——你只能二選一,不能同時擁有。回到正常模式,乾淨地跑了一次,再跑一次確認。
又被殺掉了。設定正確的情況下兩次乾淨的失敗不是運氣不好。這是 Memgraph 的記憶體內交易引擎有個真正的記憶體底限,256MB 的機器無法通過。
ArangoDB 則是因為完全不同的原因失敗。它的文件深處提到:除非明確告訴它,否則它不會真的偵測 Docker 容器的記憶體限制。若放任不管,它會根據主機機器的 RAM 來調整內部快取,而不是容器的。設定了覆寫後,有進展,但還是掛了。
到了這個階段,誠實的選擇是:放寬上限直到所有東西都能塞進去(這會完全失去測試的意義),或者交付少於要求的資料庫數量。兩個都不對,所以專案中途加入了第五個平台——Kùzu,一個嵌入式圖資料庫,它直接在你自己的行程內執行,而不是作為獨立的伺服器。在你載入任何一列資料前,不會有閒置的常駐程式。
使用它時的峰值記憶體:1.94MB。在 256MB 中。而 Memgraph 和 ArangoDB 試圖存放相同資料時,就在這個數字上掛掉。
最終的實際結果比「本地端獲勝」更有趣。Kùzu 的「索引」查詢根本不是索引——它是一次完整掃描,設計上就不支援傳統索引。它還是比 AuraDB 真正的索引查詢快了大約 40 倍(4.7ms 對 196.7ms)。這不是更聰明的查詢引擎,而是直接測量出雲端資料庫的延遲有多少只是網路來回,而不是實際的工作量。
FalkorDB 和 Kùzu 的表現也沒有乾淨地分出高下。FalkorDB 贏了所有遍歷查詢,但在聚合上輸得很慘——在那個項目上比真正的雲端資料庫還慢。不同的引擎、不同的優勢,沒有單一贏家。
這些結果都沒有變成乾淨的五個綠勾勾表格,而這正是重點。如果第一次 Memgraph 嘗試就成功了,這些事情永遠不會浮上檯面。
完整方法論、每一次失敗、每一個數字:repo link。
![]()


Published Aug 20, 2026, 11:05 PM EDT
Simon 是電腦科學學士畢業生,從 2014 年開始撰寫科技相關文章,從 Windows 3.1 時代就開始使用 Windows 機器。在一家獨立遊戲工作室工作並擔任家族電腦問題的技術支援後,他找到了寫作的熱情,並決定運用自己的技能撰寫所有科技相關內容。
自從開始寫作生涯以來,他為許多不同刊物撰稿,例如 WorldStart、Listverse,以及 MakeTechEasier。然而,在 2019 年 2 月找到 MakeUseOf 這個家後,他最終轉移到其姊妹網站 XDA,為讀者帶來 Windows、Linux 和 DIY 電子產品的最新資訊。
作為一名 Linux 粉絲,我總是喜歡慶祝它在對抗大公司時獲得勝利。然而,我從未在最瘋狂的夢想中想像過,一款 Linux 筆電的銷售量會超越其 Windows 版本超過十比一。幸運的是,我不用再做夢,因為 Framework 已確認其新款 Laptop 12 的 Fedora 預購量目前遠遠超越 Windows 版本,但如果你仔細想想,這其實非常合理。
幾天前,Framework 宣布新款 Laptop 12 的預購已經開放。這款新筆電搭載 Intel Core Series 3 處理器、Thunderbolt 4、Wi-Fi 7 連線,並可選配指紋辨識器和背光鍵盤。Framework 表示他們也與 Fedora 和 KDE 密切合作,為用戶提供基於 Linux 的 Laptop 12 版本。
嗯,看來初步銷售統計數據已經出爐,而對 Tux 團隊來說情況相當樂觀。在一則 X 貼文中,該公司確認 Fedora 版本的銷售量以超過 10 比 1 的比例超越 Windows 版本。
Framework 很快宣稱這是「Linux 桌機年」,但如果你深入了解實際情況,消費者選擇 Fedora 版本似乎是非常簡單的決定。首先,Framework 稱 Laptop 12 的 Fedora 版本是他們「有史以來價格最低的預組機」,售價只要 699 美元。在硬體危機的當下,價格親民的選擇最吸引人,尤其是如果有人已經擁有 Windows 授權,不想再多付一份錢。
此外,Framework 筆電的核心理念就是可自訂化、DIY 和自行維修。這些價值觀在 Linux 中都能找到,因此喜愛擺弄硬體的人,自然也會對他們的軟體抱持相同態度。無論如何,我還是希望真正的原因是人們現在就是越來越喜歡 Fedora 勝過 Windows。
![]()
Google 已將 Gemini Live 與其 Deep Research 功能連結,讓使用者能夠透過語音開始多步驟的研究任務,將其留在背景執行,待工作完成後再以語音或文字記錄的形式進行後續討論。這項改變將 Deep Research 從主要以提示引導的活動,轉變為更具對話性的行動工作流程,特別適合那些需要在不留在應用程式內的情況下捕捉研究請求的使用者。
關鍵區別不僅僅是語音輸入。Gemini Live 可以啟動一項研究流程,讓使用者在切換應用程式或鎖定手機時繼續進行。Google 將這種體驗描述為透過語音進行研究,當任務完成時會發出通知,並提供無縫回到對話的路徑。該公司的 Pixel 的 Gemini Deep Research 概覽 將此功能呈現為讓深度研究在行動裝置上更易使用的一部分。
Deep Research 本身旨在提供的遠不止單一回應。Google 已記錄了一個工作流程,其中 Gemini 會制定研究計畫、跨來源搜尋、視需要擴大調查,並產生包含來源連結的結構化報告。報告也可以匯出至 Google Docs。將此流程帶入 Gemini Live 改變了請求的開始方式以及使用者恢復的方式,而不是改變 Deep Research 的既定目的。
這次更新結合了對話式啟動與非同步執行。使用者可以大聲說明複雜主題,要求 Gemini Live 開始 Deep Research,然後在系統運作時轉向其他任務。當報告準備好時,使用者會收到通知,並可透過語音繼續討論或檢視文字記錄。
| 工作流程元素 | 已記錄的 Deep Research 體驗 | Gemini Live 整合 |
|---|---|---|
| 開始請求 | 研究請求可以產生結構化計畫。 | 使用者可以透過與 Gemini Live 對話來啟動 Deep Research。 |
| 研究過程 | Gemini 可以搜尋來源、擴大搜尋,並彙整報告。 | 研究可以在使用者切換任務或鎖定手機時繼續進行。 |
| 結果 | 結構化且富含引用的報告可以包含來源連結,並匯出至 Docs。 | 完成時可以觸發通知,後續進行語音討論或文字記錄檢視。 |
這種方法在任務需要時間但初始指示不需要時間時最為有用。準備會議、調查不熟悉的主題或細化問題的人,可以在想法出現時直接陳述目標,而不必立即撰寫詳細提示。其價值在於能夠交出多步驟任務,並在執行期間收回注意力。
這可以讓 AI 輔助研究 更自然地融入行動工作,因為中斷和情境切換在行動工作中很常見。
語音也改變了交付後的互動方式。Gemini Live 不再將報告視為最終成品,而是將其定位為持續對話的材料。這與 Google 將 Deep Research 描述為可精煉的迭代過程一致。使用者可以在檢視回傳內容後,探索發現、要求澄清或改變研究方向。
對組織而言,非同步研究是 AI 工具中的重要模式。它將定義問題的行為與調查所需的時間分開。這可以讓 AI 輔助研究更自然地融入行動工作,因為中斷和情境切換在行動工作中很常見。
然而,不應將宣布的工作流程誤認為是 企業研究治理解決方案。提供的 Google 資料描述了研究規劃、來源連結報告、持續精煉以及 Docs 匯出。它們並未建立組織特定的資料處理、保留、核准流程或政策執行的控制。企業在評估將 Gemini Live 用於工作研究時,因此應區分語音引導任務建立的便利性與自身處理敏感資訊和驗證輸出的需求。
同樣的限制也適用於可用性和成本。提供的資料確認了此功能,但未提供完整的定價模式、資格矩陣、區域推出時程或語音啟動 Deep Research 的開發者 API 細節。這些問題對評估部署的團隊仍具相關性,但無法從現有的公告資料中獲得解答。
對開發者而言,立即的重要性主要在體驗層面,而非已宣布的平台介面。Google 已確認使用者導向的流程,可在語音對話、背景工作、通知和報告檢視之間切換。在提供的資料中,它並未宣布對應的 API、SDK 或整合控制。產品團隊應避免假設 Gemini Live 互動會自動作為可嵌入的研究工作流程提供。
Google 的舉措也反映了更廣泛的產品方向:研究輔助正逐漸不再綁定於單一聊天工作階段。有意義的比較不是競爭平台的清單或未經支援的功能聲稱。而是在對話中等待答案與 委派有界限的研究流程(可在使用者進行其他事情時完成)之間的差異。Google 的實作將語音作為該委派模型的入口點。
對企業團隊而言,語音啟動的研究可以引入新的途徑,讓工作問題進入 AI 系統。Scalevise 可以協助評估該途徑在何處創造生產力提升、何處需要人工審核,以及 AI 研究如何符合現有的治理實務。 我們的 AI 顧問團隊 可以將新興的助理功能轉化為符合您工作流程和風險需求的實際採用計畫。請求諮詢以評估您的 AI 研究工作流程。
什麼是 Gemini Live Deep Research?
Gemini Live Deep Research 讓使用者可以透過語音向 Gemini Live 提出要求,開始多步驟的 Deep Research 任務,然後在完成後回來討論或檢視結果。
Gemini Live Deep Research 是否可以在手機鎖定時執行?
可以。Google 表示研究可以在使用者切換任務或鎖定手機時於背景繼續進行,並在完成時發出通知。
Gemini Deep Research 會產生什麼?
Google 將 Deep Research 描述為建立研究計畫、跨來源搜尋與擴大,並回傳包含來源連結的結構化報告。報告可以匯出至 Google Docs。
此公告是否確認企業治理控制或開發者 API?
否。提供的資料確認了使用者導向的研究工作流程,但未指定企業資料治理控制、定價細節或開發者 API。
Google 的 Gemini Live 整合讓 Deep Research 在行動工作日中更容易啟動和重新檢視。其重要性在於結合語音請求、背景執行,以及圍繞已記錄研究流程的對話式後續討論。對組織而言,機會在於更流暢地存取 AI 輔助研究,而治理、推出和整合問題仍需另行評估。
![]()
隨著 AI 模型變得更強大,這些模型被誤用的潛在風險也隨之增加——呼籲建立安全護欄以防止此類濫用的聲浪也日益高漲。AI 公司現在必須在尊重企業客戶隱私與監控使用情況以防可能問題之間,維持微妙的平衡。
察覺到有機會超越競爭對手 Anthropic,OpenAI 剛剛宣布了一項以隱私為中心的監控誤用安全方法。該公司正在向特定客戶預覽一項名為 Private Safety Processing 的新服務。這是一套自動化系統,能在監控潛在濫用的同時,完全不保留客戶的任何資料。
這套系統明顯與 Anthropic 最近宣布的資料保留政策背道而馳。該政策已引起部分客戶不滿,它允許這家 AI 實驗室在「涵蓋模型」的情況下,保留使用者資料(所有對話紀錄及其中的對話內容)長達 30 天。該公司表示,這些模型包括所有 Mythos 類別模型以及「具有類似能力的未來模型」。
這項在七月宣布的政策,目的是為了安全考量,讓實驗室得以篩選和分析潛在的不當行為。然而,它已深深引起某些處理大量敏感資料企業的擔憂,這些企業不希望資料被 AI 實驗室保存(或檢查)。
OpenAI——如同大多數其他 AI 公司——已透過遵守名為 Zero Data Retention 的政策,為客戶提供相對程度的隱私。ZDR 利用 OpenAI API 內的代理程式,以每個工作階段為單位監控濫用行為。透過這種方式,公司不會保留客戶資料,但仍能掃描不良活動而無需人工介入。值得注意的是,Anthropic 也大致遵守 ZDR——但「涵蓋模型」如 Fable 則屬例外。
OpenAI 表示,Private Safety Processing 是一項新技術,能擴大 ZDR 的適用範圍。它將其描述為一種長期安全監控形式,能評估多個對話的輸入與輸出——而非僅限單一對話。同樣地,監控由代理程式執行,若被觸發,便會捕捉互動並跨工作階段分析潛在誤用的跡象。
該新技術有助於 OpenAI 偵測跨越多個工作階段的惡意 AI 使用,一位發言人告訴 TechCrunch。惡意行為者——假設是試圖為網路攻擊開發惡意軟體的人——可能會分散他們的請求以避免被偵測。Private Safety Processing 能在無需人工審查使用者對話的情況下,分析這些多個對話以找出濫用跡象。
在系統被觸發的情況下,它可能會向 OpenAI 發送一個「明確定義的訊號」,警告特定類型的活動,公司表示。根據該訊號,OpenAI 便能決定是否「需要執行執法措施」,它說。若是如此,OpenAI 將聯繫客戶以取得更多脈絡,或與他們合作解決問題,而客戶可自行決定是否與 OpenAI 分享資料,發言人表示。
相較之下,Anthropic 表示對客戶資料的人工審查可能會發生,但僅限「透過受控存取路徑」,且涉及「一小群經過批准的審查者」。公司表示,每一次審查工作階段都會「記錄在防篡改的日誌中,審查者無法抑制或修改」。
OpenAI 與 Anthropic 之間的企業競爭目前相當緊張,雙方都在尋找任何能取得優勢的機會。一份近期報告顯示,OpenAI 第二季的成長速度慢於 Anthropic。Anthropic 的年化收入運行率據報現已達到 650 億美元。Anthropic 的投資者表示,它可能以 2 兆美元的估值 IPO,而 OpenAI 也正在準備其 IPO。
當您透過我們文章中的連結購買時,我們可能會賺取少量佣金。這不會影響我們的編輯獨立性。
Lucas 是 TechCrunch 的資深作家,負責報導人工智慧、消費性科技和新創公司。他之前曾在 Gizmodo 報導 AI 和網路安全。
您可以透過 [email protected] 聯繫 Lucas。
![]()
TL;DR: VIDRAFT 在「The First Gemma Challenge」排行榜上以單一 NVIDIA A10G GPU 在
google/gemma-4-E4B-it模型上取得經驗證的 510.58 tokens-per-second (TPS) 成績,同時擊敗了一個原始速度更快但未通過品質門檻的競爭對手。本文剖析使其成功的公開設定選擇,以及工程師們能在自己的推論調校工作中借鏡的技巧。
「The First Gemma Challenge」是一場受嚴格限制的推論速度競賽,有兩項硬性規則:固定使用單一 GPU(NVIDIA A10G)與固定模型(google/gemma-4-E4B-it)。參賽者無法更換更強的硬體或更輕量的模型,唯一能操作的槓桿就是軟體層級的最佳化。
評分指標為TPS(Tokens Per Second,每秒生成 token 數),但同時設有Perplexity(PPL)預算——PPL 超過約 2.42 的提交將被取消資格,無論速度多快。主辦單位還針對參賽者從未見過的保留提示集進行盲測重新評估,這意味著任何過度擬合自報基準的設定都會被抓出來。
VIDRAFT 的優勝提交——設定名稱為 vidraft-fw188-ctk49-n64-patchbridge-v1——的成績如下:
另一個競爭提交雖然錄得 535.91 TPS,但其 PPL 約為 2.44,超過品質門檻。因此這個較低的原始數值被認定為經驗證的 SOTA。
優勝設定的公開 manifest.json 揭示了三個概念性的最佳化支柱:
SLIDING_WINDOW=188)KV-cache 記憶體頻寬是自迴歸生成過程中的主要瓶頸。將注意力視窗限制在最近的 token 上能減輕這項壓力並提升吞吐量——但視窗縮得太小就會喪失上下文,導致 PPL 急遽上升。188 這個數值顯然不是整數,這強烈暗示它是透過實證調整而非直接採用預設值。團隊透過 HF_OVERRIDES 覆寫了模型的 text_config.sliding_window,並啟用 Flash Attention 滑動功能(FA_SLIDING=1)來配合。
CENTROID_TOP_K=49)此參數更接近核心層級,會同時影響吞吐量與 PPL。根據原始碼分析,團隊依序測試了 44、48、49 等數值——目標是找出在 PPL 仍在預算內的前提下所能使用的最大值。越大並不一定越好,這是在品質限制下的 Pareto 搜尋。
該設定使用了:WARMUP_BRIDGE=1、WARMUP_NUM_PROMPTS=64、WARMUP_MAX_TOKENS=1、WARMUP_SEED=42。這會在計時基準測試開始前先執行 64 個單一 token 的虛擬提示,讓 CUDA graph capture 與 JIT 編譯的成本在計時開始前就被吸收。原始文章指出這個暖機步驟大約貢獻了 15 TPS——在一個以數十 TPS 決勝負的競賽中,這是相當可觀的差距。
同樣重要的是:PRECACHE_BENCH=0 被明確設定,停用了會讓自報 TPS 虛增的旗標。團隊選擇測量盲測評估器實際會看到的真實表現。
SPECULATIVE_CONFIG 啟用,設定 num_speculative_tokens=7 與 method=mtp——這是一種先草稿再驗證的方法,能增加每次正向傳遞所生成的 token 數MAX_MODEL_LEN=4096GPU_MEMORY_UTILIZATION=0.90MAX_NUM_BATCHED_TOKENS=512MAX_NUM_SEQS=1| 提交 | TPS | PPL | 盲測 |
|---|---|---|---|
VIDRAFT(vidraft-fw188-ctk49-n64-patchbridge-v1) |
510.58 | 2.3930 | ✅ 通過 |
| 競爭提交 | 535.91 | ~2.44 | ❌ 未通過(PPL > 2.42) |
最重要的啟示:原始吞吐量排名與驗證後排名出現分歧,因為品質門檻是根據保留提示分佈來執行的,而非參賽者自己的測試集。
競賽中所使用的模型已由 Google 在 Hugging Face 公開提供:
huggingface-cli download google/gemma-4-E4B-it
Enter fullscreen mode
Exit fullscreen mode
特定的 VIDRAFT 設定(vidraft-fw188-ctk49-n64-patchbridge-v1)以及任何 VIDRAFT 專屬工具在本文撰寫時尚未確認已公開釋出。請查看 VIDRAFT 的 Hugging Face 組織 與他們的 GitHub 以取得最新資訊。如果開放取得管道,會優先在那裡公布。
Q:在「速度」競賽中,為什麼 PPL 門檻比原始 TPS 更重要?
A:因為沒有品質底線的 TPS 很容易被操縱——你可以讓模型輸出垃圾內容來快速生成。PPL 上限加上盲測重新評估,共同確保速度數字反映的是真實、可部署的推論品質。
Q:我能將這些技術應用在其他模型或 GPU 上嗎?
A:概念——品質門控的參數搜尋、暖機分離、滑動視窗調校、推測解碼——都是通用的推論工程實務。特定數值(SLIDING_WINDOW=188、CENTROID_TOP_K=49 等)是針對單一 A10G 上的 google/gemma-4-E4B-it 所調校的,應該視為其他硬體或模型設定的起點,而非直接複製貼上的目標。
Q:推測解碼(method=mtp)在這裡的作用是什麼?
A:一個更小、更快的「草稿」模型會先預測主模型接下來的幾個 token。主模型再在單一次正向傳遞中驗證這些預測。如果預測被接受,你就能在每個步驟中實際生成多個 token——在不改變模型權重或降低輸出品質的情況下提升測得的 TPS。
原文由 note(日本)於 2026-08-15 報導 — 原始文章。
![]()
為任何程式碼儲存庫產生一份精簡的操作指南,格式為 AGENTS.md:包含子系統、測試、慣例與歷史陷阱。82 項測試。基於論文 Probe-and-Refine Tuning of Repository Guidance for Coding Agents (2026)。
程式碼代理需要儲存庫的操作知識,而這些知識並未存在於程式碼中:
人類會維護 AGENTS.md 檔案來提供這些上下文,但手動建立非常耗時且容易過時。
RepoMapper 以三個步驟自動化此流程:
git clone https://github.com/amurlaniakea/repomapper.git
cd repomapper
python3 -m venv venv && source venv/bin/activate && pip install -e .
# 為某個儲存庫產生 AGENTS.md
python3 -m repomapper /path/to/repo
Enter fullscreen mode
Exit fullscreen mode
你的代理每次遇到新儲存庫時,要花多少時間學習那些早已被寫好的知識?
https://dev.to/magopredator/repomapper-v010-guia-operativa-agentsmd-para-cualquier-repositorio-d09
![]()
Cipr(Cosmic Index of Public Resources)是一個去中心化、分散式且具抗審查能力的網路索引,由網域擁有者自行掌控自己的條目。
沒有爬蟲決定你是否值得被索引。沒有策展人審核你的提交。沒有中央權威可以將你除名。如果你擁有一個網域,你只需發布一個小型 daemon、加入一筆 DNS TXT 記錄,你的網站就會在幾分鐘內出現在全球索引中。更新條目或永久離開也非常快速且容易。
Cipr 並非傳統意義上的搜尋引擎。它是一個共享的點對點目錄,每位參與者都擁有一份完整副本,並透過病毒式傳播保持同步。搜尋結果依據標準化、可公開稽核的因素(基於擁有者宣告之元資料的 BM25)進行排名,而非不透明的演算法或廣告收益。
審查 Cipr 條目需要 DNS 等級的介入——這與讓一個網域下架所需的基礎設施層級動作相同。沒有中央伺服器可以關閉,沒有 API 金鑰可以撤銷,也沒有服務條款可以違反。
它最適合小型網路:個人部落格、家庭實驗室服務、獨立專案、社群資源——這些主流搜尋引擎越來越常埋沒或完全忽略的網站類型。
Ciprnode zero 是 Cipr 通訊協定的第一個也是參考實作。它完全基於 Deno 建構,沒有任何 runtime npm 依賴。本地索引使用 SQLite 資料庫,搭配外部內容的 FTS5 虛擬表格與 BM25 排名。API 採用嚴格的語義 RESTful 實作,透過 HAL+JSON 完全符合 HATEOAS 規範,並實際使用 QUERY HTTP 方法(draft-ietf-httpbis-safe-method-with-body)。
內建的網頁介面(ciprface)提供搜尋功能,可依語言、地理鄰近度、冒犯程度與時間戳記進行篩選。驗證採用 DNS TXT 三重驗證(透過自訂 TLS 連線使用 3 個隨機 DoH 解析器)與病毒式 P2P 傳播。沒有中央伺服器、沒有區塊鏈、沒有代幣。採用 MIT 授權、可自行架設,能編譯成適用於 Linux、Windows 和 macOS 的獨立執行檔。
更多資訊:https://cipr.info
目前有三個節點正在運行:
https://dev.to/barriteau/cipr-and-ciprnode-zero-1b89
https://www.worldprogramming.org/posts/cipr-and-ciprnode-zero-xozdup