@tencentcloud/sdk-mcp 1.7.6 → 1.7.8
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 +18 -0
- package/README.md +8 -5
- package/index.js +23 -1
- package/knowledge/doc-url-resolver.js +16 -9
- package/knowledge/embedding/build-vectors.js +92 -0
- package/knowledge/embedding/embed-model.js +41 -0
- package/knowledge/embedding/vector-augment.js +126 -0
- package/knowledge/embedding/vector-index.js +70 -0
- package/knowledge/search/exact-lookup.js +145 -16
- package/knowledge/search/filter-pool.js +68 -32
- package/knowledge/search/requires-framework.js +1 -1
- package/knowledge/search/search-core.js +18 -12
- package/knowledge/search/search-knowledge.js +24 -34
- package/knowledge/search/search-pass.js +373 -168
- package/knowledge/search/url-row-scorer.js +24 -6
- package/package.json +3 -1
- package/resource/index/manifest.json +1 -1
- package/resource/index/vectors/index.json +4 -0
- package/resource/integration/chat/preset/react/integration.md +9 -5
- package/resource/integration/chat/preset/uni-app/integration.md +4 -0
- package/resource/integration/chat/preset/vue/integration.md +10 -6
- package/resource/integration/roomkit/preset/vue/integration.md +154 -208
- package/resource/knowledge/callkit/preset/miniprogram/integration.md +2 -1
- package/resource/knowledge/chat/preset/ios-uikit/integration.md +0 -16
- package/resource/knowledge/chat/preset/react/h5/integration.md +16 -5
- package/resource/knowledge/chat/preset/vue/features/conversation-list.md +6 -3
- package/resource/knowledge/chat/preset/vue/h5/integration.md +7 -4
- package/resource/knowledge/push/config/android/honor.md +16 -1
- package/resource/knowledge/push/config/android/huawei.md +20 -4
- package/resource/knowledge/push/config/android/meizu.md +19 -5
- package/resource/knowledge/push/config/android/vivo.md +20 -1
- package/resource/knowledge/push/config/harmonyos.md +21 -5
- package/resource/knowledge/push/integration/flutter.md +1 -1
- package/resource/knowledge/roomkit/preset/uniapp-app/integration.md +18 -14
- package/resource/knowledge/rtcengine/web/plugin/23-advanced-support-detection.md +30 -0
- package/resource/knowledge/rtcengine/web/plugin/31-advanced-plugins-overview.md +1 -1
- package/resource/knowledge/rtcengine/web/plugin/35-advanced-ai-denoiser.md +1 -1
- package/resource/knowledge/rtcengine/web/plugin/36-advanced-virtual-background.md +4 -0
- package/resource/knowledge/trtc/android/TRTCCloud.md +94 -85
- package/resource/knowledge/trtc/android/TRTCCloudDef.md +48 -46
- package/resource/knowledge/trtc/android/TRTCCloudListener.md +48 -48
- package/resource/knowledge/trtc/android/TXAudioEffectManager.md +18 -18
- package/resource/knowledge/trtc/android/TXDeviceManager.md +4 -4
- package/resource/knowledge/trtc/android/TXLiteAVCode.md +1 -0
- package/resource/knowledge/trtc/c++/ITRTCCloud.md +90 -90
- package/resource/knowledge/trtc/c++/ITXAudioEffectManager.md +18 -18
- package/resource/knowledge/trtc/c++/ITXDeviceManager.md +9 -9
- package/resource/knowledge/trtc/c++/TRTCCloudCallback.md +47 -47
- package/resource/knowledge/trtc/c++/TRTCTypeDef.md +47 -45
- package/resource/knowledge/trtc/c++/TXLiteAVCode.md +1 -0
- package/resource/knowledge/trtc/ios/TRTCCloud.md +93 -93
- package/resource/knowledge/trtc/ios/TRTCCloudDelegate.md +50 -50
- package/resource/knowledge/trtc/ios/TXAudioEffectManager.md +8 -8
- package/resource/knowledge/trtc/ios/TXDeviceManager.md +9 -9
- package/resource/knowledge/trtc/ios/TXLiteAVCode.md +1 -0
- package/resource/knowledge/trtc/ios/TXLiveSDKTypeDef.md +48 -46
- package/server-config.js +20 -6
- package/server.js +1 -1
- package/tools/common/present_framework_choice.js +1 -1
- package/tools/common/search_trtc_knowledge.js +20 -1
- package/utils/normalize-error.js +13 -0
- package/utils/preflight-route.js +8 -0
- package/utils/report-cls-client.js +2 -2
- package/utils/report-es-client.js +1 -1
|
@@ -1,103 +1,40 @@
|
|
|
1
|
-
import { attachPoolContent } from '../content-loader.js';
|
|
1
|
+
import { attachChunkContent, attachPoolContent } from '../content-loader.js';
|
|
2
2
|
import { buildReferences } from '../doc-url-resolver.js';
|
|
3
3
|
import { rankSubsetWithIndex, tokenize } from '../bm25.js';
|
|
4
4
|
import { getProductsBM25Index } from '../bm25-cache.js';
|
|
5
|
-
import { filterPool } from './filter-pool.js';
|
|
5
|
+
import { filterPool, filterPoolHard } from './filter-pool.js';
|
|
6
6
|
import { runExactLookup } from './exact-lookup.js';
|
|
7
7
|
import { computeGenericAnchorBoosts } from './generic-rerank.js';
|
|
8
|
+
import { embedQuery, withQueryInstruction } from '../embedding/embed-model.js';
|
|
9
|
+
import { queryVectors } from '../embedding/vector-index.js';
|
|
8
10
|
import { applyRankMultipliers, computeApiNameCoverage, computeMissingApiTokens, isUrlMappingSource, postProcess, } from './search-core.js';
|
|
9
11
|
function isBestPracticeSource(source) {
|
|
10
12
|
return /\/best-practice\//i.test(source);
|
|
11
13
|
}
|
|
12
|
-
function normalizeTopicText(text) {
|
|
13
|
-
return text
|
|
14
|
-
.toLowerCase()
|
|
15
|
-
.replace(/[(())【】\[\]{}《》"""'`]/g, '')
|
|
16
|
-
.replace(/[\s/\\,,。::;;|·\-—_]+/g, '');
|
|
17
|
-
}
|
|
18
|
-
const TOPIC_EN_NOISE_WORDS = new Set([
|
|
19
|
-
'a', 'an', 'the', 'and', 'or', 'to', 'of', 'in', 'on', 'for', 'with',
|
|
20
|
-
'is', 'are', 'was', 'were', 'be', 'how', 'what', 'why', 'when', 'where',
|
|
21
|
-
'chat', 'message', 'messages', 'api', 'sdk', 'doc', 'docs',
|
|
22
|
-
]);
|
|
23
|
-
function extractTopicTokens(text) {
|
|
24
|
-
const tokens = new Set();
|
|
25
|
-
for (const match of text.matchAll(/\b\d{2,}\b/g)) {
|
|
26
|
-
tokens.add(match[0]);
|
|
27
|
-
}
|
|
28
|
-
for (const match of text.matchAll(/\b[a-z][a-z0-9_]{1,}\b/gi)) {
|
|
29
|
-
const token = match[0].toLowerCase();
|
|
30
|
-
if (TOPIC_EN_NOISE_WORDS.has(token))
|
|
31
|
-
continue;
|
|
32
|
-
tokens.add(token);
|
|
33
|
-
}
|
|
34
|
-
const cjkSegments = text.match(/[\u4e00-\u9fff]+/g) ?? [];
|
|
35
|
-
for (const seg of cjkSegments) {
|
|
36
|
-
if (seg.length >= 2)
|
|
37
|
-
tokens.add(seg);
|
|
38
|
-
for (let i = 0; i < seg.length - 1; i += 1) {
|
|
39
|
-
tokens.add(seg.slice(i, i + 2));
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return [...tokens].filter((token) => token.length >= 2);
|
|
43
|
-
}
|
|
44
|
-
function countTopicTokenHits(tokens, normalizedTarget) {
|
|
45
|
-
if (!tokens.length || !normalizedTarget)
|
|
46
|
-
return 0;
|
|
47
|
-
let hits = 0;
|
|
48
|
-
for (const token of tokens) {
|
|
49
|
-
const normalizedToken = normalizeTopicText(token);
|
|
50
|
-
if (normalizedToken.length < 2)
|
|
51
|
-
continue;
|
|
52
|
-
if (normalizedTarget.includes(normalizedToken))
|
|
53
|
-
hits += 1;
|
|
54
|
-
}
|
|
55
|
-
return hits;
|
|
56
|
-
}
|
|
57
|
-
const TOPIC_GENERIC_GUARD_WORDS = new Set([
|
|
58
|
-
...TOPIC_EN_NOISE_WORDS,
|
|
59
|
-
'web', 'ios', 'android', 'flutter', 'react', 'vue',
|
|
60
|
-
'直播', '消息', '文档', '教程', '指南', '重要', '保证', '丢失', '聊天',
|
|
61
|
-
]);
|
|
62
|
-
/**
|
|
63
|
-
* 提取更强区分度的话题锚点(通用策略,不针对任何单 case):
|
|
64
|
-
* - 英文:长度 >= 4 或包含数字(如 AVChatRoom / trtc123)
|
|
65
|
-
* - 中文:连续 3 字以上片段(避免 2-gram 过宽)
|
|
66
|
-
*/
|
|
67
|
-
function extractStrongTopicAnchors(text) {
|
|
68
|
-
const anchors = new Set();
|
|
69
|
-
for (const match of text.matchAll(/\b[a-z][a-z0-9_]{3,}\b/gi)) {
|
|
70
|
-
const token = match[0].toLowerCase();
|
|
71
|
-
if (TOPIC_GENERIC_GUARD_WORDS.has(token))
|
|
72
|
-
continue;
|
|
73
|
-
anchors.add(token);
|
|
74
|
-
}
|
|
75
|
-
for (const segment of text.match(/[\u4e00-\u9fff]{3,}/g) ?? []) {
|
|
76
|
-
if (TOPIC_GENERIC_GUARD_WORDS.has(segment))
|
|
77
|
-
continue;
|
|
78
|
-
anchors.add(segment);
|
|
79
|
-
}
|
|
80
|
-
return [...anchors];
|
|
81
|
-
}
|
|
82
14
|
function extractPrimaryHeading(content) {
|
|
83
15
|
const heading = content.split('\n').find((line) => /^#\s+/.test(line.trim()));
|
|
84
16
|
return heading ? heading.replace(/^#\s+/, '').trim() : '';
|
|
85
17
|
}
|
|
86
|
-
function buildBestPracticeTopicText(chunk) {
|
|
87
|
-
if (!chunk)
|
|
88
|
-
return '';
|
|
89
|
-
return normalizeTopicText([
|
|
90
|
-
chunk.title,
|
|
91
|
-
...(chunk.key_phrases ?? []),
|
|
92
|
-
extractPrimaryHeading(chunk.content),
|
|
93
|
-
].filter(Boolean).join(' '));
|
|
94
|
-
}
|
|
95
18
|
const DIAGNOSIS_INTENT_RE = /丢消息|消息丢失|message\s*loss|消息可靠性|message\s*reliability|超时|timeout|失败|fail(?:ed|ure)?|排查|诊断|troubleshoot(?:ing)?/i;
|
|
96
19
|
const DIAGNOSIS_DOC_RE = /diagnosis|troubleshoot|排查|诊断|丢失|message\s*loss|reliability|可靠性|timeout|fail(?:ed|ure)?/i;
|
|
97
20
|
const TUTORIAL_DOC_RE = /audio[\s-]*message|voice[\s-]*message|语音消息|录音|recorder|create[a-z]+message|sendmessage|standard\s*(approach|usage)|标准用法|如何发送|how\s*to\s*send/i;
|
|
98
21
|
function isDiagnosisIntent(prompt) {
|
|
99
22
|
return DIAGNOSIS_INTENT_RE.test(prompt);
|
|
100
23
|
}
|
|
24
|
+
function decidePromptLockPolicy(prompt) {
|
|
25
|
+
const integrationIntent = /集成|接入|指南|步骤|教程|快速开始|初始化|安装|跑通|demo|sample|example/i.test(prompt);
|
|
26
|
+
const productIntent = /配置|限制|上限|规则|权限|计费|套餐|价格|资费|多端登录|错误码|命名|长度|开通|购买/i.test(prompt);
|
|
27
|
+
if (integrationIntent && !productIntent) {
|
|
28
|
+
return {
|
|
29
|
+
enableContentPriority: true,
|
|
30
|
+
enableFullDocumentLock: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
enableContentPriority: false,
|
|
35
|
+
enableFullDocumentLock: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
101
38
|
function buildChunkIntentText(chunk) {
|
|
102
39
|
return [
|
|
103
40
|
chunk.title,
|
|
@@ -131,16 +68,21 @@ function applyDiagnosisIntentGuard(ranked, chunksById, prompt) {
|
|
|
131
68
|
adjusted = true;
|
|
132
69
|
return { ...item, score: item.score - penalty };
|
|
133
70
|
});
|
|
134
|
-
return adjusted ? next
|
|
71
|
+
return adjusted ? stableSortRanked(next) : ranked;
|
|
135
72
|
}
|
|
136
73
|
/**
|
|
137
|
-
* A3:
|
|
138
|
-
*
|
|
74
|
+
* A3: best-practice 后处理轻校准(不再做“主通道抢首位”)
|
|
75
|
+
*
|
|
76
|
+
* 目标:
|
|
77
|
+
* - 保留 BP 的可见性优势(在接近相关时可轻微上浮)
|
|
78
|
+
* - 避免强行顶到 Top1 导致“策略叠策略”的排序失真
|
|
79
|
+
*
|
|
80
|
+
* 触发条件(更保守):
|
|
139
81
|
* - 当前 Top1 不是 best-practice
|
|
140
|
-
* -
|
|
141
|
-
* -
|
|
82
|
+
* - Top20 内存在 BP 候选
|
|
83
|
+
* - 候选与 Top1 分差不大(gap <= 20%)且具备基础强度
|
|
142
84
|
*/
|
|
143
|
-
function prioritizeBestPracticeWhenCompetitive(ranked, chunksById,
|
|
85
|
+
function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, _prompt) {
|
|
144
86
|
if (ranked.length < 2)
|
|
145
87
|
return ranked;
|
|
146
88
|
const top = ranked[0];
|
|
@@ -148,14 +90,12 @@ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
|
|
|
148
90
|
if (!topChunk || isBestPracticeSource(topChunk.source) || top.score <= 0) {
|
|
149
91
|
return ranked;
|
|
150
92
|
}
|
|
151
|
-
|
|
93
|
+
const searchWindow = Math.min(ranked.length, 20);
|
|
152
94
|
let bestPracticeIdx = -1;
|
|
153
95
|
let bestPracticeScore = -Infinity;
|
|
154
|
-
for (let i = 1; i <
|
|
96
|
+
for (let i = 1; i < searchWindow; i += 1) {
|
|
155
97
|
const chunk = chunksById.get(ranked[i].id);
|
|
156
|
-
if (!chunk)
|
|
157
|
-
continue;
|
|
158
|
-
if (!isBestPracticeSource(chunk.source))
|
|
98
|
+
if (!chunk || !isBestPracticeSource(chunk.source))
|
|
159
99
|
continue;
|
|
160
100
|
if (ranked[i].score > bestPracticeScore) {
|
|
161
101
|
bestPracticeScore = ranked[i].score;
|
|
@@ -166,71 +106,25 @@ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
|
|
|
166
106
|
return ranked;
|
|
167
107
|
const candidate = ranked[bestPracticeIdx];
|
|
168
108
|
const gapRatio = (top.score - candidate.score) / top.score;
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const topIsFaq = topChunk.source.includes('/faq/');
|
|
178
|
-
// chat/urls 话题一致性:收紧为“高精度 + 多信号”
|
|
179
|
-
// 1) prompt 中数字/错误码(高区分信号)
|
|
180
|
-
// 2) prompt 切词与 BP 主题 token 命中
|
|
181
|
-
// 3) URL 行标题切词与 BP 主题 token 命中(跨语言兜底)
|
|
182
|
-
// 4) 通用强锚点(长 token / CJK>=3)需与 BP 主题有交集(若 prompt 存在强锚点)
|
|
183
|
-
let chatUrlRowTopicMatch = true;
|
|
184
|
-
let chatUrlTopicSignalCount = 0;
|
|
185
|
-
if (topIsChatUrlRow) {
|
|
186
|
-
const bpChunk = chunksById.get(candidate.id);
|
|
187
|
-
const bpTopicText = buildBestPracticeTopicText(bpChunk);
|
|
188
|
-
const promptDigits = prompt.match(/\b\d{2,}\b/g) ?? [];
|
|
189
|
-
const digitMatch = promptDigits.some((d) => bpTopicText.includes(d));
|
|
190
|
-
const promptTokens = extractTopicTokens(prompt);
|
|
191
|
-
const promptTokenHitCount = countTopicTokenHits(promptTokens, bpTopicText);
|
|
192
|
-
const promptTokenMatch = promptTokenHitCount >= 1;
|
|
193
|
-
const urlTopicRaw = (topChunk.title ?? '').replace(/[((].*$/, '').trim();
|
|
194
|
-
const urlTopicTokens = extractTopicTokens(urlTopicRaw);
|
|
195
|
-
const urlTitleHitCount = countTopicTokenHits(urlTopicTokens, bpTopicText);
|
|
196
|
-
const urlTitleMatch = urlTitleHitCount >= 1;
|
|
197
|
-
const strongAnchors = extractStrongTopicAnchors(prompt);
|
|
198
|
-
const strongAnchorHitCount = countTopicTokenHits(strongAnchors, bpTopicText);
|
|
199
|
-
const strongAnchorMatch = strongAnchors.length === 0 || strongAnchorHitCount >= 1;
|
|
200
|
-
chatUrlTopicSignalCount = Number(digitMatch) + Number(promptTokenMatch) + Number(urlTitleMatch);
|
|
201
|
-
const hasHighPrecisionSignal = digitMatch || urlTitleMatch;
|
|
202
|
-
chatUrlRowTopicMatch = hasHighPrecisionSignal
|
|
203
|
-
&& chatUrlTopicSignalCount >= 2
|
|
204
|
-
&& strongAnchorMatch;
|
|
205
|
-
}
|
|
206
|
-
const bestPracticeStrongEnough = candidate.score >= Math.max(12, top.score * 0.05);
|
|
207
|
-
const urlRatioThreshold = topIsChatUrlRow ? 0.045 : 0.05;
|
|
208
|
-
const urlAbsoluteThreshold = topIsChatUrlRow ? 20 : 16;
|
|
209
|
-
const urlStrongEnough = candidate.score >= Math.max(urlAbsoluteThreshold, top.score * urlRatioThreshold);
|
|
210
|
-
const urlMappingShadowing = topIsUrlMapping
|
|
211
|
-
&& urlStrongEnough
|
|
212
|
-
&& chatUrlRowTopicMatch;
|
|
213
|
-
const faqShadowing = topIsFaq && bestPracticeStrongEnough;
|
|
214
|
-
// Feature/API 文档抢首位场景:top1 是功能文档(非 integration、非 BP),
|
|
215
|
-
// BP 的精准实践应优先于功能文档的泛匹配。
|
|
216
|
-
// 条件更严格:BP 分数需达到 top 的 40%(避免完全不相关的 BP 被误提)。
|
|
217
|
-
const topIsFeatureDoc = !topIsUrlMapping && !topIsFaq
|
|
218
|
-
&& !isBestPracticeSource(topChunk.source)
|
|
219
|
-
&& !isIntegrationSource(topChunk.source);
|
|
220
|
-
const featureDocShadowing = topIsFeatureDoc && candidate.score >= top.score * 0.4;
|
|
221
|
-
if (!normalCompetitive && !urlMappingShadowing && !faqShadowing && !featureDocShadowing)
|
|
109
|
+
const competitiveEnough = gapRatio <= 0.2;
|
|
110
|
+
const strengthEnough = candidate.score >= Math.max(14, top.score * 0.3);
|
|
111
|
+
if (!competitiveEnough || !strengthEnough)
|
|
112
|
+
return ranked;
|
|
113
|
+
// 轻量校准:最多只允许轻微越过 top,避免“强提权抢首位”。
|
|
114
|
+
const lift = Math.max(top.score * 0.03, 1.2);
|
|
115
|
+
const boostedScore = Math.min(top.score * 1.01, candidate.score + lift);
|
|
116
|
+
if (boostedScore <= candidate.score)
|
|
222
117
|
return ranked;
|
|
223
118
|
const next = [...ranked];
|
|
224
|
-
next[bestPracticeIdx] = { ...candidate, score:
|
|
225
|
-
return next
|
|
119
|
+
next[bestPracticeIdx] = { ...candidate, score: boostedScore };
|
|
120
|
+
return stableSortRanked(next);
|
|
226
121
|
}
|
|
227
122
|
/**
|
|
228
|
-
* P0-R1/P0-R2:best-practice 单 chunk
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* 即使分数未达 80 也信任 promote 结果执行收敛。
|
|
123
|
+
* P0-R1/P0-R2:best-practice 单 chunk 收敛(高置信门控版)。
|
|
124
|
+
* 仅当 BP 在跨来源竞争中具备明显优势时,才锁定 preferredSource + fullDocumentMode。
|
|
125
|
+
* 不再因为轻量校准后的微小领先而触发整文锁定。
|
|
232
126
|
*/
|
|
233
|
-
function pickDominantBestPracticeSource(ranked, chunksById) {
|
|
127
|
+
function pickDominantBestPracticeSource(ranked, chunksById, fullDocumentMaxChars) {
|
|
234
128
|
const top = ranked[0];
|
|
235
129
|
if (!top || top.score <= 0)
|
|
236
130
|
return { fullDocumentMode: false };
|
|
@@ -259,11 +153,12 @@ function pickDominantBestPracticeSource(ranked, chunksById) {
|
|
|
259
153
|
.length;
|
|
260
154
|
const sourceSections = [...chunksById.values()].filter((chunk) => chunk.source === source);
|
|
261
155
|
const sourceTotalChars = sourceSections.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
156
|
+
const scoreStrongEnough = top.score >= 85;
|
|
157
|
+
const dominanceStrongEnough = secondCrossScore === 0
|
|
158
|
+
? top.score >= 65
|
|
159
|
+
: (top.score / secondCrossScore) >= 1.25;
|
|
160
|
+
const crossSourceNotCompeting = strongCrossSourceCount === 0;
|
|
161
|
+
const withinLengthBudget = sourceTotalChars <= fullDocumentMaxChars;
|
|
267
162
|
if (scoreStrongEnough && dominanceStrongEnough && crossSourceNotCompeting && withinLengthBudget) {
|
|
268
163
|
return {
|
|
269
164
|
preferredSource: source,
|
|
@@ -276,6 +171,51 @@ function isIntegrationSource(source) {
|
|
|
276
171
|
return /\/integration\.md$/i.test(source)
|
|
277
172
|
|| /\/preset\/.+integration/i.test(source);
|
|
278
173
|
}
|
|
174
|
+
function isNavigationIntent(prompt) {
|
|
175
|
+
return /文档|链接|地址|url|网址|入口|在哪看|在哪里看|官网|文档地址|sdk文档/i.test(prompt);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 通用语义仲裁(非 integration 特化):
|
|
179
|
+
* 当 top1 落在 urls 映射行(导航型来源),且向量 top1 指向内容型文档时,
|
|
180
|
+
* 对“非导航意图”的 query 优先选择内容文档来源。
|
|
181
|
+
*
|
|
182
|
+
* 目的:避免 BM25 在 urls 索引行上的词面优势压制真实内容页。
|
|
183
|
+
*/
|
|
184
|
+
function resolveSemanticSourcePriorityByVector(params) {
|
|
185
|
+
const { ranked, chunksById, vectorTopId, prompt, fullDocumentMaxChars, } = params;
|
|
186
|
+
if (!vectorTopId || ranked.length === 0)
|
|
187
|
+
return null;
|
|
188
|
+
if (isNavigationIntent(prompt))
|
|
189
|
+
return null;
|
|
190
|
+
const top = ranked[0];
|
|
191
|
+
const topChunk = chunksById.get(top.id);
|
|
192
|
+
const vectorTopChunk = chunksById.get(vectorTopId);
|
|
193
|
+
if (!topChunk || !vectorTopChunk)
|
|
194
|
+
return null;
|
|
195
|
+
// 仅在“导航来源(top1) vs 内容来源(vectorTop)”冲突时触发
|
|
196
|
+
if (!isUrlMappingSource(topChunk.source))
|
|
197
|
+
return null;
|
|
198
|
+
if (isUrlMappingSource(vectorTopChunk.source))
|
|
199
|
+
return null;
|
|
200
|
+
if (topChunk.source === vectorTopChunk.source)
|
|
201
|
+
return null;
|
|
202
|
+
const vectorTopRanked = ranked.find((item) => item.id === vectorTopId);
|
|
203
|
+
if (!vectorTopRanked)
|
|
204
|
+
return null;
|
|
205
|
+
// 置信门槛:vector top 不能离 top1 太远,避免误切
|
|
206
|
+
const scoreGapOk = vectorTopRanked.score >= top.score * 0.45;
|
|
207
|
+
if (!scoreGapOk)
|
|
208
|
+
return null;
|
|
209
|
+
const sourceTotalChars = [...chunksById.values()]
|
|
210
|
+
.filter((chunk) => chunk.source === vectorTopChunk.source)
|
|
211
|
+
.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
212
|
+
if (sourceTotalChars > fullDocumentMaxChars)
|
|
213
|
+
return null;
|
|
214
|
+
return {
|
|
215
|
+
preferredSource: vectorTopChunk.source,
|
|
216
|
+
fullDocumentMode: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
279
219
|
/**
|
|
280
220
|
* 覆盖度竞争:在 A3 之前执行。
|
|
281
221
|
* 当 integration 的多个 section 被查询广泛命中时,选择 integration 完整文档;
|
|
@@ -287,16 +227,16 @@ function isIntegrationSource(source) {
|
|
|
287
227
|
* - BP 分数远高于 integration(>1.2×)→ 直接返回 null
|
|
288
228
|
* - 否则 → 返回 null
|
|
289
229
|
*/
|
|
290
|
-
function resolveContentPriority(ranked, chunksById) {
|
|
230
|
+
function resolveContentPriority(ranked, chunksById, fullDocumentMaxChars) {
|
|
291
231
|
if (ranked.length < 2)
|
|
292
232
|
return null;
|
|
293
233
|
// 找 BP 最高分(在 top-30 范围内)
|
|
294
234
|
let bpTopScore = -1;
|
|
295
235
|
for (let i = 0; i < Math.min(ranked.length, 30); i++) {
|
|
296
236
|
const chunk = chunksById.get(ranked[i].id);
|
|
297
|
-
if (!chunk
|
|
237
|
+
if (!chunk)
|
|
298
238
|
continue;
|
|
299
|
-
if (ranked[i].score > bpTopScore) {
|
|
239
|
+
if (isBestPracticeSource(chunk.source) && ranked[i].score > bpTopScore) {
|
|
300
240
|
bpTopScore = ranked[i].score;
|
|
301
241
|
}
|
|
302
242
|
}
|
|
@@ -334,9 +274,13 @@ function resolveContentPriority(ranked, chunksById) {
|
|
|
334
274
|
bestIntTopScore = stats.topScore;
|
|
335
275
|
}
|
|
336
276
|
}
|
|
277
|
+
const integrationSourceTotalChars = [...chunksById.values()]
|
|
278
|
+
.filter((chunk) => chunk.source === bestIntSource)
|
|
279
|
+
.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
280
|
+
const withinLengthBudget = integrationSourceTotalChars <= fullDocumentMaxChars;
|
|
337
281
|
// 无 BP 候选 → 选 integration(如果有足够 section 命中)
|
|
338
282
|
if (bpTopScore < 0) {
|
|
339
|
-
return bestHitCount >= 2
|
|
283
|
+
return bestHitCount >= 2 && withinLengthBudget
|
|
340
284
|
? { preferredSource: bestIntSource, fullDocumentMode: true }
|
|
341
285
|
: null;
|
|
342
286
|
}
|
|
@@ -345,12 +289,135 @@ function resolveContentPriority(ranked, chunksById) {
|
|
|
345
289
|
return null;
|
|
346
290
|
// 覆盖度判定
|
|
347
291
|
const BREADTH_THRESHOLD = 3;
|
|
348
|
-
if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6) {
|
|
292
|
+
if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6 && withinLengthBudget) {
|
|
349
293
|
return { preferredSource: bestIntSource, fullDocumentMode: true };
|
|
350
294
|
}
|
|
351
295
|
return null;
|
|
352
296
|
}
|
|
353
|
-
|
|
297
|
+
const DEFAULT_VECTOR_TOP_K = 30;
|
|
298
|
+
const DEFAULT_RRF_K = 60;
|
|
299
|
+
const DEFAULT_RERANK_TOP_K = 20;
|
|
300
|
+
function readEnvNumber(name, fallback) {
|
|
301
|
+
const raw = process.env[name];
|
|
302
|
+
if (raw === undefined || raw === '')
|
|
303
|
+
return fallback;
|
|
304
|
+
const val = Number(raw);
|
|
305
|
+
return Number.isFinite(val) ? val : fallback;
|
|
306
|
+
}
|
|
307
|
+
function stableSortRanked(items) {
|
|
308
|
+
return [...items].sort((a, b) => {
|
|
309
|
+
if (b.score !== a.score)
|
|
310
|
+
return b.score - a.score;
|
|
311
|
+
return a.id.localeCompare(b.id);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function fuseByRRF(params) {
|
|
315
|
+
const { bm25Ranked, vectorHits, rrfK, baseBeta, alpha, profile, } = params;
|
|
316
|
+
const bmWeight = profile?.bmWeight ?? 1;
|
|
317
|
+
const vecWeight = profile?.vecWeight ?? 1;
|
|
318
|
+
const alphaScale = profile?.alphaScale ?? 1;
|
|
319
|
+
if (!bm25Ranked.length && !vectorHits.length) {
|
|
320
|
+
return { ranked: [], bm25AdjustedCount: 0, vectorOnlyAddedCount: 0 };
|
|
321
|
+
}
|
|
322
|
+
const topBM25 = bm25Ranked[0]?.score ?? baseBeta;
|
|
323
|
+
const bm25ById = new Map(bm25Ranked.map((item) => [item.id, item]));
|
|
324
|
+
const bm25Rank = new Map();
|
|
325
|
+
bm25Ranked.forEach((item, idx) => bm25Rank.set(item.id, idx + 1));
|
|
326
|
+
const vecRank = new Map();
|
|
327
|
+
vectorHits.forEach((hit, idx) => vecRank.set(hit.id, idx + 1));
|
|
328
|
+
const vecMin = vectorHits.length ? Math.min(...vectorHits.map((h) => h.score)) : 0;
|
|
329
|
+
const vecMax = vectorHits.length ? Math.max(...vectorHits.map((h) => h.score)) : 1;
|
|
330
|
+
const vecRange = vecMax - vecMin || 1;
|
|
331
|
+
const vecNorm = new Map();
|
|
332
|
+
for (const hit of vectorHits) {
|
|
333
|
+
vecNorm.set(hit.id, (hit.score - vecMin) / vecRange);
|
|
334
|
+
}
|
|
335
|
+
const unionIds = new Set([
|
|
336
|
+
...bm25Ranked.map((item) => item.id),
|
|
337
|
+
...vectorHits.map((item) => item.id),
|
|
338
|
+
]);
|
|
339
|
+
const rrfRaw = new Map();
|
|
340
|
+
let maxRRF = 0;
|
|
341
|
+
for (const id of unionIds) {
|
|
342
|
+
const bRank = bm25Rank.get(id);
|
|
343
|
+
const vRank = vecRank.get(id);
|
|
344
|
+
const score = (bRank ? bmWeight / (rrfK + bRank) : 0)
|
|
345
|
+
+ (vRank ? vecWeight / (rrfK + vRank) : 0);
|
|
346
|
+
rrfRaw.set(id, score);
|
|
347
|
+
if (score > maxRRF)
|
|
348
|
+
maxRRF = score;
|
|
349
|
+
}
|
|
350
|
+
let bm25AdjustedCount = 0;
|
|
351
|
+
let vectorOnlyAddedCount = 0;
|
|
352
|
+
// 类别权重也影响 hybrid 融合权重,避免 rrf 权重与 sim 权重口径分裂。
|
|
353
|
+
const totalWeight = bmWeight + vecWeight || 1;
|
|
354
|
+
const simWeight = vecWeight / totalWeight; // 0..1
|
|
355
|
+
const rrfWeight = 1 - simWeight;
|
|
356
|
+
const fused = [...unionIds].map((id) => {
|
|
357
|
+
const original = bm25ById.get(id);
|
|
358
|
+
const rrfNorm = maxRRF > 0 ? (rrfRaw.get(id) ?? 0) / maxRRF : 0;
|
|
359
|
+
const simNorm = vecNorm.get(id) ?? 0;
|
|
360
|
+
const hybridNorm = rrfWeight * rrfNorm + simWeight * simNorm;
|
|
361
|
+
const effectiveAlpha = alpha * alphaScale;
|
|
362
|
+
if (original) {
|
|
363
|
+
const hasVectorSignal = vecRank.has(id);
|
|
364
|
+
if (hasVectorSignal)
|
|
365
|
+
bm25AdjustedCount += 1;
|
|
366
|
+
const boost = hasVectorSignal ? original.score * effectiveAlpha * 0.25 * hybridNorm : 0;
|
|
367
|
+
return { id, score: original.score + boost };
|
|
368
|
+
}
|
|
369
|
+
vectorOnlyAddedCount += 1;
|
|
370
|
+
return { id, score: topBM25 * baseBeta * (1 + effectiveAlpha * 0.9 * hybridNorm) };
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
ranked: stableSortRanked(fused),
|
|
374
|
+
bm25AdjustedCount,
|
|
375
|
+
vectorOnlyAddedCount,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function resolveFusionProfile(prompt, queryApiTokens) {
|
|
379
|
+
const trimmed = prompt.trim();
|
|
380
|
+
if (queryApiTokens.length > 0) {
|
|
381
|
+
return { name: 'api_token', bmWeight: 1.3, vecWeight: 0.8, alphaScale: 0.7 };
|
|
382
|
+
}
|
|
383
|
+
// 长自然语言:字符数或分词粒度足够
|
|
384
|
+
const tokenCount = tokenize(trimmed).filter((t) => t.length >= 2).length;
|
|
385
|
+
const isLongNatural = trimmed.length >= 14 || tokenCount >= 6;
|
|
386
|
+
if (isLongNatural) {
|
|
387
|
+
return { name: 'natural_language', bmWeight: 0.85, vecWeight: 1.2, alphaScale: 1.15 };
|
|
388
|
+
}
|
|
389
|
+
const isShortKeyword = trimmed.length <= 6 && tokenCount <= 2;
|
|
390
|
+
if (isShortKeyword) {
|
|
391
|
+
return { name: 'short_keyword', bmWeight: 1.0, vecWeight: 1.0, alphaScale: 0.9 };
|
|
392
|
+
}
|
|
393
|
+
return { name: 'default', bmWeight: 1.0, vecWeight: 1.0, alphaScale: 1.0 };
|
|
394
|
+
}
|
|
395
|
+
function rerankTopK(params) {
|
|
396
|
+
const { prompt, ranked, chunksById, topK } = params;
|
|
397
|
+
if (ranked.length < 2 || topK <= 0)
|
|
398
|
+
return { ranked, applied: false };
|
|
399
|
+
const head = ranked.slice(0, topK);
|
|
400
|
+
const tail = ranked.slice(topK);
|
|
401
|
+
const promptTokens = [...new Set(tokenize(prompt).filter((t) => t.length >= 2))];
|
|
402
|
+
if (!promptTokens.length)
|
|
403
|
+
return { ranked, applied: false };
|
|
404
|
+
const topScore = head[0]?.score ?? 0;
|
|
405
|
+
const bumped = head.map((item) => {
|
|
406
|
+
const chunk = chunksById.get(item.id);
|
|
407
|
+
if (!chunk)
|
|
408
|
+
return item;
|
|
409
|
+
const text = [chunk.title, ...(chunk.key_phrases ?? [])].join(' ').toLowerCase();
|
|
410
|
+
const overlap = promptTokens.reduce((acc, token) => (text.includes(token.toLowerCase()) ? acc + 1 : acc), 0);
|
|
411
|
+
if (overlap <= 0)
|
|
412
|
+
return item;
|
|
413
|
+
const boost = topScore * 0.002 * overlap;
|
|
414
|
+
return { ...item, score: item.score + boost };
|
|
415
|
+
});
|
|
416
|
+
const rerankedHead = stableSortRanked(bumped);
|
|
417
|
+
const applied = rerankedHead.some((item, idx) => item.id !== head[idx]?.id);
|
|
418
|
+
return { ranked: [...rerankedHead, ...tail], applied };
|
|
419
|
+
}
|
|
420
|
+
export async function runSearchPass(params) {
|
|
354
421
|
const { allChunks, passNorm, retrievalMode, queryApiTokens, opts, } = params;
|
|
355
422
|
const effectivePassNorm = retrievalMode === 'discovery'
|
|
356
423
|
? { ...passNorm, types: undefined }
|
|
@@ -360,6 +427,10 @@ export function runSearchPass(params) {
|
|
|
360
427
|
if (pool.length === 0 && effectivePassNorm.types?.length) {
|
|
361
428
|
pool = filterPool(allChunks, { ...effectivePassNorm, types: undefined });
|
|
362
429
|
}
|
|
430
|
+
// 分层过滤:向量语义召回仅受 Hard 层(product + retrievable_scope)约束,
|
|
431
|
+
// framework/types/scenario 转到 applyRankMultipliers 做软降权,保留向量的跨平台/类型自愈能力。
|
|
432
|
+
const hardPool = filterPoolHard(allChunks, effectivePassNorm);
|
|
433
|
+
const hardChunksById = new Map(hardPool.map((chunk) => [chunk.id, chunk]));
|
|
363
434
|
const { boosts, exactHits, urlRowHitChunkIds } = runExactLookup(effectivePassNorm.prompt, pool, effectivePassNorm);
|
|
364
435
|
// A2 补充正向路由:UserSig 问法下允许 share 命中且优先于无关 urls 噪声
|
|
365
436
|
if (/usersig|sdkappid|secretkey|鉴权|签名|gentestusersig/i.test(passNorm.prompt)) {
|
|
@@ -387,36 +458,169 @@ export function runSearchPass(params) {
|
|
|
387
458
|
boosts[id] = (boosts[id] ?? 0) + amount;
|
|
388
459
|
}
|
|
389
460
|
const ranked = rankSubsetWithIndex(effectivePassNorm.prompt, pool.map((chunk) => chunk.id), productIndex, boosts);
|
|
390
|
-
|
|
461
|
+
// 固定链路:Hybrid(BM25 + Vector)→ RRF 融合 → 轻量重排。
|
|
462
|
+
// 兼容开关:DISABLE_VECTOR_AUGMENT=1 时只关闭向量相关步骤,BM25 主链路保持不变。
|
|
463
|
+
const vectorAugmentDisabled = process.env.DISABLE_VECTOR_AUGMENT === '1';
|
|
464
|
+
const alphaOverride = process.env.VECTOR_AUGMENT_ALPHA
|
|
465
|
+
? Number(process.env.VECTOR_AUGMENT_ALPHA)
|
|
466
|
+
: undefined;
|
|
467
|
+
const alpha = alphaOverride !== undefined && !Number.isNaN(alphaOverride) ? alphaOverride : 0.15;
|
|
468
|
+
const baseBeta = readEnvNumber('VECTOR_AUGMENT_BASE_BETA', 0.3);
|
|
469
|
+
const topK = Math.max(1, Math.floor(readEnvNumber('VECTOR_TOP_K', DEFAULT_VECTOR_TOP_K)));
|
|
470
|
+
const rrfK = Math.max(1, Math.floor(readEnvNumber('VECTOR_RRF_K', DEFAULT_RRF_K)));
|
|
471
|
+
const rerankTopKLimit = Math.max(0, Math.floor(readEnvNumber('VECTOR_RERANK_TOP_K', DEFAULT_RERANK_TOP_K)));
|
|
472
|
+
const fullDocumentMaxChars = Math.max(1000, Math.floor(readEnvNumber('SEARCH_FULL_DOCUMENT_MAX_CHARS', 7000)));
|
|
473
|
+
let rankedFused = ranked;
|
|
474
|
+
// 向量归因埋点采集(终态归因在函数末尾统一构造):
|
|
475
|
+
// - bm25TopIdSnapshot:向量介入前的 BM25 单路头名(即 ranked[0],融合前的原始头名)
|
|
476
|
+
// - vectorTopIdSnapshot:向量单路头名(vecHits[0],经 hardChunksById 过滤后)
|
|
477
|
+
// - vecScoreById:向量单路每个候选的 sim 分,供 final_top1_id 查表得 vector_score_of_final_top1
|
|
478
|
+
// - bm25RankById:向量介入前 BM25 单路的 1-based 名次索引,供 rank_shift 与 promoted_by_vector_count 计算
|
|
479
|
+
const bm25TopIdSnapshot = ranked[0]?.id ?? null;
|
|
480
|
+
const bm25RankById = new Map();
|
|
481
|
+
ranked.forEach((item, idx) => bm25RankById.set(item.id, idx + 1));
|
|
482
|
+
let vectorTopIdSnapshot = null;
|
|
483
|
+
let vecScoreById = new Map();
|
|
484
|
+
if (!vectorAugmentDisabled) {
|
|
485
|
+
try {
|
|
486
|
+
const [qvecInst, qvecRaw] = await Promise.all([
|
|
487
|
+
embedQuery(withQueryInstruction(effectivePassNorm.prompt)),
|
|
488
|
+
embedQuery(effectivePassNorm.prompt),
|
|
489
|
+
]);
|
|
490
|
+
const [hitsInst, hitsRaw] = await Promise.all([
|
|
491
|
+
queryVectors(passNorm.product, qvecInst, topK),
|
|
492
|
+
queryVectors(passNorm.product, qvecRaw, topK),
|
|
493
|
+
]);
|
|
494
|
+
const merged = new Map();
|
|
495
|
+
for (const h of hitsInst) {
|
|
496
|
+
merged.set(h.id, Math.max(merged.get(h.id) ?? Number.NEGATIVE_INFINITY, h.score));
|
|
497
|
+
}
|
|
498
|
+
for (const h of hitsRaw) {
|
|
499
|
+
const weighted = h.score * 0.97;
|
|
500
|
+
merged.set(h.id, Math.max(merged.get(h.id) ?? Number.NEGATIVE_INFINITY, weighted));
|
|
501
|
+
}
|
|
502
|
+
const hits = [...merged.entries()]
|
|
503
|
+
.map(([id, score]) => ({ id, score }))
|
|
504
|
+
.sort((a, b) => {
|
|
505
|
+
if (b.score !== a.score)
|
|
506
|
+
return b.score - a.score;
|
|
507
|
+
return a.id.localeCompare(b.id);
|
|
508
|
+
})
|
|
509
|
+
.slice(0, topK);
|
|
510
|
+
// 分层过滤:向量仅受 Hard 层(product + retrievable_scope)约束,
|
|
511
|
+
// framework/types/scenario 交给 applyRankMultipliers 做软降权。
|
|
512
|
+
// 向量补召回但不在 BM25 pool 内的 chunk 需要挂到 chunksById,供下游 rerank / multiplier / postProcess 读取。
|
|
513
|
+
const vecHits = hits.filter((h) => hardChunksById.has(h.id));
|
|
514
|
+
for (const hit of vecHits) {
|
|
515
|
+
if (chunksById.has(hit.id))
|
|
516
|
+
continue;
|
|
517
|
+
const chunk = hardChunksById.get(hit.id);
|
|
518
|
+
if (!chunk)
|
|
519
|
+
continue;
|
|
520
|
+
chunksById.set(hit.id, attachChunkContent(chunk));
|
|
521
|
+
}
|
|
522
|
+
vectorTopIdSnapshot = vecHits[0]?.id ?? null;
|
|
523
|
+
vecScoreById = new Map(vecHits.map((h) => [h.id, h.score]));
|
|
524
|
+
if (vecHits.length > 0) {
|
|
525
|
+
const fusionProfile = resolveFusionProfile(effectivePassNorm.prompt, queryApiTokens);
|
|
526
|
+
const fused = fuseByRRF({
|
|
527
|
+
bm25Ranked: ranked,
|
|
528
|
+
vectorHits: vecHits,
|
|
529
|
+
rrfK,
|
|
530
|
+
baseBeta,
|
|
531
|
+
alpha,
|
|
532
|
+
profile: fusionProfile,
|
|
533
|
+
});
|
|
534
|
+
const reranked = rerankTopK({
|
|
535
|
+
prompt: effectivePassNorm.prompt,
|
|
536
|
+
ranked: fused.ranked,
|
|
537
|
+
chunksById,
|
|
538
|
+
topK: rerankTopKLimit,
|
|
539
|
+
});
|
|
540
|
+
rankedFused = reranked.ranked;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
// 向量链路失败(embed/query 异常):保持 rankedFused = ranked,
|
|
545
|
+
// 快照变量维持初始值即可。归因阶段 vector_top1_id 为 null 天然反映降级。
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const rankedWithMultipliers = applyRankMultipliers(rankedFused, chunksById, {
|
|
391
549
|
product: passNorm.product,
|
|
392
550
|
framework: passNorm.framework,
|
|
393
551
|
frameworks: passNorm.frameworks,
|
|
394
552
|
});
|
|
395
553
|
const rankedAfterIntentGuard = applyDiagnosisIntentGuard(rankedWithMultipliers, chunksById, passNorm.prompt);
|
|
396
|
-
|
|
397
|
-
const
|
|
554
|
+
const promptLockPolicy = decidePromptLockPolicy(passNorm.prompt);
|
|
555
|
+
const allowContentPriority = promptLockPolicy.enableContentPriority;
|
|
556
|
+
const allowFullDocumentLock = promptLockPolicy.enableFullDocumentLock;
|
|
557
|
+
// 覆盖度竞争先于 A3 执行(仅在前置策略允许时开启)
|
|
558
|
+
// 若覆盖度竞争未触发,再尝试“导航来源 vs 内容来源”的通用语义仲裁。
|
|
559
|
+
const integrationPriority = allowContentPriority
|
|
560
|
+
? (resolveContentPriority(rankedAfterIntentGuard, chunksById, fullDocumentMaxChars)
|
|
561
|
+
?? resolveSemanticSourcePriorityByVector({
|
|
562
|
+
ranked: rankedAfterIntentGuard,
|
|
563
|
+
chunksById,
|
|
564
|
+
vectorTopId: vectorTopIdSnapshot,
|
|
565
|
+
prompt: passNorm.prompt,
|
|
566
|
+
fullDocumentMaxChars,
|
|
567
|
+
}))
|
|
568
|
+
: null;
|
|
398
569
|
let dominantBestPractice;
|
|
399
570
|
let rankedAfterBestPractice;
|
|
400
571
|
if (integrationPriority) {
|
|
401
572
|
// integration 胜出 → 跳过 A3,直接走完整文档路径
|
|
402
573
|
rankedAfterBestPractice = rankedAfterIntentGuard;
|
|
403
|
-
dominantBestPractice =
|
|
574
|
+
dominantBestPractice = allowFullDocumentLock
|
|
575
|
+
? integrationPriority
|
|
576
|
+
: { fullDocumentMode: false };
|
|
404
577
|
}
|
|
405
578
|
else {
|
|
406
579
|
// integration 未胜出 → A3 提升 BP → BP 收敛判定
|
|
407
580
|
rankedAfterBestPractice = prioritizeBestPracticeWhenCompetitive(rankedAfterIntentGuard, chunksById, passNorm.prompt);
|
|
408
|
-
dominantBestPractice =
|
|
581
|
+
dominantBestPractice = allowFullDocumentLock
|
|
582
|
+
? pickDominantBestPracticeSource(rankedAfterBestPractice, chunksById, fullDocumentMaxChars)
|
|
583
|
+
: { fullDocumentMode: false };
|
|
409
584
|
}
|
|
410
585
|
const pinnedUrlRowChunkIds = new Set(urlRowHitChunkIds);
|
|
586
|
+
// 独立信号:存在精确 URL 命中 → 启用 35% 断崖截断。
|
|
587
|
+
// 与 pin 语义解耦:即便 pin 集合发生变化或 BP 校准影响 fragments 组成,
|
|
588
|
+
// 截断策略仍由“是否存在精确 URL 命中”这一确定性信号决定。
|
|
589
|
+
const strictCutoffOnExactUrlHit = pinnedUrlRowChunkIds.size > 0;
|
|
411
590
|
const fragments = postProcess(rankedAfterBestPractice, chunksById, passNorm.limit, {
|
|
412
591
|
pinnedUrlRowChunkIds,
|
|
592
|
+
strictCutoffOnExactUrlHit,
|
|
413
593
|
preferredSource: dominantBestPractice.preferredSource,
|
|
414
594
|
fullDocumentMode: dominantBestPractice.fullDocumentMode,
|
|
595
|
+
fullDocumentMaxChars,
|
|
415
596
|
});
|
|
416
597
|
const urlMappingSources = [...new Set(fragments.map((f) => f.source).filter((source) => isUrlMappingSource(source)))];
|
|
417
598
|
const references = buildReferences(fragments);
|
|
418
599
|
const apiNameCoverage = computeApiNameCoverage(queryApiTokens, poolWithContent);
|
|
419
600
|
const missingApiTokens = computeMissingApiTokens(queryApiTokens, poolWithContent);
|
|
601
|
+
// === 向量归因(终态构造)===
|
|
602
|
+
// final_top1_id:postProcess 之后 fragments[0] 的 chunk_id(enrichL1Fragments 后续 in-place enrich
|
|
603
|
+
// content 但不改动 chunk_id / 顺序,所以此处快照即最终外部感知的 top1)。
|
|
604
|
+
const finalTop1Id = fragments[0]?.chunk_id ?? null;
|
|
605
|
+
const finalTop1BmRank = finalTop1Id ? (bm25RankById.get(finalTop1Id) ?? null) : null;
|
|
606
|
+
const finalTop1VecScore = finalTop1Id ? (vecScoreById.get(finalTop1Id) ?? null) : null;
|
|
607
|
+
// promoted_by_vector_count:BM25 原始名次 > 3 且经融合/后处理进入 final top3 的 chunk 数量。
|
|
608
|
+
// 只需交集 |{chunk : bm25Rank(chunk) > 3 AND chunk ∈ finalTop3}|;ranked 为空自然记 0。
|
|
609
|
+
const finalTop3Ids = fragments.slice(0, 3).map((f) => f.chunk_id);
|
|
610
|
+
let promotedByVectorCount = 0;
|
|
611
|
+
for (const id of finalTop3Ids) {
|
|
612
|
+
const bmRank = bm25RankById.get(id);
|
|
613
|
+
if (bmRank !== undefined && bmRank > 3)
|
|
614
|
+
promotedByVectorCount += 1;
|
|
615
|
+
}
|
|
616
|
+
const vectorDiagnostics = {
|
|
617
|
+
bm25_top1_id: bm25TopIdSnapshot,
|
|
618
|
+
vector_top1_id: vectorTopIdSnapshot,
|
|
619
|
+
final_top1_id: finalTop1Id,
|
|
620
|
+
rank_shift_from_bm25: finalTop1BmRank,
|
|
621
|
+
vector_score_of_final_top1: finalTop1VecScore,
|
|
622
|
+
promoted_by_vector_count: promotedByVectorCount,
|
|
623
|
+
};
|
|
420
624
|
return {
|
|
421
625
|
pool,
|
|
422
626
|
poolWithContent,
|
|
@@ -429,6 +633,7 @@ export function runSearchPass(params) {
|
|
|
429
633
|
missingApiTokens,
|
|
430
634
|
urlMapRelaxed: pinnedUrlRowChunkIds.size > 0,
|
|
431
635
|
urlMapExactHitCount: pinnedUrlRowChunkIds.size,
|
|
636
|
+
vectorDiagnostics,
|
|
432
637
|
preferredSource: dominantBestPractice.preferredSource,
|
|
433
638
|
fullDocumentMode: dominantBestPractice.fullDocumentMode,
|
|
434
639
|
};
|