Skip to content
Embabel 文档版本 0.3.0-SNAPSHOT 源快照 3abf127d 更新于 2026-09-29

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 响应的护栏;
  • 护栏通过 withGuardRails API 注册,并且可以链式添加;
  • 护栏校验返回 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 依赖,而不是在构造器或字段初始化器里。 在构造时刻,SpringContextHolder bean 与 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。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。