llm.service.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { config } from '../config.ts';
  2. interface BriefAnalysisResult {
  3. requirements: Array<{ label: string; value: string; confidence: number }>;
  4. searchCriteria: {
  5. platforms: string[];
  6. keywords: string[];
  7. fanRange: { min: number; max: number };
  8. budgetRange: { min: number; max: number };
  9. region?: string;
  10. gender?: string;
  11. contentTags?: string[];
  12. excludeTags?: string[];
  13. };
  14. }
  15. const BRIEF_ANALYSIS_PROMPT = `你是一个专业的媒介策划助手。请分析以下客户Brief文件内容,提取出结构化的投放需求。
  16. 要求提取以下信息:
  17. 1. 客户/品牌名称
  18. 2. 投放目标(种草、转化、品宣等)
  19. 3. 平台需求(小红书、抖音、B站等及各平台需求人数)
  20. 4. 粉丝数要求范围
  21. 5. 预算范围
  22. 6. 内容风格要求
  23. 7. 地区要求
  24. 8. 排除项(竞品、不合适的类型等)
  25. 9. 合作形式(图文、视频等)
  26. 请以JSON格式输出,包含requirements数组和searchCriteria对象。
  27. requirements格式:[{label: "字段名", value: "提取值", confidence: 0-100}]
  28. searchCriteria格式:{platforms: [], keywords: [], fanRange: {min, max}, budgetRange: {min, max}, region?, gender?, contentTags?: [], excludeTags?: []}
  29. Brief内容:
  30. `;
  31. export async function analyzeBrief(briefText: string): Promise<BriefAnalysisResult> {
  32. if (!config.llm.apiKey) {
  33. console.warn('[LLM] No API key configured, using fallback analysis');
  34. return fallbackAnalysis(briefText);
  35. }
  36. try {
  37. console.log('[LLM] 发起请求:', { url: `${config.llm.baseUrl}/chat/completions`, model: config.llm.model, briefLength: briefText.length });
  38. const response = await fetch(`${config.llm.baseUrl}/chat/completions`, {
  39. method: 'POST',
  40. headers: {
  41. 'Content-Type': 'application/json',
  42. Authorization: `Bearer ${config.llm.apiKey}`,
  43. },
  44. body: JSON.stringify({
  45. model: config.llm.model,
  46. messages: [
  47. { role: 'system', content: '你是专业的媒介策划AI助手,擅长从Brief文档中提取结构化投放需求。你的回复必须是纯JSON格式,不要包含markdown代码块标记。' },
  48. { role: 'user', content: BRIEF_ANALYSIS_PROMPT + briefText },
  49. ],
  50. temperature: 0.3,
  51. }),
  52. });
  53. if (!response.ok) {
  54. const errorBody = await response.text();
  55. console.error(`[LLM] API error: ${response.status} ${response.statusText}`, errorBody);
  56. return fallbackAnalysis(briefText);
  57. }
  58. const data = await response.json();
  59. const content = data.choices?.[0]?.message?.content;
  60. if (!content) {
  61. return fallbackAnalysis(briefText);
  62. }
  63. // 提取 JSON(可能被 markdown 代码块包裹)
  64. let jsonStr = content.trim();
  65. const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
  66. if (codeBlockMatch) {
  67. jsonStr = codeBlockMatch[1].trim();
  68. }
  69. const parsed = JSON.parse(jsonStr);
  70. const result = {
  71. requirements: parsed.requirements || [],
  72. searchCriteria: parsed.searchCriteria || { platforms: [], keywords: [], fanRange: { min: 10000, max: 500000 }, budgetRange: { min: 2000, max: 20000 } },
  73. };
  74. console.log('[LLM] 解析结果:', JSON.stringify(result, null, 2));
  75. return result;
  76. } catch (error) {
  77. console.error('[LLM] Analysis failed:', error);
  78. return fallbackAnalysis(briefText);
  79. }
  80. }
  81. function fallbackAnalysis(briefText: string): BriefAnalysisResult {
  82. // 简单的关键词匹配作为降级方案
  83. const platforms: string[] = [];
  84. if (briefText.includes('小红书') || briefText.includes('红书')) platforms.push('xiaohongshu');
  85. if (briefText.includes('抖音') || briefText.includes('TikTok')) platforms.push('douyin');
  86. if (briefText.includes('B站') || briefText.includes('哔哩哔哩')) platforms.push('bilibili');
  87. if (briefText.includes('微博')) platforms.push('weibo');
  88. if (briefText.includes('微信') || briefText.includes('公众号')) platforms.push('weixin');
  89. if (platforms.length === 0) platforms.push('xiaohongshu', 'douyin');
  90. const keywords: string[] = [];
  91. const keywordPatterns = ['护肤', '美妆', '母婴', '数码', '穿搭', '美食', '家居', '运动', '旅行', '教育'];
  92. for (const kw of keywordPatterns) {
  93. if (briefText.includes(kw)) keywords.push(kw);
  94. }
  95. if (keywords.length === 0) keywords.push('生活方式');
  96. return {
  97. requirements: [
  98. { label: '平台需求', value: platforms.join('、'), confidence: 75 },
  99. { label: '内容关键词', value: keywords.join('、'), confidence: 70 },
  100. { label: '预算范围', value: '2000-20000', confidence: 60 },
  101. { label: '粉丝范围', value: '1万-50万', confidence: 60 },
  102. ],
  103. searchCriteria: {
  104. platforms,
  105. keywords,
  106. fanRange: { min: 10000, max: 500000 },
  107. budgetRange: { min: 2000, max: 20000 },
  108. },
  109. };
  110. }