第2222篇:多模态AI的Prompt工程——怎样描述图片才能得到准确输出
2026/4/30大约 10 分钟
第2222篇:多模态AI的Prompt工程——怎样描述图片才能得到准确输出
适读人群:使用多模态大模型做应用开发的工程师和产品经理 | 阅读时长:约15分钟 | 核心价值:掌握多模态Prompt的设计方法,大幅提升图片理解和生成任务的准确率
刚开始用 GPT-4V 的时候,我以为只要把图片丢进去,然后问问题就够了。
第一版 Prompt 是这样的:"请分析这张图片。"
然后我得到了一段无关痛痒的通用描述——模型在说废话,因为它不知道我想要什么。
后来改成了更具体的问法,准确率直接跳了 30%。
多模态 Prompt 工程和纯文本 Prompt 工程有不同之处:你不只是在引导模型的思考方向,还在引导模型"看哪里"和"怎么看"。
多模态 Prompt 的核心挑战
纯文本 Prompt 的挑战是语义模糊。多模态 Prompt 多了一层挑战:
挑战一:模型不知道你关注哪个区域。 一张复杂图片有很多元素,如果不明确指定关注点,模型会随机描述它认为重要的部分。
挑战二:语言与图像的歧义对应。 "图中的数字"可能指价格数字、图表数值、页码,不同场景意义完全不同。
挑战三:输出格式不可控。 不指定输出格式,模型可能返回一段散文,难以程序化处理。
挑战四:图片分辨率和细节限制。 模型看到的图片经过压缩,某些细节可能不清晰,需要在 Prompt 中引导模型处理这种不确定性。
Prompt 设计的五个维度
维度一:精确的任务定义
差的 Prompt: "请分析这张发票图片。"
好的 Prompt: "这是一张增值税发票图片。请提取以下字段:
- 发票号码
- 开票日期(格式:YYYY-MM-DD)
- 购买方名称
- 销售方名称
- 价税合计金额(数字,单位:元)
只输出JSON,不要任何说明文字。"
/**
* 多模态任务 Prompt 模板库
* 按任务类型提供预定义的高质量 Prompt 模板
*/
@Component
public class MultimodalPromptLibrary {
/**
* 发票信息提取 Prompt
*/
public String invoiceExtractionPrompt(List<String> requiredFields) {
StringBuilder sb = new StringBuilder();
sb.append("这是一张发票图片。请仔细识别并提取以下信息:\n\n");
for (int i = 0; i < requiredFields.size(); i++) {
sb.append(String.format("%d. %s\n", i + 1, requiredFields.get(i)));
}
sb.append("""
注意事项:
- 字段不存在或无法识别时,输出 null
- 金额字段只输出数字,不含货币符号
- 日期统一格式为 YYYY-MM-DD
- 如果图片模糊导致无法确认某字符,用[?]标注
输出格式:严格JSON,不要任何额外文字
""");
return sb.toString();
}
/**
* 图表数据提取 Prompt
*/
public String chartDataExtractionPrompt(String chartContext) {
return String.format("""
这是一张数据图表(%s)。请提取图表中的数据信息。
请按以下步骤处理:
步骤1:确认图表类型(折线图/柱状图/饼图/散点图/其他)
步骤2:读取坐标轴标签和单位
步骤3:提取所有可见的数据点和数值
步骤4:用一句话描述趋势或核心结论
输出JSON格式:
{
"chartType": "图表类型",
"xAxis": {"label": "X轴标签", "unit": "单位"},
"yAxis": {"label": "Y轴标签", "unit": "单位"},
"series": [
{"name": "系列名", "dataPoints": [{"x": "值", "y": "值"}, ...]}
],
"conclusion": "趋势结论"
}
如果某个数据点数值模糊,在值后加"?"标注(如"1234?")
""", chartContext);
}
/**
* 产品图片属性提取 Prompt(电商场景)
*/
public String productAttributeExtractionPrompt(String category,
List<String> attributes) {
String attrList = attributes.stream()
.map(a -> "- " + a)
.collect(Collectors.joining("\n"));
return String.format("""
这是一张%s的产品图片。
请从图片中提取以下属性(只描述图片中可以直接看到的内容):
%s
重要规则:
1. 只描述图片中明确可见的属性,不要推断
2. 颜色描述要准确(区分"红色"和"深红色/玫红色"等)
3. 如果某属性在图中看不清或不可见,填写 "不可见"
4. 不要描述背景,只描述主体产品
输出JSON,属性名作为key:
""", category, attrList);
}
/**
* 通用视觉问答 Prompt(带不确定性引导)
*/
public String visualQaPrompt(String question, boolean requireEvidence) {
String evidenceRequirement = requireEvidence ?
"\n5. 在回答末尾引用支持回答的图片区域(如:'如图左上角所示...')" : "";
return String.format("""
请回答以下关于图片的问题:
问题:%s
回答规则:
1. 只描述图片中确实可见的内容
2. 如果无法从图片中确认,明确说"图片中未见"或"图片不够清晰"
3. 不要基于常识补充图片中不存在的信息
4. 如果有多种可能的解读,列出所有可能%s
""", question, evidenceRequirement);
}
}维度二:视觉焦点引导
当图片内容复杂时,告诉模型"看哪里":
/**
* 视觉焦点引导策略
* 通过 Prompt 将模型注意力引导到关键区域
*/
@Component
public class VisualFocusGuide {
/**
* 区域引导:用文字描述图片区域
*/
public String buildRegionGuidedPrompt(String baseTask, String regionDescription) {
return String.format("""
%s
请重点关注图片的以下区域:%s
其他区域只需简单描述,不用详细分析。
""", baseTask, regionDescription);
}
/**
* 对比引导:同时分析多张图片
*/
public String buildComparisonPrompt(String comparisonAspect, int imageCount) {
StringBuilder sb = new StringBuilder();
sb.append("以下提供了").append(imageCount).append("张图片进行对比分析。\n\n");
sb.append("请从以下方面进行对比:").append(comparisonAspect).append("\n\n");
sb.append("输出格式:\n");
sb.append("【共同点】\n...\n\n");
sb.append("【差异点】\n...\n\n");
sb.append("【综合结论】\n...");
return sb.toString();
}
/**
* 序列引导:图片序列的时序分析
*/
public String buildSequenceAnalysisPrompt(String sequenceContext) {
return String.format("""
以下是一组按时间顺序排列的图片(共[N]张)。
背景信息:%s
请分析:
1. 每张图片的状态描述(用一句话)
2. 相邻图片之间发生了什么变化
3. 整体趋势或发展方向
输出格式:
图片1:[状态]
图片2:[状态](相比图片1:[变化])
...
整体趋势:[总结]
""", sequenceContext);
}
/**
* 层次引导:先整体后局部
* 引导模型先描述全局再深入细节,减少细节遗漏
*/
public String buildHierarchicalPrompt(String analysisGoal) {
return String.format("""
请按以下层次分析图片(目标:%s):
第一层:整体描述(30字以内,描述图片的全貌)
第二层:主要元素(列出5个以内的主要组成部分)
第三层:关键细节(针对目标%s,详细描述相关细节)
按此格式输出,每层独立段落。
""", analysisGoal, analysisGoal);
}
}维度三:输出格式控制
不同用途需要不同的输出格式:
/**
* 结构化输出格式控制器
* 确保多模态模型输出符合程序处理要求
*/
@Component
public class StructuredOutputController {
/**
* JSON Schema 约束输出
* 通过在 Prompt 中给出 JSON Schema,引导模型输出严格格式
*/
public String buildJsonSchemaPrompt(String task, Class<?> outputClass) {
ObjectMapper mapper = new ObjectMapper();
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
try {
JsonSchema schema = schemaGen.generateSchema(outputClass);
String schemaStr = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(schema);
return String.format("""
%s
严格按照以下 JSON Schema 输出,不要任何额外文字或 markdown 代码块:
Schema:
%s
""", task, schemaStr);
} catch (Exception e) {
return task + "\n\n请输出 JSON 格式。";
}
}
/**
* 置信度标注输出
* 让模型同时输出结果和置信度
*/
public String buildConfidenceAnnotatedPrompt(String task) {
return task + """
对每个识别结果,同时给出置信度评分(0.0-1.0):
- 0.9-1.0:完全清晰,可以确认
- 0.7-0.9:基本清晰,较为确定
- 0.5-0.7:有些模糊,推断结果
- 低于0.5:非常模糊,结果不可靠
输出格式:
{
"results": [
{"field": "字段名", "value": "识别值", "confidence": 0.95}
]
}
""";
}
/**
* 解析带置信度的模型输出,低置信度字段标记为待审核
*/
public ParsedResult<Map<String, Object>> parseConfidenceAnnotatedOutput(
String modelOutput, double reviewThreshold) {
try {
String cleaned = modelOutput.replaceAll("```json\\s*", "")
.replaceAll("```\\s*", "").trim();
JsonNode root = new ObjectMapper().readTree(cleaned);
Map<String, Object> values = new HashMap<>();
List<String> lowConfidenceFields = new ArrayList<>();
JsonNode results = root.get("results");
if (results != null) {
for (JsonNode item : results) {
String field = item.get("field").asText();
String value = item.get("value").asText(null);
double confidence = item.get("confidence").asDouble(0.5);
values.put(field, value);
if (confidence < reviewThreshold) {
lowConfidenceFields.add(field);
}
}
}
return ParsedResult.<Map<String, Object>>builder()
.data(values)
.lowConfidenceFields(lowConfidenceFields)
.requiresReview(!lowConfidenceFields.isEmpty())
.build();
} catch (Exception e) {
throw new OutputParseException("模型输出解析失败: " + modelOutput, e);
}
}
}维度四:Few-Shot 多模态示例
给模型提供示例是最有效的 Prompt 技巧之一:
/**
* Few-Shot 多模态 Prompt 构建器
* 通过图文示例引导模型输出期望格式
*/
@Service
public class FewShotMultimodalPromptBuilder {
/**
* 构建带示例的多模态 Prompt
*
* 注意:Few-Shot 示例中的图片最好与目标任务类型相似
* 但不能是完全相同的图片(避免模型记忆而非理解)
*/
public List<Message> buildFewShotPrompt(
String systemInstruction,
List<FewShotExample> examples,
String userQuestion,
byte[] targetImageBytes) {
List<Message> messages = new ArrayList<>();
messages.add(SystemMessage.of(systemInstruction));
// 添加示例对
for (FewShotExample example : examples) {
// 用户展示示例图片
String exampleBase64 = Base64.getEncoder()
.encodeToString(example.getImageBytes());
messages.add(UserMessage.ofMultipart(
TextPart.of(example.getQuestion()),
ImagePart.ofBase64(exampleBase64, "image/jpeg")
));
// 助手给出期望的输出格式
messages.add(AssistantMessage.of(example.getExpectedOutput()));
}
// 实际任务
String targetBase64 = Base64.getEncoder().encodeToString(targetImageBytes);
messages.add(UserMessage.ofMultipart(
TextPart.of(userQuestion),
ImagePart.ofBase64(targetBase64, "image/jpeg")
));
return messages;
}
/**
* 构建示例库
* 从已标注的数据中选取高质量示例
*/
public List<FewShotExample> buildExampleLibrary(String taskType,
List<AnnotatedSample> samples) {
// 从样本中选取最具代表性的示例
// 策略:选择不同类型/难度的样本,覆盖边界情况
return samples.stream()
.filter(s -> s.getConfidence() > 0.95) // 只用高置信度样本
.filter(s -> s.getTaskType().equals(taskType))
.sorted(Comparator.comparingDouble(AnnotatedSample::getDiversity).reversed())
.limit(3) // Few-Shot 通常3-5个示例就够
.map(s -> FewShotExample.builder()
.imageBytes(s.getImageBytes())
.question(s.getQuestion())
.expectedOutput(s.getExpectedOutput())
.build())
.collect(Collectors.toList());
}
}维度五:迭代优化与 A/B 测试
Prompt 优化需要系统性的测试框架:
/**
* 多模态 Prompt A/B 测试框架
* 量化对比不同 Prompt 版本的效果
*/
@Service
@Slf4j
public class PromptAbTestingService {
@Autowired
private OpenAiClient openAiClient;
@Autowired
private PromptEvaluator evaluator;
/**
* 在评估集上对比多个 Prompt 版本
*/
public AbTestReport runAbTest(List<PromptVariant> variants,
List<EvalSample> evalSamples) {
Map<String, List<EvalScore>> variantScores = new HashMap<>();
for (PromptVariant variant : variants) {
log.info("评估 Prompt 变体: {}", variant.getName());
List<EvalScore> scores = new ArrayList<>();
for (EvalSample sample : evalSamples) {
String prompt = variant.buildPrompt(sample.getContext());
String output = openAiClient.chatMultimodal(
prompt,
Base64.getEncoder().encodeToString(sample.getImageBytes()),
"image/jpeg"
);
EvalScore score = evaluator.evaluate(output, sample.getExpectedOutput(),
variant.getEvaluationCriteria());
scores.add(score);
}
variantScores.put(variant.getName(), scores);
}
return buildReport(variants, variantScores, evalSamples.size());
}
private AbTestReport buildReport(List<PromptVariant> variants,
Map<String, List<EvalScore>> variantScores,
int sampleCount) {
List<VariantResult> results = new ArrayList<>();
for (PromptVariant variant : variants) {
List<EvalScore> scores = variantScores.get(variant.getName());
double avgScore = scores.stream()
.mapToDouble(EvalScore::getOverallScore)
.average().orElse(0.0);
double formatPassRate = scores.stream()
.filter(EvalScore::isFormatValid).count() * 1.0 / scores.size();
double avgLatency = scores.stream()
.mapToDouble(EvalScore::getLatencyMs)
.average().orElse(0.0);
results.add(VariantResult.builder()
.variantName(variant.getName())
.averageScore(avgScore)
.formatPassRate(formatPassRate)
.averageLatencyMs(avgLatency)
.prompt(variant.getPromptTemplate())
.build());
}
results.sort(Comparator.comparingDouble(VariantResult::getAverageScore).reversed());
return AbTestReport.builder()
.results(results)
.sampleCount(sampleCount)
.winner(results.get(0).getVariantName())
.testTime(Instant.now())
.build();
}
}常见场景的 Prompt 最佳实践速查
| 场景 | 关键要素 | 常见错误 |
|---|---|---|
| 发票/单据提取 | 列出所有字段+格式要求+空值处理 | 只说"提取发票信息",输出不稳定 |
| 图表数据读取 | 指定坐标轴意义+要求输出原始数值 | 让模型"分析图表",得到模糊描述 |
| 产品图属性识别 | 指定品类+列出属性+强调只描述可见内容 | 模型推断出图片中没有的属性 |
| 文档OCR | 强调逐字识别+不确定字符处理方式 | 模型"聪明"地补全了看不清的字 |
| 场景理解 | 先整体后局部+指定关注焦点 | 模型随机选择描述细节 |
| 异常检测 | 定义"正常"的参照+指定输出格式 | 模型不知道什么是异常,全部说正常 |
工程化:Prompt 版本管理
/**
* Prompt 版本管理
* 像代码一样管理 Prompt,支持回滚和追溯
*/
@Repository
public interface PromptVersionRepository extends JpaRepository<PromptVersion, String> {
Optional<PromptVersion> findByTaskTypeAndVersionAndActive(
String taskType, String version, boolean active);
List<PromptVersion> findByTaskTypeOrderByCreatedAtDesc(String taskType);
}
@Entity
@Data
public class PromptVersion {
@Id
private String promptId;
private String taskType; // 任务类型:invoice_extraction/chart_reading等
private String version; // 版本号:v1.0/v1.1
private String promptTemplate; // Prompt模板内容
private boolean active; // 是否为当前生效版本
private double evaluationScore; // 在评估集上的得分
private String changeLog; // 变更说明
private Instant createdAt;
private String createdBy;
}Prompt 是多模态系统最重要的配置之一。像管理代码一样管理 Prompt:版本化、测试、灰度发布、回滚。
