跳到主要内容

护栏(Guardrails)

备注

护栏是一项实验性功能。其 API 与行为可能在未来版本中发生变化。

护栏是用于校验 LLM 输入与输出、确保其符合预期的机制。借助护栏,你可以完成例如以下事情:

  • 验证用户输入是否超出范围
  • 在调用 LLM 之前确保输入满足某些条件(例如防范 提示注入攻击
  • 确保输出格式正确(例如是符合正确 schema 的 JSON 文档)
  • 确保 LLM 输出与业务规则与约束一致(例如若这是公司 X 的聊天机器人,则响应中不应包含对竞争对手 Y 的任何引用)
  • 检测幻觉(hallucinations)

以上只是示例。你还可以用护栏做许多其他事情。

备注

护栏仅在使用 AI Services 时可用。它们是更高层的抽象,不能应用于 ChatModelStreamingChatModel

GuardrailsGuardrails;

该实现最初在 Quarkus LangChain4j 扩展 中完成,并回移植到此处。

实现护栏

理想情况下,护栏实现应遵循 单一职责原则,即每个护栏类只校验一件事。然后把护栏串联起来,以同时防范多种问题。

护栏链中的顺序很重要。链中第一个失败的护栏会触发整体失败。应把最容易捕获失败的护栏放在链的前面,而那些更具体、很少失败的护栏放在链的后面。

还需注意,护栏本身可以调用其他服务,甚至发起其他 LLM 交互。如果这类护栏有执行开销或金钱成本,请把这一点考虑进去。你可能希望把更「昂贵」的护栏放在链的末尾。

备注

术语 昂贵(expensive) 可以指某件事执行需要一定时间,也可以指与之相关的金钱成本。

输入护栏

输入护栏是在调用 LLM 之前执行的函数。输入护栏失败会阻止调用 LLM。输入护栏是调用 LLM 之前的最后一步。它们在任何 RAG 操作完成之后才会被调用。

实现输入护栏

输入护栏通过实现 InputGuardrail 接口来完成。InputGuardrail 接口有两个版本的 validate 方法,至少需要实现其中一个:

InputGuardrailResult validate(UserMessage userMessage);
InputGuardrailResult validate(InputGuardrailRequest params);

第一个变体用于简单护栏,或护栏只需要访问 UserMessage 时。

第二个变体用于更复杂的护栏,需要更多信息,例如聊天记忆/历史、用户消息模板、增强(augmentation)结果,或传递给模板的变量。详见 InputGuardrailRequest

你可以做的一些示例:

  • 检查增强结果中是否有足够的文档
  • 确保用户没有多次询问同一个问题
  • 缓解潜在的提示注入攻击
  • 使用社区 Prompt Repetition 模块重写符合条件的单文本输入

无论操作是同步还是异步/流式,都可以使用输入护栏。

输入护栏结果

输入护栏可以产生以下结果。InputGuardrail 接口上有辅助方法可用于提供这些结果:

结果InputGuardrail 上的辅助方法说明
successsuccess()- 输入有效。
- 执行链中的下一个护栏。
- 若最后一个护栏通过,则调用 LLM。
success with alternate resultsuccessWith(String)success 类似,但在进入下一步(链中下一个护栏或调用 LLM)之前会改写用户消息。
failurefailure(String)failure(String, Throwable)- 输入无效,但链中后续护栏会继续执行,以便累积所有可能的校验问题。
- 不会调用 LLM。
- 若传入了 Throwable,调用方可以捕获 InputGuardrailException 并检查 cause。它就是此处传入的 Throwable
fatalfatal(String)fatal(String, Throwable)- 输入无效,并以 InputGuardrailException 中止执行。
- 不会调用 LLM。
- 若传入了 Throwable,调用方可以捕获 InputGuardrailException 并检查 cause。它就是此处传入的 Throwable

声明输入护栏

有多种方式声明输入护栏,按优先级从高到低列出如下:

  1. 直接在 AiServices 构建器上设置的 InputGuardrail 实现类名或实例。
  2. 标注在单个 AI Service 方法上的 @InputGuardrails 注解
  3. 标注在 AI Service 类上的 @InputGuardrails 注解。 无论以何种方式声明,输入护栏始终按列表中出现的顺序执行。

AiServices 构建器

直接在 AiServices 构建器上设置的 InputGuardrail 实现类名或实例具有最高优先级,意味着若以其他方式也声明了护栏,将使用构建器上直接声明的那个。

public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}

var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.inputGuardrailClasses(FirstInputGuardrail.class, SecondInputGuardrail.class)
.build();

public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}

var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.inputGuardrails(new FirstInputGuardrail(), new SecondInputGuardrail())
.build();
信息

若你想要一个现成的实验性输入护栏,用提示重复(prompt repetition)重写符合条件的单文本输入,请参见社区 Prompt Repetition 模块。

在第一种场景中,传入的是实现 InputGuardrail 的类。会通过反射动态创建这些类的新实例。

信息

类转换为实例的方式可以自定义。例如,使用依赖注入的框架(如 QuarkusSpring)可以利用扩展点,按其管理类实例的方式提供实例,而不是每次都通过反射创建新实例。

标注在单个 AI Service 方法上

标注在单个 AI Service 方法上的 @InputGuardrails 注解 具有次高优先级。

public interface Assistant {
@InputGuardrails({ FirstInputGuardrail.class, SecondInputGuardrail.class })
String chat(String question);

String doSomethingElse(String question);
}

var assistant = AiServices.create(Assistant.class, chatModel);

在此示例中,只有 chat 方法有护栏。

  • chat 方法上,首先调用 FirstInputGuardrail
  • 仅当它成功时才会调用 LLM。
  • 仅当 FirstInputGuardrail 未产生 fatal 结果时,才会调用 SecondInputGuardrail
  • FirstInputGuardrailSecondInputGuardrail 都可以重写用户消息。
  • FirstInputGuardrail 重写了用户消息,则 SecondInputGuardrail 会收到新的用户消息作为输入。

doSomethingElse 方法没有任何护栏。

标注在 AI Service 类上

标注在 AI Service 类上的 @InputGuardrails 注解 具有最低优先级。

@InputGuardrails({ FirstInputGuardrail.class, SecondInputGuardrail.class })
public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}

var assistant = AiServices.create(Assistant.class, chatModel);

在此示例中,chatdoSomethingElse 方法都有护栏。

  • 与上一个示例一样,首先调用 FirstInputGuardrail
  • 仅当它成功时才会调用 LLM。
  • 仅当 FirstInputGuardrail 未产生 fatal 结果时,才会调用 SecondInputGuardrail
  • FirstInputGuardrailSecondInputGuardrail 都可以重写用户消息。
  • FirstInputGuardrail 重写了用户消息,则 SecondInputGuardrail 会收到新的用户消息作为输入。

对输入护栏进行单元测试

langchain4j-test 模块中有一些基于 AssertJ 的单元测试工具。

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-test</artifactId>
<scope>test</scope>
</dependency>

有了依赖后,你可以进行这类校验:

import static dev.langchain4j.test.guardrail.GuardrailAssertions.assertThat;

import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.guardrail.GuardrailResult.Result;

class Tests {
MyInputGuardrail inputGuardrail = new MyInputGuardrail();

@Test
void test() {
var userMessage = UserMessage.from("Some user message");
var result = inputGuardrail.validate(userMessage);

// These are just some examples of what you can do
assertThat(result)
.isSuccessful()
.hasResult(Result.FATAL)
.hasFailures()
.hasSingleFailureWithMessage("Prompt injection detected")
.assertSingleFailureSatisfied(failure -> assertThat(failure)...)
.withFailures().....
}
}
信息

更多细节请参见 GuardrailAssertionsInputGuardrailResultAssert 类。

开箱即用的输入护栏

对于若干常见用例,LangChain4j 提供了输入护栏的实现:

护栏类说明
MessageModeratorInputGuardrail使用 ModerationModel 校验用户消息的输入护栏,用于检测潜在有害、不当或违反策略的内容。
- 检查传入消息是否包含仇恨言论、暴力、自残、色情内容,或审核模型定义的其他类别。
- 若消息被标记,校验以 fatal 结果失败,阻止消息被进一步处理。
- 适用于在将用户输入发送给 LLM 之前,确保其符合内容策略。
PatternBasedPromptInjectionGuardrail基于模式的输入护栏,使用源自 OWASP LLM01 的正则表达式检测提示注入尝试。
- 覆盖指令覆盖、角色劫持、越狱、系统提示泄露、分隔符注入以及编码载荷。
- 零外部依赖且延迟亚毫秒级,适合作为护栏链中第一个(最便宜的)关卡,置于任何基于 LLM 的分类器之前。
- 子类可以添加领域特定模式,并自定义失败消息。

输出护栏

输出护栏是在 LLM 产生输出之后执行的函数。输出护栏失败允许更高级的场景,例如重试(retry)重新提示(reprompt),以帮助改进响应。它们在所有其他操作(包括函数/工具调用)完成之后才会被调用。

实现输出护栏

与输入护栏类似,输出护栏通过实现 OutputGuardrail 接口来完成。OutputGuardrail 接口有两个版本的 validate 方法,至少需要实现其中一个:

OutputGuardrailResult validate(AiMessage responseFromLLM);
OutputGuardrailResult validate(OutputGuardrailRequest params);

第一个变体用于简单护栏,或护栏只需要访问结果 AiMessage 时。

第二个变体用于更复杂的护栏,需要更多信息,例如完整的聊天响应、聊天记忆/历史、用户消息模板,或传递给模板的变量。详见 OutputGuardrailRequest

你可以做的一些示例:

  • 确保输出格式正确(例如是符合正确 schema 的 JSON 文档)
  • 检测 LLM 幻觉
  • 校验 LLM 响应是否包含某些信息

输出护栏结果

输出护栏可以产生以下结果。OutputGuardrail 接口上有辅助方法可用于提供这些结果:

结果OutputGuardrail 上的辅助方法说明
successsuccess()- 输出有效。
- 执行链中的下一个护栏。若最后一个护栏通过,则将输出返回给调用方。
success with rewritesuccessWith(String)successWith(String, Object)- 与 success 类似,但输出在原始形式上无效,已被重写为有效形式。
- 对重写后的输出执行下一个护栏。若最后一个护栏通过,则将输出返回给调用方。
failurefailure(String)failure(String, Throwable)- 输出无效,但链中后续护栏会继续执行,以便累积所有可能的校验问题。
- 校验失败以 OutputGuardrailException 的形式返回给用户。
fatalfatal(String)fatal(String, Throwable)输出无效,并以抛向调用方的 OutputGuardrailException 中止执行。
fatal with retryretry(String)retry(String, Throwable)- 与 fatal 类似,但会使用与原始调用相同的提示与聊天历史再次调用 LLM。
- 若在可配置的重试次数之后失败仍然存在,则以抛向调用方的 OutputGuardrailException 中止执行。
- 若护栏在重试后通过,则从开头重新执行整个护栏链。
fatal with repromptreprompt(String, String)reprompt(String, Throwable, String)- 与 fatal with retry 类似,但会使用护栏提供的新提示再次调用 LLM。
- 在此情况下,护栏提供一条附加消息追加到先前的用户消息,然后用新的用户消息与原始聊天历史向 LLM 发送新请求。
- 若在可配置的重试次数之后失败仍然存在,则以抛向调用方的 OutputGuardrailException 中止执行。
- 若护栏在重新提示后通过,则从开头重新执行整个护栏链。

声明输出护栏

有多种方式声明输出护栏,按优先级从高到低列出如下:

  1. 直接在 AiServices 构建器上设置的 OutputGuardrail 实现类名或实例。
  2. 标注在单个 AI Service 方法上的 @OutputGuardrails 注解
  3. 标注在 AI Service 类上的 @OutputGuardrails 注解

无论以何种方式声明,输出护栏始终按列表中出现的顺序执行。

AiServices 构建器

直接在 AiServices 构建器上设置的 OutputGuardrail 实现类名或实例具有最高优先级,意味着若以其他方式也声明了护栏,将使用构建器上声明的那个。

public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}

var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.outputGuardrailClasses(FirstOutputGuardrail.class, SecondOutputGuardrail.class)
.build();

public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}

var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.outputGuardrails(new FirstOutputGuardrail(), new SecondOutputGuardrail())
.build();

在第一种场景中,传入的是实现 OutputGuardrail 的类。会通过反射动态创建这些类的新实例。

信息

类转换为实例的方式可以自定义。例如,使用依赖注入的框架(如 QuarkusSpring)可以利用扩展点,按其管理类实例的方式提供实例,而不是每次都通过反射创建新实例。

标注在单个 AI Service 方法上

标注在单个 AI Service 方法上的 @OutputGuardrails 注解 具有次高优先级。

public interface Assistant {
@OutputGuardrails({ FirstOutputGuardrail.class, SecondOutputGuardrail.class })
String chat(String question);

String doSomethingElse(String question);
}

var assistant = AiServices.create(Assistant.class, chatModel);

在此示例中,只有 chat 方法有护栏。

  • chat 方法上,首先调用 FirstOutputGuardrail
  • 仅当它成功时才会将结果返回给调用方。仅当 FirstOutputGuardrail 未产生 fatalfatal with retryfatal with reprompt 结果时,才会调用 SecondOutputGuardrail
  • SecondOutputGuardrail 会收到 FirstOutputGuardrail 的输出。
  • SecondOutputGuardrail 在重试或重新提示后成功,则 FirstOutputGuardrailSecondOutputGuardrail 都会被重新执行。

doSomethingElse 方法没有任何护栏。

标注在 AI Service 类上

标注在 AI Service 类上的 @OutputGuardrails 注解 具有最低优先级。

@OutputGuardrails({ FirstOutputGuardrail.class, SecondOutputGuardrail.class })
public interface Assistant {
String chat(String question);
String doSomethingElse(String question);
}

var assistant = AiServices.create(Assistant.class, chatModel);

在此示例中,chatdoSomethingElse 方法都有护栏。

  • 与上一个示例一样,首先调用 FirstOutputGuardrail
  • 仅当它成功时才会将结果返回给调用方。仅当 FirstOutputGuardrail 未产生 fatalfatal with retryfatal with reprompt 结果时,才会调用 SecondOutputGuardrail
  • SecondOutputGuardrail 会收到 FirstOutputGuardrail 的输出。
  • SecondOutputGuardrail 在重试或重新提示后成功,则 FirstOutputGuardrailSecondOutputGuardrail 都会被重新执行。

配置

输出护栏还有以下可提供的附加配置:

配置说明
maxRetries- 在执行重试或重新提示时,输出护栏的最大重试次数。
- 默认为 2
- 设为 0 可禁用重试。
标注在单个 AI Service 方法上
public interface MethodLevelAssistant {
@OutputGuardrails(
value = { FirstOutputGuardrail.class, SecondOutputGuardrail.class },
maxRetries = 10
)
String chat(String question);
}

var assistant = AiServices.create(MethodLevelAssistant.class, chatModel);
标注在 AI Service 类上
@OutputGuardrails(
value = { FirstOutputGuardrail.class, SecondOutputGuardrail.class },
maxRetries = 10
)
public interface ClassLevelAssistant {
String chat(String question);
}

var assistant = AiServices.create(ClassLevelAssistant.class, chatModel);
AiServices 构建器
public interface Assistant {
String chat(String message);
}

var outputGuardrailsConfig = OutputGuardrailsConfig.builder()
.maxRetries(10)
.build();

var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.outputGuardrailsConfig(outputGuardrailsConfig)
.outputGuardrailClasss(FirstOutputGuardrail.class, SecondOutputGuardrail.class)
.build();

流式响应上的输出护栏

输出护栏也可用于带有流式响应的操作:

public interface StreamingAssistant {
@OutputGuardrails({ FirstOutputGuardrail.class, SecondOutputGuardrail.class })
TokenStream streamingChat(String message);
}

在此场景中,输出护栏会在整个流完成时执行,更具体地说,是在调用 TokenStream.onCompleteResponse 时。onPartialResponse 会被缓冲,并在护栏成功后重放。

若链中的 retryreprompt 最终成功,则整个链会 同步 重新执行。每个护栏会按原始顺序逐个重新执行。链完成后,结果会传入 TokenStream.onCompleteResponse

开箱即用的输出护栏

对于若干常见用例,LangChain4j 提供了输出护栏的实现:

护栏类说明
JsonExtractorOutputGuardrail检查响应是否能成功从 JSON 反序列化为某一类型对象的输出护栏。
- 使用 Jackson ObjectMapper 尝试反序列化对象。
- 若响应无法反序列化为期望的对象类型,则会对 LLM 进行重新提示。
- 可直接使用,也可扩展并自定义(有若干 protected 方法可重写以自定义行为)。

对输出护栏进行单元测试

langchain4j-test 模块中有一些基于 AssertJ 的单元测试工具。

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-test</artifactId>
<scope>test</scope>
</dependency>

有了依赖后,你可以进行这类校验:

import static dev.langchain4j.test.guardrail.GuardrailAssertions.assertThat;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.guardrail.GuardrailResult.Result;

class Tests {
MyOutputGuardrail outputGuardrail = new MyOutputGuardrail();

@Test
void test() {
var aiMessage = AiMessage.from("Some output");
var result = outputGuardrail.validate(aiMessage);

// These are just some examples of what you can do
assertThat(result)
.isSuccessful()
.hasResult(Result.FATAL)
.hasFailures()
.hasSingleFailureWithMessage("Hallucination detected!")
.hasSingleFailureWithMessageAndReprompt("Hallucination detected!", "Please LLM don't hallucinate!")
.assertSingleFailureSatisfied(failure -> assertThat(failure)...)
.withFailures().....
}
}
信息

更多细节请参见 GuardrailAssertionsOutputGuardrailResultAssert 类。

混合使用

你可以按任意方式混合搭配输入护栏与输出护栏!

public class MyObjectJsonOutputGuardrail extends JsonExtractorOutputGuardrail<MyObject> {
public MyObjectJsonOutputGuardrail() {
super(MyObject.class);
}
}

@InputGuardrails({ FirstInputGuardrail.class, SecondInputGuardrail.class })
@OutputGuardrails(value = SomeOutputGuardrail.class, maxRetries = 5)
public interface Assistant {
String chat(String message);

@InputGuardrails(PatternBasedPromptInjectionGuardrail.class)
@OutputGuardrails(MyObjectJsonOutputGuardrail.class)
MyObject chatAndReturnJson(String message);
}

var outputGuardrailsConfig = OutputGuardrailsConfig.builder()
.maxRetries(10)
.build();

var assistant = AiServices.builder(Assistant.class)
.chatModel(chatModel)
.inputGuardrails(new AnotherInputGuardrail())
.outputGuardrailsConfig(outputGuardrailsConfig)
.build();

在此示例中,由于在 AiServices 构建器上设置了输入护栏,Assistant 上的所有方法都有一个输入护栏 AnotherInputGuardrail。此外,由于配置也在 AiServices 构建器上设置,所有输出护栏的 maxRetries 值都等于 10

chat 方法有一个输出护栏 SomeOutputGuardrail,其 maxRetries 值等于 10

chatAndReturnJson 方法有一个输出护栏 MyObjectJsonOutputGuardrail,其 maxRetries 值等于 10

扩展点

护栏系统设计为可组合,以便能在其他下游框架(例如 QuarkusSpring Boot)中扩展与复用。本节描述所提供的一些扩展点或「钩子」。

所有这些扩展点都利用了 Java Service Provider Interface(Java SPI)

扩展点接口用途
ClassInstanceFactory提供类的实例。
- 旨在将实例创建/获取委托给其他方式。
- 若未提供,则使用反射通过默认构造函数创建实例。
- 其他框架(如 Quarkus 或 Spring)可能使用自己的 bean 容器来提供类实例。这些框架会提供实现。
- Quarkus 实现可能类似于 CDIClassInstanceFactory
- Spring 实现可能类似于 ApplicationContextClassInstanceFactory
ClassMetadataProviderFactory提供对类元数据的访问。
- 用于扫描 AiService 接口上的方法,并查找与处理 @InputGuardrails/@OutputGuardrails 注解。
- 若未找到其他实现,默认实现为 ReflectionBasedClassMetadataProviderFactory,使用反射提供类元数据。
GuardrailServiceBuilderFactory提供用于构建 GuardrailService 实例的构建器实例。若应用或框架需要自定义构建 GuardrailService 实例的方式,应实现此接口。
InputGuardrailsConfigBuilderFactory- 用于覆盖和/或扩展默认 InputGuardrailsConfigBuilder 的 SPI
- 其他框架可能提供带有额外输入护栏配置的自有实现。
- 也允许其他框架通过其他机制(例如属性文件)驱动输入护栏配置。
OutputGuardrailsConfigBuilderFactory- 用于覆盖和/或扩展默认 OutputGuardrailsConfigBuilder 的 SPI
- 其他框架可能提供带有额外输出护栏配置的自有实现。
- 也允许其他框架通过其他机制(例如属性文件)驱动输出护栏配置。
InputGuardrailExecutorBuilderFactory- 用于覆盖和/或扩展默认 InputGuardrailExecutorBuilder 的 SPI,该构建器负责构建 InputGuardrailExecutor 实例。
OutputGuardrailExecutorBuilderFactory- 用于覆盖和/或扩展默认 OutputGuardrailExecutorBuilder 的 SPI,该构建器负责构建 OutputGuardrailExecutor 实例。