第2097篇:AI驱动的商品描述生成——从SKU数据到高转化文案
2026/4/30大约 13 分钟
第2097篇:AI驱动的商品描述生成——从SKU数据到高转化文案
适读人群:电商技术团队的工程师 | 阅读时长:约18分钟 | 核心价值:掌握商品文案的结构化生成、多风格适配、质量评估和批量处理的完整工程方案
电商平台的商品描述有一个隐藏成本:新品上架时,运营人员需要手工写标题、卖点、详情描述,一个SKU少则半小时,多则两三个小时。对于SKU数量多的平台,这个成本大得离谱。
AI生成商品描述不是新话题,但"接个API生成一段文字"和"在生产环境稳定运行、效果可控"之间还有很大的工程距离。
这篇文章把电商商品描述生成的完整工程方案讲清楚,包括结构化输入设计、多风格控制、生成质量评估,以及如何批量处理数万SKU。
商品描述的结构分解
/**
* 商品描述不是一块文字,而是由多个结构化组件构成
*
* 组件1:标题(25-30字,包含核心关键词,影响搜索排名)
* 组件2:主卖点(3-5条,每条15-20字,突出差异化优势)
* 组件3:详情描述(200-500字,说明使用场景、技术参数、用户人群)
* 组件4:SEO描述(80-120字,为搜索引擎优化的简短摘要)
* 组件5:标签(5-10个关键词,用于搜索和分类)
*
* 为什么要分组件?
* - 各组件用途不同,质量评估标准不同
* - 分开生成可以独立优化、独立审核
* - 可以灵活组合:有些场景只需要标题,有些需要全套
*/
@Data
@Builder
public class ProductContent {
private String title; // 商品标题
private List<String> keySellingPoints; // 核心卖点
private String detailedDescription; // 详情描述
private String seoDescription; // SEO描述
private List<String> searchTags; // 搜索标签
private Map<String, String> metadata; // 附加元数据(质量分等)
}
/**
* 商品基础数据(输入)
*/
@Data
@Builder
public class ProductData {
private String productId;
private String categoryPath; // 如:电子设备/手机/智能手机
private String brandName;
private String modelName;
private Map<String, String> specs; // 规格参数:处理器/内存/屏幕等
private List<String> features; // 产品特性(非结构化描述)
private double price;
private String targetAudience; // 目标人群描述
private ContentStyle style; // 文案风格
public enum ContentStyle {
PROFESSIONAL, // 专业技术风格(适合3C电子)
LIFESTYLE, // 生活方式风格(适合家居、时尚)
CONCISE, // 简洁直接风格(适合快消品)
STORY_TELLING // 故事化风格(适合食品、美妆)
}
}商品描述生成服务
/**
* 商品描述生成核心服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductDescriptionGenerator {
private final ChatLanguageModel llm;
private final ProductCategoryPromptLibrary promptLibrary;
private final ContentQualityEvaluator qualityEvaluator;
/**
* 生成完整商品内容
*/
public ProductContent generate(ProductData product) {
String categoryType = classifyCategory(product.getCategoryPath());
String styleGuide = getStyleGuide(product.getStyle());
// 分步生成(比一次生成全部内容效果更好)
String title = generateTitle(product, categoryType, styleGuide);
List<String> keyPoints = generateKeyPoints(product, categoryType, styleGuide, title);
String description = generateDescription(product, categoryType, styleGuide, title, keyPoints);
String seoDesc = generateSeoDescription(product, title, keyPoints);
List<String> tags = generateTags(product, title, keyPoints);
ProductContent content = ProductContent.builder()
.title(title)
.keySellingPoints(keyPoints)
.detailedDescription(description)
.seoDescription(seoDesc)
.searchTags(tags)
.build();
// 质量评估
ContentQualityScore score = qualityEvaluator.evaluate(product, content);
content.setMetadata(Map.of(
"qualityScore", String.valueOf(score.overall()),
"titleScore", String.valueOf(score.titleScore()),
"needsReview", String.valueOf(score.overall() < 70)
));
return content;
}
/**
* 生成商品标题
*
* 标题是最关键的组件:影响搜索排名、点击率
* 标题生成比其他组件要求更严格
*/
private String generateTitle(ProductData product, String categoryType, String styleGuide) {
String prompt = """
你是一个电商标题优化专家。请为以下商品生成一个高质量的标题。
**品类类型**: %s
**品牌**: %s
**型号**: %s
**核心规格**: %s
**目标人群**: %s
**文案风格**: %s
**标题要求**:
1. 长度:25-32个字符
2. 必须包含:品牌名 + 核心型号/系列 + 1-2个核心卖点
3. 包含高搜索量关键词(根据品类自然融入)
4. 不能使用"最"、"第一"、"极致"等绝对化词汇
5. 不能有无意义的标点符号堆砌
只返回标题文字,不要任何解释或引号。
""".formatted(
categoryType,
product.getBrandName(),
product.getModelName(),
formatSpecs(product.getSpecs()),
product.getTargetAudience(),
styleGuide
);
String title = llm.generate(prompt).trim();
// 标题后处理:去除多余引号、标点,确保长度
title = cleanTitle(title);
log.debug("生成标题: productId={}, title={}", product.getProductId(), title);
return title;
}
/**
* 生成核心卖点
*/
private List<String> generateKeyPoints(
ProductData product, String categoryType,
String styleGuide, String title) {
String prompt = """
你是电商文案专家。请为商品提炼3-5个核心卖点。
商品标题(已生成):%s
品类:%s
规格参数:%s
产品特性:%s
目标人群:%s
风格:%s
**卖点要求**:
- 每条15-25字
- 聚焦用户价值(用户得到什么),而不只是参数列举
- 差异化:突出与同价位竞品的不同点
- 格式:每行一条卖点,不加序号或符号
直接输出卖点内容,每行一条,不要额外文字。
""".formatted(
title,
categoryType,
formatSpecs(product.getSpecs()),
String.join(";", product.getFeatures()),
product.getTargetAudience(),
styleGuide
);
String response = llm.generate(prompt);
return Arrays.stream(response.split("\n"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.limit(5)
.toList();
}
/**
* 生成详情描述
*/
private String generateDescription(
ProductData product, String categoryType,
String styleGuide, String title, List<String> keyPoints) {
String prompt = """
基于以下信息,撰写商品详情描述。
标题:%s
核心卖点:%s
规格参数:%s
品类:%s
目标人群:%s
文案风格:%s
**详情描述要求**:
- 总长度:300-500字
- 结构:开篇场景引入(50字)→ 核心优势阐述 → 使用场景展开 → 适用人群说明
- %s
- 自然融入核心关键词(不要关键词堆砌)
- 语气亲切,避免过度营销感
直接输出描述内容,不要标题或分节标注。
""".formatted(
title,
String.join("\n", keyPoints),
formatSpecs(product.getSpecs()),
categoryType,
product.getTargetAudience(),
styleGuide,
getDescriptionSpecialInstructions(product.getStyle())
);
return llm.generate(prompt).trim();
}
private String generateSeoDescription(
ProductData product, String title, List<String> keyPoints) {
String prompt = """
为以下商品生成一段SEO优化的简短描述(80-120字)。
商品标题:%s
核心卖点:%s
SEO描述要求:
- 包含2-3个核心搜索关键词
- 概括商品价值
- 自然流畅,不是关键词堆砌
只输出描述文字。
""".formatted(title, String.join(",", keyPoints.subList(0, Math.min(3, keyPoints.size()))));
return llm.generate(prompt).trim();
}
private List<String> generateTags(ProductData product, String title, List<String> keyPoints) {
String prompt = """
从以下商品信息中提取8-12个搜索标签(关键词)。
商品标题:%s
卖点:%s
规格:%s
标签要求:
- 每个标签2-6个字
- 包含:品牌词、品类词、功能词、场景词、人群词
- 按搜索量高低排序(高的放前面)
每行一个标签,只输出标签本身。
""".formatted(
title,
String.join(";", keyPoints),
formatSpecs(product.getSpecs())
);
return Arrays.stream(llm.generate(prompt).split("\n"))
.map(String::trim)
.filter(s -> !s.isEmpty() && s.length() <= 10)
.limit(12)
.toList();
}
private String classifyCategory(String categoryPath) {
// 根据品类路径返回类型标签
if (categoryPath.contains("手机") || categoryPath.contains("电脑")) return "3C数码";
if (categoryPath.contains("服装") || categoryPath.contains("鞋")) return "服饰";
if (categoryPath.contains("食品") || categoryPath.contains("零食")) return "食品";
if (categoryPath.contains("家居") || categoryPath.contains("家具")) return "家居";
return "通用商品";
}
private String getStyleGuide(ProductData.ContentStyle style) {
return switch (style) {
case PROFESSIONAL -> "专业技术风格:数据说话,理性客观,面向专业用户和发烧友";
case LIFESTYLE -> "生活方式风格:场景化叙述,情感化表达,强调生活品质提升";
case CONCISE -> "简洁直接风格:短句为主,直指痛点,不废话";
case STORY_TELLING -> "故事化风格:用故事和画面感吸引读者,建立情感连接";
};
}
private String getDescriptionSpecialInstructions(ProductData.ContentStyle style) {
return switch (style) {
case PROFESSIONAL -> "多用数据和技术对比";
case LIFESTYLE -> "多描绘使用场景和生活画面";
case CONCISE -> "控制在300字内,语句简短有力";
case STORY_TELLING -> "用一个具体用户故事开篇";
};
}
private String formatSpecs(Map<String, String> specs) {
if (specs == null || specs.isEmpty()) return "暂无规格";
return specs.entrySet().stream()
.map(e -> e.getKey() + ":" + e.getValue())
.collect(Collectors.joining("、"));
}
private String cleanTitle(String title) {
// 去除引号
title = title.replaceAll("[\"'""'']", "");
// 去除开头结尾的标点
title = title.replaceAll("^[,。!、]+|[,。!、]+$", "");
return title.trim();
}
}内容质量评估
/**
* 生成内容质量评估
*
* 不能全靠人工审核,需要自动化质量把关
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ContentQualityEvaluator {
private final ChatLanguageModel llm;
// 违禁词(不能出现在商品描述中的词)
private final Set<String> FORBIDDEN_WORDS = Set.of(
"最", "第一", "唯一", "极致", "无与伦比", "天下无双",
"国家级", "国际级", "世界级", "全球首创"
);
public ContentQualityScore evaluate(ProductData product, ProductContent content) {
// 1. 规则检查(快速,不调LLM)
int titleScore = evaluateTitle(content.getTitle(), product);
int keyPointScore = evaluateKeyPoints(content.getKeySellingPoints());
int descriptionScore = evaluateDescription(content.getDetailedDescription());
// 2. LLM评分(用于综合质量判断)
int llmScore = evaluateWithLlm(product, content);
// 综合评分(LLM评分权重更高)
int overall = (titleScore * 2 + keyPointScore * 2 +
descriptionScore * 2 + llmScore * 4) / 10;
return new ContentQualityScore(overall, titleScore, keyPointScore,
descriptionScore, llmScore);
}
private int evaluateTitle(String title, ProductData product) {
if (title == null || title.isBlank()) return 0;
int score = 100;
// 长度检查
if (title.length() < 20) score -= 20;
if (title.length() > 40) score -= 15;
// 违禁词检查
for (String word : FORBIDDEN_WORDS) {
if (title.contains(word)) {
score -= 30;
break;
}
}
// 品牌名是否包含
if (product.getBrandName() != null &&
!title.contains(product.getBrandName())) {
score -= 15;
}
// 是否有实质性词汇(不是纯废话)
if (title.matches(".*[\\u4e00-\\u9fa5]{2,}.*")) {
score += 0; // 有中文实质内容,加0分(基础分)
} else {
score -= 20;
}
return Math.max(0, Math.min(100, score));
}
private int evaluateKeyPoints(List<String> keyPoints) {
if (keyPoints == null || keyPoints.isEmpty()) return 0;
int score = 100;
// 数量检查
if (keyPoints.size() < 3) score -= 30;
if (keyPoints.size() > 6) score -= 10;
// 长度检查:每条卖点应该适中
long tooShort = keyPoints.stream().filter(k -> k.length() < 8).count();
long tooLong = keyPoints.stream().filter(k -> k.length() > 30).count();
score -= tooShort * 10;
score -= tooLong * 5;
// 违禁词
for (String point : keyPoints) {
for (String word : FORBIDDEN_WORDS) {
if (point.contains(word)) {
score -= 15;
break;
}
}
}
return Math.max(0, Math.min(100, score));
}
private int evaluateDescription(String description) {
if (description == null || description.isBlank()) return 0;
int score = 100;
// 长度检查
int len = description.length();
if (len < 100) score -= 40;
else if (len < 200) score -= 20;
else if (len > 800) score -= 10;
// 违禁词检查
for (String word : FORBIDDEN_WORDS) {
if (description.contains(word)) {
score -= 15;
}
}
// 是否有段落结构(有换行)
if (!description.contains("\n")) score -= 10;
return Math.max(0, Math.min(100, score));
}
private int evaluateWithLlm(ProductData product, ProductContent content) {
String prompt = """
请对以下商品描述内容进行质量评分(0-100分)。
商品品类:%s
【标题】
%s
【核心卖点】
%s
【详情描述】
%s
评分维度:
1. 信息完整性(是否覆盖核心参数和卖点)
2. 表达质量(语句通顺,无明显错误)
3. 营销效果(是否突出价值主张,吸引目标人群)
4. 合规性(无违禁词,无虚假宣传)
请直接回复一个0-100的整数分数,不要其他文字。
""".formatted(
product.getCategoryPath(),
content.getTitle(),
content.getKeySellingPoints() != null ?
String.join("\n", content.getKeySellingPoints()) : "",
content.getDetailedDescription()
);
try {
String response = llm.generate(prompt).trim();
// 提取数字
return Integer.parseInt(response.replaceAll("[^0-9]", ""));
} catch (Exception e) {
log.warn("LLM质量评分解析失败: {}", e.getMessage());
return 70; // 解析失败给默认分
}
}
public record ContentQualityScore(
int overall, int titleScore, int keyPointScore,
int descriptionScore, int llmScore
) {}
}批量生成和任务调度
/**
* 批量商品描述生成
*
* 生产环境里通常需要批量处理数千到数万个SKU
* 需要:速率限制、失败重试、进度跟踪、断点续传
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class BatchDescriptionGenerationService {
private final ProductDescriptionGenerator generator;
private final ProductRepository productRepository;
private final GenerationResultRepository resultRepository;
// 控制并发:LLM API有速率限制,通常10-50 QPS
private final Semaphore rateLimiter = new Semaphore(5); // 最大5并发
/**
* 批量生成任务
*/
public BatchGenerationJob startBatchJob(BatchJobConfig config) {
String jobId = "batch-" + System.currentTimeMillis();
log.info("启动批量生成任务: jobId={}, 商品数={}", jobId, config.getProductIds().size());
// 异步执行,不阻塞调用方
CompletableFuture.runAsync(() -> executeBatch(jobId, config));
return new BatchGenerationJob(jobId, config.getProductIds().size(), "RUNNING");
}
private void executeBatch(String jobId, BatchJobConfig config) {
List<String> productIds = config.getProductIds();
AtomicInteger processed = new AtomicInteger(0);
AtomicInteger failed = new AtomicInteger(0);
AtomicInteger skipped = new AtomicInteger(0);
// 过滤掉已经生成过的(断点续传)
List<String> toProcess = productIds.stream()
.filter(id -> !resultRepository.existsSuccessResult(id))
.toList();
skipped.set(productIds.size() - toProcess.size());
log.info("需要处理: {}, 已有结果跳过: {}", toProcess.size(), skipped.get());
// 分批处理,每批100个
List<List<String>> batches = partitionList(toProcess, 100);
for (List<String> batch : batches) {
// 检查任务是否被取消
if (isCancelled(jobId)) {
log.info("任务已取消: jobId={}", jobId);
break;
}
List<CompletableFuture<Void>> futures = batch.stream()
.map(productId -> processOneProduct(productId, config, processed, failed))
.toList();
// 等待当前批次完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
log.info("批次完成: jobId={}, 已处理={}/{}, 失败={}",
jobId, processed.get(), toProcess.size(), failed.get());
// 更新任务进度
updateJobProgress(jobId, processed.get(), failed.get(), toProcess.size());
}
log.info("批量任务完成: jobId={}, 总处理={}, 失败={}, 跳过={}",
jobId, processed.get(), failed.get(), skipped.get());
}
private CompletableFuture<Void> processOneProduct(
String productId, BatchJobConfig config,
AtomicInteger processed, AtomicInteger failed) {
return CompletableFuture.runAsync(() -> {
try {
rateLimiter.acquire();
try {
ProductData product = productRepository.getProductData(productId);
if (product == null) {
log.warn("商品数据不存在: productId={}", productId);
failed.incrementAndGet();
return;
}
// 应用配置的风格
if (config.getDefaultStyle() != null) {
product.setStyle(config.getDefaultStyle());
}
ProductContent content = generator.generate(product);
// 保存结果
resultRepository.saveResult(productId, content);
processed.incrementAndGet();
} finally {
rateLimiter.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
failed.incrementAndGet();
} catch (Exception e) {
log.error("商品处理失败: productId={}, error={}", productId, e.getMessage());
failed.incrementAndGet();
// 记录失败,方便后续重试
resultRepository.saveFailure(productId, e.getMessage());
}
});
}
/**
* 重试失败的商品
*/
public void retryFailed(String jobId) {
List<String> failedIds = resultRepository.getFailedProductIds(jobId);
log.info("重试失败商品: count={}", failedIds.size());
BatchJobConfig retryConfig = BatchJobConfig.builder()
.productIds(failedIds)
.build();
startBatchJob(retryConfig);
}
private boolean isCancelled(String jobId) {
// 检查Redis中的取消标志
return false; // 简化实现
}
private void updateJobProgress(String jobId, int processed, int failed, int total) {
// 更新Redis中的进度
}
private <T> List<List<T>> partitionList(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
partitions.add(list.subList(i, Math.min(i + size, list.size())));
}
return partitions;
}
@Data
@Builder
public static class BatchJobConfig {
private List<String> productIds;
private ProductData.ContentStyle defaultStyle;
private boolean overwriteExisting;
}
public record BatchGenerationJob(String jobId, int totalCount, String status) {}
}多平台适配
/**
* 不同电商平台的内容规范不同
*
* 京东:标题不超过60字,卖点最多5条
* 淘宝:标题30字以内,有关键词密度要求
* 拼多多:标题直接、突出低价和促销
*
* 用适配器模式处理多平台差异
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class PlatformContentAdapter {
/**
* 适配到目标平台规范
*/
public ProductContent adaptForPlatform(
ProductContent source, Platform platform) {
return switch (platform) {
case JD -> adaptForJD(source);
case TAOBAO -> adaptForTaobao(source);
case PINDUODUO -> adaptForPdd(source);
default -> source;
};
}
private ProductContent adaptForJD(ProductContent source) {
// 京东标题最长60字
String title = source.getTitle();
if (title.length() > 60) {
title = title.substring(0, 60);
}
// 京东卖点最多5条
List<String> keyPoints = source.getKeySellingPoints().stream()
.limit(5)
.toList();
return ProductContent.builder()
.title(title)
.keySellingPoints(keyPoints)
.detailedDescription(source.getDetailedDescription())
.seoDescription(source.getSeoDescription())
.searchTags(source.getSearchTags())
.build();
}
private ProductContent adaptForTaobao(ProductContent source) {
// 淘宝标题30字以内
String title = source.getTitle();
if (title.length() > 30) {
// 截断但保持语义完整
title = truncateTitle(title, 30);
}
return ProductContent.builder()
.title(title)
.keySellingPoints(source.getKeySellingPoints())
.detailedDescription(source.getDetailedDescription())
.seoDescription(source.getSeoDescription())
.searchTags(source.getSearchTags())
.build();
}
private ProductContent adaptForPdd(ProductContent source) {
// 拼多多风格:突出价格、促销、实惠
// 如果标题里没有价值词,在结尾加
String title = source.getTitle();
if (!title.contains("价") && !title.contains("特") && !title.contains("惠")) {
if (title.length() < 25) {
title += " 高性价比";
}
}
return ProductContent.builder()
.title(title)
.keySellingPoints(source.getKeySellingPoints())
.detailedDescription(source.getDetailedDescription())
.seoDescription(source.getSeoDescription())
.searchTags(source.getSearchTags())
.build();
}
private String truncateTitle(String title, int maxLen) {
// 按标点切割,取不超过maxLen的最长前缀
String[] parts = title.split("[,、]");
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (sb.length() + part.length() <= maxLen) {
if (sb.length() > 0) sb.append(",");
sb.append(part);
} else {
break;
}
}
return sb.length() > 0 ? sb.toString() : title.substring(0, maxLen);
}
public enum Platform { JD, TAOBAO, PINDUODUO, DOUYIN }
}实践中的教训
提示词工程投入比模型选择更重要
我们最开始用GPT-4生成,效果还行但成本高。后来改用Qwen-7B本地部署,第一版效果很差,但经过两周提示词优化后,效果追上了GPT-4的85%,成本却只有1/20。商品描述生成是提示词敏感型任务,花在提示词上的时间绝对值得。
分品类优化比通用优化效果好
不同品类的商品描述差异很大:3C商品要技术参数,食品要口感描述,服装要穿搭建议。一套通用提示词做出来的描述,哪个品类都不够好。我们最终按大品类维护了8套提示词模板,每套单独优化,效果比通用版好30%以上。
质量评估的阈值要靠A/B测试定
质量分的阈值(比如70分以下需要人工审核)不能拍脑袋,要根据"低于X分的内容投诉率"来定。我们最开始设60分,结果50-60分区间有大量内容点击率很低,说明用户隐性投诉(不买)。调整到75分后,低分商品全走人工,整体转化率提升了8%。
