Spring AI Prompts

提示词(Prompts)

提示词是引导AI模型生成特定输出的输入内容。这些提示词的设计和措辞会显著影响模型的响应。

在Spring AI中与AI模型进行最低层次的交互时,处理提示词的方式与Spring MVC中管理"视图(View)"有些类似。这涉及到创建包含动态内容占位符的大量文本。然后,这些占位符会根据用户请求或应用程序中的其他代码被替换。另一个类比是包含特定表达式占位符的SQL语句。

随着Spring AI的发展,它将引入更高级别的抽象来与AI模型交互。本节中描述的基础类,在角色和功能上可以类比为JDBC。例如,ChatModel类类似于JDK中的核心JDBC库。ChatClient类可以类比为JdbcClient,它构建在ChatModel之上,并通过Advisor(顾问)提供了更高级的构造,用以考虑与模型过往的交互、用额外的上下文文档扩充提示词,以及引入代理行为(agentic behavior)。

在AI领域,提示词的结构随着时间的推移而演变。最初,提示词是简单的字符串。随着时间的推移,它们发展到包含特定输入的占位符,例如AI模型能够识别的"USER:"。OpenAI通过在处理之前将多个消息字符串分类到不同的角色中,为提示词引入了更多的结构。

API概览

Prompt(提示词)

通常使用ChatModelcall()方法,该方法接收一个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():返回提示词中的最后一条用户消息,如果不存在则返回空的UserMessage
    • getSystemMessage():返回提示词中的第一条系统消息,如果不存在则返回空的SystemMessage
    • getLastUserOrToolResponseMessage():返回最后一条用户或工具响应消息,对于对话的连续性很有用。
  • 多条消息访问

    • getUserMessages():返回提示词中所有用户消息的列表,保持其顺序。
    • getSystemMessages():返回提示词中所有系统消息的列表,保持其顺序。

这些方法在处理多轮对话或需要按角色处理消息时特别有用。

Message(消息)

Message接口封装了提示词的文本内容、一组元数据属性以及一个称为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();
}

Message接口的各种实现对应于AI模型可以处理的不同类别的消息。模型根据对话角色来区分消息类别。

Spring AI Message API

这些角色通过MessageType(如下所述)进行了有效映射。

角色(Roles)

每条消息都被分配了一个特定的角色。这些角色对消息进行分类,为AI模型阐明提示词中每个部分的上下文和目的。这种结构化的方法增强了与AI沟通的细微差别和有效性,因为提示词的每个部分在交互中都扮演着独特且明确的角色。

主要角色包括:

  • 系统(System)角色:指导AI的行为和响应风格,为AI如何解释和回复输入设定参数或规则。这类似于在开始对话之前向AI提供指示。

  • 用户(User)角色:代表用户的输入------他们向AI提出的问题、命令或陈述。这个角色是基础性的,因为它构成了AI响应的基础。

  • 助手(Assistant)角色:AI对用户输入的响应。它不仅仅是一个答案或反应,对于保持对话的流畅性也至关重要。通过跟踪AI之前的响应(其"助手角色"消息),系统确保了连贯且与上下文相关的交互。助手消息也可能包含功能工具调用(Function Tool Call)请求信息。这就像AI中的一个特殊功能,在需要执行特定功能(如计算、获取数据或其他超出单纯对话的任务)时使用。

  • 工具/功能(Tool/Function)角色:工具/功能角色专注于响应工具调用助手消息而返回附加信息。

角色在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 {

    // 其他方法将在后面讨论
}

此类使用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("""
            告诉我5部由<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研讨会关于PromptTemplates的简单示例如下所示。

java 复制代码
PromptTemplate promptTemplate = new PromptTemplate("给我讲一个关于{topic}的{adjective}笑话");

Prompt prompt = promptTemplate.create(Map.of("adjective", adjective, "topic", topic));

return chatModel.call(prompt).getResult();

另一个来自AI研讨会关于Roles的示例如下所示。

java 复制代码
String userText = """
    告诉我三位来自海盗黄金时代的著名海盗以及他们做了什么。
    为每位海盗至少写一句话。
    """;

Message userMessage = new UserMessage(userText);

String systemText = """
  你是一个乐于助人的AI助手,帮助人们查找信息。
  你的名字是{name}
  你应该用你的名字并以{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实例。然后,将用户角色的消息与系统角色的消息组合形成提示词。最后,将提示词传递给ChatModel以获取生成响应。

使用自定义模板渲染器

您可以通过实现TemplateRenderer接口并将其传递给PromptTemplate构造函数来使用自定义模板渲染器。您也可以继续使用默认的StTemplateRenderer,但使用自定义配置。

默认情况下,模板变量由{}语法标识。如果您计划在提示词中包含JSON,您可能希望使用不同的语法以避免与JSON语法冲突。例如,您可以使用<>分隔符。

java 复制代码
PromptTemplate promptTemplate = PromptTemplate.builder()
    .renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
    .template("""
            告诉我5部由<composer>创作配乐的电影名称。
            """)
    .build();

String prompt = promptTemplate.render(Map.of("composer", "John Williams"));

使用资源代替原始字符串

Spring AI支持org.springframework.core.io.Resource抽象,因此您可以将提示词数据放在一个文件中,并直接在PromptTemplate中使用。例如,您可以在Spring管理的组件中定义一个字段来获取资源。

java 复制代码
@Value("classpath:/prompts/system-message.st")
private Resource systemResource;

然后直接将此资源传递给SystemPromptTemplate

java 复制代码
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource);

提示词工程(Prompt Engineering)

在生成式AI中,提示词的创建对开发者来说是一项至关重要的任务。这些提示词的质量和结构显著影响AI输出的有效性。投入时间和精力设计周到的提示词可以极大地改善AI的结果。

在AI社区中,分享和讨论提示词是一种常见的做法。这种协作方法不仅创造了一个共享的学习环境,而且还能识别和使用高效的提示词。

这一领域的研究通常涉及分析和比较不同的提示词,以评估它们在不同情况下的有效性。例如,一项重要研究表明,以"深呼吸,逐步解决这个问题"开头的提示词显著提高了问题解决的效率。这突显了精心选择的语言对生成式AI系统性能的影响。

掌握最有效地使用提示词的方法,尤其是在AI技术快速发展的背景下,是一个持续的挑战。您应该认识到提示词工程的重要性,并考虑利用社区和研究的见解来改进提示词创建策略。

创建有效的提示词

在开发提示词时,整合几个关键组件以确保清晰性和有效性非常重要:

  • 指令(Instructions):向AI提供清晰直接的指令,类似于与人类沟通的方式。这种清晰性对于帮助AI"理解"期望是什么至关重要。

  • 外部上下文(External Context):必要时,包含相关的背景信息或针对AI响应的具体指导。这种"外部上下文"构建了提示词的框架,并帮助AI把握整体场景。

  • 用户输入(User Input):这是简单的部分------构成提示词核心的用户直接请求或问题。

  • 输出指示(Output Indicator):这方面可能比较棘手。它涉及到指定AI响应的期望格式,例如JSON。但是,请注意AI可能并不总是严格遵守此格式。例如,它可能在实际JSON数据之前添加类似"这是您的JSON"的短语,或者有时生成不准确的类JSON结构。

在制作提示词时,向AI提供预期的问答格式示例可能非常有益。这种做法有助于AI"理解"您查询的结构和意图,从而产生更精确和相关的响应。虽然本文档未深入探讨这些技术,但它们为进一步探索AI提示词工程提供了一个起点。

以下是一些供进一步研究的资源列表。

简单技术

  • 文本摘要(Text Summarization):将大量文本缩减为简洁的摘要,捕捉关键点和主要思想,同时省略不太重要的细节。

  • 问答(Question Answering):侧重于根据提供的文本,基于用户提出的问题得出具体答案。这涉及根据查询精确定位和提取相关信息。

  • 文本分类(Text Classification):系统地将文本归类到预定义的类别或组中,分析文本并根据其内容将其分配到最合适的类别。

  • 对话(Conversation):创建交互式对话,AI可以与用户进行来回沟通,模拟自然的对话流程。

  • 代码生成(Code Generation):根据特定的用户需求或描述生成功能性的代码片段,将自然语言指令转换为可执行代码。

高级技术

  • 零样本(Zero-shot)、少样本(Few-shot)学习:使模型能够在没有或仅有极少特定问题类型先验示例的情况下,做出准确的预测或响应,利用学到的泛化能力来理解和处理新任务。

  • 思维链(Chain-of-Thought):链接多个AI响应,以创建连贯且具有上下文感知的对话。它帮助AI保持讨论的脉络,确保相关性和连续性。

  • ReAct(推理+行动,Reason + Act):在这种方法中,AI首先分析(推理)输入,然后确定最合适的行动或响应方案。它结合了理解和决策。

微软指南(Microsoft Guidance)

  • 提示词创建和优化框架:微软提供了一种结构化的方法来开发和优化提示词。该框架指导用户创建有效的提示词,以从AI模型中引出所需的响应,优化交互的清晰度和效率。

Tokens(令牌)

Tokens在AI模型处理文本的方式中至关重要,它充当着将单词(我们理解的形式)转换为AI模型可以处理的格式的桥梁。这种转换分两个阶段进行:输入时,单词被转换为tokens;输出时,这些tokens再被转换回单词。

分词(Tokenization),即把文本分解为tokens的过程,是AI模型理解和处理语言的基础。AI模型使用这种tokenized格式来理解和响应提示词。

为了更好地理解tokens,可以把它们想象成单词的一部分。通常,一个token代表大约一个单词的四分之三。例如,莎士比亚的全部作品,总计约90万字,将转换为大约120万个tokens。

可以尝试使用OpenAI Tokenizer UI来查看单词是如何转换为tokens的。

Tokens除了在AI处理中的技术角色外,还有实际影响,特别是在计费和模型能力方面:

  • 计费(Billing):AI模型服务通常根据token使用量计费。输入(提示词)和输出(响应)都计入总token数,这使得更短的提示词更具成本效益。

  • 模型限制(Model Limits):不同的AI模型有不同的token限制,这定义了它们的"上下文窗口"(context window)------即它们一次可以处理的最大信息量。例如,GPT-3的限制是4K tokens,而Claude 2和Meta Llama 2等其他模型的限制是100K tokens,一些研究模型甚至可以处理高达100万tokens。

  • 上下文窗口(Context Window):模型的token限制决定了其上下文窗口。超过此限制的输入将不会被模型处理。只发送最少量有效的信息进行处理至关重要。例如,当询问关于"哈姆雷特"的问题时,没有必要包含莎士比亚所有其他作品的tokens。

  • 响应元数据(Response Metadata):来自AI模型的响应的元数据包括使用的tokens数量,这是管理使用情况和成本的重要信息。