内容生成平台
大约 4 分钟
第十八章:内容生成平台
18.1 项目架构:把“生成”做成可运营能力
内容生成平台通常不是一个接口那么简单,它更像一个“生产线”:
- 输入:模板 + 参数
- 生成:选择模型、组合 prompt、调用工具(可选)
- 审核:人工或自动检测(敏感词、事实性、风格)
- 发布:落库、发布到 CMS、同步到渠道
建议第一版先把“任务化 + 模板化 + 审核流”做扎实。
18.2 核心功能:模板 + 任务 + 版本
建议的数据概念:
- Template:一段可参数化的 prompt(带变量)
- Job:一次生成任务(输入参数、模型、状态)
- Version:一次 Job 可能生成多个版本(多模型、多次重试)
18.3 最小实现(可复制运行)
下面是一个最小任务化实现:
/content/jobs:提交生成任务,返回 jobId/content/jobs/{id}:查询任务状态与结果
18.3.1 Job 状态与实体
package com.example.content.domain;
public enum JobStatus {
PENDING,
RUNNING,
SUCCEEDED,
FAILED
}package com.example.content.domain;
import java.time.Instant;
import java.util.Optional;
public record ContentJob(
String id,
String templateId,
String variablesJson,
JobStatus status,
Optional<String> result,
Optional<String> error,
Instant createdAt
) {
}18.3.2 内存 Job 仓库(生产请换 DB)
package com.example.content.repo;
import com.example.content.domain.ContentJob;
import com.example.content.domain.JobStatus;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;
@Repository
public class InMemoryJobRepository {
private final ConcurrentHashMap<String, ContentJob> store = new ConcurrentHashMap<>();
public ContentJob create(String templateId, String variablesJson) {
String id = UUID.randomUUID().toString();
ContentJob job = new ContentJob(id, templateId, variablesJson, JobStatus.PENDING, Optional.empty(), Optional.empty(), Instant.now());
store.put(id, job);
return job;
}
public Optional<ContentJob> find(String id) {
return Optional.ofNullable(store.get(id));
}
public void update(ContentJob job) {
store.put(job.id(), job);
}
}18.3.3 模板:用 PromptTemplate 组装
package com.example.content.template;
import java.util.Map;
import org.springframework.ai.prompt.Prompt;
import org.springframework.ai.prompt.PromptTemplate;
import org.springframework.stereotype.Component;
@Component
public class TemplateRegistry {
public Prompt buildPrompt(String templateId, Map<String, Object> variables) {
PromptTemplate template = switch (templateId) {
case "product-intro" -> new PromptTemplate("""
你是资深电商运营。请为以下商品写一段介绍文案,要求:
- 120~180 字
- 口吻自然
- 包含 3 个卖点(用短句分行)
商品信息:
标题:{title}
亮点:{highlights}
""");
default -> new PromptTemplate("""
请基于以下信息生成一段专业但易懂的内容:
{content}
""");
};
return template.create(variables);
}
}18.3.4 生成器:异步执行 Job
package com.example.content.service;
import com.example.content.domain.ContentJob;
import com.example.content.domain.JobStatus;
import com.example.content.repo.InMemoryJobRepository;
import com.example.content.template.TemplateRegistry;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.prompt.Prompt;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class ContentJobRunner {
private final InMemoryJobRepository repo;
private final TemplateRegistry templates;
private final ChatClient chatClient;
private final ObjectMapper mapper = new ObjectMapper();
public ContentJobRunner(InMemoryJobRepository repo, TemplateRegistry templates, ChatClient chatClient) {
this.repo = repo;
this.templates = templates;
this.chatClient = chatClient;
}
@Async
public void run(String jobId) {
ContentJob job = repo.find(jobId).orElseThrow();
repo.update(new ContentJob(job.id(), job.templateId(), job.variablesJson(), JobStatus.RUNNING, Optional.empty(), Optional.empty(), job.createdAt()));
try {
Map<String, Object> vars = mapper.readValue(job.variablesJson(), new TypeReference<>() {});
Prompt prompt = templates.buildPrompt(job.templateId(), vars);
String content = chatClient.prompt(prompt).call().content();
repo.update(new ContentJob(job.id(), job.templateId(), job.variablesJson(), JobStatus.SUCCEEDED, Optional.of(content), Optional.empty(), job.createdAt()));
} catch (Exception e) {
repo.update(new ContentJob(job.id(), job.templateId(), job.variablesJson(), JobStatus.FAILED, Optional.empty(), Optional.of(e.getMessage()), job.createdAt()));
}
}
}启用异步:
package com.example.content;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class ContentApplication {
}18.3.5 API:提交任务与查询结果
package com.example.content.api;
import com.example.content.domain.ContentJob;
import com.example.content.repo.InMemoryJobRepository;
import com.example.content.service.ContentJobRunner;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ContentJobController {
private final InMemoryJobRepository repo;
private final ContentJobRunner runner;
public ContentJobController(InMemoryJobRepository repo, ContentJobRunner runner) {
this.repo = repo;
this.runner = runner;
}
@PostMapping("/content/jobs")
public ApiResponse<Map<String, String>> create(@RequestBody CreateJobRequest request) {
ContentJob job = repo.create(request.templateId(), request.variablesJson());
runner.run(job.id());
return ApiResponse.ok(Map.of("jobId", job.id()));
}
@GetMapping("/content/jobs/{id}")
public ApiResponse<ContentJob> get(@PathVariable String id) {
return repo.find(id)
.map(ApiResponse::ok)
.orElseGet(() -> ApiResponse.fail("job not found"));
}
public record CreateJobRequest(String templateId, String variablesJson) {
}
}18.4 用户体验:生成、审核、发布三段式
上线后你会发现:生成只是第一步,质量与合规更重要。
建议逐步加入:
- 敏感词检测与合规策略
- 多版本生成 + 评分(让运营选择)
- A/B 模型对比与提示词版本管理 点击这里👇🏻获取:100万QPS短链系统、复杂的商城微服务系统、智能翻译助手AI Agent、SaaS点餐系统、刷题吧小程序、商城系统、秒杀系统、AI项目、代码生成神器、苏三demo项目、智能天气播报AI Agent、智能代码审查AI Agent等 10 个项目的:项目源代码、开发教程和技术答疑
