docs(memory): 请求上界按条数维+字符维两维算准(review-m61 P2-1)
embedding.Embed 同时受条数界(8)与字符界(480 字)约束 ⇒ 一次 embedOnce(B 条) 会发 C 个请求(C 由正文长度决定,1..B),因此: 正常批 C 含一条坏正文 ≤ C + B(短正文 9;均长 75 汉字的现实批 10;病理上限 2B=16) 服务整体挂死 首个 chunk 即失败 + 逐条 B ⇒ 1 + B = 9(与字符维无关) 修正 embed_isolation.go 文件头的上界表、approxRequests 的「两者一致」表述、 embedReuseStats.Sent 的注释,以及 embed_resilience_test.go 的上界注释与日志文案。 零行为改动:判据、实现、请求数都未动,测试全绿。
This commit is contained in:
1 parent
8f7017fc0b
commit
058fa141bf
3 files changed
+40
-17
No files matched your search
@@ -51,7 +51,8 @@ type embedReuseStats struct {
|
||||
Requested int
|
||||
// Reused 是索引里已有向量、一次远端都没发的条目数。
|
||||
Reused int
|
||||
// Sent 是真正送进 embedder 的**条数**(≈ 请求数 × 批上限)。
|
||||
// Sent 是真正送进 embedder 的**条数**;HTTP 请求数 ≥ ⌈Sent/批上限⌉ ——
|
||||
// embedding 包还会按字符界(480 字)再切,长正文时请求数更多(见 approxRequests)。
|
||||
Sent int
|
||||
// Dropped 是逐条隔离重试后仍失败、本轮没有向量的条目数(缺口)。
|
||||
Dropped int
|
||||
@@ -62,8 +63,10 @@ type embedReuseStats struct {
|
||||
}
|
||||
|
||||
// approxRequests 估算本轮发出的 HTTP 请求数:条数 ÷ 批上限(向上取整)。
|
||||
// 它是**观测用的估算**(embedding 包内部还按同一个批上限切片,因此两者一致),
|
||||
// 不是任何判据。
|
||||
//
|
||||
// 🔴 它是**按条数界算的下限估算**,不是精确值:embedding.Embed 还按字符界
|
||||
// (DefaultBatchChars=480)切片,长正文批会被多切一刀 ⇒ 真实请求数 ≥ 本估算
|
||||
// (短正文、≤480 字/批时两者相等)。它只进日志做观测,不是任何判据。
|
||||
func (s embedReuseStats) approxRequests(batch int) int {
|
||||
if s.Sent <= 0 {
|
||||
return 0
|
||||
|
||||
@@ -19,11 +19,11 @@ import (
|
||||
//
|
||||
// ## 为什么是「逐条」而不是递归二分
|
||||
//
|
||||
// 二分定位一条坏文本要 1+2+2+2 = 7 个请求(比逐条的 9 少 2 个),但它需要一套
|
||||
// 失败预算的簿记(哪一层还允许拆、拆到多深收手),而那套簿记本身就是「韧性变成
|
||||
// 放大」的风险点:预算给紧了会把好正文一起丢掉,给松了会在服务挂死时把请求数
|
||||
// 放大成 2^深度。逐条隔离的代价**恒定且可算**:一批最多 1+B 个请求(B=8 ⇒ 9),
|
||||
// 定位精度与二分相同(精确到单条),没有任何深度参数。
|
||||
// 二分定位一条坏文本在短正文下要 1+2+2+2 = 7 个请求(比逐条的 9 少 2 个),但它
|
||||
// 需要一套失败预算的簿记(哪一层还允许拆、拆到多深收手),而那套簿记本身就是
|
||||
// 「韧性变成放大」的风险点:预算给紧了会把好正文一起丢掉,给松了会在服务挂死时
|
||||
// 把请求数放大成 2^深度。逐条隔离的代价**恒定且可算**:`embedOnce` 调用 ≤ 1+B
|
||||
// (B = 批上限),定位精度与二分相同(精确到单条),没有任何深度参数。
|
||||
//
|
||||
// ## 三条必须同时成立的约束
|
||||
//
|
||||
@@ -32,11 +32,23 @@ import (
|
||||
// 只损失坏的那几条 —— 绝不把「算不出来」说成「没有相似」;
|
||||
// 3. **预算**:父 ctx 已到期时不再逐条重试(预算花完了,重试只会把 worker 拖住)。
|
||||
//
|
||||
// 请求数的上界(B = 批上限,生产 = 8):
|
||||
// ## HTTP 请求数的上界(必须按**两维**切片算,B = 批上限 = 8)
|
||||
//
|
||||
// 正常批 1
|
||||
// 含一条坏正文 1 + B = 9(定位到它自己)
|
||||
// 服务整体挂死 1 + B = 9(一次之后 embedMissing 立刻停手,不再发后续批)
|
||||
// `embedding.Embed` 不是「一批一个请求」:它按 budget.go 的**条数界**
|
||||
// (DefaultBatchLimit=8)**与字符界**(DefaultBatchChars=480)同时切片
|
||||
// (chunkTexts),因此一次 `embedOnce(B 条)` 会发出 C 个请求,C 由正文长度决定:
|
||||
// 全批 ≤480 字 ⇒ C=1;均长 75 汉字(budget.go 的实测口径)时 8×75=600 > 480 ⇒ C=2;
|
||||
// 每条自己就超过 480 字 ⇒ 每条独占一片 ⇒ C=B(病理上限)。
|
||||
// 单条重试**永不切分**(它自己就是一片)⇒ 每条恰好 1 个请求。于是:
|
||||
//
|
||||
// 正常批 C(短正文 C=1;长正文 2 起)
|
||||
// 含一条坏正文 首次失败发生在第 1~C 个 chunk 之一,随后逐条 B 次
|
||||
// ⇒ ≤ C + B(短正文 9;现实长正文 10;病理上限 2B=16)
|
||||
// 服务整体挂死 首个 chunk 即失败 + 逐条 B 次 ⇒ **1 + B = 9**(与字符维无关)
|
||||
//
|
||||
// 口径与 budget.go 同源:那里的「建模上界 ≠ 延迟保证」是同一类提醒 —— 这里的
|
||||
// 「≤1+B」是**embedOnce 调用次数**的上界,HTTP 请求数必须再按字符维的 chunk 数算
|
||||
// (正常批 C、含坏正文 ≤C+B);只有「服务整体挂死 = 1+B」这一条与字符维无关。
|
||||
|
||||
// ErrEmbedUnavailable 在「一条向量都没拿到」时返回:调用方据此走既有的词项余弦
|
||||
// 降级(记忆一条不丢、判定规则一字不改),而不是把「算不出来」当成「没有相似」。
|
||||
|
||||
@@ -8,10 +8,15 @@ import (
|
||||
|
||||
// 本文件是「单分片韧性」的断言(M61,2026-10-30;design/23 §15.3 已登记口径):
|
||||
// 一条慢/坏分片**不丢整条**——同一批里其它条目的向量照常返回,只损失那一条;
|
||||
// 而整体性故障(服务挂死)**不放大**:最多 3 个请求就收手。
|
||||
// 而整体性故障(服务挂死)**不放大**:整批在第一个 chunk 即失败 + 逐条 B 次
|
||||
// ⇒ 1+B 个请求就收手(B = 批上限 8)。
|
||||
//
|
||||
// ⚠️ 请求数口径是**两维**的(见 embed_isolation.go 文件头):本文件的正文都很短
|
||||
// (8 条合计远小于 DefaultBatchChars=480)⇒ chunkTexts 只按条数界切、C=1,因此
|
||||
// 「含一条坏正文 = 1+B」在这些用例里是精确值;长正文批的真实上界是 C+B。
|
||||
|
||||
// TestEmbedIsolation_OneBadTextKeepsTheRest 是韧性的主断言:批里有一条坏正文时,
|
||||
// 其余 7 条照常拿到向量,坏的那条记为缺口,请求数被二分定位的成本封顶。
|
||||
// 其余 7 条照常拿到向量,坏的那条记为缺口,请求数被逐条隔离的成本封顶。
|
||||
//
|
||||
// 删掉 embedIsolated(改回「一次 Embed 定生死」)这一条会打红:整批返回错误、
|
||||
// 7 条好正文的向量一起丢掉。
|
||||
@@ -58,16 +63,19 @@ func TestEmbedIsolation_OneBadTextKeepsTheRest(t *testing.T) {
|
||||
if kept != len(texts)-1 {
|
||||
t.Fatalf("保住的条数 = %d, want %d", kept, len(texts)-1)
|
||||
}
|
||||
// 逐条隔离的成本上界:整批 1 次 + 逐条重试 B 次(B = 批上限 8)⇒ 9。
|
||||
// 逐条隔离的成本上界:整批 1 个 chunk(本例正文短 ⇒ C=1)+ 逐条重试 B 次
|
||||
// ⇒ 1+B = 9。长正文批的真实上界是 C+B(C 由 480 字字符界决定,见文件头)。
|
||||
if limit := 1 + tidyEmbedBatch; reqs > limit {
|
||||
t.Fatalf("定位一条坏分片花了 %d 个请求,超过逐条上界 %d(韧性变成了放大)", reqs, limit)
|
||||
}
|
||||
t.Logf("一条坏分片:保住 %d/%d 条,代价 %d 个请求(上界 1+B = %d)", kept, len(texts), reqs, 1+tidyEmbedBatch)
|
||||
t.Logf("一条坏分片:保住 %d/%d 条,代价 %d 个请求(短正文上界 1+B = %d)",
|
||||
kept, len(texts), reqs, 1+tidyEmbedBatch)
|
||||
}
|
||||
|
||||
// TestEmbedIsolation_OutageStopsFast 是**反放大**的断言:服务整体挂死时不许把
|
||||
// 「用请求换韧性」变成请求风暴 —— 逐条重试一条都没拿到即判定整体性故障,当场
|
||||
// 返回错误,且请求数被 1+B 封顶。
|
||||
// 返回错误,且请求数被 1+B 封顶(整批在第一个 chunk 就失败 ⇒ 1 个请求 + 逐条 B;
|
||||
// 这一条与字符维的 chunk 数无关,本例正文短、C 本来就是 1)。
|
||||
func TestEmbedIsolation_OutageStopsFast(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in new issue
Block a user