dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -19
- package/cordis.patch.yml +25 -10
- package/lib/client.js +6400 -3326
- package/lib/doc-page.js +7761 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +63 -11
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +7 -4
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +52 -14
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/convert/types.d.ts +2 -2
- package/lib/types/convert/types.d.ts.map +1 -1
- package/lib/types/convert/types.js +2 -2
- package/lib/types/convert/types.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +57 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +245 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +19 -2
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +95 -25
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +297 -166
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/extract-model.d.ts +67 -27
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js +225 -97
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract.d.ts +16 -0
- package/lib/types/extract.d.ts.map +1 -1
- package/lib/types/extract.js +19 -2
- package/lib/types/extract.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +11 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +41 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +466 -63
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/types.js +1 -1
- package/lib/types/intent/types.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +23 -12
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +37 -44
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/layout.d.ts +3 -3
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +4 -4
- package/lib/types/layout.js.map +1 -1
- package/lib/types/migrate.d.ts +1 -1
- package/lib/types/migrate.js +2 -2
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +99 -0
- package/lib/types/model-text.d.ts.map +1 -0
- package/lib/types/model-text.js +103 -0
- package/lib/types/model-text.js.map +1 -0
- package/lib/types/present.d.ts +239 -0
- package/lib/types/present.d.ts.map +1 -0
- package/lib/types/present.js +255 -0
- package/lib/types/present.js.map +1 -0
- package/lib/types/recall-rank.d.ts +37 -0
- package/lib/types/recall-rank.d.ts.map +1 -1
- package/lib/types/recall-rank.js +41 -1
- package/lib/types/recall-rank.js.map +1 -1
- package/lib/types/runtime.d.ts +564 -31
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +2645 -292
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +4 -2
- package/lib/types/settings-schema.d.ts.map +1 -1
- package/lib/types/settings-schema.js +2 -0
- package/lib/types/settings-schema.js.map +1 -1
- package/lib/types/source-images.d.ts +118 -0
- package/lib/types/source-images.d.ts.map +1 -0
- package/lib/types/source-images.js +288 -0
- package/lib/types/source-images.js.map +1 -0
- package/lib/types/types.d.ts +356 -28
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +112 -18
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +140 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -0
- package/lib/types/wiki-finalize.js +269 -0
- package/lib/types/wiki-finalize.js.map +1 -0
- package/lib/types/wiki-types.d.ts +10 -2
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +27 -30
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +25 -10
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/DrawerSection.tsx +18 -0
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +458 -158
- package/src/client/KnowledgePanel.tsx +44 -10
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/KnowledgeToolRow.tsx +331 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +178 -43
- package/src/client/RecallPanel.tsx +418 -127
- package/src/client/TypeDefinitionDrawer.tsx +154 -54
- package/src/client/WikiCardReview.tsx +266 -0
- package/src/client/WikiFlowCard.tsx +57 -95
- package/src/client/WikiTypeEditor.tsx +197 -95
- package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +25 -10
- package/src/client/architecture.tsx +66 -41
- package/src/client/asset-images.ts +125 -0
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +501 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +280 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +36 -4
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +242 -0
- package/src/client/index.ts +18 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/problem-domains.ts +1 -1
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +397 -54
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +68 -9
- package/src/convert/ingest.ts +61 -18
- package/src/convert/types.ts +2 -2
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +251 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +29 -4
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +310 -173
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/extract-model.ts +254 -99
- package/src/extract.ts +35 -3
- package/src/fs-atomic.ts +41 -0
- package/src/index.ts +499 -62
- package/src/intent/types.ts +1 -1
- package/src/knowledge-bases.ts +50 -49
- package/src/layout.ts +4 -4
- package/src/migrate.ts +2 -2
- package/src/model-text.ts +126 -0
- package/src/present.ts +367 -0
- package/src/recall-rank.ts +57 -2
- package/src/runtime.ts +2802 -293
- package/src/settings-schema.ts +2 -0
- package/src/source-images.ts +326 -0
- package/src/types.ts +446 -45
- package/src/wiki-finalize.ts +310 -0
- package/src/wiki-types.ts +28 -32
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* `chunk_index` / `score` 一起给出 —— 只报「命中 N 条」看不出召回质量。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useEffect, useState, type ReactNode } from 'react'
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { Fragment, useEffect, useState, type ReactNode } from 'react'
|
|
13
|
+
import { ArrowUpDown, ChevronRight, CircleHelp, GitBranch, Play, Search } from '@tita-data-agent/ui/icons'
|
|
14
|
+
import { Button, Card, Input, SelectMenu, cn } from '@tita-data-agent/ui/components'
|
|
15
15
|
import { styles } from './styles.ts'
|
|
16
16
|
|
|
17
17
|
/** 一条命中的 wikiNote。`match` 是**真正贡献了这次命中**的路线。 */
|
|
@@ -24,6 +24,8 @@ export interface RecallHitView {
|
|
|
24
24
|
score: number
|
|
25
25
|
status?: string
|
|
26
26
|
entityType?: string
|
|
27
|
+
/** 这条 note 属于哪张卡片(点开预览用)。 */
|
|
28
|
+
entityId?: string
|
|
27
29
|
rerankScore?: number
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -36,8 +38,47 @@ export interface ContextItemView {
|
|
|
36
38
|
rerankScore?: number
|
|
37
39
|
wikiTitle?: string
|
|
38
40
|
wikiId?: string
|
|
41
|
+
/** 这张卡片是命中的还是 1 跳扩展来的(0050)。 */
|
|
42
|
+
reason?: 'hit' | 'expansion'
|
|
43
|
+
/** 扩展条目的来路:「哪张卡(哪条边)」。 */
|
|
44
|
+
via?: string
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
/** 账本里的一行(0050):这一路对这个候选算出的分数与名次。 */
|
|
48
|
+
export interface LedgerRowView {
|
|
49
|
+
unitId: string
|
|
50
|
+
title: string
|
|
51
|
+
entityType: string
|
|
52
|
+
entityId: string
|
|
53
|
+
score: number
|
|
54
|
+
rank: number
|
|
55
|
+
kept: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 去重后的命中卡片(0050)。 */
|
|
59
|
+
export interface WikiCardView {
|
|
60
|
+
entityId: string
|
|
61
|
+
entityType: string
|
|
62
|
+
title: string
|
|
63
|
+
summary?: string
|
|
64
|
+
/** 它有几条 note 进了上下文(命中的小节数)。 */
|
|
65
|
+
noteCount: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 1 跳扩展进来的卡片(0050)。 */
|
|
69
|
+
export interface ExpansionNodeView {
|
|
70
|
+
entityType: string
|
|
71
|
+
entityId: string
|
|
72
|
+
title: string
|
|
73
|
+
/** 来路:`来源卡片的 key|关系`。 */
|
|
74
|
+
via: string[]
|
|
75
|
+
/** 真的进了上下文吗(被预算挡住的也在列表里,只是标出来)。 */
|
|
76
|
+
included: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 召回链路的三个步骤(0050 改版:召回 → 重排 → 扩展)。 */
|
|
80
|
+
export type RecallStep = 'recall' | 'rerank' | 'expand'
|
|
81
|
+
|
|
41
82
|
export interface RecallResultView {
|
|
42
83
|
count: number
|
|
43
84
|
backend: string
|
|
@@ -65,6 +106,31 @@ export interface RecallResultView {
|
|
|
65
106
|
encoder?: { ok: boolean; reason?: string }
|
|
66
107
|
hits: RecallHitView[]
|
|
67
108
|
context: ContextItemView[]
|
|
109
|
+
/** 面板账本(0050):每一路的全部候选 + 重排的全部候选(含被丢弃的)。 */
|
|
110
|
+
ledger?: {
|
|
111
|
+
fts: LedgerRowView[]
|
|
112
|
+
vector: LedgerRowView[]
|
|
113
|
+
rerank: Array<LedgerRowView & { fused: number; logit?: number; rerankScore?: number; dropped: boolean }>
|
|
114
|
+
}
|
|
115
|
+
/** 去重后的命中卡片(0050)。 */
|
|
116
|
+
wiki: WikiCardView[]
|
|
117
|
+
/** 1 跳扩展(0050)。 */
|
|
118
|
+
expansion?: { nodes: ExpansionNodeView[]; edgeCount: number; included: string[] }
|
|
119
|
+
/**
|
|
120
|
+
* 各阶段耗时(毫秒,0050 第二版)。
|
|
121
|
+
*
|
|
122
|
+
* 它进界面的理由:这几个数量级差得很远(查询编码 270ms vs 首次加载 8s),
|
|
123
|
+
* 只给「这次测试很慢」的感受,人没法判断该不该管它。
|
|
124
|
+
*/
|
|
125
|
+
timings?: {
|
|
126
|
+
ftsMs: number; vectorMs: number; embedMs?: number; scanMs?: number
|
|
127
|
+
graphMs: number; fuseMs: number; rerankMs?: number; assembleMs: number; totalMs: number
|
|
128
|
+
}
|
|
129
|
+
/** 命中几张卡 + 1 跳扩展几张(0050)。 */
|
|
130
|
+
hitCards?: number
|
|
131
|
+
expandedCards?: number
|
|
132
|
+
omittedCards?: number
|
|
133
|
+
overflowPath?: string
|
|
68
134
|
kbName?: string
|
|
69
135
|
indexVersion?: number
|
|
70
136
|
}
|
|
@@ -85,6 +151,95 @@ async function requestJson<T>(url: string): Promise<Envelope<T>> {
|
|
|
85
151
|
return payload
|
|
86
152
|
}
|
|
87
153
|
|
|
154
|
+
/**
|
|
155
|
+
* 账本列表(0050):**每一路的全部候选**,没过阈值的也在(虚线 + 淡色 + 一句原因)。
|
|
156
|
+
*
|
|
157
|
+
* 它与 `HitList` 的分工:`HitList` 是「这次召回最后剩了什么」(融合 + 重排之后),
|
|
158
|
+
* 这里是「这一路各自看到了什么、丢了什么」。面板要回答「为什么没召回」,靠的是后者。
|
|
159
|
+
*/
|
|
160
|
+
export function LedgerList({ rows, scoreLabel, droppedLabel, emptyLabel }: {
|
|
161
|
+
rows: Array<LedgerRowView & { fused?: number; logit?: number; rerankScore?: number; dropped?: boolean }>
|
|
162
|
+
scoreLabel: (row: LedgerRowView & { fused?: number; logit?: number; rerankScore?: number }) => string
|
|
163
|
+
droppedLabel?: string
|
|
164
|
+
emptyLabel?: string
|
|
165
|
+
}): ReactNode {
|
|
166
|
+
if (rows.length === 0) return <div className={styles.emptyState}><CircleHelp size={16} />{emptyLabel ?? '这一路没有候选'}</div>
|
|
167
|
+
return <ul className={styles.hitList}>
|
|
168
|
+
{rows.map(row => {
|
|
169
|
+
const dropped = row.kept === false || row.dropped === true
|
|
170
|
+
return <li key={row.unitId} className={cn(styles.hitRow, dropped && styles.hitRowDropped)}>
|
|
171
|
+
<div className={styles.hitHead}>
|
|
172
|
+
<span className={styles.hitRank}>{row.rank}</span>
|
|
173
|
+
<span className={styles.hitTitle} title={row.unitId}>{row.title}</span>
|
|
174
|
+
{dropped && <span className={styles.hitDropped}>{droppedLabel ?? '已丢弃'}</span>}
|
|
175
|
+
<span className={styles.hitScore}>{scoreLabel(row)}</span>
|
|
176
|
+
</div>
|
|
177
|
+
<div className={styles.hitFoot}>
|
|
178
|
+
<span className={styles.kbCode}>{row.entityType}/{row.entityId}</span>
|
|
179
|
+
</div>
|
|
180
|
+
</li>
|
|
181
|
+
})}
|
|
182
|
+
</ul>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 命中卡片(去重后)与 1 跳扩展两块(0050 的「扩展」tab)。 */
|
|
186
|
+
export function ExpansionView({ wiki, expansion, context, onOpenWiki }: {
|
|
187
|
+
wiki: WikiCardView[]
|
|
188
|
+
expansion?: RecallResultView['expansion']
|
|
189
|
+
context: ContextItemView[]
|
|
190
|
+
onOpenWiki?: OpenWikiCard
|
|
191
|
+
}): ReactNode {
|
|
192
|
+
const expandedInContext = context.filter(item => item.reason === 'expansion')
|
|
193
|
+
/** 卡片标题:宿主给了抽屉就是按钮,没给就是纯文本 —— 面板不自己造一个抽屉出来。 */
|
|
194
|
+
const cardTitle = (card: { entityType: string; entityId: string; title: string }): ReactNode =>
|
|
195
|
+
onOpenWiki
|
|
196
|
+
? <button type="button" className={styles.cardLink} onClick={() => onOpenWiki(card)}>{card.title}</button>
|
|
197
|
+
: <span className={styles.hitTitle}>{card.title}</span>
|
|
198
|
+
return <>
|
|
199
|
+
<div className={styles.kbSubHead}><h4>命中的 wiki 卡片(按卡片去重后)</h4><span>{wiki.length} 张</span></div>
|
|
200
|
+
<p className={styles.kbHint}>召回与重排的对象是 wikiNotes(片段),这里是把它们折回卡片的结果:同一张卡片的多个小节只算一张,等级取它最高那条 note。</p>
|
|
201
|
+
{wiki.length === 0
|
|
202
|
+
? <div className={styles.emptyState}><CircleHelp size={16} />这次没有命中的卡片</div>
|
|
203
|
+
: <ul className={styles.hitList}>
|
|
204
|
+
{wiki.map(card => <li key={`${card.entityType}:${card.entityId}`} className={styles.hitRow}>
|
|
205
|
+
<div className={styles.hitHead}>
|
|
206
|
+
{cardTitle(card)}
|
|
207
|
+
<span className={styles.hitScore}>{card.noteCount} 条片段</span>
|
|
208
|
+
</div>
|
|
209
|
+
{card.summary && <p className={styles.hitSnippet}>{card.summary}</p>}
|
|
210
|
+
<div className={styles.hitFoot}><span className={styles.kbCode}>{card.entityType}/{card.entityId}</span></div>
|
|
211
|
+
</li>)}
|
|
212
|
+
</ul>}
|
|
213
|
+
|
|
214
|
+
<div className={styles.kbSubHead}>
|
|
215
|
+
<h4>1 跳知识图谱扩展</h4>
|
|
216
|
+
<span>{expansion?.nodes.length ?? 0} 张卡片 · {expansion?.edgeCount ?? 0} 条边</span>
|
|
217
|
+
</div>
|
|
218
|
+
<p className={styles.kbHint}>从上面那些卡片出发,沿图上的关系边与「共同来源」各走一跳。<b>它在去重之后发生</b> —— 所以这里列的都是邻居,不是召回来的。</p>
|
|
219
|
+
{(expansion?.nodes.length ?? 0) === 0
|
|
220
|
+
? <div className={styles.emptyState}><CircleHelp size={16} />这些卡片在图上没有邻居</div>
|
|
221
|
+
: <ul className={styles.hitList}>
|
|
222
|
+
{expansion?.nodes.map(node => <li key={`${node.entityType}:${node.entityId}`} className={cn(styles.hitRow, !node.included && styles.hitRowDropped)}>
|
|
223
|
+
<div className={styles.hitHead}>
|
|
224
|
+
{cardTitle({ entityType: node.entityType, entityId: node.entityId, title: node.title })}
|
|
225
|
+
{!node.included && <span className={styles.hitDropped}>超预算</span>}
|
|
226
|
+
<span className={styles.hitScore}>{node.via.length} 条来路</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div className={styles.hitFoot}>
|
|
229
|
+
<span className={styles.kbCode}>{node.entityType}/{node.entityId}</span>
|
|
230
|
+
<span>来自 {node.via.map(via => via.split('|')[0]).join('、')}</span>
|
|
231
|
+
</div>
|
|
232
|
+
</li>)}
|
|
233
|
+
</ul>}
|
|
234
|
+
{(expansion?.nodes.length ?? 0) > 0 && <p className={styles.kbHint}>
|
|
235
|
+
其中 {expandedInContext.length} 张进了上下文
|
|
236
|
+
{expandedInContext.length < (expansion?.nodes.length ?? 0)
|
|
237
|
+
? `,其余 ${(expansion?.nodes.length ?? 0) - expandedInContext.length} 张被上下文预算挡在外面(完整内容在溢出文件里)`
|
|
238
|
+
: ''}。
|
|
239
|
+
</p>}
|
|
240
|
+
</>
|
|
241
|
+
}
|
|
242
|
+
|
|
88
243
|
/**
|
|
89
244
|
* 召回是**两路**(关键词 BM25 + 向量)加一个图谱 boost(0031)。
|
|
90
245
|
*
|
|
@@ -96,17 +251,23 @@ export const RECALL_ROUTES: Array<{ id: string; label: string; hint: string }> =
|
|
|
96
251
|
{ id: 'vector', label: '向量', hint: 'WeMM 向量近邻(binary 预筛 + int8 精算余弦)' },
|
|
97
252
|
]
|
|
98
253
|
|
|
254
|
+
/**
|
|
255
|
+
* 路线选项**只有两路**(0050 改版)。
|
|
256
|
+
*
|
|
257
|
+
* 图谱 boost 不是一条路(它不能制造候选,只给已在池里的条目加权),所以它不该出现在
|
|
258
|
+
* 「选哪一路」的下拉里 —— 上一版那两条「融合 + 图谱 boost」「同融合」正是这个错:
|
|
259
|
+
* 把加权当成了路线。融合是两路的**组合方式**,写在选项文字里,不是第三个选项。
|
|
260
|
+
*/
|
|
99
261
|
export const RECALL_MODES: Array<{ id: string; label: string }> = [
|
|
100
|
-
{ id: 'hybrid', label: '
|
|
262
|
+
{ id: 'hybrid', label: '关键词 + 向量(融合,默认)' },
|
|
101
263
|
{ id: 'fts', label: '仅关键词' },
|
|
102
264
|
{ id: 'vector', label: '仅向量' },
|
|
103
|
-
{ id: 'graph', label: '同融合(图谱已不是独立一路)' },
|
|
104
265
|
]
|
|
105
266
|
|
|
106
267
|
const HIT_ROUTES: Record<string, string> = {
|
|
107
268
|
fts: styles.hitRouteFts, vector: styles.hitRouteVector, graph: styles.hitRouteGraph, hybrid: styles.hitRouteHybrid,
|
|
108
269
|
}
|
|
109
|
-
const HIT_ROUTE_LABEL: Record<string, string> = { fts: '关键词', vector: '向量',
|
|
270
|
+
const HIT_ROUTE_LABEL: Record<string, string> = { fts: '关键词', vector: '向量', hybrid: '关键词+向量', graph: '图谱加权' }
|
|
110
271
|
|
|
111
272
|
/** 去掉 markdown 脚手架,让命中读起来是一段正文而不是源码。 */
|
|
112
273
|
export function snippetOf(content: string, limit = 200): string {
|
|
@@ -119,86 +280,38 @@ export function snippetOf(content: string, limit = 200): string {
|
|
|
119
280
|
return plain.length > limit ? `${plain.slice(0, limit)}…` : plain
|
|
120
281
|
}
|
|
121
282
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
*
|
|
125
|
-
* 0031 起它读的是 `diagnostics.routes` 而不是「`routes` 里有没有这个 id」:**降级也要看得见**。
|
|
126
|
-
* 原来的写法只能表达「跑没跑」,于是向量路坏掉时芯片显示「(未跑)」而 `routes` 里却写着
|
|
127
|
-
* `vector` —— 同一屏上两处互相矛盾,而且都没说为什么。
|
|
128
|
-
*/
|
|
129
|
-
export function RecallRoutes({ routes, backend, routesDetail, graphBoost }: {
|
|
130
|
-
routes: string[]
|
|
131
|
-
backend?: string
|
|
132
|
-
routesDetail?: RecallResultView['routesDetail']
|
|
133
|
-
graphBoost?: RecallResultView['graphBoost']
|
|
134
|
-
}): ReactNode {
|
|
135
|
-
const active = new Set(routes.map(route => route.toLocaleLowerCase()))
|
|
136
|
-
type RouteDetail = NonNullable<RecallResultView['routesDetail']>[number]
|
|
137
|
-
const detailOf = (id: string): RouteDetail | undefined => routesDetail?.find(item => item.route === id)
|
|
138
|
-
return <div className={styles.routeRow} aria-label="召回路线">
|
|
139
|
-
{RECALL_ROUTES.map(route => {
|
|
140
|
-
const detail = detailOf(route.id)
|
|
141
|
-
const ran = detail ? detail.status === 'ran' : (active.has(route.id) || (route.id === 'fts' && active.has('keyword')))
|
|
142
|
-
const degraded = detail?.status === 'degraded'
|
|
143
|
-
const title = degraded && detail?.reason ? `${route.hint} —— ${detail.reason}` : route.hint
|
|
144
|
-
const suffix = degraded ? '(降级)' : ran ? (detail?.kept !== undefined ? ` ${detail.kept}` : '') : '(未跑)'
|
|
145
|
-
return <span key={route.id} className={cn(styles.routeChip, ran && styles.routeChipOn, degraded && styles.routeChipOff)} title={title}>
|
|
146
|
-
{route.label}{suffix}
|
|
147
|
-
</span>
|
|
148
|
-
})}
|
|
149
|
-
{/* 图谱是 boost:显示它加权了几条,而不是「跑没跑」。 */}
|
|
150
|
-
<span className={styles.routeChip} title="图谱只给已在候选池里的条目加权,不能自己制造候选">
|
|
151
|
-
图谱 boost {graphBoost && graphBoost.applied > 0 ? `+${graphBoost.applied}` : '未生效'}
|
|
152
|
-
</span>
|
|
153
|
-
<span className={styles.routeBackend}>{backend ? backend.toUpperCase() : 'FTS'}</span>
|
|
154
|
-
</div>
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function HitList({ hits }: { hits: RecallHitView[] }): ReactNode {
|
|
283
|
+
function HitList({ hits, onOpenWiki }: { hits: RecallHitView[]; onOpenWiki?: OpenWikiCard }): ReactNode {
|
|
284
|
+
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
|
158
285
|
if (hits.length === 0) return null
|
|
159
286
|
return <ul className={styles.hitList}>
|
|
160
|
-
{hits.map(hit =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<div className={styles.hitFoot}>
|
|
168
|
-
<span className={styles.kbCode}>{hit.unitId}</span>
|
|
169
|
-
{hit.status && <span>状态 {hit.status}</span>}
|
|
170
|
-
{hit.entityType && <span>{hit.entityType}</span>}
|
|
171
|
-
{hit.rerankScore !== undefined && <span>rerank {hit.rerankScore.toFixed(3)}</span>}
|
|
172
|
-
</div>
|
|
173
|
-
</li>)}
|
|
174
|
-
</ul>
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 进入 Context Pack 的 topN。重排这一栏必须显示得分的**来源**:
|
|
179
|
-
* rerank 真跑过就给 rerank 分数,降级时按融合分数排就只给融合分数,不混着显示。
|
|
180
|
-
*/
|
|
181
|
-
function ContextList({ items, provider }: { items: ContextItemView[]; provider?: string }): ReactNode {
|
|
182
|
-
if (items.length === 0) return null
|
|
183
|
-
const reranked = provider !== 'none' && items.some(item => item.rerankScore !== undefined)
|
|
184
|
-
return <ol className={styles.contextList}>
|
|
185
|
-
{items.map((item, index) => <li key={item.unitId} className={styles.contextRow}>
|
|
186
|
-
<span className={styles.contextRank}>{index + 1}</span>
|
|
187
|
-
<div className={styles.contextCopy}>
|
|
188
|
-
<div className={styles.contextHead}>
|
|
189
|
-
<span className={styles.hitTitle} title={item.unitId}>{item.title}</span>
|
|
190
|
-
<span className={styles.hitScore}>{reranked && item.rerankScore !== undefined ? `rerank ${item.rerankScore.toFixed(3)}` : `融合 ${item.score.toFixed(3)}`}</span>
|
|
287
|
+
{hits.map(hit => {
|
|
288
|
+
const open = expanded[hit.unitId] === true
|
|
289
|
+
return <li key={hit.unitId} className={styles.hitRow}>
|
|
290
|
+
<div className={styles.hitHead}>
|
|
291
|
+
<span className={cn(styles.hitRoute, HIT_ROUTES[hit.match] ?? styles.hitRouteFts)}>{HIT_ROUTE_LABEL[hit.match] ?? hit.match}</span>
|
|
292
|
+
<span className={styles.hitTitle} title={hit.unitId}>{hit.title}{hit.heading ? ` · ${hit.heading}` : ''}</span>
|
|
293
|
+
<span className={styles.hitScore}>{hit.score.toFixed(3)}</span>
|
|
191
294
|
</div>
|
|
192
|
-
|
|
295
|
+
{/* 正文默认只占两行:一条笔记的片段常有几百字,全摊开一屏只能看两条。
|
|
296
|
+
要读全文就点「展开正文」—— 数据没被截掉,只是不再默认占满屏幕。 */}
|
|
297
|
+
<p className={cn(styles.hitSnippet, !open && styles.hitSnippetClamp)}>{hit.snippet || '(这条 wikiNote 没有正文)'}</p>
|
|
193
298
|
<div className={styles.hitFoot}>
|
|
194
|
-
<span className={styles.kbCode}>{
|
|
195
|
-
{
|
|
299
|
+
<span className={styles.kbCode}>{hit.unitId}</span>
|
|
300
|
+
{hit.status && <span>状态 {hit.status}</span>}
|
|
301
|
+
{hit.rerankScore !== undefined && <span>rerank {hit.rerankScore.toFixed(3)}</span>}
|
|
302
|
+
{onOpenWiki && hit.entityType && hit.entityId && <button type="button" className={styles.hitToggle}
|
|
303
|
+
onClick={() => onOpenWiki({ entityType: hit.entityType!, entityId: hit.entityId!, title: hit.title })}>打开卡片</button>}
|
|
304
|
+
{hit.snippet.length > 80 && <button type="button" className={styles.hitToggle}
|
|
305
|
+
onClick={() => setExpanded(current => ({ ...current, [hit.unitId]: !open }))}>{open ? '收起' : '展开正文'}</button>}
|
|
196
306
|
</div>
|
|
197
|
-
</
|
|
198
|
-
|
|
199
|
-
</
|
|
307
|
+
</li>
|
|
308
|
+
})}
|
|
309
|
+
</ul>
|
|
200
310
|
}
|
|
201
311
|
|
|
312
|
+
/** 打开某张 wiki 卡片的预览抽屉(由宿主提供;召回的扩展结果要能点进去看)。 */
|
|
313
|
+
export type OpenWikiCard = (card: { entityType: string; entityId: string; title: string }) => void
|
|
314
|
+
|
|
202
315
|
export interface RecallPanelProps {
|
|
203
316
|
/** 作用范围:当前知识库;空表示默认库。 */
|
|
204
317
|
kbId?: string
|
|
@@ -207,7 +320,7 @@ export interface RecallPanelProps {
|
|
|
207
320
|
/** `recall` 看命中切片,`rerank` 看进入上下文的 topN,`runner` 是评测里的紧凑版。 */
|
|
208
321
|
/**
|
|
209
322
|
* `recall` = 知识库 → **召回重排**那一栏(0033 起召回与重排合成一栏,一次运行
|
|
210
|
-
*
|
|
323
|
+
* 从上到下给出两路命中 → 融合排序 → 精排丢了多少 → 进上下文的 topN)。
|
|
211
324
|
* `runner` = 评测里的紧凑版单问题检查。
|
|
212
325
|
*/
|
|
213
326
|
variant?: 'recall' | 'runner'
|
|
@@ -220,9 +333,11 @@ export interface RecallPanelProps {
|
|
|
220
333
|
* 参数住在设置抽屉里,这里只是那扇离现场更近的门。
|
|
221
334
|
*/
|
|
222
335
|
settingsChip?: ReactNode
|
|
336
|
+
/** 点某张卡片时打开它的预览抽屉。不给就是纯文本(面板不自己造一个抽屉出来)。 */
|
|
337
|
+
onOpenWiki?: OpenWikiCard
|
|
223
338
|
}
|
|
224
339
|
|
|
225
|
-
export function RecallPanel({ kbId, kbName, visible, variant = 'recall', defaults, resetKey, settingsChip }: RecallPanelProps): ReactNode {
|
|
340
|
+
export function RecallPanel({ kbId, kbName, visible, variant = 'recall', defaults, resetKey, settingsChip, onOpenWiki }: RecallPanelProps): ReactNode {
|
|
226
341
|
const [query, setQuery] = useState('销售额')
|
|
227
342
|
const [mode, setMode] = useState(defaults?.mode ?? 'hybrid')
|
|
228
343
|
const [topK, setTopK] = useState(defaults?.topK ?? 10)
|
|
@@ -230,6 +345,10 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
230
345
|
const [busy, setBusy] = useState(false)
|
|
231
346
|
const [error, setError] = useState<string | null>(null)
|
|
232
347
|
const [result, setResult] = useState<RecallResultView | null>(null)
|
|
348
|
+
/** 三个 tab(0050):召回 / 重排 / 扩展 —— 一条链路的三段,各自回答一个问题。 */
|
|
349
|
+
const [tab, setTab] = useState<RecallStep>('recall')
|
|
350
|
+
/** 召回 tab 里正在看哪一路(`all` = 看融合后的命中)。 */
|
|
351
|
+
const [routeView, setRouteView] = useState<'all' | 'fts' | 'vector'>('all')
|
|
233
352
|
|
|
234
353
|
// 知识库换了或设置存了:面板上的默认值跟着走,否则显示的还是上一个库的参数。
|
|
235
354
|
// 用 effect 而不是渲染期 setState:这条同步是「外部输入变了」而不是渲染推导。
|
|
@@ -252,9 +371,16 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
252
371
|
const boundedTopN = Math.min(topN, topK)
|
|
253
372
|
const params = new URLSearchParams({ query: trimmed, top_k: String(topK), top_n: String(boundedTopN), mode })
|
|
254
373
|
if (kbId) params.set('kb', kbId)
|
|
374
|
+
// `detail=true`:面板账本(每一路的候选、重排的全部候选)。工具调用不带它。
|
|
255
375
|
const payload = await requestJson<{
|
|
256
376
|
hits?: Array<Partial<RecallHitView> & { content?: string }>
|
|
257
|
-
context?: Array<{
|
|
377
|
+
context?: Array<{
|
|
378
|
+
note?: Partial<RecallHitView> & { content?: string }
|
|
379
|
+
wiki?: { entityId?: string; entityType?: string; title?: string; summary?: string } | null
|
|
380
|
+
reason?: 'hit' | 'expansion'
|
|
381
|
+
via?: { title?: string; relation?: string }
|
|
382
|
+
}>
|
|
383
|
+
budget?: { hitCards?: number; expandedCards?: number; omittedCards?: number; overflowPath?: string }
|
|
258
384
|
backend?: string
|
|
259
385
|
routes?: string[]
|
|
260
386
|
recallTopK?: number
|
|
@@ -269,6 +395,14 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
269
395
|
encoder?: { ok: boolean; reason?: string }
|
|
270
396
|
}
|
|
271
397
|
indexVersion?: number
|
|
398
|
+
ledger?: {
|
|
399
|
+
fts?: LedgerRowView[]
|
|
400
|
+
vector?: LedgerRowView[]
|
|
401
|
+
rerank?: Array<LedgerRowView & { fused?: number; logit?: number; rerankScore?: number; dropped?: boolean }>
|
|
402
|
+
}
|
|
403
|
+
wiki?: Array<{ entityId?: string; entityType?: string; title?: string; summary?: string }>
|
|
404
|
+
expansion?: { nodes?: Array<{ entityType?: string; entityId?: string; title?: string; via?: string[] }>; edges?: unknown[]; included?: string[] }
|
|
405
|
+
timings?: RecallResultView['timings']
|
|
272
406
|
}>(`/knowledge/search?${params.toString()}`)
|
|
273
407
|
const data = payload.data ?? {}
|
|
274
408
|
const hits: RecallHitView[] = (data.hits ?? []).map(hit => ({
|
|
@@ -280,22 +414,39 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
280
414
|
score: Number(hit.score ?? 0),
|
|
281
415
|
...(hit.status ? { status: hit.status } : {}),
|
|
282
416
|
...(hit.entityType ? { entityType: hit.entityType } : {}),
|
|
417
|
+
...(hit.entityId ? { entityId: hit.entityId } : {}),
|
|
283
418
|
...(hit.rerankScore === undefined ? {} : { rerankScore: hit.rerankScore }),
|
|
284
419
|
}))
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
420
|
+
/**
|
|
421
|
+
* 0050 起上下文按**卡片**去重:一条 = 一张卡片 + 它命中的那些 note。
|
|
422
|
+
* 1 跳扩展来的卡片没有 note,所以这里用卡片自己兜底,并带上 `reason` ——
|
|
423
|
+
* 界面必须能分清「召回来的」与「邻居」(前者是证据,后者是延伸)。
|
|
424
|
+
*/
|
|
425
|
+
const context: ContextItemView[] = (data.context ?? []).map(item => {
|
|
426
|
+
const note = item.note
|
|
427
|
+
const card = item.wiki
|
|
428
|
+
const expansion = item.reason === 'expansion'
|
|
429
|
+
return {
|
|
430
|
+
unitId: note?.unitId ?? (card?.entityType && card?.entityId ? `${card.entityType}:${card.entityId}` : 'unknown'),
|
|
431
|
+
title: note?.title ?? card?.title ?? '未命名',
|
|
432
|
+
snippet: snippetOf(note?.content ?? card?.summary ?? ''),
|
|
433
|
+
score: Number(note?.score ?? 0),
|
|
434
|
+
reason: expansion ? 'expansion' : 'hit',
|
|
435
|
+
...(note?.rerankScore === undefined ? {} : { rerankScore: note.rerankScore }),
|
|
436
|
+
...(card?.title ? { wikiTitle: card.title } : {}),
|
|
437
|
+
...(card?.entityId ? { wikiId: card.entityId } : {}),
|
|
438
|
+
...(expansion && item.via ? { via: `${item.via.title}(${item.via.relation})` } : {}),
|
|
439
|
+
}
|
|
440
|
+
})
|
|
294
441
|
setResult({
|
|
295
442
|
count: hits.length,
|
|
296
443
|
backend: data.backend ?? 'fts',
|
|
297
444
|
routes: data.routes,
|
|
298
445
|
contextCount: context.length,
|
|
446
|
+
...(data.budget?.hitCards === undefined ? {} : { hitCards: data.budget.hitCards }),
|
|
447
|
+
...(data.budget?.expandedCards === undefined ? {} : { expandedCards: data.budget.expandedCards }),
|
|
448
|
+
...(data.budget?.omittedCards === undefined ? {} : { omittedCards: data.budget.omittedCards }),
|
|
449
|
+
...(data.budget?.overflowPath === undefined ? {} : { overflowPath: data.budget.overflowPath }),
|
|
299
450
|
recallTopK: topK,
|
|
300
451
|
candidatePool: data.candidatePool,
|
|
301
452
|
contextTopN: Math.min(topN, topK),
|
|
@@ -313,6 +464,35 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
313
464
|
...(data.diagnostics?.encoder === undefined ? {} : { encoder: data.diagnostics.encoder }),
|
|
314
465
|
hits,
|
|
315
466
|
context,
|
|
467
|
+
// 账本(0050):每一路的全部候选 + 重排的全部候选。缺字段时给空数组而不是
|
|
468
|
+
// undefined —— 界面按「这一路没有候选」渲染,与「这次没取到账本」不是一回事,
|
|
469
|
+
// 而后者只可能出现在老服务端上。
|
|
470
|
+
ledger: {
|
|
471
|
+
fts: data.ledger?.fts ?? [],
|
|
472
|
+
vector: data.ledger?.vector ?? [],
|
|
473
|
+
rerank: (data.ledger?.rerank ?? []).map(row => ({ ...row, fused: Number(row.fused ?? row.score ?? 0), dropped: row.dropped === true })),
|
|
474
|
+
},
|
|
475
|
+
wiki: (data.wiki ?? []).map(card => ({
|
|
476
|
+
entityId: card.entityId ?? '',
|
|
477
|
+
entityType: card.entityType ?? '',
|
|
478
|
+
title: card.title ?? card.entityId ?? '未命名卡片',
|
|
479
|
+
...(card.summary ? { summary: card.summary } : {}),
|
|
480
|
+
noteCount: context.filter(item => item.wikiId === card.entityId && item.reason === 'hit').length,
|
|
481
|
+
})),
|
|
482
|
+
...(data.expansion === undefined ? {} : {
|
|
483
|
+
expansion: {
|
|
484
|
+
nodes: (data.expansion?.nodes ?? []).map(node => ({
|
|
485
|
+
entityType: node.entityType ?? '',
|
|
486
|
+
entityId: node.entityId ?? '',
|
|
487
|
+
title: node.title ?? node.entityId ?? '未命名卡片',
|
|
488
|
+
via: node.via ?? [],
|
|
489
|
+
included: (data.expansion?.included ?? []).includes(`${node.entityType}:${node.entityId}`),
|
|
490
|
+
})),
|
|
491
|
+
edgeCount: (data.expansion?.edges ?? []).length,
|
|
492
|
+
included: data.expansion?.included ?? [],
|
|
493
|
+
},
|
|
494
|
+
}),
|
|
495
|
+
...(data.timings === undefined ? {} : { timings: data.timings }),
|
|
316
496
|
...(kbName ? { kbName } : {}),
|
|
317
497
|
...(data.indexVersion === undefined ? {} : { indexVersion: data.indexVersion }),
|
|
318
498
|
})
|
|
@@ -323,22 +503,66 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
323
503
|
}
|
|
324
504
|
}
|
|
325
505
|
|
|
326
|
-
const title = variant === 'runner' ? '单问题召回检查' : '
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
506
|
+
const title = variant === 'runner' ? '单问题召回检查' : '召回 · 精排'
|
|
507
|
+
// 这一栏没有「一句话说明」:那条步骤轨就是说明。每一步带自己的计数,
|
|
508
|
+
// 「两路各自命中了什么 → 融合排序 → 精排丢了多少 → 进上下文的是哪几条」
|
|
509
|
+
// 这句话由轨上的真实数字说,比一句固定文案准,也不会随实现变化而过期。
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 三步的事实行(0050 改版):每一步说清「这一段现在是多少」。
|
|
513
|
+
*
|
|
514
|
+
* 没跑过时写「未运行」,不编一个 0 —— 与知识库那条步骤轨同一条纪律:没有数据的那一步
|
|
515
|
+
* 宁可只写动作。三个步骤**一直在**(它是这条链路的结构,不是运行结果的一部分)。
|
|
516
|
+
*/
|
|
517
|
+
const routesDetail = result?.routesDetail ?? []
|
|
518
|
+
const keptOf = (route: string): number | undefined => routesDetail.find(item => item.route === route)?.kept
|
|
519
|
+
/**
|
|
520
|
+
* 耗时怎么读:小于 1 秒给毫秒(向量扫描只有几毫秒,显示成「0.0s」等于没显示),
|
|
521
|
+
* 超过 1 秒给秒(首次加载模型是 8 秒,写 8300ms 反而难读)。
|
|
522
|
+
*/
|
|
523
|
+
const ms = (value: number | undefined): string => {
|
|
524
|
+
if (value === undefined || !Number.isFinite(value)) return ''
|
|
525
|
+
return value < 1000 ? `${Math.round(value)}ms` : `${(value / 1000).toFixed(1)}s`
|
|
526
|
+
}
|
|
527
|
+
const timings = result?.timings
|
|
528
|
+
const steps: Array<{ id: RecallStep; icon: typeof Search; label: string; verb: string; value: string; note: string; cost: string; costTitle: string }> = [
|
|
529
|
+
{
|
|
530
|
+
id: 'recall', icon: Search, label: '召回', verb: '找',
|
|
531
|
+
value: result ? `关键词 ${keptOf('fts') ?? 0} · 向量 ${keptOf('vector') ?? 0}` : '未运行',
|
|
532
|
+
note: '两路各自过阈值的条数',
|
|
533
|
+
cost: timings ? ms(timings.ftsMs + timings.vectorMs) : '',
|
|
534
|
+
costTitle: timings
|
|
535
|
+
? `关键词 ${ms(timings.ftsMs)}${timings.embedMs === undefined ? '' : ` · 查询编码 ${ms(timings.embedMs)}${timings.scanMs === undefined ? '' : ` · 索引扫描 ${ms(timings.scanMs)}`}`}`
|
|
536
|
+
: '',
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: 'rerank', icon: ArrowUpDown, label: '重排', verb: '排',
|
|
540
|
+
value: result ? `保留 ${result.count} · 丢 ${result.rerankDropped ?? 0}` : '未运行',
|
|
541
|
+
note: result?.rerankThreshold === undefined
|
|
542
|
+
? 'cross-encoder 精排候选池'
|
|
543
|
+
: `阈值 ${result.rerankThreshold} · 最高 ${(result.rerankTopLogit ?? 0).toFixed(2)}`,
|
|
544
|
+
cost: timings?.rerankMs === undefined ? '' : ms(timings.rerankMs),
|
|
545
|
+
costTitle: result?.rerank !== 'ready' ? '这次没跑重排' : 'cross-encoder 打分(首次含加载模型)',
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: 'expand', icon: GitBranch, label: '扩展', verb: '展',
|
|
549
|
+
value: result ? `${result.hitCards ?? 0} 张 + ${result.expandedCards ?? 0} 张` : '未运行',
|
|
550
|
+
note: '命中的卡片 + 1 跳邻居',
|
|
551
|
+
cost: timings ? ms(timings.assembleMs) : '',
|
|
552
|
+
costTitle: timings ? `组装:按卡片去重 + 1 跳扩展 + 上下文预算` : '',
|
|
553
|
+
},
|
|
554
|
+
]
|
|
555
|
+
const finalWiki = (result?.hitCards ?? 0) + (result?.expandedCards ?? 0)
|
|
330
556
|
|
|
331
557
|
return <Card className={styles.workspaceCard}>
|
|
332
558
|
<div className={styles.cardHeader}>
|
|
333
559
|
<div><h3><Search size={14} />{title}</h3></div>
|
|
334
|
-
<div className={styles.cardHeaderActions}>
|
|
335
|
-
{settingsChip}
|
|
336
|
-
<Badge tone={result?.backend === 'hybrid' ? 'success' : 'neutral'}>{result?.backend === 'hybrid' ? '三路融合' : 'FTS fallback'}</Badge>
|
|
337
|
-
</div>
|
|
560
|
+
<div className={styles.cardHeaderActions}>{settingsChip}</div>
|
|
338
561
|
</div>
|
|
339
562
|
<div className={styles.recallTop}>
|
|
340
|
-
<p className={styles.cardDescription}>{description}</p>
|
|
341
563
|
<span className={styles.recallScope}>作用范围 <b>{result?.kbName ?? kbName ?? '默认知识库'}</b></span>
|
|
564
|
+
{/* 总耗时放在作用范围这一行:它是这一屏的抬头,不属于任何一步。 */}
|
|
565
|
+
{result?.timings && <span className={styles.recallTiming} title="从查询进来到上下文组装完成">本次 {ms(result.timings.totalMs)}</span>}
|
|
342
566
|
</div>
|
|
343
567
|
<div className={styles.inputRow}>
|
|
344
568
|
<Input value={query} placeholder="例如:销售额如何计算"
|
|
@@ -348,17 +572,13 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
348
572
|
{variant === 'runner' && <Play size={13} />}{busy ? '测试中…' : variant === 'runner' ? '运行检查' : '测试'}
|
|
349
573
|
</Button>
|
|
350
574
|
</div>
|
|
575
|
+
{/* 旋钮集中在**一处**(0050 第二版):路线、召回 topK、上下文 topN 是同一次运行的参数,
|
|
576
|
+
分散到各步骤里意味着「想改 topN 得先切到第三步」—— 那是把布局当成了状态。 */}
|
|
351
577
|
<div className={styles.recallControls}>
|
|
352
578
|
<div className={styles.recallControl}>
|
|
353
579
|
<span>路线</span>
|
|
354
|
-
<SelectMenu
|
|
355
|
-
|
|
356
|
-
value={mode}
|
|
357
|
-
disabled={busy}
|
|
358
|
-
onValueChange={setMode}
|
|
359
|
-
aria-label="召回路线"
|
|
360
|
-
options={RECALL_MODES.map(item => ({ value: item.id, label: item.label }))}
|
|
361
|
-
/>
|
|
580
|
+
<SelectMenu className="w-auto" value={mode} disabled={busy} onValueChange={setMode} aria-label="召回路线"
|
|
581
|
+
options={RECALL_MODES.map(item => ({ value: item.id, label: item.label }))} />
|
|
362
582
|
</div>
|
|
363
583
|
<label>召回 topK
|
|
364
584
|
<Input type="number" min={1} max={50} value={topK} disabled={busy}
|
|
@@ -370,30 +590,101 @@ export function RecallPanel({ kbId, kbName, visible, variant = 'recall', default
|
|
|
370
590
|
</label>
|
|
371
591
|
</div>
|
|
372
592
|
{error && <p className={cn(styles.kbNotice, styles.kbNoticeError)} role="alert">{error}</p>}
|
|
373
|
-
<RecallRoutes routes={result?.routes ?? []} backend={result?.backend} routesDetail={result?.routesDetail} graphBoost={result?.graphBoost} />
|
|
374
593
|
{/* 弃答不是失败,是一个结论:知识库里没有能回答这个问题的东西。如实说出来,
|
|
375
594
|
而不是把 5 条噪声塞给模型让它编。 */}
|
|
376
595
|
{result?.insufficientEvidence && <p className={cn(styles.kbNotice, styles.kbNoticeError)} role="status">
|
|
377
596
|
<CircleHelp size={13} /><span>知识库里没有能回答这个问题的内容{result.diagnosticsReason ? `:${result.diagnosticsReason}` : ''}(判据:{result.abstention === 'rerank-threshold' ? '重排阈值' : result.abstention === 'vector-threshold' ? '向量余弦阈值' : '关键词相对阈值,判据较弱'})</span>
|
|
378
597
|
</p>}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
598
|
+
|
|
599
|
+
{/* 一条**步骤轨**,不是三个并列的 tab:召回 → 重排 → 扩展是有方向的(找 → 排 → 展),
|
|
600
|
+
方向本身就是信息。每一步带上它自己的计数,所以「这次到底发生了什么」不用再写一句
|
|
601
|
+
技术总结 —— 那句话说不了三步各自的账。 */}
|
|
602
|
+
<nav className={cn(styles.kbSteps, styles.recallSteps)} aria-label="召回链路">
|
|
603
|
+
{steps.map((step, index) => {
|
|
604
|
+
const Icon = step.icon
|
|
605
|
+
const on = tab === step.id
|
|
606
|
+
return <Fragment key={step.id}>
|
|
607
|
+
{index > 0 && <span className={styles.kbStepArrow} aria-hidden="true"><ChevronRight size={13} /></span>}
|
|
608
|
+
<button type="button" className={cn(styles.kbStep, on && styles.kbStepOn)}
|
|
609
|
+
aria-current={on ? 'step' : undefined}
|
|
610
|
+
title={`${step.label}:${step.value} · ${step.note}`}
|
|
611
|
+
onClick={() => setTab(step.id)}>
|
|
612
|
+
<span className={styles.kbStepTop}>
|
|
613
|
+
<Icon size={13} aria-hidden="true" /><b>{step.label}</b><em className={styles.kbStepVerb}>{step.verb}</em>
|
|
614
|
+
{step.cost && <em className={styles.kbStepCost} title={step.costTitle}>{step.cost}</em>}
|
|
615
|
+
</span>
|
|
616
|
+
<span className={styles.kbStepFacts}>
|
|
617
|
+
<span className={styles.kbStepValue}>{step.value}</span>
|
|
618
|
+
<span className={styles.kbStepNote}>{step.note}</span>
|
|
619
|
+
</span>
|
|
620
|
+
</button>
|
|
621
|
+
</Fragment>
|
|
622
|
+
})}
|
|
623
|
+
</nav>
|
|
624
|
+
|
|
625
|
+
{tab === 'recall' && <>
|
|
626
|
+
{/* 路由标签:点一下就只看这一路的候选(该路自己的分数与名次)。 */}
|
|
627
|
+
<div className={styles.recallRoutes}>
|
|
628
|
+
<button type="button" className={cn(styles.routeChip, routeView === 'all' && styles.routeChipOn)} onClick={() => setRouteView('all')}>
|
|
629
|
+
融合后命中 {result?.hits.length ?? 0}
|
|
630
|
+
</button>
|
|
631
|
+
{RECALL_ROUTES.map(route => {
|
|
632
|
+
const detail = routesDetail.find(item => item.route === route.id)
|
|
633
|
+
const rows = route.id === 'fts' ? result?.ledger?.fts ?? [] : result?.ledger?.vector ?? []
|
|
634
|
+
const kept = detail?.kept ?? rows.filter(row => row.kept).length
|
|
635
|
+
const total = detail?.candidates ?? rows.length
|
|
636
|
+
// 降级与「这一路没有候选」是两件事:前者要能一眼看出来(0031 的诚实边界)。
|
|
637
|
+
const degraded = detail?.status === 'degraded'
|
|
638
|
+
return <button key={route.id} type="button"
|
|
639
|
+
title={degraded && detail?.reason ? `${route.hint} —— ${detail.reason}` : route.hint}
|
|
640
|
+
className={cn(styles.routeChip, routeView === route.id && styles.routeChipOn, degraded && styles.routeChipOff)}
|
|
641
|
+
onClick={() => setRouteView(route.id as 'fts' | 'vector')}>
|
|
642
|
+
{route.label} {degraded ? '降级' : `${kept}/${total}`}
|
|
643
|
+
</button>
|
|
644
|
+
})}
|
|
645
|
+
</div>
|
|
646
|
+
{!result
|
|
647
|
+
? <div className={styles.emptyState}><CircleHelp size={16} />先在上面输入一个查询,点「测试」跑一次召回</div>
|
|
648
|
+
: routeView === 'all'
|
|
649
|
+
? (result.hits.length > 0
|
|
650
|
+
? <HitList hits={result.hits} />
|
|
651
|
+
: <div className={styles.emptyState}><CircleHelp size={16} />这个路线组合没有命中任何 wikiNote</div>)
|
|
652
|
+
: <LedgerList
|
|
653
|
+
rows={routeView === 'fts' ? result.ledger?.fts ?? [] : result.ledger?.vector ?? []}
|
|
654
|
+
scoreLabel={row => row.score.toFixed(3)}
|
|
655
|
+
droppedLabel="低于阈值"
|
|
656
|
+
/>}
|
|
388
657
|
</>}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
<div
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
658
|
+
|
|
659
|
+
{tab === 'rerank' && (result === null
|
|
660
|
+
? <div className={styles.emptyState}><CircleHelp size={16} />先跑一次召回,这里显示候选池里每一条的重排分</div>
|
|
661
|
+
: result.rerank !== 'ready'
|
|
662
|
+
? <>
|
|
663
|
+
<p className={styles.kbHint}>这次没有跑重排({result.rerank ?? 'skipped'}),下面是融合顺序 —— 分数是 RRF 融合分,不是模型分。</p>
|
|
664
|
+
<LedgerList rows={result.ledger?.rerank ?? []} emptyLabel="这次没有参与重排的候选" scoreLabel={row => `融合 ${(row.fused ?? row.score).toFixed(4)}`} />
|
|
665
|
+
</>
|
|
666
|
+
: <>
|
|
667
|
+
<p className={styles.kbHint}>
|
|
668
|
+
cross-encoder 对候选池的 <b>每一条</b> 打分(原始 logit,阈值 {result.rerankThreshold ?? 0.2}):低于阈值的被丢弃并置灰 —— 这样「为什么没召回」看得见。
|
|
669
|
+
</p>
|
|
670
|
+
<LedgerList
|
|
671
|
+
rows={result.ledger?.rerank ?? []}
|
|
672
|
+
emptyLabel="这次没有参与重排的候选"
|
|
673
|
+
scoreLabel={row => row.logit === undefined ? `融合 ${(row.fused ?? row.score).toFixed(4)}` : `logit ${row.logit.toFixed(2)} · ${(row.rerankScore ?? 0).toFixed(3)}`}
|
|
674
|
+
droppedLabel={`低于阈值 ${result.rerankThreshold ?? 0.2}`}
|
|
675
|
+
/>
|
|
676
|
+
</>)}
|
|
677
|
+
|
|
678
|
+
{tab === 'expand' && (result === null
|
|
679
|
+
? <div className={styles.emptyState}><CircleHelp size={16} />先跑一次召回,这里显示最终进上下文的卡片与它们的 1 跳邻居</div>
|
|
680
|
+
: <>
|
|
681
|
+
<ExpansionView wiki={result.wiki} expansion={result.expansion} context={result.context} {...(onOpenWiki ? { onOpenWiki } : {})} />
|
|
682
|
+
<p className={styles.kbHint}>
|
|
683
|
+
最终 <b>{finalWiki} 个 wiki</b> 进上下文
|
|
684
|
+
{result.omittedCards ? `(另有 ${result.omittedCards} 张因为超出上下文预算没进去,完整内容在临时文件里)` : ''}
|
|
685
|
+
。
|
|
686
|
+
</p>
|
|
687
|
+
</>)}
|
|
688
|
+
|
|
398
689
|
</Card>
|
|
399
690
|
}
|