Agentes de IA Explicables: Capturar el razonamiento de llamadas a herramientas LLM con Spring AI
1. Visión general
Cuando construimos agentes de IA con capacidades de llamada a herramientas, a menudo vemos qué herramienta el LLM seleccionó, pero no vemos por qué tomó esa decisión. Esta falta de visión hace que la depuración sea más difícil, reduce la observabilidad y limita la confianza en los sistemas impulsados por IA. Para agentes de grado de producción, comprender el razonamiento del modelo no es opcional. Los agentes de IA explicables resuelven este problema capturando contexto adicional del LLM durante la selección de herramientas.
En este artículo revisaremos el Tool Argument Augmenter en un ejemplo práctico. Veremos cómo capturar el razonamiento del LLM durante las llamadas a herramientas y cómo usar esos datos dentro de una aplicación Spring AI.
2. Problema de llamada a herramientas
Usamos una llamada a herramientas cuando el modelo no puede responder de manera fiable solo con sus datos de entrenamiento. Por ejemplo, lo usamos cuando el LLM necesita datos en tiempo real, como precios actuales o información específica del usuario, cuando requiere acceso a sistemas externos como bases de datos o servicios internos, o cuando debe desencadenar acciones como crear un registro o enviar una notificación.
En Spring AI, el modelo delega el trabajo al código de la aplicación a través de la llamada a herramientas, mientras que el LLM se centra en comprender la solicitud del usuario y generar la respuesta final. Supongamos que nuestra aplicación expone dos herramientas:
@Tool(description = "Get patient health status")
public String retrievePatientHealthStatus(String patientId) {
return HEALTH_DATA.get(patientId).status();
}
@Tool(description = "Get when patient health status was updated")
public LocalDate retrievePatientHealthStatusChangeDate(String patientId) {
return HEALTH_DATA.get(patientId).changeDate();
}
Con @Tool, marcamos un método como disponible para la llamada a herramientas del LLM. Le preguntamos a nuestra aplicación ¿El paciente está estable? y aquí es lo que sucede tras bastidores:
- Spring AI envía ambas definiciones de herramientas, incluidos sus esquemas de entrada, al LLM.
- El LLM analiza la solicitud y evalúa las herramientas disponibles.
- Decide llamar a retrievePatientHealthStatus.
- El LLM devuelve una solicitud de llamada a herramienta con los argumentos requeridos.
- La gestión de herramientas despacha e invoca la herramienta seleccionada.
- La herramienta devuelve su resultado al LLM, que genera la respuesta final.
Desde la perspectiva de la aplicación, solo vemos que la herramienta fue seleccionada. El problema es que no vemos el razonamiento detrás de esa elección. Esta falta de razonamiento limita la observabilidad y dificulta la depuración. Podemos confirmar qué herramienta se llamó, pero no podemos explicar por qué el LLM la eligió. El Tool Argument Augmenter de Spring AI está diseñado específicamente para abordar esta limitación.
3. El Tool Argument Augmenter
El Tool Argument Augmenter agrega una capa de explicabilidad sobre la llamada a herramientas estándar. Extendemos dinámicamente el JSON Schema de la herramienta con argumentos adicionales. Estos argumentos capturan metadatos que la aplicación necesita, como razonamiento, ideas o confianza. La herramienta en sí permanece sin cambios y sin conocimiento de esta ampliación. Con @ToolParam, describimos los parámetros individuales del método para que el modelo entienda qué entradas debe proporcionar:
@ToolParam(description = """
Your step-by-step reasoning for why you're calling this tool and what you expect.
Add evidences why did you choose specific tool to call.
""", required = true)
String innerThought
Después de habilitar el Tool Argument Augmenter, el flujo de llamada a herramientas cambia de la siguiente manera:
- Preguntamos ¿El paciente está estable?
- Spring AI envía las definiciones de herramientas para retrievePatientHealthStatus() y retrievePatientHealthStatusChangeDate() al Tool Call Advisor.
- El Tool Argument Augmenter intercepta ambas definiciones de herramientas.
- El aumentador extiende cada esquema JSON de la herramienta con el argumento innerThought.
- Spring AI envía los esquemas de herramientas aumentados al LLM.
- El LLM decide llamar a retrievePatientHealthStatus() y devuelve una solicitud de llamada a herramienta que incluye el argumento original y un argumento aumentado, innerThought, explicando por qué se seleccionó esta herramienta.
- El aumentador extrae innerThought y lo envía a un consumidor para registro, almacenamiento en memoria o análisis.
- Spring AI invoca retrievePatientHealthStatus() usando solo el argumento esperado.
- El LLM genera la respuesta final usando el resultado de la herramienta.
Este enfoque captura por qué el LLM seleccionó una herramienta específica. Podemos registrar el razonamiento, almacenarlo como memoria a largo plazo o usarlo para depuración y análisis. Al mismo tiempo, mantenemos las herramientas limpias y reutilizables mientras obtenemos explicabilidad y confianza en el comportamiento del agente sin cambiar los contratos de herramienta existentes.
4. Ejemplo de verificador de estado de salud del paciente
Vamos a implementar una aplicación simple de verificador de estado de salud del paciente. En este caso, tendremos algunas herramientas que proporcionan diferentes tipos de información sobre el estado de salud del paciente. Luego, el usuario llamará a nuestro verificador con preguntas sobre diferentes pacientes. Basado en eso, el LLM decidirá qué herramienta llamar para proporcionar la información requerida.
4.1. Dependencias
Comenzamos añadiendo la dependencia spring-ai-starter-model-openai:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
Esta dependencia ya incluye las clases de Spring AI bajo el capó. Además, proporciona la integración del modelo OpenAI que usaremos en esta aplicación.
4.2. Especificación de herramientas
Creamos una clase PatientHealthInformationTools. Esta clase expondrá métodos de herramienta que nuestro agente de IA podrá llamar para recuperar información de salud del paciente. Actúa como puente entre el LLM y nuestra fuente interna de datos de salud:
public class PatientHealthInformationTools {
public static final Map<String, HealthStatus> HEALTH_DATA = Map.of(
"P001", new HealthStatus("Healthy", LocalDate.ofYearDay(2025, 100)),
"P002", new HealthStatus("Has cough", LocalDate.ofYearDay(2025, 200)),
"P003", new HealthStatus("Healthy", LocalDate.ofYearDay(2025, 300)),
"P004", new HealthStatus("Has increased blood pressure", LocalDate.ofYearDay(2025, 350)),
"P005", new HealthStatus("Healthy", LocalDate.ofYearDay(2026, 10)));
@Tool(description = "Get patient health status")
public String retrievePatientHealthStatus(String patientId) {
return HEALTH_DATA.get(patientId).status();
}
@Tool(description = "Get when patient health status was updated")
public LocalDate retrievePatientHealthStatusChangeDate(String patientId) {
return HEALTH_DATA.get(patientId).changeDate();
}
}
En esta clase, primero hemos añadido dos herramientas. Inicialmente, en retrievePatientHealthStatus() , devolvemos el estado del paciente por patientId. Luego, en retrievePatientHealthStatusChangeDate() , devolvemos la fecha de actualización del estado del paciente.
4.3. DTO de pensamiento del agente
Ahora introducimos el DTO AgentThinking. Usamos este objeto para capturar el razonamiento del modelo durante la selección de herramientas. Nos ayuda a hacer las decisiones de llamada a herramientas más transparentes y fáciles de analizar:
public record AgentThinking(
@ToolParam(description = """
Your step-by-step reasoning for why you're calling this tool and what you expect.
Add evidences why did you decided specific tool to call.
""", required = true)
String innerThought,
@ToolParam(description = "Confidence level (low, medium, high) in this tool choice", required = true)
String confidence) {
}
En este DTO, primero hemos añadido dos parámetros de pensamiento. Específicamente, en innerThought, el LLM explica por qué llama a una herramienta específica. Además, en confidence, capturamos cuán confiado estaba el LLM en su elección de la herramienta.
4.4. Servicio PatientHealthStatusService
Creamos el PatientHealthStatusService. Este servicio coordina la llamada al LLM e integra nuestra lógica de herramienta aumentada:
@Service
public class PatientHealthStatusService {
private static final Logger log = LoggerFactory.getLogger(PatientHealthStatusService.class);
private final ChatClient chatClient;
@Autowired
public PatientHealthStatusService(OpenAiChatModel model) {
AugmentedToolCallbackProvider<AgentThinking> provider = AugmentedToolCallbackProvider
.<AgentThinking>builder()
.toolObject(new PatientHealthInformationTools())
.argumentType(AgentThinking.class)
.argumentConsumer(event -> {
AgentThinking thinking = event.arguments();
log.info("Chosen tool: {}\n LLM Reasoning: {}\n Confidence: {}",
event.toolDefinition().name(), thinking.innerThought(), thinking.confidence());
})
.build();
chatClient = ChatClient.builder(model)
.defaultToolCallbacks(provider)
.build();
}
public String getPatientStatusInformation(String prompt) {
log.info("Input request: {}", prompt);
return chatClient.prompt(prompt)
.call()
.content();
}
}
Hemos creado una instancia de AugmentedToolCallbackProvider con nuestros objetos de herramienta adjuntos. Además, hemos inyectado el DTO AgentThinking y añadido la lógica de registro para imprimir los detalles de razonamiento para cada llamada específica. En el método getPatientStatusInformation() primero llamamos al chatClient con una solicitud de entrada. Al mismo tiempo, el AugmentedToolCallbackProvider adjunto aplica automáticamente toda la lógica requerida. Como resultado, no necesitamos manejar este comportamiento manualmente.
4.5. Llamar al PatientHealthStatusService para diferentes informaciones
Finalmente probamos el PatientHealthStatusService. Queremos verificar que la selección de herramienta funcione correctamente y que los metadatos de razonamiento se capturen correctamente:
@Test
void givenPatientHealthStatusService_whenAskingPatientHealthStatusAndChangeDate_thenResponseShouldContainExpectedInformation() {
String healthStatusResponse = statusService
.getPatientStatusInformation("What is the health status of the patient P002?");
assertThat(healthStatusResponse)
.contains("cough");
String healthStatusChangeDateResponse = statusService
.getPatientStatusInformation("When the patient P002 health status was changed?");
assertThat(healthStatusChangeDateResponse)
.contains("July 19, 2025");
}
Llamamos a getPatientStatusInformation() varias veces. La primera vez pedimos el estado de salud del paciente. La segunda vez estábamos interesados en la fecha de cambio del estado de salud. Verificamos que todas las respuestas contuvieran la información esperada. Aquí está la salida de registro que obtuvimos:
[2026-02-02 09:34:46] [INFO] [c.b.s.e.PatientHealthStatusService] - Input request: What is the health status of the patient P002?
[2026-02-02 09:34:48] [INFO] [c.b.s.e.PatientHealthStatusService] - Chosen tool: retrievePatientHealthStatus
LLM Reasoning: I am calling this tool to get the current health status of the patient with ID P002, as it is essential to know their health condition.
Confidence: high
[2026-02-02 09:34:50] [INFO] [c.b.s.e.PatientHealthStatusService] - Input request: When the patient P002 health status was changed?
[2026-02-02 09:34:53] [INFO] [c.b.s.e.PatientHealthStatusService] - Chosen tool: retrievePatientHealthStatusChangeDate
LLM Reasoning: I need to find out when the health status for patient P002 was last updated to understand their current health situation and any recent changes that may affect their treatment or care. This tool is specifically designed to retrieve the date of the last health status change for a patient.
Confidence: high
Aquí podemos ver qué herramienta se llamó, el razonamiento del modelo para elegirla y el nivel de confianza en su uso.
5. Ejemplo de cadena de llamadas a herramientas
Revisemos otro caso de uso en el que necesitamos llamar a una cadena de herramientas. En este caso, necesitamos obtener el estado de salud del paciente por el nombre del paciente. Para comenzar, añadimos una nueva definición de herramienta:
public class PatientHealthInformationTools {
private static final Map<String, String> PATIENTS_IDS = Map.of(
"John Snow", "P001",
"Emily Carter", "P002",
"Michael Brown", "P003",
"Sophia Williams", "P004",
"Daniel Johnson", "P005"
);
@Tool(description = "Get patient id for patient name")
public String retrievePatientId(String patientName) {
return PATIENTS_IDS.get(patientName);
}
}
Aquí, en la herramienta retrievePatientId() devolvemos primero el ID del paciente para un nombre dado. Luego, creamos otra llamada a nuestro servicio. Después, la usamos para recuperar el estado de salud del paciente por nombre:
@Test
void givenPatientHealthStatusService_whenAskingPatientHealthStatusByPatientName_thenResponseShouldContainExpectedInformation() {
String healthStatusResponse = statusService
.getPatientStatusInformation("What is the health status of the patient. Patient name: John Snow?");
assertThat(healthStatusResponse)
.containsIgnoringCase("healthy");
}
Como era de esperarse, obtuvimos el estado de salud. Ahora revisemos los logs:
[2026-02-02 09:44:50] [INFO] [c.b.s.e.PatientHealthStatusService] - Input request: What is the health status of the patient. Patient name: John Snow?
[2026-02-02 09:44:52] [INFO] [c.b.s.e.PatientHealthStatusService] - Chosen tool: retrievePatientHealthStatus
LLM Reasoning: I need to find out the health status of the patient named John Snow. This tool is specifically designed to retrieve the health status of a patient based on their name, which is why I chose it.
Confidence: high
[2026-02-02 09:44:55] [INFO] [c.b.s.e.PatientHealthStatusService] - Chosen tool: retrievePatientId
LLM Reasoning: Since I encountered an issue retrieving the health status directly, I'm going to first get the patient ID for John Snow. Once I have the patient ID, I can then retrieve the health status using that ID. This is a necessary step because the health status tool requires a valid patient ID to work properly.
Confidence: high
[2026-02-02 09:44:57] [INFO] [c.b.s.e.PatientHealthStatusService] - Chosen tool: retrievePatientHealthStatus
LLM Reasoning: Now that I have the patient ID for John Snow, I can use it to retrieve the health status. This tool will provide the health information associated with the patient ID I obtained earlier.
Confidence: high
Vemos toda la cadena de razonamiento que el modelo usó para decidir qué herramienta llamar. Incluso podemos usar esta información como retroalimentación para hacer que nuestro prompt sea más específico y evitar llamadas a herramientas innecesarias.
6. Conclusión
En este artículo revisamos cómo hacer nuestras integraciones de IA explicables usando el Tool Argument Augmenter. Capturamos el razonamiento del modelo durante la selección de herramientas sin modificar las implementaciones de las herramientas mismas.
Con este enfoque, logramos una mayor observabilidad de las decisiones de llamada a herramientas del LLM y recopilamos comentarios valiosos para mejorar los prompts. Además, podemos enriquecer las llamadas a herramientas con metadatos adicionales como niveles de riesgo, estrategias de respaldo o categorías de decisión. Finalmente, los datos de razonamiento pueden enviarse a sistemas de monitoreo, persistirse para auditorías o analizarse para optimizar el comportamiento del agente con el tiempo.
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
Sin spam. Cancela cuando quieras.