主题
提示词
提示词是引导 AI 模型生成特定输出的输入。 这些提示词的设计与措辞会显著影响模型的响应。
在 Spring AI 中与 AI 模型交互的最底层,处理提示词在某种程度上类似于管理 Spring MVC 中的“视图(View)”。 这涉及创建带有动态内容占位符的大量文本。 随后根据用户请求或应用中的其他代码替换这些占位符。 另一个类比是包含某些表达式占位符的 SQL 语句。
随着 Spring AI 的演进,它会引入更高层的抽象来与 AI 模型交互。 本节所描述的基础类,其角色与功能可类比为 JDBC。 例如,ChatModel 类类似于 JDK 中的核心 JDBC 库。 ChatClient 类则可类比为 JdbcClient,它构建于 ChatModel 之上,并通过 Advisor 提供更高级的构造, 以考虑与模型的过往交互、用额外的上下文文档增强提示词,并引入智能体(agentic)行为。
在 AI 领域内,提示词的结构随时间不断演化。 起初,提示词只是简单的字符串。 随着时间推移,它们加入了针对特定输入(如 "USER:")的占位符,供 AI 模型识别。 OpenAI 则通过在 AI 模型处理之前,把多个消息字符串归类为不同角色,为提示词引入了更多结构。
API 概览
Prompt
常见做法是使用 ChatModel 的 call() 方法,它接收一个 Prompt 实例并返回一个 ChatResponse。
Prompt 类充当一个容器,容纳一系列组织有序的 Message 对象和一个请求 ChatOptions。 每个 Message 都体现提示词中一个独特的角色,在内容与意图上各不相同。 这些角色可以涵盖多种元素,从用户询问、AI 生成的响应,到相关的背景信息。 这种安排使得与 AI 模型的交互可以复杂而细致,因为提示词由多条消息构建而成,每条消息都被赋予在对话中所扮演的特定角色。
下面是 Prompt 类的精简版本,为简洁起见省略了构造器与工具方法:
java
public class Prompt implements ModelRequest<List<Message>> {
private final List<Message> messages;
private ChatOptions chatOptions;
}便捷方法
Prompt 类提供了若干便捷方法,用于按角色访问消息:
单条消息访问:
getUserMessage():返回提示词中最后一条用户消息,若不存在则返回一个空的UserMessagegetSystemMessage():返回提示词中第一条系统消息,若不存在则返回一个空的SystemMessagegetLastUserOrToolResponseMessage():返回最后一条用户消息或工具响应消息,适用于对话连续性
多条消息访问:
getUserMessages():返回提示词中所有用户消息的列表,保持其顺序getSystemMessages():返回提示词中所有系统消息的列表,保持其顺序
当处理多轮对话、或需要按角色处理消息时,这些方法尤其有用。
Message
Message 接口封装了一个 Prompt 文本内容、一组元数据属性,以及一个称为 MessageType 的分类。
该接口定义如下:
java
public interface Content {
String getContent();
Map<String, Object> getMetadata();
}
public interface Message extends Content {
MessageType getMessageType();
}多模态消息类型还实现了 MediaContent 接口,提供一个 Media 内容对象列表。
java
public interface MediaContent extends Content {
Collection<Media> getMedia();
}AbstractMessage 的所有子类都把其内容作为有序的 MessagePart 条目列表持有,可通过 getParts() 获取。 getText() 连接所有 TextPart 条目,而(UserMessage 中的)getMedia() 按部件顺序选取所有 MediaPart 条目,因此这两个访问器始终是这些部件之上的视图。 于是在用户消息中,文本与媒体可以交错:按它们应抵达模型的顺序添加部件即可。
java
UserMessage message = UserMessage.builder()
.part(TextPart.of("What is in this picture?"))
.part(MediaPart.of(image))
.part(TextPart.of("Answer in one sentence."))
.build();text(...) 和 media(...) 构建器方法仍可使用,并会替换相应的部件,因此一个通过 mutate().text(...) 重写问题的顾问会保留媒体。 不带显式部件的构建器会产生命名的传统顺序:先是文本,然后是媒体。 getMedia() 列表是一个不可修改的视图;请通过构建器添加媒体。
Message 接口的各种实现对应于 AI 模型可处理的不同消息类别。 模型会基于对话角色来区分消息类别。
如下文所述,这些角色实际上由 MessageType 映射。
角色
每条消息都被赋予一个特定角色。 这些角色对消息进行分类,为 AI 模型厘清提示词每一段的上下文与目的。 这种结构化的方法提升了与 AI 沟通的细腻度与有效性,因为提示词的每一部分都在交互中扮演着独特而明确的角色。
主要角色有:
- 系统角色(System Role):引导 AI 的行为与响应风格,为 AI 如何解读并回应输入设定参数或规则。它类似于在发起对话前向 AI 提供指令。
- 用户角色(User Role):代表用户的输入——他们向 AI 提出的问题、命令或陈述。这一角色是根本性的,因为它构成了 AI 响应的基础。
- 助手角色(Assistant Role):AI 对用户输入的响应。 它不仅仅是一个答案或反应,对于维持对话的流畅也至关重要。 通过追踪 AI 先前的响应(即它的“助手角色”消息),系统确保交互连贯且切合上下文。 助手消息还可能包含函数工具调用(Function Tool Call)请求信息。 它就像 AI 的一个特殊功能,在需要时执行计算、获取数据或其他超出单纯对话的任务。
- 工具/函数角色(Tool/Function Role):工具/函数角色专注于针对工具调用(Tool Call)助手消息返回额外信息。
如下所示,角色在 Spring AI 中以枚举形式表示
java
public enum MessageType {
USER("user"),
ASSISTANT("assistant"),
SYSTEM("system"),
TOOL("tool");
...
}PromptTemplate
Spring AI 中用于提示词模板化的一个关键组件是 PromptTemplate 类,它旨在便于创建结构化的提示词,随后发送给 AI 模型处理
java
public class PromptTemplate implements PromptTemplateActions, PromptTemplateMessageActions {
// Other methods to be discussed later
}该类使用 TemplateRenderer API 来渲染模板。默认情况下,Spring AI 使用 StTemplateRenderer 实现,它基于由 Terence Parr 开发的开源 StringTemplate 引擎。模板变量以 {} 语法标识,但你也可以配置定界符以使用其他语法。
java
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。
.使用以 '<' 和 '>' 为定界符的自定义 StringTemplate 渲染器
java
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 以生成响应。
虽然这些接口在许多项目中未必被广泛使用,但它们展示了提示词创建的不同途径。
所实现的接口是
java
public interface PromptTemplateStringActions {
String render();
String render(Map<String, Object> model);
}方法 String render():在没有外部输入的情况下,把提示词模板渲染为最终的字符串形式,适用于没有占位符或动态内容的模板。
方法 String render(Map<String, Object> model):增强渲染功能以纳入动态内容。它使用一个 Map<String, Object>,其中映射键是提示词模板中的占位符名称,值是要插入的动态内容。
java
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>,其中每个条目代表消息模板中的一个占位符及其对应的动态值。
java
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 create(ChatOptions modelOptions):在没有外部数据输入、但带有聊天请求特定选项的情况下生成一个 Prompt 对象。
方法 Prompt create(Map<String, Object> model):扩展提示词创建能力以纳入动态内容,接收一个 Map<String, Object>,其中每个映射条目是提示词模板中的一个占位符及其关联的动态值。
方法 Prompt create(Map<String, Object> model, ChatOptions modelOptions):扩展提示词创建能力以纳入动态内容,接收一个 Map<String, Object>,其中每个映射条目是提示词模板中的一个占位符及其关联的动态值,并带有聊天请求的特定选项。
用法示例
下面展示取自 AI Workshop on PromptTemplates 的一个简单示例。
java
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 的另一个示例。
java
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 创建一个带系统角色、传入占位符值的 Message,从而逐步构建 Prompt 实例。 随后把带 user 角色的消息与带 system 角色的消息组合成提示词。 再把该提示词传给 ChatModel 以获得生成式响应。
使用自定义模板渲染器
你可以通过实现 TemplateRenderer 接口并把它传给 PromptTemplate 构造器,来使用自定义模板渲染器。你也可以继续使用默认的 StTemplateRenderer,只是采用自定义配置。
默认情况下,模板变量以 {} 语法标识。如果你打算在提示词中包含 JSON,可能想使用不同的语法以避免与 JSON 语法冲突。例如,你可以使用 < 和 > 定界符。
java
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 抽象,因此你可以把提示词数据放入一个文件,并直接在 PromptTemplate 中使用它。 例如,你可以在受 Spring 管理的组件中定义一个字段来获取该 Resource。
java
@Value("classpath:/prompts/system-message.st")
private Resource systemResource;然后把该资源直接传给 SystemPromptTemplate。
java
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);提示词工程
在生成式 AI 中,提示词的创建对开发者而言是一项关键任务。 这些提示词的质量与结构会显著影响 AI 输出的有效性。 在设计富有巧思的提示词上投入时间与精力,能极大地改善 AI 的结果。
共享并讨论提示词是 AI 社区中的一种常见做法。 这种协作方式不仅营造了共同学习的环境,也促成了对高效提示词的识别与运用。
该领域的研究通常涉及分析并比较不同的提示词,以评估它们在各种情形下的有效性。 例如,一项重要研究表明,以 “Take a deep breath and work on this problem step by step” 开头的提示词能显著提升解决问题的效率。 这凸显了精心选择的语言对生成式 AI 系统性能的影响。
鉴于 AI 技术的快速进步,把握提示词的最有效用法是一场持续的挑戰。 你应认识到提示词工程的重要性,并考虑借鉴社区与研究的洞见来改进你的提示词创建策略。
创建有效的提示词
在开发提示词时,重要的是融入几个关键组成部分,以确保清晰与有效:
指令(Instructions):向 AI 提供清晰直接的指令,就像你与人沟通一样。这种清晰度对于帮助 AI“理解”期望至关重要。
外部上下文(External Context):在必要时纳入相关的背景信息或对 AI 响应的具体指引。这种“外部上下文”为提示词定调,帮助 AI 把握整体情境。
用户输入(User Input):这是最直接的部分——用户直接的请求或问题,构成提示词的核心。
输出指示(Output Indicator):这一方面可能棘手。它涉及指定 AI 响应的期望格式,例如 JSON。然而要注意,AI 未必总是严格遵循这一格式。例如,它可能在实际 JSON 数据前加一句“这是你的 JSON”,或有时生成一个并不准确的类 JSON 结构。
在编写提示词时,为 AI 提供预期问答格式的示例会非常有益。 这种做法帮助 AI“理解”你查询的结构与意图,从而带来更精确、更相关的响应。 虽然本文档不会深入这些技术,但它们为 AI 提示词工程的进一步探索提供了一个起点。
以下是供进一步研究的资源列表。
简单技术
文本摘要: + 把长篇文本归约为简明的摘要,在捕捉要点与主要思想的同时省略不那么关键的细节。
问答: + 专注于根据用户提出的问题,从所提供的文本中推导具体答案。它在于针对查询精准定位并提取相关信息。
文本分类: + 系统性地把文本归入预定义的类别或分组,分析文本并依据其内容将其归入最合适的类别。
对话: + 创建交互式对话,让 AI 能与用户进行来回沟通,模拟自然的对话流。
代码生成: + 根据具体的用户需求或描述生成功能性的代码片段,把自然语言指令转换为可执行的代码。
高级技术
零样本(Zero-shot)、少样本学习(Few-shot Learning): + 让模型在几乎没有或完全没有特定问题类型先前示例的情况下做出准确预测或响应,运用学到的泛化来理解并处理新任务。
思维链(Chain-of-Thought): + 把多个 AI 响应串联起来,形成连贯且具备上下文感知的对话。它帮助 AI 维持讨论的脉络,确保相关性与连续性。
ReAct(推理 + 行动): + 在这一方法中,AI 首先分析(推理)输入,然后确定最合适的行动或响应路径。它把理解与决策结合起来。
Microsoft 指南
- 提示词创建与优化框架: + 微软提供了开发和打磨提示词的结构化方法。这一框架指导用户创建有效的提示词,从 AI 模型中引出期望的响应,并为清晰度与效率优化交互。
令牌
令牌在 AI 模型处理文本的方式中至关重要,充当一座桥梁,把词语(以我们理解的形式)转换为 AI 模型可处理的格式。 这一转换分两个阶段发生:输入时把词语转换为令牌,输出时再把这些令牌转换回词语。
分词(Tokenization),即把文本拆解为令牌的过程,是 AI 模型理解并处理语言的基础。 AI 模型以这种分词后的格式来理解并响应提示词。
为了更好地理解令牌,可以把它们当作词的一部分来理解。通常,一个令牌约占一个词的四分之三。例如,莎士比亚全集总计约 90 万个词,转换后约为 120 万个令牌。
试用 OpenAI Tokenizer UI,看看词语如何被转换为令牌。
令牌在 AI 处理的技术角色之外还有实际影响,尤其在计费和模型能力方面:
计费:AI 模型服务通常按令牌用量计费。输入(提示词)和输出(响应)都会计入总令牌数,因此更短的提示词更具成本效益。
模型限额:不同的 AI 模型有不同的令牌上限,界定了它们的“上下文窗口”——一次能处理的信息最大量。例如,GPT-3 的上限是 4K 个令牌,而其他模型如 Claude 2 和 Meta Llama 2 的上限为 100K 个令牌,一些研究模型则能处理多达 100 万个令牌。
上下文窗口:模型的令牌上限决定了它的上下文窗口。超过此限制的输入不会被模型处理。仅发送供处理的最少有效信息集至关重要。例如,在询问“哈姆雷特”时,没有必要包含莎士比亚其他所有作品的令牌。
响应元数据:AI 模型响应的元数据包含所使用的令牌数,这对管理用量和成本而言是至关重要的信息。