領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多
JSON 是 LLM 工具響應的首選格式,但最近關於 TOON(面向令牌的物件表示法)等替代格式的討論聲稱在令牌效率和效能方面具有潛在優勢。儘管爭論仍在繼續——批判性分析 指出 結果具有上下文依賴性——問題是:如何在你的 Spring AI 應用程式中試驗這些格式?
本文演示了如何配置 Spring AI 以在 JSON、TOON、XML、CSV 和 YAML 之間轉換工具響應,使你能夠決定哪種格式最適合你的特定用例。
讓我們簡要回顧一下 Spring AI 工具呼叫 的工作原理
ToolCallback 介面是此過程的核心。每個工具都封裝在一個 `ToolCallback` 中,用於處理序列化和執行邏輯。
我們可以在兩個關鍵點攔截和轉換響應格式
這兩種方法都有其優點,選擇取決於您的具體要求。讓我們詳細探討每種方法。
重要提示:僅適用於本地工具實現,例如 `@[Tool](https://docs.springframework.tw/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
嘗試不同的格式並在您的特定環境中衡量它們的影響,以確定最適合您用例的方案。