Spring AI PromptTemplate插入JSON时花括号冲突怎么办?三种可靠解决方案

文章摘要

Spring AI默认使用花括号识别Prompt模板变量。当Prompt中包含JSON Schema、示例对象或代码时,JSON自身的 {} 可能被误当成模板变量,导致渲染报错、变量缺失或内容被错误替换。本文通过错误示例,介绍自定义 `` 分隔符、NoOpTemplateRenderer和外部Resource模板三种解决方式,并说明ChatClient模板与Advisor内部模板的作用范围差异。

一、典型问题

Prompt:

String template = """
请根据用户输入返回JSON:

{
  "name": "{name}",
  "age": 18
}
""";

这里存在两类花括号:

JSON对象花括号
模板变量{name}

Spring AI默认模板渲染器使用 {variable} 识别变量。

复杂JSON中可能出现:

  • 把JSON字段误当变量;
  • 提示缺少变量;
  • Schema渲染失败;
  • 示例对象被修改;
  • Advisor模板报错。

二、方案一:修改模板变量分隔符

这是最推荐的方法。

使用:


作为变量语法。

import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.template.st.StTemplateRenderer;

PromptTemplate promptTemplate =
        PromptTemplate.builder()
                .renderer(
                        StTemplateRenderer.builder()
                                .startDelimiterToken('')
                                .build()
                )
                .template("""
                        请根据用户信息返回JSON:

                        {
                          "name": "",
                          "department": "",
                          "enabled": true
                        }
                        """)
                .build();

String result = promptTemplate.render(
        Map.of(
                "name", "张三",
                "department", "产品部"
        )
);

JSON花括号保持原样,只有 会被替换。

三、在ChatClient中配置自定义Renderer

@Bean
ChatClient jsonFriendlyChatClient(
        ChatModel chatModel
) {
    return ChatClient.builder(chatModel)
            .templateRenderer(
                    StTemplateRenderer.builder()
                            .startDelimiterToken('')
                            .build()
            )
            .build();
}

调用:

String response = chatClient.prompt()
        .user(user -> user
                .text("""
                        把以下信息转换为JSON:

                        {
                          "customer": "",
                          "requirement": ""
                        }
                        """)
                .param(
                        "customer",
                        "某白酒企业"
                )
                .param(
                        "requirement",
                        "渠道动销分析"
                )
        )
        .call()
        .content();

注意:

ChatClient配置的TemplateRenderer只影响直接在ChatClient中定义的user和system模板。

它不会自动修改Advisor内部使用的模板。

四、方案二:不需要模板时使用NoOpTemplateRenderer

如果Prompt已经完整,不需要变量替换:

String prompt = """
请严格返回以下JSON结构:

{
  "status": "SUCCESS",
  "data": {
    "value": 100
  }
}
""";

可以关闭模板渲染:

@Bean
ChatClient noTemplateChatClient(
        ChatModel chatModel
) {
    return ChatClient.builder(chatModel)
            .templateRenderer(
                    NoOpTemplateRenderer.INSTANCE
            )
            .build();
}

适合:

  • 动态字符串已经提前生成;
  • Prompt中大量JSON;
  • Prompt由外部系统渲染;
  • 不需要Spring AI变量替换。

缺点是不能再使用 .param(...)

因此,是否使用NoOp应按客户端用途拆分,不要全局关闭后又期待模板变量生效。

五、方案三:外部Resource模板

将Prompt放到:

src/main/resources/prompts/customer-analysis.st

模板:

你是企业经营分析助手。

客户:
分析周期:

请返回:

{
  "summary": "分析结论",
  "risks": [
    {
      "name": "风险名称",
      "level": "HIGH"
    }
  ]
}

加载:

@Value(
    "classpath:/prompts/customer-analysis.st"
)
private Resource promptResource;

构建:

PromptTemplate template =
        PromptTemplate.builder()
                .resource(promptResource)
                .renderer(
                        StTemplateRenderer.builder()
                                .startDelimiterToken('')
                                .build()
                )
                .build();

String prompt = template.render(
        Map.of(
                "customer",
                "某渠道客户",
                "period",
                "2026年7月"
        )
);

外部文件便于:

  • 版本控制;
  • 代码审查;
  • 单独测试;
  • 多语言;
  • Prompt复用;
  • 减少Java字符串噪声。

六、为什么不推荐手工转义所有花括号

问题是:

  • 不同模板引擎转义规则不同;
  • JSON Schema非常长;
  • 可读性差;
  • 容易漏掉嵌套对象;
  • 模板升级后可能失效;
  • 开发者难以区分JSON与变量。

改变变量分隔符通常更清晰。

七、JSON Schema场景

复杂Schema中花括号密集,推荐:

模板变量使用
JSON继续使用{ }

例如:

String template = """
任务:

输出必须符合以下Schema:

{
  "type": "object",
  "properties": {
    "result": {
      "type": "string"
    }
  }
}
""";

八、Advisor内部模板要单独配置

假设你配置了:

ChatClient.builder(chatModel)
        .templateRenderer(customRenderer)

QuestionAnswerAdvisor仍然可能使用自己的模板。

原因是:

ChatClient模板
Advisor内部模板

属于不同作用范围。

自定义RAG模板时,需要在Advisor Builder中设置对应PromptTemplate。

排查方法:

普通user模板是否正常
RAG开启后是否报错
错误是否来自QuestionAnswerAdvisor
Advisor是否有独立模板配置

九、模板变量缺失如何提前发现

不要等到运行时才发现。

可以写单元测试:

class PromptTemplateTest {

    @Test
    void shouldRenderJsonTemplate() {
        PromptTemplate template =
                createTemplate();

        String result = template.render(
                Map.of(
                        "name",
                        "张三"
                )
        );

        assertThat(result)
                .contains("\"name\": \"张三\"");

        assertThat(result)
                .contains("\"enabled\": true");
    }
}

测试:

  • 所有变量可以渲染;
  • JSON格式保留;
  • 没有未替换变量;
  • 中文和换行正确;
  • 示例代码没有被破坏。

十、检测未替换变量

private static final Pattern UNRESOLVED =
        Pattern.compile("");

public static void validateRenderedPrompt(
        String prompt
) {
    Matcher matcher = UNRESOLVED.matcher(prompt);

    if (matcher.find()) {
        throw new IllegalArgumentException(
                "存在未替换变量:"
                + matcher.group()
        );
    }
}

推荐变量统一命名:


避免与普通XML标签混淆。

十一、用户输入中包含花括号怎么办

变量值可能是:

请解释Java中的Map和JSON { }。

正常模板替换不会再次递归解析变量值。

不要对渲染结果重复执行模板渲染,否则用户输入中的花括号可能被第二次解释。

十二、模板安全

PromptTemplate不是安全过滤器。

用户变量仍可能包含:

忽略之前的指令

需要独立处理:

  • Prompt Injection;
  • 输入长度;
  • 敏感信息;
  • HTML;
  • 日志脱敏;
  • 数据权限。

模板渲染只负责变量替换,不负责内容可信度。

十三、选择建议

Prompt包含JSON并需要变量

使用自定义分隔符

Prompt不需要变量

使用NoOpTemplateRenderer

Prompt较长、需要版本控制

使用Resource外部模板
+自定义分隔符

Advisor内部Prompt

在Advisor中单独配置模板

总结

Spring AI PromptTemplate与JSON冲突的根因是:

模板变量和JSON共用花括号

最可靠的解决方式是将变量改为:


并通过单元测试确认JSON结构和变量替换都正确。

延伸阅读

如果你正在关注企业级 AI 应用、Agent、RAG、MCP 与大模型工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。