dsh-engramory-recall 0.12.2 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +36 -2
- package/package.json +1 -1
- package/providers/openai-embeddings.mjs +53 -12
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
3
3
|
本项目的所有重要变更都记在这里。格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
4
4
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
5
5
|
|
|
6
|
+
## [0.12.4] — 2026-10-15
|
|
7
|
+
|
|
8
|
+
### 修复
|
|
9
|
+
|
|
10
|
+
- **`npm run bench:embed -- --real` 之前根本跑不起来**:生成的一次性提供方模块用了默认导入,
|
|
11
|
+
而参考提供方只有命名导出 ⇒ 每条查询都记成 `load-error`,质量列直接掉到 0。改成 namespace 导入。
|
|
12
|
+
(用本机假 embeddings 服务把 `--real` 那条完整路径跑通了才发现 —— 也就是说,**这条文档里写着的
|
|
13
|
+
一条命令,之前只有真去接端点的人才会撞上**。)
|
|
14
|
+
|
|
15
|
+
### 新增
|
|
16
|
+
|
|
17
|
+
- `bench:embed` 支持 `--timeout <ms>`(默认 20000):给端点的预算,**超时降级为词面结果而不是报错**。
|
|
18
|
+
实测:服务端每次慢 1.5 秒 + 预算 800 ms ⇒ 每次检索 p50 = 805 ms(预算被当真),
|
|
19
|
+
全列 `timeout×20`、质量退回词面基线。
|
|
20
|
+
|
|
21
|
+
## [0.12.3] — 2026-10-15
|
|
22
|
+
|
|
23
|
+
### 新增
|
|
24
|
+
|
|
25
|
+
- **参考提供方会自动重试限流与临时故障**:`429 / 408 / 409 / 425 / 500 / 502 / 503 / 504` 重试 2 次,
|
|
26
|
+
尊重服务端 `Retry-After`(秒数或 HTTP 日期,上限 5 秒),等待可被接缝的超时预算打断;
|
|
27
|
+
`400 / 401 / 403` 不重试。重试到上限的错误写成 `embeddings HTTP 503(已重试 2 次):<服务端原话>`。
|
|
28
|
+
新增 3 条测试(重试后成功 / 重试到上限 / 4xx 不重试),都打本地假服务。
|
|
29
|
+
|
|
30
|
+
### 文档
|
|
31
|
+
|
|
32
|
+
- README 补「哪些服务能用」:常见可用名单(OpenAI / 智谱 / 阿里百炼 / 硅基流动 / Mistral / Together /
|
|
33
|
+
本机 Ollama / 本机 LM Studio 的 `BASE_URL` + `MODEL`),以及**不能用的两家及其原因** ——
|
|
34
|
+
Anthropic 没有 embeddings 接口;**DeepSeek 官方 API 只有对话模型**(已对照官方模型表),
|
|
35
|
+
所以现有的 DeepSeek key 走不了这条路,零成本试法是把 Ollama / LM Studio 起在本机。
|
|
36
|
+
|
|
6
37
|
## [0.12.2] — 2026-10-15
|
|
7
38
|
|
|
8
39
|
### 新增
|
package/README.md
CHANGED
|
@@ -718,8 +718,38 @@ export ENGRAMORY_EMBED_MODEL=text-embedding-3-small
|
|
|
718
718
|
`https://dashscope.aliyuncs.com/compatible-mode/v1` + `text-embedding-v4`、智谱
|
|
719
719
|
`https://open.bigmodel.cn/api/paas/v4` + `embedding-3`、硅基流动 `https://api.siliconflow.cn/v1` + `BAAI/bge-m3`。
|
|
720
720
|
|
|
721
|
-
|
|
722
|
-
|
|
721
|
+
#### 哪些服务能用(以及 DeepSeek 为什么不行)
|
|
722
|
+
|
|
723
|
+
要求只有一条:提供 `POST {base}/embeddings`。常见可用名单(`BASE_URL` + `MODEL` 照填):
|
|
724
|
+
|
|
725
|
+
| 供应商 | BASE_URL | 模型示例 |
|
|
726
|
+
|---|---|---|
|
|
727
|
+
| OpenAI | `https://api.openai.com/v1` | `text-embedding-3-small` / `-large` |
|
|
728
|
+
| 智谱 | `https://open.bigmodel.cn/api/paas/v4` | `embedding-3` |
|
|
729
|
+
| 阿里百炼 | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `text-embedding-v4` |
|
|
730
|
+
| 硅基流动 | `https://api.siliconflow.cn/v1` | `BAAI/bge-m3` |
|
|
731
|
+
| Mistral | `https://api.mistral.ai/v1` | `mistral-embed` |
|
|
732
|
+
| Together | `https://api.together.xyz/v1` | `BAAI/bge-large-en-v1.5` |
|
|
733
|
+
| 本机 Ollama | `http://localhost:11434/v1` | `nomic-embed-text` / `bge-m3` |
|
|
734
|
+
| 本机 LM Studio | `http://localhost:1234/v1` | 你加载的那个 embedding 模型 |
|
|
735
|
+
|
|
736
|
+
**不能用的两家**,原因不同:
|
|
737
|
+
|
|
738
|
+
- **Anthropic(Claude)没有 embeddings 接口** —— 「Claude 协议」跟向量无关,填了也只会 404。
|
|
739
|
+
官方推荐的做法是另找一家 embeddings 服务。
|
|
740
|
+
- **DeepSeek 官方 API 只有对话模型**([官方模型表](https://api-docs.deepseek.com/quick_start/pricing)
|
|
741
|
+
里只有 `deepseek-flash` / `deepseek-v4-pro`,没有 `/embeddings`)⇒ **你现在这把 DeepSeek key 用不上这条路**。
|
|
742
|
+
想零成本试就把 Ollama / LM Studio 在本机起一个(完全不出网),想真量就挑上面任一家。
|
|
743
|
+
|
|
744
|
+
一探便知:`node providers/openai-embeddings.mjs --check` —— **404 = 这家没这个接口;401 = 密钥不对**;
|
|
745
|
+
成功了会报维度与一次往返耗时。
|
|
746
|
+
|
|
747
|
+
**限流与临时故障会自动扛**:`429 / 500 / 502 / 503 / 504`(以及 408/409/425)重试 2 次,尊重服务端
|
|
748
|
+
`Retry-After`(上限 5 秒,而且等待可被超时预算打断);`400 / 401 / 403` 这类不重试 —— 重试改不了
|
|
749
|
+
鉴权与请求体的问题。重试到上限的错误会写成 `embeddings HTTP 503(已重试 2 次):<服务端原话>`。
|
|
750
|
+
|
|
751
|
+
**一句实话**:这条路径的代码、测试、失败降级、重试都有,但**还没有在真 key 上量过召回**(要花 token,
|
|
752
|
+
得你先点头)。接缝本身的上限是量过的(上表 86.6%),提供方的质量只能真调一次才知道。
|
|
723
753
|
|
|
724
754
|
#### 真端点怎么量:`npm run bench:embed`
|
|
725
755
|
|
|
@@ -728,6 +758,10 @@ export ENGRAMORY_EMBED_MODEL=text-embedding-3-small
|
|
|
728
758
|
真库那半边要显式加 `--store ~/memory/engramory` —— 加了就意味着**你的笔记片段会发给那个端点**,
|
|
729
759
|
所以默认不开。
|
|
730
760
|
|
|
761
|
+
`--timeout <ms>`(默认 20000)是给这条端点的预算:**超时即降级为词面结果**,不是报错。
|
|
762
|
+
拿本机慢服务自证过:服务端故意每次慢 1.5 秒、预算给 800 ms ⇒ 每次检索 p50 卡在 805 ms
|
|
763
|
+
(预算被当真了),结果全列 `超时/失败 = 20(timeout×20)`,质量列退回词面基线 —— 端点在抖也不会拖死检索。
|
|
764
|
+
|
|
731
765
|
假向量(哈希向量 = 最坏提供方)跑出来的两组数字,正好把上面那张融合表变成实测:
|
|
732
766
|
|
|
733
767
|
| 卷子 | 词面 hit@1 / hit@4 | 假向量 + `trust` | 假向量 + `rrf` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engramory-recall",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Local BM25 auto-recall, fail-closed write guard, static doctor and a memory panel for the Engramory markdown store — no network, no model, no daemon.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -60,6 +60,11 @@ const DEFAULTS = {
|
|
|
60
60
|
/** 默认配置文件位置(可用 ENGRAMORY_EMBED_CONFIG 换)。 */
|
|
61
61
|
export const DEFAULT_CONFIG_PATH = '~/.config/engramory/embed.json';
|
|
62
62
|
|
|
63
|
+
/** 可重试的状态码:限流与网关/服务端临时故障。真端点第一次限流就失败的话,接缝等于不能用。 */
|
|
64
|
+
const RETRY_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
65
|
+
const RETRY_LIMIT = 2; // 首次 + 2 次重试
|
|
66
|
+
const RETRY_BASE_MS = 250;
|
|
67
|
+
|
|
63
68
|
function expandHome(value) {
|
|
64
69
|
const text = String(value ?? '').trim();
|
|
65
70
|
if (text === '~') return homedir();
|
|
@@ -147,19 +152,26 @@ function textOf(doc, cfg) {
|
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
async function embedBatch(cfg, texts, signal) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
let response;
|
|
156
|
+
let body = '';
|
|
157
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
158
|
+
response = await fetch(endpoint(cfg.baseUrl), {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
signal,
|
|
161
|
+
headers: {
|
|
162
|
+
'content-type': 'application/json',
|
|
163
|
+
authorization: `Bearer ${cfg.apiKey}`,
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({ model: cfg.model, input: texts }),
|
|
166
|
+
});
|
|
167
|
+
body = await response.text();
|
|
168
|
+
if (response.ok || !RETRY_STATUS.has(response.status) || attempt >= RETRY_LIMIT) break;
|
|
169
|
+
await sleep(retryDelayMs(response, attempt), signal);
|
|
170
|
+
}
|
|
160
171
|
if (!response.ok) {
|
|
161
|
-
// 状态码 +
|
|
162
|
-
|
|
172
|
+
// 状态码 + 服务端原话(截断):密钥绝不进消息。重试过的话说明这是重试后的终态。
|
|
173
|
+
const tried = RETRY_STATUS.has(response.status) ? `(已重试 ${RETRY_LIMIT} 次)` : '';
|
|
174
|
+
throw new Error(`embeddings HTTP ${response.status}${tried}:${body.replace(/\s+/g, ' ').slice(0, 200)}`);
|
|
163
175
|
}
|
|
164
176
|
let parsed;
|
|
165
177
|
try {
|
|
@@ -202,6 +214,35 @@ export async function embedTexts(cfg, texts, signal) {
|
|
|
202
214
|
return out;
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
/** 等服务端让等的时间:优先 `Retry-After`(秒数或 HTTP 日期),否则指数退避。上限 5 秒。 */
|
|
218
|
+
function retryDelayMs(response, attempt) {
|
|
219
|
+
const header = response?.headers?.get?.('retry-after');
|
|
220
|
+
if (header) {
|
|
221
|
+
const seconds = Number(header);
|
|
222
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(5000, seconds * 1000);
|
|
223
|
+
const when = Date.parse(header);
|
|
224
|
+
if (Number.isFinite(when)) return Math.min(5000, Math.max(0, when - Date.now()));
|
|
225
|
+
}
|
|
226
|
+
return Math.min(5000, RETRY_BASE_MS * 2 ** attempt);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 可被 AbortSignal 打断的等待:接缝有超时预算,重试不许把预算吃光。 */
|
|
230
|
+
function sleep(ms, signal) {
|
|
231
|
+
if (!ms) return Promise.resolve();
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const timer = setTimeout(() => {
|
|
234
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
235
|
+
resolve();
|
|
236
|
+
}, ms);
|
|
237
|
+
const onAbort = () => {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
reject(signal?.reason ?? new Error('aborted'));
|
|
240
|
+
};
|
|
241
|
+
if (signal?.aborted) onAbort();
|
|
242
|
+
else signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
205
246
|
function cosine(a, b) {
|
|
206
247
|
let dot = 0;
|
|
207
248
|
let na = 0;
|