超越 JSON:將 Spring AI 工具響應格式轉換為 TOON、XML、CSV、YAML 等

工程 | Christian Tzolov | 2025年11月25日 | ...

JSON 是 LLM 工具響應的首選格式,但最近關於 TOON(面向令牌的物件表示法)等替代格式的討論聲稱在令牌效率和效能方面具有潛在優勢。儘管爭論仍在繼續——批判性分析 指出 結果具有上下文依賴性——問題是:如何在你的 Spring AI 應用程式中試驗這些格式?

本文演示了如何配置 Spring AI 以在 JSONTOONXMLCSVYAML 之間轉換工具響應,使你能夠決定哪種格式最適合你的特定用例。

Spring AI 工具呼叫:快速概覽

讓我們簡要回顧一下 Spring AI 工具呼叫 的工作原理

  1. 工具定義(名稱、描述、引數 schema)被新增到聊天請求中。
  2. 當模型決定呼叫工具時,它會發送工具名稱和輸入引數。
  3. Spring AI 使用提供的引數識別並執行工具。
  4. Spring AI 處理工具結果。
  5. Spring AI 將工具結果作為對話歷史的一部分發送回模型。
  6. 模型使用工具結果作為附加上下文生成最終響應。

ToolCallback 介面是此過程的核心。每個工具都封裝在一個 `ToolCallback` 中,用於處理序列化和執行邏輯。

我們可以在兩個關鍵點攔截和轉換響應格式

  • 工具結果級別:工具執行後但在 JSON 序列化之前(方法 1)
  • 響應級別:JSON 序列化後,將 JSON 轉換為另一種格式(方法 2)

這兩種方法都有其優點,選擇取決於您的具體要求。讓我們詳細探討每種方法。

方法 1:自定義 ToolCallResultConverter 配置

重要提示:僅適用於本地工具實現,例如 `@[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 轉換器,然後使用 JToontoon4j 等庫轉換為 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 註冊到 FunctionToolCallbackMethodToolCallback 構建器中。

限制

  • 不相容 MCP:不適用於 `@[McpTool](https://docs.springframework.tw/spring-ai/reference/api/tools.html#_model_context_protocol_tools)`(模型上下文協議工具)
  • 重複:必須為每個需要轉換的工具實現並註冊
  • 維護開銷:更改需要更新多個工具定義

Application2.java 提供了一個實現示例。

方法 2:全域性工具響應配置

使用自定義 `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)

元件 1:DelegatorToolCallbackProvider

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` 包裝器。格式引數指定要轉換為的格式。

元件 2: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 響應轉換為所需的格式。

元件 3:ResponseConverter 實用程式

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 將打印出工具響應,讓您看到目標格式的輸出。

格式轉換詳情

讓我們檢查每種支援的格式,看看輸出是什麼樣的。

JSON(預設)

[{"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"}]  

TOON

[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

XML


<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>

YAML

---
- 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"

CSV

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 總 Token
CSV 293 522 815
TOON 308 538 846
JSON 447 545 992
YAML 548 380 928
XML 599 572 1171

最佳實踐和建議

  • 從 JSON 開始——它經過驗證、安全且普遍理解
  • 在您的特定上下文中衡量效能;不要假設替代方案總是更好
  • 避免將複雜的巢狀結構轉換為 CSV 或 TOON
  • 在所有轉換器中包含錯誤處理
  • 當轉換失敗時提供 JSON 回退
  • 記錄轉換指標以進行監控

結論

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

嘗試不同的格式並在您的特定環境中衡量它們的影響,以確定最適合您用例的方案。


資源

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有