领先一步
VMware 提供培训和认证,助您加速进步。
了解更多
JSON 是 LLM 工具响应的首选格式,但最近关于 TOON(面向令牌的对象表示法)等替代格式的讨论声称在令牌效率和性能方面具有潜在优势。尽管争论仍在继续——批判性分析 指出 结果具有上下文依赖性——问题是:如何在你的 Spring AI 应用程序中试验这些格式?
本文演示了如何配置 Spring AI 以在 JSON、TOON、XML、CSV 和 YAML 之间转换工具响应,使你能够决定哪种格式最适合你的特定用例。
让我们简要回顾一下 Spring AI 工具调用 的工作原理
ToolCallback 接口是此过程的核心。每个工具都封装在一个 `ToolCallback` 中,用于处理序列化和执行逻辑。
我们可以在两个关键点拦截和转换响应格式
这两种方法都有其优点,选择取决于您的具体要求。让我们详细探讨每种方法。
重要提示:仅适用于本地工具实现,例如 `@[Tool](https://docs.springframework.org.cn/spring-ai/reference/api/tools.html#_tool)`、`FunctionToolCallback` 和 `MethodToolCallback`。目前,MCP 工具不支持此功能。
ToolCallResultConverter 接口提供对单个工具格式的细粒度控制。DefaultToolCallResultConverter 将结果序列化为 JSON,但您可以通过提供自己的 ToolCallResultConverter 实现来自定义序列化过程。例如,自定义 ToonToolCallResultConverter 可以是这样的
public static class ToonToolCallResultConverter implements ToolCallResultConverter {
private ToolCallResultConverter delegate = new DefaultToolCallResultConverter();
@Override
public String convert(@Nullable Object result, @Nullable Type returnType) {
// First convert to JSON using the default converter
String json = this.delegate.convert(result, returnType);
// Then convert JSON to TOON
return JToon.encodeJson(json);
}
}
它使用默认的 JSON 转换器,然后使用 JToon 或 toon4j 等库转换为 TOON。
使用 @Tool 注册
@Tool(description = "Get random titanic passengers",
resultConverter = ToonToolCallResultConverter.class) // (1)
public List<String> randomTitanicToon(
@ToolParam(description = "Number of records to return") int count) {
return TitanicData.getRandomTitanicPassengers(count);
}
使用 `resultConverter` 属性设置自定义 ToonToolCallResultConverter。
执行流程: 工具执行 → 默认转换器创建 JSON → TOON 转换器转换 JSON → LLM 接收 TOON 响应。
您还可以通过编程方式将 ToolCallResultConverter 注册到 FunctionToolCallback 和 MethodToolCallback 构建器中。
限制
Application2.java 提供了一个实现示例。
使用自定义 `ToolCallbackProvider` 全局应用格式转换,该提供程序使用委托模式包装现有提供程序
Original ToolCallbackProvider
↓ wrapped by
DelegatorToolCallbackProvider
↓ creates wrapped callbacks
DelegatorToolCallback (for each tool)
↓ intercepts call() method
↓ converts response
JSON → Target Format (TOON/XML/CSV/YAML)
public class DelegatorToolCallbackProvider implements ToolCallbackProvider {
private final ToolCallbackProvider delegate;
private final ResponseConverter.Format format;
public DelegatorToolCallbackProvider(ToolCallbackProvider delegate,
ResponseConverter.Format format) {
this.delegate = delegate;
this.format = format;
}
@Override
public ToolCallback[] getToolCallbacks() {
return Stream.of(this.delegate.getToolCallbacks())
.map(callback -> new DelegatorToolCallback(callback, this.format))
.toArray(ToolCallback[]::new);
}
}
此提供程序包装现有的 `ToolCallbackProvider`,并为每个工具回调创建一个 `DelegatorToolCallback` 包装器。格式参数指定要转换为的格式。
public static class DelegatorToolCallback implements ToolCallback {
private final ToolCallback delegate;
private final ResponseConverter.Format format;
public DelegatorToolCallback(ToolCallback delegate,
ResponseConverter.Format format) {
this.delegate = delegate;
this.format = format;
}
@Override
public ToolDefinition getToolDefinition() {
return this.delegate.getToolDefinition();
}
@Override
public String call(String toolInput) {
// Call the original tool to get JSON response
String jsonResponse = this.delegate.call(toolInput);
// Convert to target format
return ResponseConverter.convert(jsonResponse, this.format);
}
}
回调包装器拦截 `call()` 方法,允许原始工具正常执行,然后将其 JSON 响应转换为所需的格式。
public class ResponseConverter {
public enum Format {
TOON, YAML, XML, CSV, JSON
}
public static String convert(String json, Format format) {
switch (format) {
case TOON: return jsonToToon(json);
case YAML: return jsonToYaml(toJsonNode(json));
case XML: return jsonToXml(toJsonNode(json));
case CSV: return jsonToCsv(toJsonNode(json));
case JSON: return json;
}
throw new IllegalStateException("Unsupported format: " + format);
}
private static String jsonToToon(String jsonString) {...}
private static String jsonToYaml(JsonNode jsonNode) {...}
private static String jsonToXml(JsonNode jsonNode) {...}
private static String jsonToCsv(JsonNode jsonNode) {...}
}
ResponseConverter 为每种支持的格式提供转换方法,处理每种格式的特定要求(例如为 XML 包装数组或为 CSV 构建动态 schema)。
使用示例
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder,
ToolCallbackProvider toolCallbackProvider) {
// Wrap the provider with format conversion
var provider = new DelegatorToolCallbackProvider(
toolCallbackProvider,
ResponseConverter.Format.TOON
);
// Configure ChatClient with the wrapped provider
var chatClient = chatClientBuilder
.defaultToolCallbacks(provider)
.build();
return args -> {
var response = chatClient
.prompt("Please show me 10 Titanic passengers?")
.call()
.chatResponse();
System.out.println(String.format("""
RESPONSE: %s
USAGE: %s
""",
response.getResult().getOutput().getText(),
response.getMetadata().getUsage()));
};
}
@Bean
MethodToolCallbackProvider methodToolCallbackProvider() {
return MethodToolCallbackProvider.builder()
.toolObjects(new MyTools())
.build();
}
static class MyTools {
@Tool(description = "Get titanic passengers")
public List<String> randomTitanicToon(
@ToolParam(description = "Number of records to return") int count) {
return TitanicData.getTitanicPassengersInRange(30, count);
}
}
}
执行流程: 用户提示 → LLM 调用工具 → 包装器拦截 → 工具执行 → 创建 JSON → 格式转换器转换 → LLM 接收转换后的响应。
Application 示例利用了 ToolCallAdvisor(例如,将工具执行作为 Advisor 链的一部分)和一个自定义日志记录 Advisor `MyLogAdvisor`,它有助于查看不同格式的实际工具响应。此 Advisor 将打印出工具响应,让您看到目标格式的输出。
让我们检查每种支持的格式,看看输出是什么样的。
[{"PassengerId":"31","Survived":"0","Pclass":"1","Name":"Uruchurtu, Don. Manuel E","Sex":"male","Age":40,"SibSp":"0","Parch":"0","Ticket":"PC 17601","Fare":27.7208,"Cabin":null,"Embarked":"C"},
{"PassengerId":"32","Survived":"1","Pclass":"1","Name":"Spencer, Mrs. William Augustus (Marie Eugenie)","Sex":"female","Age":null,"SibSp":"1","Parch":"0","Ticket":"PC 17569","Fare":146.5208,"Cabin":"B78","Embarked":"C"},
{"PassengerId":"33","Survived":"1","Pclass":"3","Name":"Glynn, Miss. Mary Agatha","Sex":"female","Age":null,"SibSp":"0","Parch":"0","Ticket":"335677","Fare":7.75,"Cabin":null,"Embarked":"Q"},
{"PassengerId":"34","Survived":"0","Pclass":"2","Name":"Wheadon, Mr. Edward H","Sex":"male","Age":66,"SibSp":"0","Parch":"0","Ticket":"C.A. 24579","Fare":10.5,"Cabin":null,"Embarked":"S"},
{"PassengerId":"35","Survived":"0","Pclass":"1","Name":"Meyer, Mr. Edgar Joseph","Sex":"male","Age":28,"SibSp":"1","Parch":"0","Ticket":"PC 17604","Fare":82.1708,"Cabin":null,"Embarked":"C"}]
[5]{PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked}:
"31","0","1","Uruchurtu, Don. Manuel E",male,40,"0","0",PC 17601,27.7208,null,C
"32","1","1","Spencer, Mrs. William Augustus (Marie Eugenie)",female,null,"1","0",PC 17569,146.5208,B78,C
"33","1","3","Glynn, Miss. Mary Agatha",female,null,"0","0","335677",7.75,null,Q
"34","0","2","Wheadon, Mr. Edward H",male,66,"0","0",C.A. 24579,10.5,null,S
"35","0","1","Meyer, Mr. Edgar Joseph",male,28,"1","0",PC 17604,82.1708,null,C
<ObjectNode>
<root><PassengerId>31</PassengerId><Survived>0</Survived><Pclass>1</Pclass><Name>Uruchurtu, Don. Manuel E</Name><Sex>male</Sex><Age>40</Age><SibSp>0</SibSp><Parch>0</Parch><Ticket>PC 17601</Ticket><Fare>27.7208</Fare><Cabin/><Embarked>C</Embarked></root>
<root><PassengerId>32</PassengerId><Survived>1</Survived><Pclass>1</Pclass><Name>Spencer, Mrs. William Augustus (Marie Eugenie)</Name><Sex>female</Sex><Age/><SibSp>1</SibSp><Parch>0</Parch><Ticket>PC 17569</Ticket><Fare>146.5208</Fare><Cabin>B78</Cabin><Embarked>C</Embarked></root>
<root><PassengerId>33</PassengerId><Survived>1</Survived><Pclass>3</Pclass><Name>Glynn, Miss. Mary Agatha</Name><Sex>female</Sex><Age/><SibSp>0</SibSp><Parch>0</Parch><Ticket>335677</Ticket><Fare>7.75</Fare><Cabin/><Embarked>Q</Embarked></root>
<root><PassengerId>34</PassengerId><Survived>0</Survived><Pclass>2</Pclass><Name>Wheadon, Mr. Edward H</Name><Sex>male</Sex><Age>66</Age><SibSp>0</SibSp><Parch>0</Parch><Ticket>C.A. 24579</Ticket><Fare>10.5</Fare><Cabin/><Embarked>S</Embarked></root>
<root><PassengerId>35</PassengerId><Survived>0</Survived><Pclass>1</Pclass><Name>Meyer, Mr. Edgar Joseph</Name><Sex>male</Sex><Age>28</Age><SibSp>1</SibSp><Parch>0</Parch><Ticket>PC 17604</Ticket><Fare>82.1708</Fare><Cabin/><Embarked>C</Embarked></root>
</ObjectNode>
---
- PassengerId: "31"
Survived: "0"
Pclass: "1"
Name: "Uruchurtu, Don. Manuel E"
Sex: "male"
Age: 40
SibSp: "0"
Parch: "0"
Ticket: "PC 17601"
Fare: 27.7208
Cabin: null
Embarked: "C"
...
- PassengerId: "35"
Survived: "0"
Pclass: "1"
Name: "Meyer, Mr. Edgar Joseph"
Sex: "male"
Age: 28
SibSp: "1"
Parch: "0"
Ticket: "PC 17604"
Fare: 82.1708
Cabin: null
Embarked: "C"
PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked
31,0,1,"Uruchurtu, Don. Manuel E",male,40,0,0,"PC 17601",27.7208,,C
32,1,1,"Spencer, Mrs. William Augustus (Marie Eugenie)",female,,1,0,"PC 17569",146.5208,B78,C
33,1,3,"Glynn, Miss. Mary Agatha",female,,0,0,335677,7.75,,Q
34,0,2,"Wheadon, Mr. Edward H",male,66,0,0,"C.A. 24579",10.5,,S
35,0,1,"Meyer, Mr. Edgar Joseph",male,28,1,0,"PC 17604",82.1708,,C
以下是每种格式的 Token 用量估算
| 格式 | 提示 Token | 完成 Token | 总 Token |
|---|---|---|---|
| CSV | 293 | 522 | 815 |
| TOON | 308 | 538 | 846 |
| JSON | 447 | 545 | 992 |
| YAML | 548 | 380 | 928 |
| XML | 599 | 572 | 1171 |
Spring AI 通过两种不同的方法提供了尝试工具响应格式的灵活性。当您需要细粒度控制时,使用 `ToolCallResultConverter` 进行选择性、按工具转换。选择全局 `DelegatorToolCallbackProvider` 方法,以在所有工具(包括 MCP 工具)中实现一致的格式转换。两者都支持多种格式——TOON、YAML、XML、CSV 和 JSON——让您可以自由地针对您的特定用例进行优化。
注意:以下代码仅用于演示目的,在没有适当的测试、错误处理和安全考虑的情况下,不应在生产中使用。
完整的演示可在 GitHub 上获取。使用不同的格式运行它
./mvnw spring-boot:run -Dspring.ai.tool.response.format=TOON
./mvnw spring-boot:run -Dspring.ai.tool.response.format=CSV
./mvnw spring-boot:run -Dspring.ai.tool.response.format=YAML
尝试不同的格式并在您的特定环境中衡量它们的影响,以确定最适合您用例的方案。