perf(bom-costing): compact product synthesis facts
This commit is contained in:
parent
b32fc52427
commit
634a2dad9d
@ -1004,6 +1004,54 @@ describe('BOM 多模态分析器', () => {
|
||||
expect(JSON.stringify(compactInput)).not.toContain('不属于当前批次')
|
||||
})
|
||||
|
||||
it('高密度详情事实只保留最有成本价值的一组产品级证据', () => {
|
||||
const factSnapshot = createCompleteFactSnapshot(snapshot)
|
||||
const factKinds = [
|
||||
'DIMENSION',
|
||||
'MATERIAL',
|
||||
'STRUCTURE',
|
||||
'MECHANISM',
|
||||
'HARDWARE',
|
||||
'PART',
|
||||
'PROCESS',
|
||||
'PACKAGING',
|
||||
'WEIGHT',
|
||||
'SKU_DIFFERENCE',
|
||||
'TEXT',
|
||||
] as const
|
||||
factSnapshot.imageFacts.push(...Array.from({ length: 18 }, (_, index) => ({
|
||||
imageId: `DETAIL_DENSE_${index + 1}`,
|
||||
sourceImageUrl: `https://img.example.com/detail-dense-${index + 1}.jpg`,
|
||||
kind: 'DETAIL' as const,
|
||||
label: `高密度详情图 ${index + 1}`,
|
||||
externalSkuIds: [],
|
||||
observedTexts: [],
|
||||
facts: [{
|
||||
factId: `fact_dense_${index + 1}`,
|
||||
kind: factKinds[index % factKinds.length]!,
|
||||
subject: `部件 ${index + 1}`,
|
||||
value: `事实 ${index + 1}`,
|
||||
relatedSkuIds: [],
|
||||
basis: 'IMAGE' as const,
|
||||
confidence: 0.8,
|
||||
}],
|
||||
})))
|
||||
|
||||
const compactInput = buildSynthesisFactInput(
|
||||
factSnapshot,
|
||||
[snapshot.skus[0]!],
|
||||
)
|
||||
const detailImages = compactInput.images.filter(
|
||||
(image) => image.kind === 'DETAIL',
|
||||
)
|
||||
const retainedKinds = new Set(
|
||||
detailImages.flatMap((image) => image.facts.map((fact) => fact.k)),
|
||||
)
|
||||
|
||||
expect(detailImages).toHaveLength(14)
|
||||
expect([...factKinds].every((kind) => retainedKinds.has(kind))).toBe(true)
|
||||
})
|
||||
|
||||
it('复用图片事实时,GPT-5 最终汇总在网关截断前结束等待', async () => {
|
||||
const timeoutSpy = jest
|
||||
.spyOn(AbortSignal, 'timeout')
|
||||
|
||||
@ -75,7 +75,7 @@ const SYSTEM_PROMPT = `你是家具产品结构与成本工程师。你的任务
|
||||
14. 每条 evidence 必须设置 basis:图片直接可见为 IMAGE;图片标注、详情文字或商品文案为 SOURCE_TEXT;SKU 名称、规格或属性为 SKU_METADATA;由装配或机构功能必需性推断的隐藏部件为 STRUCTURAL_INFERENCE。
|
||||
15. STRUCTURAL_INFERENCE 只允许用于实现已确认结构或功能所必需的隐藏五金、连接件或保护包材;必须写明推断原因、数量估算和待确认项,evidence.confidence 与 BOM 行 confidence 都不得高于 0.55。
|
||||
16. 安装知识只用于提醒检查,不是当前商品的事实。不得因为知识库列出了某种五金,就在缺少当前商品结构或功能依据时把它写进 BOM。
|
||||
17. 逐图事实已经覆盖尺寸标注、局部小图、剖面图、结构示意、折叠前后状态和产品信息图;合并时不得遗漏这些事实。
|
||||
17. 系统会从已归档的逐图事实中挑选与 BOM 最相关的产品级证据,并附上当前批次关联的 SKU 图片事实;不得遗漏本次输入中给出的事实。
|
||||
18. SKU 名称或属性中的“折叠、升降、推拉、抽屉、脚轮、电动”等功能词必须触发对应机构检查;同时区分收拢态、展开态与整机尺寸。
|
||||
19. 对板件之间、玻璃与框架、抽屉与柜体、门板与柜体等装配界面,必须检查连接方式;看不清时可以给出低置信度的必需连接件估算,但不得编造具体型号。
|
||||
20. 每个结构组必须至少包含材料类(BOARD、STEEL、ACCESSORY 中至少一条)、PROCESSING、LABOR 和 PACKAGING,确保系统能立即形成一套出厂成本预估;不确定项用低置信度估算并在 evidence 中说明,不得直接省略整个成本组。`
|
||||
@ -237,6 +237,20 @@ const DEFAULT_SYNTHESIS_BATCH_CONCURRENCY = 3
|
||||
const MAX_EXPLICIT_FACT_SKU_IDS = 20
|
||||
const MAX_SYNTHESIS_CONTRACT_ATTEMPTS = 2
|
||||
const SYNTHESIS_IMAGE_REFERENCE_PREFIX = 'bom-image:'
|
||||
const MAX_PRODUCT_LEVEL_FACT_IMAGES = 14
|
||||
const PRODUCT_LEVEL_FACT_KIND_WEIGHTS: Record<string, number> = {
|
||||
DIMENSION: 8,
|
||||
MATERIAL: 8,
|
||||
STRUCTURE: 7,
|
||||
MECHANISM: 7,
|
||||
HARDWARE: 7,
|
||||
PART: 6,
|
||||
PROCESS: 5,
|
||||
PACKAGING: 5,
|
||||
WEIGHT: 5,
|
||||
SKU_DIFFERENCE: 4,
|
||||
TEXT: 1,
|
||||
}
|
||||
|
||||
function describeHighCardinalitySkuCatalog(skus: MarketSkuSnapshot[]) {
|
||||
const valuesByAttribute = new Map<string, Set<string>>()
|
||||
@ -335,11 +349,87 @@ function createSynthesisImageReferenceMap(
|
||||
)
|
||||
}
|
||||
|
||||
function getSynthesisFactSignature(
|
||||
fact: BomCostingSourceFactSnapshot['imageFacts'][number]['facts'][number],
|
||||
) {
|
||||
return JSON.stringify([
|
||||
fact.kind,
|
||||
fact.subject,
|
||||
fact.value,
|
||||
fact.numericValue ?? null,
|
||||
fact.unit ?? null,
|
||||
fact.basis,
|
||||
])
|
||||
}
|
||||
|
||||
function selectProductLevelSynthesisImageIds(
|
||||
factSnapshot: BomCostingSourceFactSnapshot,
|
||||
) {
|
||||
const selectedIds = new Set(
|
||||
factSnapshot.imageFacts
|
||||
.filter((image) => image.kind === 'MAIN')
|
||||
.map((image) => image.imageId),
|
||||
)
|
||||
const remainingDetails = factSnapshot.imageFacts
|
||||
.filter((image) => image.kind === 'DETAIL')
|
||||
.map((image, originalIndex) => ({ image, originalIndex }))
|
||||
const seenFactSignatures = new Set<string>()
|
||||
const seenFactKinds = new Set<string>()
|
||||
|
||||
for (
|
||||
let selectedCount = 0;
|
||||
selectedCount < MAX_PRODUCT_LEVEL_FACT_IMAGES && remainingDetails.length > 0;
|
||||
selectedCount += 1
|
||||
) {
|
||||
let bestIndex = 0
|
||||
let bestScore = Number.NEGATIVE_INFINITY
|
||||
for (const [index, candidate] of remainingDetails.entries()) {
|
||||
const candidateSignatures = new Set<string>()
|
||||
let score = 0
|
||||
let newKindScore = 0
|
||||
for (const fact of candidate.image.facts) {
|
||||
const signature = getSynthesisFactSignature(fact)
|
||||
if (candidateSignatures.has(signature)) continue
|
||||
candidateSignatures.add(signature)
|
||||
const weight = PRODUCT_LEVEL_FACT_KIND_WEIGHTS[fact.kind] ?? 1
|
||||
if (!seenFactKinds.has(fact.kind)) newKindScore += weight
|
||||
score += seenFactSignatures.has(signature) ? weight : weight * 3
|
||||
}
|
||||
// A detail image with no structured fact can still contain a small,
|
||||
// useful text fragment. Keep it behind all structured evidence.
|
||||
if (candidateSignatures.size === 0 && candidate.image.observedTexts.length > 0) {
|
||||
score = 0.5
|
||||
}
|
||||
// First retain at least one image for every available fact category.
|
||||
// This prevents a large number of dimensions or parts from crowding out
|
||||
// a lower-volume but cost-relevant category such as packaging.
|
||||
if (newKindScore > 0) score += 10_000 + newKindScore
|
||||
if (
|
||||
score > bestScore ||
|
||||
(score === bestScore && candidate.originalIndex < remainingDetails[bestIndex]!.originalIndex)
|
||||
) {
|
||||
bestIndex = index
|
||||
bestScore = score
|
||||
}
|
||||
}
|
||||
const [selected] = remainingDetails.splice(bestIndex, 1)
|
||||
if (!selected) break
|
||||
selectedIds.add(selected.image.imageId)
|
||||
selected.image.facts.forEach((fact) => {
|
||||
seenFactSignatures.add(getSynthesisFactSignature(fact))
|
||||
seenFactKinds.add(fact.kind)
|
||||
})
|
||||
}
|
||||
|
||||
return selectedIds
|
||||
}
|
||||
|
||||
export function buildSynthesisFactInput(
|
||||
factSnapshot: BomCostingSourceFactSnapshot,
|
||||
skus: MarketSkuSnapshot[],
|
||||
) {
|
||||
const batchSkuIds = new Set(skus.map((sku) => sku.externalSkuId))
|
||||
const productLevelImageIds = selectProductLevelSynthesisImageIds(factSnapshot)
|
||||
return {
|
||||
schemaVersion: factSnapshot.schemaVersion,
|
||||
fieldReference: {
|
||||
@ -348,8 +438,10 @@ export function buildSynthesisFactInput(
|
||||
},
|
||||
images: factSnapshot.imageFacts
|
||||
.filter((image) => {
|
||||
if (image.kind !== 'SKU') return true
|
||||
return image.externalSkuIds.some((skuId) => batchSkuIds.has(skuId))
|
||||
if (image.kind === 'SKU') {
|
||||
return image.externalSkuIds.some((skuId) => batchSkuIds.has(skuId))
|
||||
}
|
||||
return productLevelImageIds.has(image.imageId)
|
||||
})
|
||||
.map((image) => {
|
||||
const batchImageSkuIds = image.externalSkuIds.filter((skuId) =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user