主题
33 · 护栏(Guardrails)
护栏(guardrail)已成为智能体 AI 系统中不可或缺的组件。 它让你可以用可配置的策略来校验用户输入与 LLM 响应。
厂商自带的护栏见:
- Guardrails Hub
- Amazon Bedrock Guardrails
Embabel 提供的是一个构建自定义护栏的框架,让开发者能接入自己选择的校验逻辑。
动机(Motivation)
虽然你可以用自定义 validator 校验用户提示词或 thinking 块, 但 Embabel 通过 withGuardRails API 提供了标准化的框架。 护栏既可以实现为 POJO,也可以实现为实现 Embabel 护栏接口的 Spring bean。
护栏的常见用途:
- 输入校验:用普通、流式或 thinking 的 prompt runner 校验用户提示词;
- 带 thinking 的响应校验:thinking API 让你能访问 LLM 的 thinking 块,即便 LLM 无法构造出对象;
- 对象响应校验:当 LLM 构造出对象时,你仍然可以校验输出(被校验的内容是该对象的 JSON 表示);
- 流式校验:在流式模式下,
StreamingEvent.Thinking通过doOnNext回调直接提供 LLM 的推理内容。
这个框架的一个关键好处是能访问 Blackboard 对象 —— 它让护栏逻辑可以考虑到参与该智能体工作流的其它实体。
概念(Concepts)
UserInputGuardRail与AssistantMessageGuardRail两个接口,分别定义用户输入与 LLM 响应的护栏;- 护栏通过
withGuardRailsAPI 注册,并且可以链式添加; - 护栏校验返回
ValidationResult对象; - 校验错误按
ValidationSeverity级别排序,并以对应级别记录日志; CRITICAL严重级别会导致用户输入护栏抛出GuardRailViolationException,阻止该 LLM 操作执行;- 按设计,
createObjectIfPossible会优雅地处理异常并不构造对象而正常结束; 但在 thinking 模式下,GuardRailViolationException会被包装进ThinkingResponse中。
自定义消息合并(Customizing Message Combining)
在多轮对话中,护栏往往需要校验的不只是单条提示词,而是整段对话历史。 当 doTransform 之类的方法被传入多个 UserMessage 时, 每个 UserInputGuardRail 都会收到完整列表,并且必须把它们合并成一个字符串再校验。
combineMessages 方法控制这个合并如何进行。不同护栏可能需要不同格式:
- 毒性过滤器可能想把所有消息拼起来,检查整体语气;
- 审计护栏可能想给每条消息打上它在对话中的位置标记;
- PII 检测器可能想要清晰的分隔符,以定位敏感数据在哪条消息里。
默认实现用换行符连接消息:
java
default String combineMessages(List<UserMessage> userMessages) {
return userMessages.stream()
.map(UserMessage::getContent)
.collect(Collectors.joining("\n"));
}例如三条消息 ["Hello", "How are you?", "Tell me about X"] 会变成:
Hello
How are you?
Tell me about X要自定义这一行为,在你的护栏里覆写 combineMessages:
java
class AuditGuardRail implements UserInputGuardRail {
@Override
public @NotNull String getName() {
return "AuditGuard";
}
@Override
public @NotNull String getDescription() {
return "Logs conversation with message markers for audit trail";
}
@Override
public @NotNull String combineMessages(@NotNull List<UserMessage> userMessages) {
// 为审计日志给每条消息打上位置标记
StringBuilder result = new StringBuilder();
for (int i = 0; i < userMessages.size(); i++) {
if (i > 0) {
result.append("\n");
}
result.append("[Turn ").append(i + 1).append("]: ")
.append(userMessages.get(i).getContent());
}
return result.toString();
}
@Override
public @NotNull ValidationResult validate(@NotNull String input, @NotNull Blackboard blackboard) {
// 此时 input 的内容是:"[Turn 1]: Hello\n[Turn 2]: How are you?\n[Turn 3]: Tell me about X"
logger.info("Audit trail: {}", input);
return ValidationResult.VALID;
}
}示例:用 CRITICAL 校验错误阻止 LLM 执行
这个例子演示:CRITICAL 严重级别的护栏如何通过抛出 GuardRailViolationException 阻止 LLM 执行。
第 1 步:定义护栏
先定义一个返回 CRITICAL 校验错误的用户输入护栏:
java
/**
* A guardrail that blocks execution by returning a CRITICAL validation error.
*/
class CriticalUserInputGuardRail implements UserInputGuardRail {
@Override
public @NotNull String getName() {
return "CriticalUserInputGuardRail";
}
@Override
public @NotNull String getDescription() {
return "Blocks execution when critical policy violations are detected";
}
@Override
public @NotNull ValidationResult validate(@NotNull String input, @NotNull Blackboard blackboard) {
// 返回 CRITICAL 错误以阻止 LLM 执行
return new ValidationResult(true, List.of(
new ValidationError("policy-violation", "Content violates safety policy", ValidationSeverity.CRITICAL)
));
}
}再定义一个校验 LLM 响应的 assistant message 护栏:
java
/**
* A guardrail that validates LLM thinking blocks.
*/
class ThinkingBlocksGuardRail implements AssistantMessageGuardRail {
@Override
public @NotNull String getName() {
return "ThinkingBlocksGuardRail";
}
@Override
public @NotNull String getDescription() {
return "Validates LLM thinking blocks for compliance";
}
@Override
public @NotNull ValidationResult validate(@NotNull ThinkingResponse<?> response, @NotNull Blackboard blackboard) {
logger.info("Validating thinking blocks: {}", response.getThinkingBlocks());
return new ValidationResult(true, Collections.emptyList());
}
@Override
public @NotNull ValidationResult validate(@NotNull String input, @NotNull Blackboard blackboard) {
return new ValidationResult(true, Collections.emptyList());
}
}第 2 步:在 PromptRunner 上使用护栏
java
// 为 PromptRunner 配置护栏
PromptRunner runner = ai.withDefaultLlm() // 该示例使用 claude-sonnet-4-5
.withToolObject(Tooling.class)
.withGenerateExamples(true)
.withGuardRails(new CriticalUserInputGuardRail(), new ThinkingBlocksGuardRail());
String prompt = """
What is the hottest month in Florida and provide its temperature.
The name should be the month name, temperature should be in Fahrenheit.
""";
try {
// 尝试用 thinking 创建对象
ThinkingResponse<MonthItem> response = runner
.thinking()
.createObject(prompt, MonthItem.class);
} catch (GuardRailViolationException ex) {
// CRITICAL 校验错误会抛出该异常,从而阻止该 LLM 操作执行
logger.error("Guardrail blocked execution: {}", ex.getMessage());
}示例:用护栏做响应分析
当 LLM 无法构造对象时(例如提示词含糊),护栏仍然可以分析 LLM 的思考过程。 这对于理解"对象创建为什么失败"、或从推理中提取洞见都很有用。
第 1 步:定义一个简单的用户输入护栏
java
/**
* A guardrail that logs user input with INFO-level validation messages.
*/
class LoggingUserInputGuardRail implements UserInputGuardRail {
@Override
public @NotNull String getName() {
return "LoggingUserInputGuardRail";
}
@Override
public @NotNull String getDescription() {
return "Logs user input for auditing purposes";
}
@Override
public @NotNull ValidationResult validate(@NotNull String input, @NotNull Blackboard blackboard) {
logger.info("Processing user input: {}", input);
// 返回 INFO 级消息(不阻止执行)
return new ValidationResult(true, List.of(
new ValidationError("audit", "Input logged", ValidationSeverity.INFO)
));
}
}第 2 步:把护栏与 createObjectIfPossible 一起使用
java
// 为 PromptRunner 配置链式护栏
PromptRunner runner = ai.withDefaultLlm() // 该示例使用 claude-sonnet-4-5
.withToolObject(Tooling.class)
.withGuardRails(new LoggingUserInputGuardRail())
.withGuardRails(new ThinkingBlocksGuardRail());
String prompt = "Think about the coldest month in Alaska and its temperature. Provide your analysis.";
// 用 createObjectIfPossible 处理「对象创建可能失败」的情况
ThinkingResponse<MonthItem> response = runner
.thinking()
.createObjectIfPossible(prompt, MonthItem.class);
// 若提示词含糊,LLM 可能无法构造对象
if (response.getResult() == null) {
// 分析 thinking 块,理解 LLM 的推理
logger.info("Object creation not possible. Thinking blocks: {}", response.getThinkingBlocks());
}当 LLM 无法给出确定答案时,你可能会看到类似这样的推理:
Since I must be SURE about EVERY field and cannot make assumptions or provide approximate values,
I cannot provide the success structure with confidence.护栏可以自动对 LLM 响应做进一步分析,例如使用 CoreNLP 之类的语义文本处理工具。
更多示例见:
embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/
src/test/java/com/embabel/agent/config/models/anthropic/LLMAnthropicThinkingIT.java全局护栏配置(Global Guardrails Configuration)
除了通过 withGuardRails(...) 按调用挂载护栏,Embabel 还支持通过 application properties 声明全局护栏。 全局护栏在启动时实例化一次,并应用于每一次 LLM 操作 —— 在此之上再加上 PromptRunner 上配置的交互专属护栏。
这适合那些应当始终运行的横切安全策略,无论哪个调用点发起 LLM 调用 (PII 脱敏、脏话过滤、成本上限、审计日志等)。
属性配置
全局护栏在 application.properties(或 application.yml)中定义为逗号分隔的全限定类名:
properties
# 应用于每个用户输入的护栏
embabel.agent.guardrails.user-input=com.example.ProfanityFilter,com.example.LengthValidator
# 应用于每条 assistant 消息的护栏
embabel.agent.guardrails.assistant-message=com.example.OutputValidator
# 实例化失败时是否在启动阶段 fail-fast(默认 false)
embabel.agent.guardrails.fail-on-error=false每个类必须:
- 实现对应的接口(
UserInputGuardRail对应 user-input,AssistantMessageGuardRail对应 assistant-message); - 提供公开的无参构造器(实例通过
BeanUtils.instantiateClass创建)。
⚠️ 通过这些属性注册的护栏是普通 POJO,不是 Spring 托管的 bean。 注册表直接调用
BeanUtils.instantiateClass(...),因此:
@Autowired、@Value以及构造器注入 Spring bean 都不生效;@PostConstruct/@PreDestroy生命周期回调不会被调用;ApplicationContextAware、BeanNameAware等*Aware回调不会被调用;- 动态访问模式(例如从某个 Spring 托管的 holder 组装
name字段,或通过静态ApplicationContext访问器取配置 bean) 在启动时会失败或返回null,因为 holder 可能尚未初始化。
如果你的护栏确实需要 Spring 依赖,推荐的做法是: 用一个小的 ApplicationContextAware holder 桥接 ApplicationContext, 让护栏在校验时(而不是构造器里)查找 bean 或属性(见下)。 另一种做法是把护栏声明为 @Component,并通过 PromptRunner 上的 withGuardRails(...) 按调用挂载。
从 POJO 护栏访问 Spring bean
当一个通过属性注册的护栏必须查询 Spring bean 或环境属性时 (例如 application.yml 里定义的成本上限,或一个共享的 MeterRegistry), 请通过一个静态 holder 暴露 ApplicationContext,让护栏在校验时回调它。
java
package com.example.observability;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Bridges Spring's ApplicationContext to POJO guardrails instantiated by
* embabel's GlobalGuardRailsRegistry. Those guardrails are created via
* BeanUtils.instantiateClass() with a no-arg constructor, so they cannot
* use @Autowired — they reach back through this holder to look up beans
* and properties at validation time.
*/
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static volatile ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
context = applicationContext;
}
public static ApplicationContext context() {
return context;
}
public static <T> T getBean(Class<T> type) {
ApplicationContext ctx = context;
return ctx != null ? ctx.getBean(type) : null;
}
public static String getProperty(String key, String defaultValue) {
ApplicationContext ctx = context;
return ctx != null ? ctx.getEnvironment().getProperty(key, defaultValue) : defaultValue;
}
}护栏随后就可以在 validate(...) 内部惰性解析它的依赖:
java
public class CostLimitGuardRail implements UserInputGuardRail {
@Override
public @NotNull String getName() {
return "CostLimitGuardRail";
}
@Override
public @NotNull String getDescription() {
return "Blocks requests when the monthly LLM budget is exceeded";
}
@Override
public @NotNull ValidationResult validate(@NotNull String input, @NotNull Blackboard blackboard) {
// 在校验时查找依赖 —— 此时 holder 已经接好
CostTracker tracker = SpringContextHolder.getBean(CostTracker.class);
String budget = SpringContextHolder.getProperty("app.llm.monthly-budget-usd", "100");
if (tracker != null && tracker.spentThisMonth() > Double.parseDouble(budget)) {
return new ValidationResult(false, List.of(
new ValidationError("budget-exceeded",
"Monthly LLM budget exceeded",
ValidationSeverity.CRITICAL)
));
}
return new ValidationResult(true, Collections.emptyList());
}
}请在
validate(...)内部解析 Spring 依赖,而不是在构造器或字段初始化器里。 在构造时刻,SpringContextHolderbean 与GlobalGuardRailsRegistry的处理顺序并无保证, 因此静态的 context 字段可能仍为null。而validate(...)被调用时,holder 已经完全接好。
与交互专属护栏的合并
当一次 LLM 操作执行时,全局护栏会与通过 withGuardRails(...) 设置的交互专属护栏合并:
- 全局护栏总是先运行,然后是交互专属的;
- 去重基于类的身份(而非
name字段),因此同一个类既全局注册又按调用注册时,只会被调用一次; - 去重时保留全局实例,从而保证单例语义。
严格模式:fail-on-error
默认情况下(fail-on-error=false),如果某个配置的护栏无法实例化 (类名拼写错误、缺少无参构造器、构造器抛异常),错误会被记录日志,应用带着剩余护栏继续运行。
设置 fail-on-error=true 后,出现以下任一情况都会让 Spring 启动失败并抛出 GuardRailInstantiationException:
- 类无法加载;
- 类没有实现预期的护栏接口;
- 构造器抛异常。
生产环境推荐开启严格模式 —— 在那里,"少了一个护栏"必须被当作部署错误,而不是静默遗漏。
示例:定义一个全局护栏
java
package com.example;
public class ProfanityFilter implements UserInputGuardRail {
@Override
public @NotNull String getName() {
return "ProfanityFilter";
}
@Override
public @NotNull String getDescription() {
return "Blocks user input containing profanity";
}
@Override
public @NotNull ValidationResult validate(@NotNull String input, @NotNull Blackboard blackboard) {
if (containsProfanity(input)) {
return new ValidationResult(false, List.of(
new ValidationError("profanity", "Input contains disallowed terms", ValidationSeverity.CRITICAL)
));
}
return new ValidationResult(true, Collections.emptyList());
}
private boolean containsProfanity(String input) {
// ... 实现
return false;
}
}在 application.properties 中注册之后,该护栏就应用于每一次 LLM 调用 —— 调用点无需任何代码改动:
kotlin
// ProfanityFilter 自动运行 —— 不需要显式 withGuardRails()
val response = ai.withDefaultLlm()
.createObject("Hello, world", Greeting::class.java)编程式访问(Programmatic Access)
该注册表也可以通过编程方式访问 —— 既可以作为注入的 Spring bean,也可以用它的伴生访问器:
java
// Spring 注入
public class MyService {
private final GlobalGuardRailsRegistry registry;
public MyService(GlobalGuardRailsRegistry registry) {
this.registry = registry;
}
}
// 静态访问 —— 若 Spring 尚未初始化注册表,get() 返回 null;
// 那两个列表访问器在这种情况下返回空列表。
GlobalGuardRailsRegistry registry = GlobalGuardRailsRegistry.get();
List<UserInputGuardRail> userGuards = GlobalGuardRailsRegistry.getUserInputGuardRails();
List<AssistantMessageGuardRail> assistantGuards = GlobalGuardRailsRegistry.getAssistantMessageGuardRails();与其它校验机制的关系
Agent API 框架支持 Jakarta Bean Validation(JSR-380) 用于领域对象约束。 这些约束会被注入 schema,并在对象构造期间被校验。
此外,一个规划中的 Agent Action 校验框架会复用与护栏相同的数据结构, 包括 ValidationResult、ValidationError 与 ContentValidator。
总之,护栏与 bean validator 是互补而非相同的:
- Bean validation 确保对象结构良好、满足业务约束;
- Guardrails 确保 AI 交互安全、符合策略。
两者可以独立启用,服务于 AI 安全栈的不同侧面。
@SecureAgentTool是第三种、正交的机制:它实施的是访问控制,而不是内容安全或数据有效性。 护栏问的是"这个内容可以接受吗?",@SecureAgentTool问的是"这个主体到底有没有权限调用这个 Agent 动作?" 两者配合得很好 ——@SecureAgentTool阻止未授权主体调用工具, 而护栏校验那些被允许的调用的输入与输出。详见@SecureAgentTool。