第1726篇:动态提示词生成——根据用户画像实时组装个性化Prompt
第1726篇:动态提示词生成——根据用户画像实时组装个性化Prompt
有一个场景让我意识到"一刀切"的系统提示有多大的局限性。
我们给某个教育平台做AI辅导功能,系统提示是统一的——讲师角色、讲解风格、回答规范。结果用户反馈两极分化:有的学生说"解释太简单了,我已经懂这些了,想要更深入的内容",有的说"太难了,根本看不懂"。
同一个Prompt,同一个模型,输出的内容在不同用户眼里天差地别。
这个问题催生了我们后来做的动态Prompt生成系统——根据用户画像,实时组装个性化的系统提示。
核心思路:Prompt模块化
动态Prompt生成的关键,是把提示词从"整块文本"拆解成"可组合的模块"。
用户画像的构建
动态Prompt的质量上限取决于用户画像的质量。画像越准确,个性化越有效。
我们用三个维度的数据构建用户画像:
@Data
@Builder
public class UserProfile {
// 基础信息
private String userId;
private String preferredLanguage; // zh-CN, en-US, etc.
private UserType userType; // DEVELOPER, BUSINESS, STUDENT, GENERAL
// 技术水平(0-10)
private double technicalLevel; // 通过历史交互推断
private Map<String, Double> domainExpertise; // 各领域的专业度,如 {"java": 8.5, "ai": 5.0}
// 交互偏好
private ResponseLengthPreference lengthPreference; // CONCISE, STANDARD, DETAILED
private boolean prefersCodeExamples;
private boolean prefersAnalogies;
private CommunicationStyle communicationStyle; // FORMAL, CASUAL, TECHNICAL
// 当前会话上下文
private String currentTaskType; // 当前在做什么
private int conversationTurnCount; // 对话轮次
private List<String> recentTopics; // 最近讨论的话题
// 历史行为信号
private int avgMessageLength; // 用户平均消息长度(反映习惯)
private double codeQuestionRatio; // 代码类问题占比
private double feedbackPositiveRate; // 正面反馈比例
}用户画像不是静态的,它需要随着交互不断更新:
@Service
public class UserProfileService {
@Autowired
private UserProfileRepository profileRepo;
@Autowired
private TechnicalLevelEstimator technicalLevelEstimator;
/**
* 根据当前消息更新用户画像
* 轻量级、低延迟,在每次请求时调用
*/
public UserProfile updateAndGet(String userId, String userMessage, Map<String, Object> sessionContext) {
UserProfile profile = profileRepo.findById(userId)
.orElse(UserProfile.createDefault(userId));
// 推断技术水平(使用专业术语密度、问题复杂度等信号)
double inferredLevel = technicalLevelEstimator.estimate(userMessage, profile);
// 指数移动平均,避免单次消息对画像影响太大
double updatedLevel = profile.getTechnicalLevel() * 0.9 + inferredLevel * 0.1;
profile.setTechnicalLevel(updatedLevel);
// 更新对话轮次
profile.setConversationTurnCount(profile.getConversationTurnCount() + 1);
// 更新消息长度统计
int currentAvgLength = (int)(profile.getAvgMessageLength() * 0.9 + userMessage.length() * 0.1);
profile.setAvgMessageLength(currentAvgLength);
// 更新最近话题
List<String> recentTopics = new ArrayList<>(profile.getRecentTopics());
String topic = extractTopic(userMessage);
if (topic != null) {
recentTopics.add(0, topic);
if (recentTopics.size() > 5) recentTopics = recentTopics.subList(0, 5);
}
profile.setRecentTopics(recentTopics);
profileRepo.save(profile);
return profile;
}
/**
* 估算用户技术水平
* 基于消息中的技术词汇密度和问题结构
*/
@Component
public static class TechnicalLevelEstimator {
private static final Set<String> BASIC_TECH_TERMS = Set.of(
"函数", "变量", "循环", "数组", "类", "对象", "接口"
);
private static final Set<String> ADVANCED_TECH_TERMS = Set.of(
"并发", "事务", "索引", "分片", "一致性", "熔断", "幂等",
"CAS", "AQS", "JVM", "GC", "Spring", "Kafka", "分布式"
);
private static final Set<String> EXPERT_TECH_TERMS = Set.of(
"时间复杂度", "CAP定理", "Raft协议", "MVCC", "锁升级",
"字节码", "JIT编译", "内存屏障", "happens-before"
);
public double estimate(String message, UserProfile currentProfile) {
long wordCount = Arrays.stream(message.split("[\\s,。!?]")).count();
if (wordCount == 0) return currentProfile.getTechnicalLevel();
long basicCount = BASIC_TECH_TERMS.stream()
.filter(message::contains).count();
long advancedCount = ADVANCED_TECH_TERMS.stream()
.filter(message::contains).count();
long expertCount = EXPERT_TECH_TERMS.stream()
.filter(message::contains).count();
// 加权计算技术水平得分
double score = (basicCount * 1.0 + advancedCount * 2.5 + expertCount * 4.0) / wordCount * 100;
return Math.min(10, score);
}
}
}Prompt模块库设计
把Prompt拆成独立的、可组合的模块,每个模块针对不同维度的变化:
@Entity
@Table(name = "prompt_module")
public class PromptModule {
@Id
private String id;
private String moduleType; // ROLE, STYLE, CONSTRAINT, EXAMPLE, FORMAT
private String dimension; // 适用的维度,如 "technical_level"
private String conditionExpression; // SpEL表达式,决定该模块是否激活
@Column(columnDefinition = "TEXT")
private String content; // 模块内容模板,支持变量替换
private int priority; // 优先级,同类型模块冲突时按优先级选择
private int tokenCount; // 预估Token数
}具体模块示例:
// 这些模块实际存储在数据库,这里用Java常量表示
public class PromptModuleLibrary {
// 角色模块(不同用户类型)
public static final String ROLE_FOR_DEVELOPER = """
你是一位经验丰富的高级Java开发工程师,擅长解答技术问题,给出工程化的解决方案。
""";
public static final String ROLE_FOR_STUDENT = """
你是一位耐心友善的技术辅导老师,擅长把复杂概念用简单易懂的方式讲解清楚。
""";
public static final String ROLE_FOR_BUSINESS = """
你是一位技术与业务兼备的顾问,能把技术方案翻译成业务语言,帮助非技术人员理解。
""";
// 技术水平适配模块
public static final String STYLE_BEGINNER = """
在回答时:
- 使用日常生活中的类比来解释技术概念
- 避免使用专业术语,必须使用时要先解释含义
- 循序渐进,不跳步骤
""";
public static final String STYLE_INTERMEDIATE = """
在回答时:
- 直接使用标准技术术语,无需解释基础概念
- 重点放在核心逻辑和设计决策上
- 适当指出常见的坑和注意事项
""";
public static final String STYLE_EXPERT = """
在回答时:
- 默认对方熟悉底层原理,直接讨论深层机制
- 可以引用RFC、论文或底层源码来佐证
- 讨论多种方案的权衡取舍,不给"唯一正确答案"
""";
// 响应长度模块
public static final String LENGTH_CONCISE = """
回答要简洁,优先用要点和代码示例,减少冗余解释。
除非被追问,否则控制在300字以内。
""";
public static final String LENGTH_DETAILED = """
提供详细的解释,包括背景知识、核心逻辑、实现步骤和注意事项。
必要时给出完整可运行的代码示例。
""";
}Prompt组装引擎
核心逻辑:根据用户画像,从模块库中选择并组装合适的模块:
@Service
public class DynamicPromptAssembler {
@Autowired
private PromptModuleRepository moduleRepo;
@Autowired
private ExpressionEvaluator expressionEvaluator;
@Autowired
private TemplateEngine templateEngine;
/**
* 为特定用户和任务组装个性化系统提示
*/
public AssembledPrompt assemble(UserProfile userProfile, TaskContext taskContext) {
log.debug("开始组装Prompt,用户ID: {}, 技术水平: {}",
userProfile.getUserId(), userProfile.getTechnicalLevel());
// 获取所有候选模块
List<PromptModule> allModules = moduleRepo.findAll();
// 按模块类型分组
Map<String, List<PromptModule>> modulesByType = allModules.stream()
.collect(Collectors.groupingBy(PromptModule::getModuleType));
// 为每个类型选择激活的模块
List<PromptModule> activatedModules = new ArrayList<>();
for (Map.Entry<String, List<PromptModule>> entry : modulesByType.entrySet()) {
String moduleType = entry.getKey();
List<PromptModule> candidates = entry.getValue();
// 找出满足条件的模块
List<PromptModule> matchingModules = candidates.stream()
.filter(m -> evaluateCondition(m, userProfile, taskContext))
.sorted(Comparator.comparingInt(PromptModule::getPriority).reversed())
.collect(Collectors.toList());
if (!matchingModules.isEmpty()) {
// 通常每个类型只取最高优先级的一个
activatedModules.add(matchingModules.get(0));
}
}
// 按预定义顺序拼接模块
String assembledContent = assembleByOrder(activatedModules, userProfile, taskContext);
// 计算Token数
int estimatedTokens = activatedModules.stream()
.mapToInt(PromptModule::getTokenCount)
.sum();
log.debug("Prompt组装完成,激活模块数: {}, 估算Token: {}",
activatedModules.size(), estimatedTokens);
return AssembledPrompt.builder()
.content(assembledContent)
.activatedModules(activatedModules.stream()
.map(PromptModule::getId)
.collect(Collectors.toList()))
.estimatedTokenCount(estimatedTokens)
.userProfileSnapshot(userProfile)
.build();
}
/**
* 评估模块的激活条件(使用SpEL表达式)
*/
private boolean evaluateCondition(PromptModule module, UserProfile profile, TaskContext context) {
String condition = module.getConditionExpression();
if (condition == null || condition.isBlank()) return true;
// 构建评估上下文
StandardEvaluationContext evalContext = new StandardEvaluationContext();
evalContext.setVariable("user", profile);
evalContext.setVariable("task", context);
// 示例条件表达式:
// "#user.technicalLevel >= 7"
// "#user.userType == 'DEVELOPER' && #task.taskType == 'CODE_REVIEW'"
// "#user.conversationTurnCount > 3 && #user.prefersCodeExamples"
try {
Expression expression = expressionEvaluator.parseExpression(condition);
return Boolean.TRUE.equals(expression.getValue(evalContext, Boolean.class));
} catch (Exception e) {
log.warn("条件表达式评估失败,模块ID: {}, 表达式: {}", module.getId(), condition);
return false;
}
}
/**
* 按照固定顺序拼接模块:角色 → 风格 → 约束 → 示例 → 格式
*/
private String assembleByOrder(List<PromptModule> modules, UserProfile profile, TaskContext context) {
List<String> moduleOrder = List.of("ROLE", "STYLE", "KNOWLEDGE", "CONSTRAINT", "EXAMPLE", "FORMAT");
Map<String, PromptModule> moduleMap = modules.stream()
.collect(Collectors.toMap(PromptModule::getModuleType, m -> m, (a, b) -> a));
StringBuilder result = new StringBuilder();
for (String type : moduleOrder) {
PromptModule module = moduleMap.get(type);
if (module != null) {
// 渲染模板变量
String rendered = renderTemplate(module.getContent(), profile, context);
result.append(rendered.trim()).append("\n\n");
}
}
return result.toString().trim();
}
/**
* 渲染模板中的变量
* 支持 {{user.name}}, {{task.domain}} 等占位符
*/
private String renderTemplate(String template, UserProfile profile, TaskContext context) {
Map<String, Object> variables = Map.of(
"userName", profile.getUserId(),
"technicalLevel", profile.getTechnicalLevel(),
"domain", context.getDomain() != null ? context.getDomain() : "技术",
"language", profile.getPreferredLanguage()
);
String result = template;
for (Map.Entry<String, Object> entry : variables.entrySet()) {
result = result.replace("{{" + entry.getKey() + "}}", entry.getValue().toString());
}
return result;
}
}实时组装的调用链
在实际请求中,整个流程如下:
@Service
public class PersonalizedChatService {
@Autowired
private UserProfileService profileService;
@Autowired
private DynamicPromptAssembler promptAssembler;
@Autowired
private LLMGateway llmGateway;
@Autowired
private PromptUsageTracker usageTracker;
public ChatResponse chat(ChatRequest request) {
long startTime = System.currentTimeMillis();
// 1. 更新并获取用户画像(约5-15ms)
UserProfile userProfile = profileService.updateAndGet(
request.getUserId(),
request.getMessage(),
request.getSessionContext()
);
// 2. 组装个性化Prompt(约1-3ms,纯内存操作)
TaskContext taskContext = TaskContext.fromRequest(request);
AssembledPrompt assembledPrompt = promptAssembler.assemble(userProfile, taskContext);
// 3. 调用LLM(主要耗时所在)
LLMResponse llmResponse = llmGateway.call(
LLMRequest.builder()
.systemPrompt(assembledPrompt.getContent())
.userMessage(request.getMessage())
.conversationHistory(request.getHistory())
.build()
);
// 4. 记录使用情况(用于后续画像更新和效果分析)
usageTracker.track(PromptUsageRecord.builder()
.userId(request.getUserId())
.assembledPromptId(assembledPrompt.getId())
.activatedModules(assembledPrompt.getActivatedModules())
.responseTokens(llmResponse.getTokenCount())
.latencyMs(System.currentTimeMillis() - startTime)
.build());
return ChatResponse.of(llmResponse.getContent());
}
}注意第一步"更新并获取用户画像"的延迟控制。这个操作要非常轻量,画像的复杂更新(如重新计算领域专业度评分)应该异步进行,不能阻塞主请求。
冷启动问题的处理
新用户没有画像数据,怎么办?这是动态Prompt系统必须面对的冷启动问题。
策略一:基于注册信息的初始画像
public UserProfile createInitialProfile(UserRegistrationInfo regInfo) {
return UserProfile.builder()
.userId(regInfo.getUserId())
.userType(inferUserType(regInfo.getOccupation(), regInfo.getJobTitle()))
.technicalLevel(estimateInitialLevel(regInfo))
.communicationStyle(CommunicationStyle.STANDARD)
.lengthPreference(ResponseLengthPreference.STANDARD)
.build();
}策略二:前几轮对话快速探测
// 在对话开始时(前3轮),使用探测性的风格,同时观察用户反应
// 如果用户给了详细、有深度的问题,提高技术水平估值
// 如果用户反馈"看不懂"或"太深了",降低技术水平估值策略三:允许用户自主设定
// 在用户设置里提供画像配置界面
// 这既解决了冷启动问题,又提高了用户参与感效果衡量与AB测试
动态Prompt的效果需要和静态Prompt做对比测试:
我们做过一次AB测试,持续两周,对比同一批用户使用静态Prompt和动态Prompt的体验指标:
| 指标 | 静态Prompt | 动态Prompt | 提升 |
|---|---|---|---|
| 用户满意度评分 | 3.6/5 | 4.1/5 | +13.9% |
| 会话平均轮次 | 3.2轮 | 4.8轮 | +50% |
| 次日留存 | 42% | 51% | +21% |
| "看不懂"类反馈 | 18% | 7% | -61% |
数字比较好看,但也有几个局限:这是特定场景(教育辅导)的数据,其他场景可能不同;会话轮次增加不一定全是好事,也可能说明动态Prompt没有一次性解决问题,需要更多轮次才到位。
我踩的一个坑:过度个性化
有一段时间,我们的技术水平估算过于激进。用户连续问了几个高技术水平的问题后,系统会把他的技术水平快速提升到8-9,然后开始用非常专业的术语回答后续所有问题。
问题是:用户在某个领域问了高质量的问题,不代表他在所有领域都是专家。一个Java架构师问"什么是k8s的Rolling Update",可能他只是K8s新手,这是完全合理的。
解决方案:技术水平按领域细分,而不是用一个全局水平值。用户在Java上是8分,在K8s上可以是2分,这两个维度独立更新。
小结
动态Prompt生成的价值核心在于:把"用好AI"的认知负担从用户转移到系统上。
用户不需要知道怎么"调教"AI,不需要每次都费心描述自己的背景。系统自动感知用户是谁,自动调整沟通方式。这是真正的"智能"助手应该有的样子。
