Kaynağa Gözat

数据获取条数优化

cb 4 ay önce
ebeveyn
işleme
3cf5284371

+ 25 - 0
docs/provider-archives/2026-05/justone_1779415074924_nnoo86_api-xiaohongshu-pgy-api-solar-cooperator-blogger-v2-v1.json

@@ -0,0 +1,25 @@
+{
+  "provider": "justone",
+  "endpoint": "/api/xiaohongshu-pgy/api/solar/cooperator/blogger/v2/v1",
+  "requestParams": {
+    "token": "wT0w78sxrwlrpJrQ",
+    "keyword": "????",
+    "page": "1",
+    "searchType": "NOTE",
+    "fansNumberLower": "10000",
+    "fansNumberUpper": "150000"
+  },
+  "responseBody": {
+    "code": 0,
+    "data": {
+      "kols": [],
+      "total": 0,
+      "trackId": "kolSearch_00fbc9950e0d451ba597c36cafd6b555",
+      "highlightWords": [],
+      "resultType": 2,
+      "redirect2NickSearch": false
+    },
+    "message": null,
+    "recordTime": "2026-05-22T09:57:54.10564724"
+  }
+}

+ 35 - 1
server/services/llm.service.ts

@@ -11,6 +11,7 @@ interface BriefAnalysisResult {
     gender?: string;
     gender?: string;
     contentTags?: string[];
     contentTags?: string[];
     excludeTags?: string[];
     excludeTags?: string[];
+    targetCount?: number;
   };
   };
 }
 }
 
 
@@ -29,7 +30,8 @@ const BRIEF_ANALYSIS_PROMPT = `你是一个专业的媒介策划助手。请分
 
 
 请以JSON格式输出,包含requirements数组和searchCriteria对象。
 请以JSON格式输出,包含requirements数组和searchCriteria对象。
 requirements格式:[{label: "字段名", value: "提取值", confidence: 0-100}]
 requirements格式:[{label: "字段名", value: "提取值", confidence: 0-100}]
-searchCriteria格式:{platforms: [], keywords: [], fanRange: {min, max}, budgetRange: {min, max}, region?, gender?, contentTags?: [], excludeTags?: []}
+searchCriteria格式:{platforms: [], keywords: [], fanRange: {min, max}, budgetRange: {min, max}, region?, gender?, contentTags?: [], excludeTags?: [], targetCount?}
+如果 Brief 写明达人数量,请额外输出 targetCount,表示最终需求达人总数(不是候选池数量)。
 
 
 Brief内容:
 Brief内容:
 `;
 `;
@@ -84,6 +86,7 @@ export async function analyzeBrief(briefText: string): Promise<BriefAnalysisResu
       requirements: parsed.requirements || [],
       requirements: parsed.requirements || [],
       searchCriteria: parsed.searchCriteria || { platforms: [], keywords: [], fanRange: { min: 10000, max: 500000 }, budgetRange: { min: 2000, max: 20000 } },
       searchCriteria: parsed.searchCriteria || { platforms: [], keywords: [], fanRange: { min: 10000, max: 500000 }, budgetRange: { min: 2000, max: 20000 } },
     };
     };
+    result.searchCriteria.targetCount = normalizeTargetCount(result.searchCriteria.targetCount) || inferTargetCount(briefText, result.requirements);
     console.log('[LLM] 解析结果:', JSON.stringify(result, null, 2));
     console.log('[LLM] 解析结果:', JSON.stringify(result, null, 2));
     return result;
     return result;
   } catch (error) {
   } catch (error) {
@@ -121,6 +124,37 @@ function fallbackAnalysis(briefText: string): BriefAnalysisResult {
       keywords,
       keywords,
       fanRange: { min: 10000, max: 500000 },
       fanRange: { min: 10000, max: 500000 },
       budgetRange: { min: 2000, max: 20000 },
       budgetRange: { min: 2000, max: 20000 },
+      targetCount: inferTargetCount(briefText, []),
     },
     },
   };
   };
 }
 }
+
+function normalizeTargetCount(value: unknown): number | undefined {
+  const count = Number(value || 0);
+  return count > 0 ? Math.ceil(count) : undefined;
+}
+
+function inferTargetCount(
+  briefText: string,
+  requirements: Array<{ label: string; value: string; confidence: number }>
+): number | undefined {
+  const text = `${briefText}\n${requirements.map((item) => `${item.label}:${item.value}`).join('\n')}`;
+
+  const perCityMatch = text.match(/(\d+)\s*(?:个|座)?城市[\s\S]{0,40}?(\d+)\s*位\s*\/\s*城市/);
+  if (perCityMatch) {
+    return Number(perCityMatch[1]) * Number(perCityMatch[2]);
+  }
+
+  const cityCountMatch = text.match(/(\d+)\s*(?:个|座)?城市/);
+  const perCityLooseMatch = text.match(/每(?:个)?城市\s*(\d+)\s*位|(\d+)\s*位\s*\/\s*城市/);
+  if (cityCountMatch && perCityLooseMatch) {
+    return Number(cityCountMatch[1]) * Number(perCityLooseMatch[1] || perCityLooseMatch[2]);
+  }
+
+  const directMatch = text.match(/(?:达人|博主|KOL|账号|人数)[^\d]{0,10}(\d+)\s*位|(\d+)\s*位\s*(?:达人|博主|KOL|账号)/i);
+  if (directMatch) {
+    return Number(directMatch[1] || directMatch[2]);
+  }
+
+  return undefined;
+}

+ 40 - 5
server/services/recommendation.service.ts

@@ -1,7 +1,7 @@
 import { config } from '../config.ts';
 import { config } from '../config.ts';
 import { searchCreators, type JustOneCreator } from './justone.service.ts';
 import { searchCreators, type JustOneCreator } from './justone.service.ts';
 import type { ContentSample } from './tikhub.service.ts';
 import type { ContentSample } from './tikhub.service.ts';
-import { searchLocalCreators } from './local-creator-db.service.ts';
+import { recordRetrievalEvent, searchLocalCreators } from './local-creator-db.service.ts';
 
 
 export interface NormalizedCandidate {
 export interface NormalizedCandidate {
   id: string;
   id: string;
@@ -43,6 +43,7 @@ interface SearchCriteria {
   gender?: string;
   gender?: string;
   contentTags?: string[];
   contentTags?: string[];
   excludeTags?: string[];
   excludeTags?: string[];
+  targetCount?: number;
 }
 }
 
 
 interface ScoringWeights {
 interface ScoringWeights {
@@ -63,6 +64,7 @@ const DEFAULT_WEIGHTS: ScoringWeights = {
 
 
 const MAX_KEYWORDS_PER_PLATFORM = 1;
 const MAX_KEYWORDS_PER_PLATFORM = 1;
 const PAGES_PER_PLATFORM = 3;
 const PAGES_PER_PLATFORM = 3;
+const DEFAULT_MIN_CANDIDATE_POOL = 30;
 
 
 /**
 /**
  * 主推荐流程:根据搜索条件从多个来源召回候选人并评分排序
  * 主推荐流程:根据搜索条件从多个来源召回候选人并评分排序
@@ -74,21 +76,27 @@ export async function generateRecommendations(
   criteria = normalizeSearchCriteria(criteria);
   criteria = normalizeSearchCriteria(criteria);
   console.log('[Recommend] ========== 开始推荐流程 ==========');
   console.log('[Recommend] ========== 开始推荐流程 ==========');
   console.log('[Recommend] 搜索条件:', JSON.stringify(criteria, null, 2));
   console.log('[Recommend] 搜索条件:', JSON.stringify(criteria, null, 2));
-  onProgress?.('search', '正在从第三方平台召回候选达人...');
+  const targetCandidatePool = resolveTargetCandidatePool(criteria);
+  onProgress?.('search', `正在检索候选达人,目标候选池 ${targetCandidatePool} 位...`);
 
 
   // Step 1: 优先从本地/自有达人库召回候选
   // Step 1: 优先从本地/自有达人库召回候选
   const allCandidates: JustOneCreator[] = [];
   const allCandidates: JustOneCreator[] = [];
-  const localCandidates = await searchLocalCreators(criteria, config.recommendation.maxCandidates);
+  const localCandidates = await searchLocalCreators(criteria, Math.min(config.recommendation.maxCandidates, targetCandidatePool));
   if (localCandidates.length > 0) {
   if (localCandidates.length > 0) {
     console.log(`[Recommend] 本地达人库命中: ${localCandidates.length} 位`);
     console.log(`[Recommend] 本地达人库命中: ${localCandidates.length} 位`);
-    onProgress?.('search', `本地达人库命中 ${localCandidates.length} 位候选达人`);
+    onProgress?.('search', `本地达人库命中 ${localCandidates.length}/${targetCandidatePool} 位候选达人`);
     allCandidates.push(...localCandidates);
     allCandidates.push(...localCandidates);
   }
   }
 
 
   // JustOne API 仅支持小红书和抖音
   // JustOne API 仅支持小红书和抖音
   const supportedPlatforms = ['xiaohongshu', '小红书', 'douyin', '抖音'];
   const supportedPlatforms = ['xiaohongshu', '小红书', 'douyin', '抖音'];
 
 
-  if (allCandidates.length === 0) {
+  let providerCandidateCount = 0;
+  if (allCandidates.length < targetCandidatePool) {
+    const shortage = targetCandidatePool - allCandidates.length;
+    console.log(`[Recommend] 本地候选不足,继续调用 JustOne 补足: 缺口 ${shortage} 位`);
+    onProgress?.('search', `本地候选不足,继续调用 JustOne 补足 ${shortage} 位...`);
+
     const searchKeywords = buildCompactSearchKeywords(criteria);
     const searchKeywords = buildCompactSearchKeywords(criteria);
     for (const platform of uniquePlatforms(criteria.platforms)) {
     for (const platform of uniquePlatforms(criteria.platforms)) {
       const platformName = mapPlatformName(platform);
       const platformName = mapPlatformName(platform);
@@ -116,10 +124,16 @@ export async function generateRecommendations(
 
 
           console.log(`[Recommend] ${platformName}/${keyword}/page-${page} 返回 ${results.length} 条结果`);
           console.log(`[Recommend] ${platformName}/${keyword}/page-${page} 返回 ${results.length} 条结果`);
           allCandidates.push(...results);
           allCandidates.push(...results);
+          providerCandidateCount += results.length;
+          if (allCandidates.length >= targetCandidatePool) break;
           if (results.length === 0) break;
           if (results.length === 0) break;
         }
         }
+        if (allCandidates.length >= targetCandidatePool) break;
       }
       }
+      if (allCandidates.length >= targetCandidatePool) break;
     }
     }
+  } else {
+    console.log(`[Recommend] 本地候选已满足目标候选池 ${targetCandidatePool} 位,跳过 JustOne`);
   }
   }
 
 
   console.log(`[Recommend] 总召回: ${allCandidates.length} 位候选达人`);
   console.log(`[Recommend] 总召回: ${allCandidates.length} 位候选达人`);
@@ -133,6 +147,13 @@ export async function generateRecommendations(
     }
     }
   }
   }
   const uniqueCandidates = Array.from(uniqueMap.values());
   const uniqueCandidates = Array.from(uniqueMap.values());
+  await recordRetrievalEvent({
+    queryText: buildCompactSearchKeywords(criteria).join(' '),
+    criteria,
+    localHitCount: localCandidates.length,
+    providerHitCount: providerCandidateCount,
+    finalCount: uniqueCandidates.length,
+  }).catch((error) => console.error('[Recommend] 记录检索事件失败:', error));
 
 
   console.log(`[Recommend] 去重后: ${uniqueCandidates.length} 位候选`);
   console.log(`[Recommend] 去重后: ${uniqueCandidates.length} 位候选`);
   onProgress?.('processing', `去重后 ${uniqueCandidates.length} 位候选,开始评分筛选...`);
   onProgress?.('processing', `去重后 ${uniqueCandidates.length} 位候选,开始评分筛选...`);
@@ -351,9 +372,23 @@ function normalizeSearchCriteria(criteria: SearchCriteria): SearchCriteria {
     budgetRange: normalizeBudgetRange(criteria.budgetRange),
     budgetRange: normalizeBudgetRange(criteria.budgetRange),
     platforms: criteria.platforms?.length ? criteria.platforms : ['xiaohongshu'],
     platforms: criteria.platforms?.length ? criteria.platforms : ['xiaohongshu'],
     keywords: criteria.keywords?.length ? criteria.keywords : ['生活方式'],
     keywords: criteria.keywords?.length ? criteria.keywords : ['生活方式'],
+    targetCount: normalizeTargetCount(criteria.targetCount),
   };
   };
 }
 }
 
 
+function resolveTargetCandidatePool(criteria: SearchCriteria): number {
+  const targetCount = normalizeTargetCount(criteria.targetCount);
+  if (targetCount > 0) {
+    return Math.min(config.recommendation.maxCandidates, Math.max(DEFAULT_MIN_CANDIDATE_POOL, targetCount * config.recommendation.multiplier));
+  }
+  return Math.min(config.recommendation.maxCandidates, DEFAULT_MIN_CANDIDATE_POOL);
+}
+
+function normalizeTargetCount(value?: number): number | undefined {
+  const count = Number(value || 0);
+  return count > 0 ? Math.ceil(count) : undefined;
+}
+
 function normalizeFanRange(range: { min: number; max: number }): { min: number; max: number } {
 function normalizeFanRange(range: { min: number; max: number }): { min: number; max: number } {
   const min = Number(range?.min || 0);
   const min = Number(range?.min || 0);
   const max = Number(range?.max || 0);
   const max = Number(range?.max || 0);

+ 1 - 0
server/services/task-manager.service.ts

@@ -28,6 +28,7 @@ export interface Task {
     gender?: string;
     gender?: string;
     contentTags?: string[];
     contentTags?: string[];
     excludeTags?: string[];
     excludeTags?: string[];
+    targetCount?: number;
   } | null;
   } | null;
   candidates: NormalizedCandidate[];
   candidates: NormalizedCandidate[];
   totalPool: number;
   totalPool: number;

+ 1 - 0
src/modules/workspace/models/task.model.ts

@@ -25,6 +25,7 @@ export interface SearchCriteria {
   gender?: string;
   gender?: string;
   contentTags?: string[];
   contentTags?: string[];
   excludeTags?: string[];
   excludeTags?: string[];
+  targetCount?: number;
 }
 }
 
 
 export interface CandidateCreator {
 export interface CandidateCreator {