提示词
Prompts 是引导 AI 模型生成特定输出的输入。这些 Prompts 的设计和措辞会显著影响模型的响应。
在与 AI 模型交互的最底层,Spring AI 中处理 prompt 有点类似于在 Spring MVC 中管理 "View"。这涉及创建包含占位符的大段文本,用于动态内容。随后,这些占位符会根据用户请求或应用中的其他代码进行替换。另一个类比是包含某些表达式占位符的 SQL 语句。
随着 Spring AI 的发展,它将引入更高层次的抽象,用于与 AI 模型交互。本节介绍的基础类在作用和功能上可以类比于 JDBC。例如,ChatModel 类类似于 JDK 中的核心 JDBC 库。ChatClient 类可以类比于 JdbcClient,它构建在 ChatModel 之上,并通过 Advisor 提供更高级的结构,以考虑与模型的历史交互、使用附加上下文文档增强提示,并引入智能体行为。
在 AI 领域,prompt 的结构随着时间推移不断演进。最初,prompt 是简单的字符串。随着时间推移,它们开始包含特定输入的占位符,例如 "USER:",AI 模型会识别该占位符。OpenAI 通过在多个消息字符串被 AI 模型处理之前将其分类为不同角色,为 prompt 引入了更多结构。
API 概览
Prompt
通常使用 ChatModel 的 call() 方法,该方法接收一个 Prompt 实例并返回一个 ChatResponse。
Prompt 类作为一个容器,用于容纳一组有组织的 Message 对象和一个请求 ChatOptions。每个 Message 都在 prompt 中体现一个独特角色,其内容和意图各不相同。这些角色可以涵盖多种元素,从用户查询到 AI 生成的响应,再到相关背景信息。这种安排支持复杂且细致的 AI 模型交互,因为 prompt 由多个消息构成,每个消息都被分配了在对话中要扮演的特定角色。
下面是 Prompt 类的精简版本,为简洁起见省略了构造函数和实用方法:
public class Prompt implements ModelRequest<List<Message>> {
private final List<Message> messages;
private ChatOptions chatOptions;
}
便捷方法
Prompt 类提供了一些便捷方法,用于根据消息的角色访问消息:
单条消息访问:
-
getUserMessage():返回 prompt 中的最后一个用户消息,如果不存在则返回空的UserMessage -
getSystemMessage():返回 prompt 中的第一个系统消息,如果不存在则返回空的SystemMessage -
getLastUserOrToolResponseMessage():返回最后一个用户或工具响应消息,适用于保持会话连续性
多消息访问:
-
getUserMessages():返回 prompt 中所有用户消息的列表,并保持其顺序 -
getSystemMessages():返回 prompt 中所有系统消息的列表,并保持其顺序
这些方法在处理多轮对话或需要按角色处理消息时特别有用。
消息
Message 接口封装了 Prompt 文本内容、一组元数据属性,以及一个称为 MessageType 的分类。
接口定义如下:
public interface Content {
String getContent();
Map<String, Object> getMetadata();
}
public interface Message extends Content {
MessageType getMessageType();
}
多模态消息类型还实现了 MediaContent 接口,该接口提供 Media 内容对象列表。
public interface MediaContent extends Content {
Collection<Media> getMedia();
}
Message 接口的各种实现对应 AI 模型可以处理的不同消息类别。模型根据对话角色区分消息类别。

如下文所述,这些角色通过 MessageType 得到有效映射。
角色
每条消息都会被分配一个特定的 role。这些 role 用于对消息进行分类,从而帮助 AI 模型理解 prompt 中每个片段的上下文和用途。这种结构化的方式增强了与 AI 交流的准确性和有效性,因为 prompt 的每个部分在交互中都承担着明确且定义清晰的角色。
主要角色包括:
-
System Role:指导 AI 的行为和响应风格,为 AI 如何解释和回复输入设置参数或规则。这类似于在开始对话之前向 AI 提供说明。
-
User Role:表示用户的输入——他们向 AI 提出的问题、命令或陈述。该角色非常基础,因为它构成 AI 响应的基础。
-
Assistant Role:AI 对用户输入的响应。它不仅仅是回答或反应,对于维持对话流也至关重要。通过跟踪 AI 先前的响应(其 'Assistant Role' 消息),系统可以确保交互连贯且与上下文相关。Assistant 消息还可能包含 Function Tool Call 请求信息。这就像 AI 中的一项特殊功能,在需要时用于执行特定功能,例如计算、获取数据,或除对话之外的其他任务。
-
Tool/Function Role:Tool/Function Role 专注于响应 Tool Call Assistant Messages 返回附加信息。
在 Spring AI 中,角色以枚举形式表示,如下所示
public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
TOOL("tool");
...
}
PromptTemplate
Spring AI 中用于提示词模板的一个关键组件是 PromptTemplate 类,它旨在促进结构化提示词的创建,然后将其发送给 AI 模型进行处理
public class PromptTemplate implements PromptTemplateActions, PromptTemplateMessageActions {
// Other methods to be discussed later
}
这个类使用 TemplateRenderer API 来渲染模板。默认情况下,Spring AI 使用 StTemplateRenderer 实现,该实现基于由 Terence Parr 开发的开源 StringTemplate 引擎。模板变量通过 {} 语法进行标识,但你也可以配置分隔符以使用其他语法。
public interface TemplateRenderer extends BiFunction<String, Map<String, Object>, String> {
@Override
String apply(String template, Map<String, Object> variables);
}
Spring AI 使用 TemplateRenderer 接口来处理将变量实际替换到模板字符串中的操作。默认实现使用 [StringTemplate]。如果你需要自定义逻辑,可以提供自己的 TemplateRenderer 实现。对于不需要模板渲染的场景(例如,模板字符串已经是完整内容),可以使用提供的 NoOpTemplateRenderer。
PromptTemplate promptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
Tell me the names of 5 movies whose soundtrack was composed by <composer>.
""")
.build();
String prompt = promptTemplate.render(Map.of("composer", "John Williams"));
此类实现的接口支持提示创建的各个方面:
PromptTemplateStringActions 专注于创建和渲染提示字符串,代表了提示生成的最基本形式。
PromptTemplateMessageActions 专为通过生成和操作 Message 对象来创建提示而设计。
PromptTemplateActions 被设计用于返回 Prompt 对象,该对象可以传递给 ChatModel 以生成响应。
虽然这些接口可能不会在许多项目中被广泛使用,但它们展示了 prompt 创建的不同方式。
已实现的接口是
public interface PromptTemplateStringActions {
String render();
String render(Map<String, Object> model);
}
方法 String render():将 prompt template 渲染为最终字符串格式,无需外部输入,适用于不含占位符或动态内容的模板。
方法 String render(Map<String, Object> model):增强渲染功能以包含动态内容。它使用一个 Map<String, Object>,其中 map 键是 prompt template 中的占位符名称,值是要插入的动态内容。
public interface PromptTemplateMessageActions {
Message createMessage();
Message createMessage(List<Media> mediaList);
Message createMessage(Map<String, Object> model);
}
方法 Message createMessage():创建一个不包含额外数据的 Message 对象,用于静态或预定义的消息内容。
方法 Message createMessage(List<Media> mediaList):创建一个包含静态文本和媒体内容的 Message 对象。
方法 Message createMessage(Map<String, Object> model):扩展消息创建以集成动态内容,接受一个 Map<String, Object>,其中每个条目表示消息模板中的一个占位符及其对应的动态值。
public interface PromptTemplateActions extends PromptTemplateStringActions {
Prompt create();
Prompt create(ChatOptions modelOptions);
Prompt create(Map<String, Object> model);
Prompt create(Map<String, Object> model, ChatOptions modelOptions);
}
方法 Prompt create():生成一个无需外部数据输入的 Prompt 对象,适用于静态或预定义 prompt。
方法 Prompt create(ChatOptions modelOptions):生成一个不包含外部数据输入,并包含聊天请求特定选项的 Prompt 对象。
方法 Prompt create(Map<String, Object> model):扩展 prompt 创建能力以包含动态内容,它接收一个 Map<String, Object>,其中每个 map 条目都是 prompt 模板中的一个占位符及其关联的动态值。
方法 Prompt create(Map<String, Object> model, ChatOptions modelOptions):扩展 prompt 创建能力以支持动态内容,它接收一个 Map<String, Object>,其中每个 map entry 是 prompt template 中的一个 placeholder 及其关联的动态值,以及 chat request 的特定选项。
示例用法
下面展示了一个取自 AI Workshop on PromptTemplates 的简单示例。
PromptTemplate promptTemplate = new PromptTemplate("Tell me a {adjective} joke about {topic}");
Prompt prompt = promptTemplate.create(Map.of("adjective", adjective, "topic", topic));
return chatModel.call(prompt).getResult();
下面是来自 AI Workshop on Roles 的另一个示例。
String userText = """
Tell me about three famous pirates from the Golden Age of Piracy and why they did.
Write at least a sentence for each pirate.
""";
Message userMessage = new UserMessage(userText);
String systemText = """
You are a helpful AI assistant that helps people find information.
Your name is {name}
You should reply to the user's request with your name and also in the style of a {voice}.
""";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
List<Generation> response = chatModel.call(prompt).getResults();
这展示了你如何通过使用 SystemPromptTemplate 创建带有 system 角色的 Message 并传入占位符值,来构建 Prompt 实例。然后,将带有 user 角色的消息与带有 system 角色的消息组合起来,形成 prompt。随后,将该 prompt 传递给 ChatModel,以获得生成式响应。
使用自定义模板渲染器
你可以通过实现 TemplateRenderer 接口并将其传递给 PromptTemplate 构造函数来使用自定义模板渲染器。你也可以继续使用默认的 StTemplateRenderer,但使用自定义配置。
默认情况下,模板变量通过 {} 语法标识。如果你计划在 prompt 中包含 JSON,你可能希望使用不同的语法,以避免与 JSON 语法冲突。例如,你可以使用 < 和 > 分隔符。
PromptTemplate promptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
Tell me the names of 5 movies whose soundtrack was composed by <composer>.
""")
.build();
String prompt = promptTemplate.render(Map.of("composer", "John Williams"));
使用资源而不是原始字符串
Spring AI 支持 org.springframework.core.io.Resource 抽象,因此你可以将 prompt 数据放入文件中,并直接用于 PromptTemplate。例如,你可以在 Spring 管理的组件中定义一个字段来获取 Resource。
@Value("classpath:/prompts/system-message.st")
private Resource systemResource;
然后直接将该资源传递给 SystemPromptTemplate。
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);
Prompt Engineering
在 generative AI 中,创建 prompts 是开发者的一项关键任务。这些 prompts 的质量和结构会显著影响 AI 输出的有效性。投入时间和精力设计经过深思熟虑的 prompts,可以显著改善 AI 的结果。
分享和讨论 prompt 是 AI 社区中的常见做法。这种协作方式不仅营造了共享学习环境,也促进了高效 prompt 的识别与应用。
这一领域的研究通常涉及分析和比较不同的 prompt,以评估它们在各种情境下的有效性。例如,一项重要研究表明,在 prompt 开头使用 "Take a deep breath and work on this problem step by step" 可以显著提升问题解决效率。这凸显了精心选择的语言对生成式 AI 系统性能的影响。
掌握 prompts 的最有效使用方式,尤其是在 AI 技术快速发展的背景下,是一项持续的挑战。你应该认识到 prompt engineering 的重要性,并考虑利用来自社区和研究的洞见来改进 prompt 创建策略。
创建有效的提示
在开发 prompts 时,整合若干关键组件以确保清晰性和有效性非常重要:
-
Instructions:向 AI 提供清晰且直接的指令,就像你与人交流一样。这种清晰性对于帮助 AI “understand” 预期内容非常重要。
-
External Context:在必要时包含相关背景信息或针对 AI 响应的具体指导。这个 “external context” 为 prompt 提供框架,帮助 AI 理解整体场景。
-
User Input:这是比较直接的部分,即用户提出的直接请求或问题,构成 prompt 的核心。
-
Output Indicator:这一部分可能比较棘手。它涉及指定 AI 响应的期望格式,例如 JSON。不过需要注意,AI 可能并不总是严格遵循该格式。例如,它可能会在实际 JSON 数据前加上一句 “here is your JSON”,或者生成一个并不准确的类 JSON 结构。
在编写 prompt 时,向 AI 提供预期问题和答案格式的示例可能会非常有益。这种做法有助于 AI “understand” 你的查询结构和意图,从而生成更精确、更相关的响应。虽然本文档不会深入探讨这些技术,但它们为进一步探索 AI prompt engineering 提供了一个起点。
以下是可供进一步研究的资源列表。
简单技术
高级技术
-
Zero-shot, Few-shot Learning:
使模型能够在几乎没有或没有特定问题类型先验示例的情况下做出准确预测或响应,并利用学习到的泛化能力来理解和执行新任务。 -
Chain-of-Thought:
将多个 AI 响应连接起来,形成连贯且具有上下文感知的对话。它有助于 AI 保持讨论主线,确保相关性和连续性。 -
ReAct (Reason + Act):
在这种方法中,AI 首先分析(推理)输入,然后确定最合适的行动方案或响应。它将理解与决策结合起来。
Microsoft 指南
- Framework for Prompt Creation and Optimization:
Microsoft 提供了一种结构化的方法,用于开发和优化 prompt。该框架指导用户创建有效的 prompt,以从 AI 模型中获得预期响应,从而提升交互的清晰度和效率。
Tokens
Tokens 在 AI 模型处理文本的方式中至关重要,它们充当桥梁,将单词(按我们理解的形式)转换为 AI 模型可以处理的格式。这种转换分为两个阶段:输入时,单词会被转换为 Tokens;输出时,这些 Tokens 再被转换回单词。
Tokenization 是将文本拆分为 tokens 的过程,是 AI 模型理解和处理语言的基础。AI 模型使用这种分词后的格式来理解并响应 prompts。
要更好地理解 token,可以将它们视为单词的片段。通常,一个 token 大约相当于四分之三个单词。例如,Shakespeare 全集总共约有 900,000 个单词,大约可转换为 1.2 百万个 token。
尝试使用 OpenAI Tokenizer UI,了解词语如何被转换为 tokens。
Tokens 除了在 AI 处理中的技术作用外,还具有实际影响,尤其是在计费和模型能力方面:
-
计费:AI 模型服务通常基于 token 用量计费。输入(prompt)和输出(response)都会计入总 token 数,因此更短的 prompt 更具成本效益。
-
模型限制:不同 AI 模型具有不同的 token 限制,这些限制定义了它们的 "context window" —— 一次能够处理的最大信息量。例如,GPT-3 的限制是 4K tokens,而 Claude 2 和 Meta Llama 2 等其他模型的限制为 100K tokens,一些研究模型最多可处理 100 万 tokens。
-
Context Window:模型的 token 限制决定了它的 context window。超过此限制的输入不会被模型处理。只发送用于处理的最小有效信息集至关重要。例如,当询问 "Hamlet" 时,无需包含 Shakespeare 其他所有作品的 tokens。
-
响应元数据:AI 模型响应的元数据包含已使用的 token 数量,这是管理用量和成本的重要信息。