@tencentcloud/sdk-mcp 1.7.7 → 1.7.9-beta.1
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 +13 -0
- package/README.md +7 -9
- package/index.js +8 -8
- package/knowledge/chunker.js +1 -0
- 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/frontmatter-meta.js +23 -0
- package/knowledge/path-meta.js +56 -0
- package/knowledge/search/exact-lookup.js +145 -16
- package/knowledge/search/filter-pool.js +77 -33
- package/knowledge/search/rank-profile.js +25 -0
- package/knowledge/search/requires-framework.js +4 -1
- package/knowledge/search/search-core.js +74 -13
- package/knowledge/search/search-knowledge.js +24 -34
- package/knowledge/search/search-pass.js +495 -169
- 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/callkit/preset/android/integration.md +0 -2
- package/resource/integration/callkit/preset/ios/integration.md +0 -2
- 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/livekit/preset/live/android/prepare-integration.md +0 -4
- package/resource/integration/livekit/preset/live/ios/prepare-integration.md +0 -4
- package/resource/integration/roomkit/preset/vue/integration.md +154 -208
- package/resource/knowledge/callkit/best-practice/android/video-chat-dating.md +0 -2
- package/resource/knowledge/callkit/core-sdk/android/answer-call.md +0 -2
- package/resource/knowledge/callkit/core-sdk/android/make-call.md +0 -2
- package/resource/knowledge/callkit/core-sdk/flutter/answer-call.md +0 -4
- package/resource/knowledge/callkit/core-sdk/flutter/make-call.md +0 -2
- package/resource/knowledge/callkit/core-sdk/flutter/offline-push.md +0 -2
- package/resource/knowledge/callkit/core-sdk/ios/answer-call.md +0 -2
- package/resource/knowledge/callkit/core-sdk/ios/make-call.md +0 -4
- package/resource/knowledge/callkit/core-sdk/ios/offline-push.md +0 -2
- package/resource/knowledge/callkit/core-sdk/uni-app/answer-call.md +0 -2
- package/resource/knowledge/callkit/core-sdk/uni-app/make-call.md +0 -2
- package/resource/knowledge/callkit/faq/ios.md +0 -2
- package/resource/knowledge/callkit/faq/uniapp-app.md +0 -2
- package/resource/knowledge/callkit/preset/miniprogram/integration.md +2 -3
- package/resource/knowledge/callkit/preset/miniprogram/uniapp-integration.md +0 -2
- 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/iot/app/activate-service.md +95 -0
- package/resource/knowledge/iot/app/demo/android.md +93 -0
- package/resource/knowledge/iot/app/demo/ios.md +87 -0
- package/resource/knowledge/iot/app/demo/miniprogram.md +79 -0
- package/resource/knowledge/iot/app/device-binding.md +378 -0
- package/resource/knowledge/iot/app/device-sharing.md +414 -0
- package/resource/knowledge/iot/app/family-management.md +762 -0
- package/resource/knowledge/iot/app/integration/android.md +328 -0
- package/resource/knowledge/iot/app/integration/harmonyos.md +326 -0
- package/resource/knowledge/iot/app/integration/ios.md +351 -0
- package/resource/knowledge/iot/app/integration/miniprogram.md +268 -0
- package/resource/knowledge/iot/app/monitor-playback.md +557 -0
- package/resource/knowledge/iot/app/remote-control.md +139 -0
- package/resource/knowledge/iot/app/video-call.md +559 -0
- package/resource/knowledge/iot/app/wechat-call/device-call-miniprogram.md +204 -0
- package/resource/knowledge/iot/app/wechat-call/miniprogram-call-device.md +193 -0
- package/resource/knowledge/iot/client-api/app/android/txiotcallsession.md +189 -0
- package/resource/knowledge/iot/client-api/app/android/txiotcloudstorage.md +77 -0
- package/resource/knowledge/iot/client-api/app/android/txiotdevicemanager.md +175 -0
- package/resource/knowledge/iot/client-api/app/android/txiotengine.md +172 -0
- package/resource/knowledge/iot/client-api/app/android/txiotenginedef.md +163 -0
- package/resource/knowledge/iot/client-api/app/android/txiotfamilymanager.md +146 -0
- package/resource/knowledge/iot/client-api/app/android/txiotmonitorsession.md +246 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotcallsession.md +191 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotcloudstorage.md +77 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotdevicemanager.md +183 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotengine.md +186 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotenginedef.md +164 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotfamilymanager.md +150 -0
- package/resource/knowledge/iot/client-api/app/harmonyos/txiotmonitorsession.md +252 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotcallsession.md +199 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotcloudstorage.md +76 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotdevicemanager.md +173 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotengine.md +173 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotenginedef.md +171 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotfamilymanager.md +146 -0
- package/resource/knowledge/iot/client-api/app/ios/txiotmonitorsession.md +250 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotcallsession.md +180 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotcloudstorage.md +76 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotdevicemanager.md +281 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotengine.md +183 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotenginedef.md +152 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotfamilymanager.md +179 -0
- package/resource/knowledge/iot/client-api/app/miniprogram/txiotmonitorsession.md +226 -0
- package/resource/knowledge/iot/client-api/device/android-smart-device.md +813 -0
- package/resource/knowledge/iot/client-api/device/linux-rtos-device.md +2027 -0
- package/resource/knowledge/iot/device/activate-service.md +167 -0
- package/resource/knowledge/iot/device/ai-voice-chat/advanced-features/custom-agent.md +171 -0
- package/resource/knowledge/iot/device/ai-voice-chat/advanced-features/smart-device-control.md +721 -0
- package/resource/knowledge/iot/device/ai-voice-chat/integration-guide/android-smart-device.md +247 -0
- package/resource/knowledge/iot/device/ai-voice-chat/integration-guide/linux-rtos-device.md +368 -0
- package/resource/knowledge/iot/device/ai-voice-chat/overview.md +84 -0
- package/resource/knowledge/iot/device/cloud-storage.md +696 -0
- package/resource/knowledge/iot/device/data-model.md +972 -0
- package/resource/knowledge/iot/device/demo/android-smart-device.md +97 -0
- package/resource/knowledge/iot/device/demo/linux-rtos-device.md +106 -0
- package/resource/knowledge/iot/device/device-ota-upgrade.md +237 -0
- package/resource/knowledge/iot/device/dynamic-registration/android-smart-device.md +275 -0
- package/resource/knowledge/iot/device/dynamic-registration/linux-rtos-device.md +297 -0
- package/resource/knowledge/iot/device/login-and-register/android-smart-device.md +131 -0
- package/resource/knowledge/iot/device/login-and-register/linux-rtos-device.md +291 -0
- package/resource/knowledge/iot/device/monitor-pushing/android-smart-device.md +237 -0
- package/resource/knowledge/iot/device/monitor-pushing/linux-rtos-device.md +493 -0
- package/resource/knowledge/iot/device/video-call/android-smart-device.md +299 -0
- package/resource/knowledge/iot/device/video-call/linux-rtos-device.md +465 -0
- package/resource/knowledge/iot/device/wechat-call/android-smart-device.md +534 -0
- package/resource/knowledge/iot/device/wechat-call/linux-rtos-device.md +681 -0
- package/resource/knowledge/iot/kids-watch/overview.md +28 -0
- package/resource/knowledge/iot/security-camera/activate-service.md +74 -0
- package/resource/knowledge/iot/security-camera/monitor-playback/app-integration.md +302 -0
- package/resource/knowledge/iot/security-camera/monitor-playback/device-integration/android.md +237 -0
- package/resource/knowledge/iot/security-camera/monitor-playback/device-integration/linux-rtos.md +493 -0
- package/resource/knowledge/iot/security-camera/overview.md +31 -0
- package/resource/knowledge/iot/security-camera/ptz-control.md +153 -0
- package/resource/knowledge/iot/security-camera/recording-and-snapshot.md +182 -0
- package/resource/knowledge/iot/security-camera/voice-intercom.md +174 -0
- package/resource/knowledge/iot/smart-vision/serviceoverview.md +114 -0
- package/resource/knowledge/iot/voice-recorder/app-demo.md +99 -0
- package/resource/knowledge/iot/voice-recorder/device-demo.md +189 -0
- package/resource/knowledge/iot/voice-recorder/overview.md +64 -0
- package/resource/knowledge/livekit/core-sdk/features/android/base-beauty.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/android/gift.md +0 -4
- package/resource/knowledge/livekit/core-sdk/features/android/live-list.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/flutter/audio-effect.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/flutter/base-beauty.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/flutter/gift.md +0 -4
- package/resource/knowledge/livekit/core-sdk/features/ios/base-beauty.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/ios/gift.md +0 -4
- package/resource/knowledge/livekit/core-sdk/features/ios/live-list.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/uni-app/base-beauty.md +0 -2
- package/resource/knowledge/livekit/core-sdk/features/uni-app/gift.md +0 -4
- package/resource/knowledge/livekit/core-sdk/live/android/co-guest-interaction.md +0 -4
- package/resource/knowledge/livekit/core-sdk/live/android/host-interaction.md +0 -10
- package/resource/knowledge/livekit/core-sdk/live/android/integration.md +0 -2
- package/resource/knowledge/livekit/core-sdk/live/flutter/co-guest-interaction.md +0 -2
- package/resource/knowledge/livekit/core-sdk/live/flutter/host-interaction.md +0 -10
- package/resource/knowledge/livekit/core-sdk/live/flutter/integration.md +0 -2
- package/resource/knowledge/livekit/core-sdk/live/ios/co-guest-interaction.md +0 -4
- package/resource/knowledge/livekit/core-sdk/live/ios/host-interaction.md +0 -10
- package/resource/knowledge/livekit/core-sdk/live/ios/integration.md +0 -2
- package/resource/knowledge/livekit/core-sdk/live/uni-app/co-guest-interaction.md +0 -4
- package/resource/knowledge/livekit/core-sdk/live/uni-app/host-interaction.md +0 -4
- package/resource/knowledge/livekit/core-sdk/live/uni-app/integration.md +0 -2
- package/resource/knowledge/livekit/core-sdk/voice/android/co-guest-interaction.md +0 -2
- package/resource/knowledge/livekit/core-sdk/voice/android/host-interaction.md +0 -4
- package/resource/knowledge/livekit/core-sdk/voice/android/integration.md +0 -4
- package/resource/knowledge/livekit/core-sdk/voice/flutter/integration.md +0 -2
- package/resource/knowledge/livekit/core-sdk/voice/ios/co-guest-interaction.md +0 -2
- package/resource/knowledge/livekit/core-sdk/voice/ios/host-interaction.md +0 -4
- package/resource/knowledge/livekit/core-sdk/voice/ios/integration.md +0 -4
- package/resource/knowledge/livekit/core-sdk/voice/uni-app/co-guest-interaction.md +0 -2
- package/resource/knowledge/livekit/core-sdk/voice/uni-app/integration.md +0 -2
- package/resource/knowledge/livekit/faq/error-code.md +0 -2
- 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/donut.md +0 -2
- package/resource/knowledge/push/config/harmonyos.md +21 -5
- package/resource/knowledge/push/integration/android.md +0 -5
- package/resource/knowledge/push/integration/donut.md +0 -3
- package/resource/knowledge/push/integration/flutter.md +1 -4
- package/resource/knowledge/push/integration/harmonyos.md +0 -3
- package/resource/knowledge/push/integration/ios.md +0 -3
- package/resource/knowledge/push/integration/react-native.md +0 -5
- package/resource/knowledge/push/integration/uni-app.md +0 -3
- package/resource/knowledge/push/integration/unity.md +0 -3
- package/resource/knowledge/push/integration/unreal-engine.md +0 -3
- package/resource/knowledge/roomkit/core-sdk/android/call-user-to-room.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/android/room-participant-state.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/android/room-state.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/android/schedule-room.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/flutter/call-user-to-room.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/flutter/room-participant-state.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/flutter/room-state.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/flutter/schedule-room.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/ios/call-user-to-room.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/ios/room-participant-state.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/ios/room-state.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/ios/schedule-room.md +0 -2
- package/resource/knowledge/roomkit/core-sdk/vue/room-view.md +0 -3
- package/resource/knowledge/roomkit/preset/uniapp-app/integration.md +18 -14
- package/resource/knowledge/rtcengine/web/how_to_run_web_sdk_demo.md +0 -2
- package/resource/knowledge/rtcengine/web/plugin/23-advanced-support-detection.md +30 -0
- package/resource/knowledge/rtcengine/web/plugin/24-advanced-network-quality.md +0 -2
- 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 +8 -9
- package/server.js +1 -1
- package/tools/common/finalize_answer.js +1 -1
- package/tools/common/present_framework_choice.js +1 -1
- package/tools/common/search_trtc_knowledge.js +22 -3
- package/tools/definitions/search_trtc_knowledge_def.js +3 -2
- package/utils/preflight-route.js +19 -2
- package/utils/report-cls-client.js +1 -1
- package/utils/report-es-client.js +1 -1
|
@@ -1,103 +1,41 @@
|
|
|
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';
|
|
11
|
+
import { resolveRankProfile } from './rank-profile.js';
|
|
9
12
|
function isBestPracticeSource(source) {
|
|
10
13
|
return /\/best-practice\//i.test(source);
|
|
11
14
|
}
|
|
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
15
|
function extractPrimaryHeading(content) {
|
|
83
16
|
const heading = content.split('\n').find((line) => /^#\s+/.test(line.trim()));
|
|
84
17
|
return heading ? heading.replace(/^#\s+/, '').trim() : '';
|
|
85
18
|
}
|
|
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
19
|
const DIAGNOSIS_INTENT_RE = /丢消息|消息丢失|message\s*loss|消息可靠性|message\s*reliability|超时|timeout|失败|fail(?:ed|ure)?|排查|诊断|troubleshoot(?:ing)?/i;
|
|
96
20
|
const DIAGNOSIS_DOC_RE = /diagnosis|troubleshoot|排查|诊断|丢失|message\s*loss|reliability|可靠性|timeout|fail(?:ed|ure)?/i;
|
|
97
21
|
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
22
|
function isDiagnosisIntent(prompt) {
|
|
99
23
|
return DIAGNOSIS_INTENT_RE.test(prompt);
|
|
100
24
|
}
|
|
25
|
+
function decidePromptLockPolicy(prompt) {
|
|
26
|
+
const integrationIntent = /集成|接入|指南|步骤|教程|快速开始|初始化|安装|跑通|demo|sample|example/i.test(prompt);
|
|
27
|
+
const productIntent = /配置|限制|上限|规则|权限|计费|套餐|价格|资费|多端登录|错误码|命名|长度|开通|购买/i.test(prompt);
|
|
28
|
+
if (integrationIntent && !productIntent) {
|
|
29
|
+
return {
|
|
30
|
+
enableContentPriority: true,
|
|
31
|
+
enableFullDocumentLock: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
enableContentPriority: false,
|
|
36
|
+
enableFullDocumentLock: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
101
39
|
function buildChunkIntentText(chunk) {
|
|
102
40
|
return [
|
|
103
41
|
chunk.title,
|
|
@@ -131,16 +69,21 @@ function applyDiagnosisIntentGuard(ranked, chunksById, prompt) {
|
|
|
131
69
|
adjusted = true;
|
|
132
70
|
return { ...item, score: item.score - penalty };
|
|
133
71
|
});
|
|
134
|
-
return adjusted ? next
|
|
72
|
+
return adjusted ? stableSortRanked(next) : ranked;
|
|
135
73
|
}
|
|
136
74
|
/**
|
|
137
|
-
* A3:
|
|
138
|
-
*
|
|
75
|
+
* A3: best-practice 后处理轻校准(不再做“主通道抢首位”)
|
|
76
|
+
*
|
|
77
|
+
* 目标:
|
|
78
|
+
* - 保留 BP 的可见性优势(在接近相关时可轻微上浮)
|
|
79
|
+
* - 避免强行顶到 Top1 导致“策略叠策略”的排序失真
|
|
80
|
+
*
|
|
81
|
+
* 触发条件(更保守):
|
|
139
82
|
* - 当前 Top1 不是 best-practice
|
|
140
|
-
* -
|
|
141
|
-
* -
|
|
83
|
+
* - Top20 内存在 BP 候选
|
|
84
|
+
* - 候选与 Top1 分差不大(gap <= 20%)且具备基础强度
|
|
142
85
|
*/
|
|
143
|
-
function prioritizeBestPracticeWhenCompetitive(ranked, chunksById,
|
|
86
|
+
function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, _prompt) {
|
|
144
87
|
if (ranked.length < 2)
|
|
145
88
|
return ranked;
|
|
146
89
|
const top = ranked[0];
|
|
@@ -148,14 +91,12 @@ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
|
|
|
148
91
|
if (!topChunk || isBestPracticeSource(topChunk.source) || top.score <= 0) {
|
|
149
92
|
return ranked;
|
|
150
93
|
}
|
|
151
|
-
|
|
94
|
+
const searchWindow = Math.min(ranked.length, 20);
|
|
152
95
|
let bestPracticeIdx = -1;
|
|
153
96
|
let bestPracticeScore = -Infinity;
|
|
154
|
-
for (let i = 1; i <
|
|
97
|
+
for (let i = 1; i < searchWindow; i += 1) {
|
|
155
98
|
const chunk = chunksById.get(ranked[i].id);
|
|
156
|
-
if (!chunk)
|
|
157
|
-
continue;
|
|
158
|
-
if (!isBestPracticeSource(chunk.source))
|
|
99
|
+
if (!chunk || !isBestPracticeSource(chunk.source))
|
|
159
100
|
continue;
|
|
160
101
|
if (ranked[i].score > bestPracticeScore) {
|
|
161
102
|
bestPracticeScore = ranked[i].score;
|
|
@@ -166,71 +107,25 @@ function prioritizeBestPracticeWhenCompetitive(ranked, chunksById, prompt) {
|
|
|
166
107
|
return ranked;
|
|
167
108
|
const candidate = ranked[bestPracticeIdx];
|
|
168
109
|
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)
|
|
110
|
+
const competitiveEnough = gapRatio <= 0.2;
|
|
111
|
+
const strengthEnough = candidate.score >= Math.max(14, top.score * 0.3);
|
|
112
|
+
if (!competitiveEnough || !strengthEnough)
|
|
113
|
+
return ranked;
|
|
114
|
+
// 轻量校准:最多只允许轻微越过 top,避免“强提权抢首位”。
|
|
115
|
+
const lift = Math.max(top.score * 0.03, 1.2);
|
|
116
|
+
const boostedScore = Math.min(top.score * 1.01, candidate.score + lift);
|
|
117
|
+
if (boostedScore <= candidate.score)
|
|
222
118
|
return ranked;
|
|
223
119
|
const next = [...ranked];
|
|
224
|
-
next[bestPracticeIdx] = { ...candidate, score:
|
|
225
|
-
return next
|
|
120
|
+
next[bestPracticeIdx] = { ...candidate, score: boostedScore };
|
|
121
|
+
return stableSortRanked(next);
|
|
226
122
|
}
|
|
227
123
|
/**
|
|
228
|
-
* P0-R1/P0-R2:best-practice 单 chunk
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* 即使分数未达 80 也信任 promote 结果执行收敛。
|
|
124
|
+
* P0-R1/P0-R2:best-practice 单 chunk 收敛(高置信门控版)。
|
|
125
|
+
* 仅当 BP 在跨来源竞争中具备明显优势时,才锁定 preferredSource + fullDocumentMode。
|
|
126
|
+
* 不再因为轻量校准后的微小领先而触发整文锁定。
|
|
232
127
|
*/
|
|
233
|
-
function pickDominantBestPracticeSource(ranked, chunksById) {
|
|
128
|
+
function pickDominantBestPracticeSource(ranked, chunksById, fullDocumentMaxChars) {
|
|
234
129
|
const top = ranked[0];
|
|
235
130
|
if (!top || top.score <= 0)
|
|
236
131
|
return { fullDocumentMode: false };
|
|
@@ -259,11 +154,12 @@ function pickDominantBestPracticeSource(ranked, chunksById) {
|
|
|
259
154
|
.length;
|
|
260
155
|
const sourceSections = [...chunksById.values()].filter((chunk) => chunk.source === source);
|
|
261
156
|
const sourceTotalChars = sourceSections.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
157
|
+
const scoreStrongEnough = top.score >= 85;
|
|
158
|
+
const dominanceStrongEnough = secondCrossScore === 0
|
|
159
|
+
? top.score >= 65
|
|
160
|
+
: (top.score / secondCrossScore) >= 1.25;
|
|
161
|
+
const crossSourceNotCompeting = strongCrossSourceCount === 0;
|
|
162
|
+
const withinLengthBudget = sourceTotalChars <= fullDocumentMaxChars;
|
|
267
163
|
if (scoreStrongEnough && dominanceStrongEnough && crossSourceNotCompeting && withinLengthBudget) {
|
|
268
164
|
return {
|
|
269
165
|
preferredSource: source,
|
|
@@ -276,6 +172,51 @@ function isIntegrationSource(source) {
|
|
|
276
172
|
return /\/integration\.md$/i.test(source)
|
|
277
173
|
|| /\/preset\/.+integration/i.test(source);
|
|
278
174
|
}
|
|
175
|
+
function isNavigationIntent(prompt) {
|
|
176
|
+
return /文档|链接|地址|url|网址|入口|在哪看|在哪里看|官网|文档地址|sdk文档/i.test(prompt);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 通用语义仲裁(非 integration 特化):
|
|
180
|
+
* 当 top1 落在 urls 映射行(导航型来源),且向量 top1 指向内容型文档时,
|
|
181
|
+
* 对“非导航意图”的 query 优先选择内容文档来源。
|
|
182
|
+
*
|
|
183
|
+
* 目的:避免 BM25 在 urls 索引行上的词面优势压制真实内容页。
|
|
184
|
+
*/
|
|
185
|
+
function resolveSemanticSourcePriorityByVector(params) {
|
|
186
|
+
const { ranked, chunksById, vectorTopId, prompt, fullDocumentMaxChars, } = params;
|
|
187
|
+
if (!vectorTopId || ranked.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
if (isNavigationIntent(prompt))
|
|
190
|
+
return null;
|
|
191
|
+
const top = ranked[0];
|
|
192
|
+
const topChunk = chunksById.get(top.id);
|
|
193
|
+
const vectorTopChunk = chunksById.get(vectorTopId);
|
|
194
|
+
if (!topChunk || !vectorTopChunk)
|
|
195
|
+
return null;
|
|
196
|
+
// 仅在“导航来源(top1) vs 内容来源(vectorTop)”冲突时触发
|
|
197
|
+
if (!isUrlMappingSource(topChunk.source))
|
|
198
|
+
return null;
|
|
199
|
+
if (isUrlMappingSource(vectorTopChunk.source))
|
|
200
|
+
return null;
|
|
201
|
+
if (topChunk.source === vectorTopChunk.source)
|
|
202
|
+
return null;
|
|
203
|
+
const vectorTopRanked = ranked.find((item) => item.id === vectorTopId);
|
|
204
|
+
if (!vectorTopRanked)
|
|
205
|
+
return null;
|
|
206
|
+
// 置信门槛:vector top 不能离 top1 太远,避免误切
|
|
207
|
+
const scoreGapOk = vectorTopRanked.score >= top.score * 0.45;
|
|
208
|
+
if (!scoreGapOk)
|
|
209
|
+
return null;
|
|
210
|
+
const sourceTotalChars = [...chunksById.values()]
|
|
211
|
+
.filter((chunk) => chunk.source === vectorTopChunk.source)
|
|
212
|
+
.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
213
|
+
if (sourceTotalChars > fullDocumentMaxChars)
|
|
214
|
+
return null;
|
|
215
|
+
return {
|
|
216
|
+
preferredSource: vectorTopChunk.source,
|
|
217
|
+
fullDocumentMode: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
279
220
|
/**
|
|
280
221
|
* 覆盖度竞争:在 A3 之前执行。
|
|
281
222
|
* 当 integration 的多个 section 被查询广泛命中时,选择 integration 完整文档;
|
|
@@ -287,16 +228,16 @@ function isIntegrationSource(source) {
|
|
|
287
228
|
* - BP 分数远高于 integration(>1.2×)→ 直接返回 null
|
|
288
229
|
* - 否则 → 返回 null
|
|
289
230
|
*/
|
|
290
|
-
function resolveContentPriority(ranked, chunksById) {
|
|
231
|
+
function resolveContentPriority(ranked, chunksById, fullDocumentMaxChars) {
|
|
291
232
|
if (ranked.length < 2)
|
|
292
233
|
return null;
|
|
293
234
|
// 找 BP 最高分(在 top-30 范围内)
|
|
294
235
|
let bpTopScore = -1;
|
|
295
236
|
for (let i = 0; i < Math.min(ranked.length, 30); i++) {
|
|
296
237
|
const chunk = chunksById.get(ranked[i].id);
|
|
297
|
-
if (!chunk
|
|
238
|
+
if (!chunk)
|
|
298
239
|
continue;
|
|
299
|
-
if (ranked[i].score > bpTopScore) {
|
|
240
|
+
if (isBestPracticeSource(chunk.source) && ranked[i].score > bpTopScore) {
|
|
300
241
|
bpTopScore = ranked[i].score;
|
|
301
242
|
}
|
|
302
243
|
}
|
|
@@ -334,9 +275,13 @@ function resolveContentPriority(ranked, chunksById) {
|
|
|
334
275
|
bestIntTopScore = stats.topScore;
|
|
335
276
|
}
|
|
336
277
|
}
|
|
278
|
+
const integrationSourceTotalChars = [...chunksById.values()]
|
|
279
|
+
.filter((chunk) => chunk.source === bestIntSource)
|
|
280
|
+
.reduce((sum, chunk) => sum + chunk.content.length, 0);
|
|
281
|
+
const withinLengthBudget = integrationSourceTotalChars <= fullDocumentMaxChars;
|
|
337
282
|
// 无 BP 候选 → 选 integration(如果有足够 section 命中)
|
|
338
283
|
if (bpTopScore < 0) {
|
|
339
|
-
return bestHitCount >= 2
|
|
284
|
+
return bestHitCount >= 2 && withinLengthBudget
|
|
340
285
|
? { preferredSource: bestIntSource, fullDocumentMode: true }
|
|
341
286
|
: null;
|
|
342
287
|
}
|
|
@@ -345,21 +290,248 @@ function resolveContentPriority(ranked, chunksById) {
|
|
|
345
290
|
return null;
|
|
346
291
|
// 覆盖度判定
|
|
347
292
|
const BREADTH_THRESHOLD = 3;
|
|
348
|
-
if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6) {
|
|
293
|
+
if (bestHitCount >= BREADTH_THRESHOLD && bestIntTopScore >= bpTopScore * 0.6 && withinLengthBudget) {
|
|
349
294
|
return { preferredSource: bestIntSource, fullDocumentMode: true };
|
|
350
295
|
}
|
|
351
296
|
return null;
|
|
352
297
|
}
|
|
353
|
-
|
|
298
|
+
const DEFAULT_VECTOR_TOP_K = 30;
|
|
299
|
+
const DEFAULT_RRF_K = 60;
|
|
300
|
+
const DEFAULT_RERANK_TOP_K = 20;
|
|
301
|
+
function readEnvNumber(name, fallback) {
|
|
302
|
+
const raw = process.env[name];
|
|
303
|
+
if (raw === undefined || raw === '')
|
|
304
|
+
return fallback;
|
|
305
|
+
const val = Number(raw);
|
|
306
|
+
return Number.isFinite(val) ? val : fallback;
|
|
307
|
+
}
|
|
308
|
+
function stableSortRanked(items) {
|
|
309
|
+
return [...items].sort((a, b) => {
|
|
310
|
+
if (b.score !== a.score)
|
|
311
|
+
return b.score - a.score;
|
|
312
|
+
return a.id.localeCompare(b.id);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function inferIotFeatureDomainFromPrompt(prompt) {
|
|
316
|
+
if (!prompt)
|
|
317
|
+
return undefined;
|
|
318
|
+
if (/安防摄像头|云台|\bPTZ\b|语音对讲|监控|回放|录像|录制|截图/i.test(prompt)) {
|
|
319
|
+
return 'security-camera';
|
|
320
|
+
}
|
|
321
|
+
if (/儿童手表/i.test(prompt))
|
|
322
|
+
return 'kids-watch';
|
|
323
|
+
if (/录音卡|语音卡/i.test(prompt))
|
|
324
|
+
return 'voice-recorder';
|
|
325
|
+
if (/智能视觉|视觉服务/i.test(prompt))
|
|
326
|
+
return 'smart-vision';
|
|
327
|
+
if (/设备端|linux|rtos|模组|芯片/i.test(prompt))
|
|
328
|
+
return 'device';
|
|
329
|
+
if (/客户端\s*api|client\s*api|\bTXIoT[A-Za-z0-9_]+\b/i.test(prompt))
|
|
330
|
+
return 'client_api';
|
|
331
|
+
if (/app\s*demo|示例工程|demo/i.test(prompt))
|
|
332
|
+
return 'app_demo';
|
|
333
|
+
if (/应用端接入|app接入|app\s*集成/i.test(prompt))
|
|
334
|
+
return 'app_integration';
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* IoT 强 feature 意图 → source 路径关键词映射。
|
|
339
|
+
* 中文意图词(云台/语音对讲)与英文文件名(ptz-control/voice-intercom)无直接词面关系,
|
|
340
|
+
* 需显式映射才能在排序层精准置顶对应 feature 文档,避免被短文本泛文档的 BM25 优势压制。
|
|
341
|
+
*
|
|
342
|
+
* 注意:仅收录「一 feature 一精准文件」的映射。像 monitor-playback 这类分布在
|
|
343
|
+
* 多端侧(app/device/security-camera)的 feature 不在此列,避免无差别 source boost
|
|
344
|
+
* 把 device 侧文档抬高、压制 app 侧(见 regression case 22)。
|
|
345
|
+
*/
|
|
346
|
+
const IOT_FEATURE_INTENT_MAP = [
|
|
347
|
+
{ re: /云台|\bPTZ\b/i, keyword: 'ptz-control' },
|
|
348
|
+
{ re: /语音对讲|对讲|intercom/i, keyword: 'voice-intercom' },
|
|
349
|
+
];
|
|
350
|
+
function inferIotFeatureSourceKeyword(prompt) {
|
|
351
|
+
if (!prompt)
|
|
352
|
+
return undefined;
|
|
353
|
+
for (const { re, keyword } of IOT_FEATURE_INTENT_MAP) {
|
|
354
|
+
if (re.test(prompt))
|
|
355
|
+
return keyword;
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
function inferPreferredIotFeatureDomain(ranked, chunksById, prompt) {
|
|
360
|
+
const byPrompt = inferIotFeatureDomainFromPrompt(prompt ?? '');
|
|
361
|
+
if (byPrompt)
|
|
362
|
+
return byPrompt;
|
|
363
|
+
const scoreByDomain = new Map();
|
|
364
|
+
const sampleWindow = Math.min(ranked.length, 80);
|
|
365
|
+
for (let i = 0; i < sampleWindow; i += 1) {
|
|
366
|
+
const item = ranked[i];
|
|
367
|
+
const chunk = chunksById.get(item.id);
|
|
368
|
+
if (!chunk || chunk.product !== 'iot' || !chunk.feature_domain)
|
|
369
|
+
continue;
|
|
370
|
+
const domain = chunk.feature_domain;
|
|
371
|
+
scoreByDomain.set(domain, (scoreByDomain.get(domain) ?? 0) + Math.max(item.score, 0));
|
|
372
|
+
}
|
|
373
|
+
let preferred;
|
|
374
|
+
let best = -1;
|
|
375
|
+
for (const [domain, score] of scoreByDomain.entries()) {
|
|
376
|
+
if (score > best) {
|
|
377
|
+
best = score;
|
|
378
|
+
preferred = domain;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return preferred;
|
|
382
|
+
}
|
|
383
|
+
function fuseByRRF(params) {
|
|
384
|
+
const { bm25Ranked, vectorHits, rrfK, baseBeta, alpha, profile, } = params;
|
|
385
|
+
const bmWeight = profile?.bmWeight ?? 1;
|
|
386
|
+
const vecWeight = profile?.vecWeight ?? 1;
|
|
387
|
+
const alphaScale = profile?.alphaScale ?? 1;
|
|
388
|
+
if (!bm25Ranked.length && !vectorHits.length) {
|
|
389
|
+
return { ranked: [], bm25AdjustedCount: 0, vectorOnlyAddedCount: 0 };
|
|
390
|
+
}
|
|
391
|
+
const topBM25 = bm25Ranked[0]?.score ?? baseBeta;
|
|
392
|
+
const bm25ById = new Map(bm25Ranked.map((item) => [item.id, item]));
|
|
393
|
+
const bm25Rank = new Map();
|
|
394
|
+
bm25Ranked.forEach((item, idx) => bm25Rank.set(item.id, idx + 1));
|
|
395
|
+
const vecRank = new Map();
|
|
396
|
+
vectorHits.forEach((hit, idx) => vecRank.set(hit.id, idx + 1));
|
|
397
|
+
const vecMin = vectorHits.length ? Math.min(...vectorHits.map((h) => h.score)) : 0;
|
|
398
|
+
const vecMax = vectorHits.length ? Math.max(...vectorHits.map((h) => h.score)) : 1;
|
|
399
|
+
const vecRange = vecMax - vecMin || 1;
|
|
400
|
+
const vecNorm = new Map();
|
|
401
|
+
for (const hit of vectorHits) {
|
|
402
|
+
vecNorm.set(hit.id, (hit.score - vecMin) / vecRange);
|
|
403
|
+
}
|
|
404
|
+
const unionIds = new Set([
|
|
405
|
+
...bm25Ranked.map((item) => item.id),
|
|
406
|
+
...vectorHits.map((item) => item.id),
|
|
407
|
+
]);
|
|
408
|
+
const rrfRaw = new Map();
|
|
409
|
+
let maxRRF = 0;
|
|
410
|
+
for (const id of unionIds) {
|
|
411
|
+
const bRank = bm25Rank.get(id);
|
|
412
|
+
const vRank = vecRank.get(id);
|
|
413
|
+
const score = (bRank ? bmWeight / (rrfK + bRank) : 0)
|
|
414
|
+
+ (vRank ? vecWeight / (rrfK + vRank) : 0);
|
|
415
|
+
rrfRaw.set(id, score);
|
|
416
|
+
if (score > maxRRF)
|
|
417
|
+
maxRRF = score;
|
|
418
|
+
}
|
|
419
|
+
let bm25AdjustedCount = 0;
|
|
420
|
+
let vectorOnlyAddedCount = 0;
|
|
421
|
+
// 类别权重也影响 hybrid 融合权重,避免 rrf 权重与 sim 权重口径分裂。
|
|
422
|
+
const totalWeight = bmWeight + vecWeight || 1;
|
|
423
|
+
const simWeight = vecWeight / totalWeight; // 0..1
|
|
424
|
+
const rrfWeight = 1 - simWeight;
|
|
425
|
+
const fused = [...unionIds].map((id) => {
|
|
426
|
+
const original = bm25ById.get(id);
|
|
427
|
+
const rrfNorm = maxRRF > 0 ? (rrfRaw.get(id) ?? 0) / maxRRF : 0;
|
|
428
|
+
const simNorm = vecNorm.get(id) ?? 0;
|
|
429
|
+
const hybridNorm = rrfWeight * rrfNorm + simWeight * simNorm;
|
|
430
|
+
const effectiveAlpha = alpha * alphaScale;
|
|
431
|
+
if (original) {
|
|
432
|
+
const hasVectorSignal = vecRank.has(id);
|
|
433
|
+
if (hasVectorSignal)
|
|
434
|
+
bm25AdjustedCount += 1;
|
|
435
|
+
const boost = hasVectorSignal ? original.score * effectiveAlpha * 0.25 * hybridNorm : 0;
|
|
436
|
+
return { id, score: original.score + boost };
|
|
437
|
+
}
|
|
438
|
+
vectorOnlyAddedCount += 1;
|
|
439
|
+
return { id, score: topBM25 * baseBeta * (1 + effectiveAlpha * 0.9 * hybridNorm) };
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
ranked: stableSortRanked(fused),
|
|
443
|
+
bm25AdjustedCount,
|
|
444
|
+
vectorOnlyAddedCount,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function resolveFusionProfile(prompt, queryApiTokens) {
|
|
448
|
+
const trimmed = prompt.trim();
|
|
449
|
+
if (queryApiTokens.length > 0) {
|
|
450
|
+
return { name: 'api_token', bmWeight: 1.3, vecWeight: 0.8, alphaScale: 0.7 };
|
|
451
|
+
}
|
|
452
|
+
// 长自然语言:字符数或分词粒度足够
|
|
453
|
+
const tokenCount = tokenize(trimmed).filter((t) => t.length >= 2).length;
|
|
454
|
+
const isLongNatural = trimmed.length >= 14 || tokenCount >= 6;
|
|
455
|
+
if (isLongNatural) {
|
|
456
|
+
return { name: 'natural_language', bmWeight: 0.85, vecWeight: 1.2, alphaScale: 1.15 };
|
|
457
|
+
}
|
|
458
|
+
const isShortKeyword = trimmed.length <= 6 && tokenCount <= 2;
|
|
459
|
+
if (isShortKeyword) {
|
|
460
|
+
return { name: 'short_keyword', bmWeight: 1.0, vecWeight: 1.0, alphaScale: 0.9 };
|
|
461
|
+
}
|
|
462
|
+
return { name: 'default', bmWeight: 1.0, vecWeight: 1.0, alphaScale: 1.0 };
|
|
463
|
+
}
|
|
464
|
+
function normalizeCompactText(input) {
|
|
465
|
+
return input.toLowerCase().replace(/[\s\-_]+/g, '');
|
|
466
|
+
}
|
|
467
|
+
function hasTitleExactMatch(prompt, title) {
|
|
468
|
+
if (!prompt || !title)
|
|
469
|
+
return false;
|
|
470
|
+
const p = normalizeCompactText(prompt);
|
|
471
|
+
const t = normalizeCompactText(title);
|
|
472
|
+
if (!p || !t)
|
|
473
|
+
return false;
|
|
474
|
+
if (t.length >= 3 && p.includes(t))
|
|
475
|
+
return true;
|
|
476
|
+
if (p.length >= 3 && t.includes(p))
|
|
477
|
+
return true;
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
function shouldTriggerLowConfidenceRerank(params) {
|
|
481
|
+
const { ranked, rankProfile } = params;
|
|
482
|
+
if (rankProfile.lowConfidenceRerankTopK <= 0 || ranked.length < 2)
|
|
483
|
+
return false;
|
|
484
|
+
const top = ranked[0]?.score ?? 0;
|
|
485
|
+
const second = ranked[1]?.score ?? 0;
|
|
486
|
+
if (top <= 0)
|
|
487
|
+
return false;
|
|
488
|
+
const ratio = second > 0 ? top / second : Number.POSITIVE_INFINITY;
|
|
489
|
+
return top <= rankProfile.lowConfidenceTopScoreThreshold || ratio <= rankProfile.lowConfidenceGapRatioThreshold;
|
|
490
|
+
}
|
|
491
|
+
function rerankTopK(params) {
|
|
492
|
+
const { prompt, ranked, chunksById, topK, rankProfile, } = params;
|
|
493
|
+
if (ranked.length < 2 || topK <= 0)
|
|
494
|
+
return { ranked, applied: false };
|
|
495
|
+
const head = ranked.slice(0, topK);
|
|
496
|
+
const tail = ranked.slice(topK);
|
|
497
|
+
const promptTokens = [...new Set(tokenize(prompt).filter((t) => t.length >= 2))];
|
|
498
|
+
if (!promptTokens.length)
|
|
499
|
+
return { ranked, applied: false };
|
|
500
|
+
const topScore = head[0]?.score ?? 0;
|
|
501
|
+
const bumped = head.map((item) => {
|
|
502
|
+
const chunk = chunksById.get(item.id);
|
|
503
|
+
if (!chunk)
|
|
504
|
+
return item;
|
|
505
|
+
const text = [chunk.title, ...(chunk.key_phrases ?? [])].join(' ').toLowerCase();
|
|
506
|
+
const overlap = promptTokens.reduce((acc, token) => (text.includes(token.toLowerCase()) ? acc + 1 : acc), 0);
|
|
507
|
+
if (overlap <= 0 && !hasTitleExactMatch(prompt, chunk.title))
|
|
508
|
+
return item;
|
|
509
|
+
let boost = topScore * 0.002 * overlap;
|
|
510
|
+
if (hasTitleExactMatch(prompt, chunk.title)) {
|
|
511
|
+
boost += Math.max(topScore * 0.015, 1.5) * (rankProfile.titleExactMultiplier - 1);
|
|
512
|
+
}
|
|
513
|
+
boost += rankProfile.titleTokenBoost * overlap;
|
|
514
|
+
return { ...item, score: item.score + boost };
|
|
515
|
+
});
|
|
516
|
+
const rerankedHead = stableSortRanked(bumped);
|
|
517
|
+
const applied = rerankedHead.some((item, idx) => item.id !== head[idx]?.id);
|
|
518
|
+
return { ranked: [...rerankedHead, ...tail], applied };
|
|
519
|
+
}
|
|
520
|
+
export async function runSearchPass(params) {
|
|
354
521
|
const { allChunks, passNorm, retrievalMode, queryApiTokens, opts, } = params;
|
|
355
522
|
const effectivePassNorm = retrievalMode === 'discovery'
|
|
356
523
|
? { ...passNorm, types: undefined }
|
|
357
524
|
: passNorm;
|
|
525
|
+
const rankProfile = resolveRankProfile({ product: effectivePassNorm.product });
|
|
358
526
|
let pool = filterPool(allChunks, effectivePassNorm);
|
|
359
527
|
// types 误传自愈:首轮保留,二次扩检也同样保留该能力
|
|
360
528
|
if (pool.length === 0 && effectivePassNorm.types?.length) {
|
|
361
529
|
pool = filterPool(allChunks, { ...effectivePassNorm, types: undefined });
|
|
362
530
|
}
|
|
531
|
+
// 分层过滤:向量语义召回仅受 Hard 层(product + retrievable_scope)约束,
|
|
532
|
+
// framework/types/scenario 转到 applyRankMultipliers 做软降权,保留向量的跨平台/类型自愈能力。
|
|
533
|
+
const hardPool = filterPoolHard(allChunks, effectivePassNorm);
|
|
534
|
+
const hardChunksById = new Map(hardPool.map((chunk) => [chunk.id, chunk]));
|
|
363
535
|
const { boosts, exactHits, urlRowHitChunkIds } = runExactLookup(effectivePassNorm.prompt, pool, effectivePassNorm);
|
|
364
536
|
// A2 补充正向路由:UserSig 问法下允许 share 命中且优先于无关 urls 噪声
|
|
365
537
|
if (/usersig|sdkappid|secretkey|鉴权|签名|gentestusersig/i.test(passNorm.prompt)) {
|
|
@@ -387,36 +559,189 @@ export function runSearchPass(params) {
|
|
|
387
559
|
boosts[id] = (boosts[id] ?? 0) + amount;
|
|
388
560
|
}
|
|
389
561
|
const ranked = rankSubsetWithIndex(effectivePassNorm.prompt, pool.map((chunk) => chunk.id), productIndex, boosts);
|
|
390
|
-
|
|
562
|
+
// 固定链路:Hybrid(BM25 + Vector)→ RRF 融合 → 轻量重排。
|
|
563
|
+
// 兼容开关:DISABLE_VECTOR_AUGMENT=1 时只关闭向量相关步骤,BM25 主链路保持不变。
|
|
564
|
+
const vectorAugmentDisabled = process.env.DISABLE_VECTOR_AUGMENT === '1';
|
|
565
|
+
const alphaOverride = process.env.VECTOR_AUGMENT_ALPHA
|
|
566
|
+
? Number(process.env.VECTOR_AUGMENT_ALPHA)
|
|
567
|
+
: undefined;
|
|
568
|
+
const alpha = alphaOverride !== undefined && !Number.isNaN(alphaOverride) ? alphaOverride : 0.15;
|
|
569
|
+
const baseBeta = readEnvNumber('VECTOR_AUGMENT_BASE_BETA', 0.3);
|
|
570
|
+
const topK = Math.max(1, Math.floor(readEnvNumber('VECTOR_TOP_K', DEFAULT_VECTOR_TOP_K)));
|
|
571
|
+
const rrfK = Math.max(1, Math.floor(readEnvNumber('VECTOR_RRF_K', DEFAULT_RRF_K)));
|
|
572
|
+
const rerankTopKLimit = Math.max(0, Math.floor(readEnvNumber('VECTOR_RERANK_TOP_K', rankProfile.rerankTopK || DEFAULT_RERANK_TOP_K)));
|
|
573
|
+
const fullDocumentMaxChars = Math.max(1000, Math.floor(readEnvNumber('SEARCH_FULL_DOCUMENT_MAX_CHARS', 7000)));
|
|
574
|
+
let rankedFused = ranked;
|
|
575
|
+
// 向量归因埋点采集(终态归因在函数末尾统一构造):
|
|
576
|
+
// - bm25TopIdSnapshot:向量介入前的 BM25 单路头名(即 ranked[0],融合前的原始头名)
|
|
577
|
+
// - vectorTopIdSnapshot:向量单路头名(vecHits[0],经 hardChunksById 过滤后)
|
|
578
|
+
// - vecScoreById:向量单路每个候选的 sim 分,供 final_top1_id 查表得 vector_score_of_final_top1
|
|
579
|
+
// - bm25RankById:向量介入前 BM25 单路的 1-based 名次索引,供 rank_shift 与 promoted_by_vector_count 计算
|
|
580
|
+
const bm25TopIdSnapshot = ranked[0]?.id ?? null;
|
|
581
|
+
const bm25RankById = new Map();
|
|
582
|
+
ranked.forEach((item, idx) => bm25RankById.set(item.id, idx + 1));
|
|
583
|
+
let vectorTopIdSnapshot = null;
|
|
584
|
+
let vecScoreById = new Map();
|
|
585
|
+
if (!vectorAugmentDisabled) {
|
|
586
|
+
try {
|
|
587
|
+
const [qvecInst, qvecRaw] = await Promise.all([
|
|
588
|
+
embedQuery(withQueryInstruction(effectivePassNorm.prompt)),
|
|
589
|
+
embedQuery(effectivePassNorm.prompt),
|
|
590
|
+
]);
|
|
591
|
+
const [hitsInst, hitsRaw] = await Promise.all([
|
|
592
|
+
queryVectors(passNorm.product, qvecInst, topK),
|
|
593
|
+
queryVectors(passNorm.product, qvecRaw, topK),
|
|
594
|
+
]);
|
|
595
|
+
const merged = new Map();
|
|
596
|
+
for (const h of hitsInst) {
|
|
597
|
+
merged.set(h.id, Math.max(merged.get(h.id) ?? Number.NEGATIVE_INFINITY, h.score));
|
|
598
|
+
}
|
|
599
|
+
for (const h of hitsRaw) {
|
|
600
|
+
const weighted = h.score * 0.97;
|
|
601
|
+
merged.set(h.id, Math.max(merged.get(h.id) ?? Number.NEGATIVE_INFINITY, weighted));
|
|
602
|
+
}
|
|
603
|
+
const hits = [...merged.entries()]
|
|
604
|
+
.map(([id, score]) => ({ id, score }))
|
|
605
|
+
.sort((a, b) => {
|
|
606
|
+
if (b.score !== a.score)
|
|
607
|
+
return b.score - a.score;
|
|
608
|
+
return a.id.localeCompare(b.id);
|
|
609
|
+
})
|
|
610
|
+
.slice(0, topK);
|
|
611
|
+
// 分层过滤:向量仅受 Hard 层(product + retrievable_scope)约束,
|
|
612
|
+
// framework/types/scenario 交给 applyRankMultipliers 做软降权。
|
|
613
|
+
// 向量补召回但不在 BM25 pool 内的 chunk 需要挂到 chunksById,供下游 rerank / multiplier / postProcess 读取。
|
|
614
|
+
const vecHits = hits.filter((h) => hardChunksById.has(h.id));
|
|
615
|
+
for (const hit of vecHits) {
|
|
616
|
+
if (chunksById.has(hit.id))
|
|
617
|
+
continue;
|
|
618
|
+
const chunk = hardChunksById.get(hit.id);
|
|
619
|
+
if (!chunk)
|
|
620
|
+
continue;
|
|
621
|
+
chunksById.set(hit.id, attachChunkContent(chunk));
|
|
622
|
+
}
|
|
623
|
+
vectorTopIdSnapshot = vecHits[0]?.id ?? null;
|
|
624
|
+
vecScoreById = new Map(vecHits.map((h) => [h.id, h.score]));
|
|
625
|
+
if (vecHits.length > 0) {
|
|
626
|
+
const fusionProfile = resolveFusionProfile(effectivePassNorm.prompt, queryApiTokens);
|
|
627
|
+
const fused = fuseByRRF({
|
|
628
|
+
bm25Ranked: ranked,
|
|
629
|
+
vectorHits: vecHits,
|
|
630
|
+
rrfK,
|
|
631
|
+
baseBeta,
|
|
632
|
+
alpha,
|
|
633
|
+
profile: fusionProfile,
|
|
634
|
+
});
|
|
635
|
+
const reranked = rerankTopK({
|
|
636
|
+
prompt: effectivePassNorm.prompt,
|
|
637
|
+
ranked: fused.ranked,
|
|
638
|
+
chunksById,
|
|
639
|
+
topK: rerankTopKLimit,
|
|
640
|
+
rankProfile,
|
|
641
|
+
});
|
|
642
|
+
rankedFused = reranked.ranked;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// 向量链路失败(embed/query 异常):保持 rankedFused = ranked,
|
|
647
|
+
// 快照变量维持初始值即可。归因阶段 vector_top1_id 为 null 天然反映降级。
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const preferredIotFeatureDomain = inferPreferredIotFeatureDomain(rankedFused, chunksById, passNorm.prompt);
|
|
651
|
+
const preferredIotFeatureSource = inferIotFeatureSourceKeyword(passNorm.prompt);
|
|
652
|
+
const rankedWithMultipliers = applyRankMultipliers(rankedFused, chunksById, {
|
|
391
653
|
product: passNorm.product,
|
|
392
654
|
framework: passNorm.framework,
|
|
393
655
|
frameworks: passNorm.frameworks,
|
|
656
|
+
preferred_iot_feature_domain: preferredIotFeatureDomain,
|
|
657
|
+
preferred_iot_feature_source: preferredIotFeatureSource,
|
|
658
|
+
prompt: passNorm.prompt,
|
|
659
|
+
rank_profile: rankProfile.name,
|
|
394
660
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
661
|
+
let rankedAfterIntentGuard = applyDiagnosisIntentGuard(rankedWithMultipliers, chunksById, passNorm.prompt);
|
|
662
|
+
if (shouldTriggerLowConfidenceRerank({ ranked: rankedAfterIntentGuard, rankProfile })) {
|
|
663
|
+
const secondPass = rerankTopK({
|
|
664
|
+
prompt: passNorm.prompt,
|
|
665
|
+
ranked: rankedAfterIntentGuard,
|
|
666
|
+
chunksById,
|
|
667
|
+
topK: rankProfile.lowConfidenceRerankTopK,
|
|
668
|
+
rankProfile,
|
|
669
|
+
});
|
|
670
|
+
if (secondPass.applied) {
|
|
671
|
+
rankedAfterIntentGuard = secondPass.ranked;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const promptLockPolicy = decidePromptLockPolicy(passNorm.prompt);
|
|
675
|
+
const allowContentPriority = promptLockPolicy.enableContentPriority;
|
|
676
|
+
const allowFullDocumentLock = promptLockPolicy.enableFullDocumentLock;
|
|
677
|
+
// 覆盖度竞争先于 A3 执行(仅在前置策略允许时开启)
|
|
678
|
+
// 若覆盖度竞争未触发,再尝试“导航来源 vs 内容来源”的通用语义仲裁。
|
|
679
|
+
const integrationPriority = allowContentPriority
|
|
680
|
+
? (resolveContentPriority(rankedAfterIntentGuard, chunksById, fullDocumentMaxChars)
|
|
681
|
+
?? resolveSemanticSourcePriorityByVector({
|
|
682
|
+
ranked: rankedAfterIntentGuard,
|
|
683
|
+
chunksById,
|
|
684
|
+
vectorTopId: vectorTopIdSnapshot,
|
|
685
|
+
prompt: passNorm.prompt,
|
|
686
|
+
fullDocumentMaxChars,
|
|
687
|
+
}))
|
|
688
|
+
: null;
|
|
398
689
|
let dominantBestPractice;
|
|
399
690
|
let rankedAfterBestPractice;
|
|
400
691
|
if (integrationPriority) {
|
|
401
692
|
// integration 胜出 → 跳过 A3,直接走完整文档路径
|
|
402
693
|
rankedAfterBestPractice = rankedAfterIntentGuard;
|
|
403
|
-
dominantBestPractice =
|
|
694
|
+
dominantBestPractice = allowFullDocumentLock
|
|
695
|
+
? integrationPriority
|
|
696
|
+
: { fullDocumentMode: false };
|
|
404
697
|
}
|
|
405
698
|
else {
|
|
406
699
|
// integration 未胜出 → A3 提升 BP → BP 收敛判定
|
|
407
700
|
rankedAfterBestPractice = prioritizeBestPracticeWhenCompetitive(rankedAfterIntentGuard, chunksById, passNorm.prompt);
|
|
408
|
-
dominantBestPractice =
|
|
701
|
+
dominantBestPractice = allowFullDocumentLock
|
|
702
|
+
? pickDominantBestPracticeSource(rankedAfterBestPractice, chunksById, fullDocumentMaxChars)
|
|
703
|
+
: { fullDocumentMode: false };
|
|
409
704
|
}
|
|
410
705
|
const pinnedUrlRowChunkIds = new Set(urlRowHitChunkIds);
|
|
706
|
+
// 独立信号:存在精确 URL 命中 → 启用 35% 断崖截断。
|
|
707
|
+
// 与 pin 语义解耦:即便 pin 集合发生变化或 BP 校准影响 fragments 组成,
|
|
708
|
+
// 截断策略仍由“是否存在精确 URL 命中”这一确定性信号决定。
|
|
709
|
+
const strictCutoffOnExactUrlHit = pinnedUrlRowChunkIds.size > 0;
|
|
411
710
|
const fragments = postProcess(rankedAfterBestPractice, chunksById, passNorm.limit, {
|
|
412
711
|
pinnedUrlRowChunkIds,
|
|
712
|
+
strictCutoffOnExactUrlHit,
|
|
413
713
|
preferredSource: dominantBestPractice.preferredSource,
|
|
414
714
|
fullDocumentMode: dominantBestPractice.fullDocumentMode,
|
|
715
|
+
fullDocumentMaxChars,
|
|
716
|
+
maxPerSource: rankProfile.maxPerSource,
|
|
415
717
|
});
|
|
416
718
|
const urlMappingSources = [...new Set(fragments.map((f) => f.source).filter((source) => isUrlMappingSource(source)))];
|
|
417
719
|
const references = buildReferences(fragments);
|
|
418
720
|
const apiNameCoverage = computeApiNameCoverage(queryApiTokens, poolWithContent);
|
|
419
721
|
const missingApiTokens = computeMissingApiTokens(queryApiTokens, poolWithContent);
|
|
722
|
+
// === 向量归因(终态构造)===
|
|
723
|
+
// final_top1_id:postProcess 之后 fragments[0] 的 chunk_id(enrichL1Fragments 后续 in-place enrich
|
|
724
|
+
// content 但不改动 chunk_id / 顺序,所以此处快照即最终外部感知的 top1)。
|
|
725
|
+
const finalTop1Id = fragments[0]?.chunk_id ?? null;
|
|
726
|
+
const finalTop1BmRank = finalTop1Id ? (bm25RankById.get(finalTop1Id) ?? null) : null;
|
|
727
|
+
const finalTop1VecScore = finalTop1Id ? (vecScoreById.get(finalTop1Id) ?? null) : null;
|
|
728
|
+
// promoted_by_vector_count:BM25 原始名次 > 3 且经融合/后处理进入 final top3 的 chunk 数量。
|
|
729
|
+
// 只需交集 |{chunk : bm25Rank(chunk) > 3 AND chunk ∈ finalTop3}|;ranked 为空自然记 0。
|
|
730
|
+
const finalTop3Ids = fragments.slice(0, 3).map((f) => f.chunk_id);
|
|
731
|
+
let promotedByVectorCount = 0;
|
|
732
|
+
for (const id of finalTop3Ids) {
|
|
733
|
+
const bmRank = bm25RankById.get(id);
|
|
734
|
+
if (bmRank !== undefined && bmRank > 3)
|
|
735
|
+
promotedByVectorCount += 1;
|
|
736
|
+
}
|
|
737
|
+
const vectorDiagnostics = {
|
|
738
|
+
bm25_top1_id: bm25TopIdSnapshot,
|
|
739
|
+
vector_top1_id: vectorTopIdSnapshot,
|
|
740
|
+
final_top1_id: finalTop1Id,
|
|
741
|
+
rank_shift_from_bm25: finalTop1BmRank,
|
|
742
|
+
vector_score_of_final_top1: finalTop1VecScore,
|
|
743
|
+
promoted_by_vector_count: promotedByVectorCount,
|
|
744
|
+
};
|
|
420
745
|
return {
|
|
421
746
|
pool,
|
|
422
747
|
poolWithContent,
|
|
@@ -429,6 +754,7 @@ export function runSearchPass(params) {
|
|
|
429
754
|
missingApiTokens,
|
|
430
755
|
urlMapRelaxed: pinnedUrlRowChunkIds.size > 0,
|
|
431
756
|
urlMapExactHitCount: pinnedUrlRowChunkIds.size,
|
|
757
|
+
vectorDiagnostics,
|
|
432
758
|
preferredSource: dominantBestPractice.preferredSource,
|
|
433
759
|
fullDocumentMode: dominantBestPractice.fullDocumentMode,
|
|
434
760
|
};
|