LangChain4j para equipos de Java: Herramientas, agentes, habilidades y comunicación A2A en servicios Spring
Cómo integrar la llamada a herramientas, las tuberías de agentes, las habilidades bajo demanda, los flujos de trabajo de pausa y reanudación y los agentes A2A remotos en un servicio Java existente con LangChain4j.
Fuentes: Java AI in 2026, LangChain4j Turns Agents into Ordinary Interfaces
Imagínese una empresa que funciona con Java. Los pedidos, el inventario, el control de riesgos y el servicio al cliente han estado gestionados con Spring Boot durante años, y ahora se necesita un agente de soporte capaz de verificar un pedido, emitir un reembolso y transferir el caso a otra persona. Las tutoriales que se encuentran en línea comienzan con FastAPI, pasan a LangChain y luego a la configuración de un entorno de Python, mientras que el servicio en Java permanece inactivo en segundo plano.
LangChain4j integra estas funcionalidades en un proyecto en Java. No se trata de una adaptación del framework de Python, ya que fue reconstruido teniendo en cuenta cómo trabajan los desarrolladores en Java. En resumen: crear agentes con él implica conectar tres capas a un servicio que ya existe, a saber, herramientas, habilidades y agentes, sin necesidad de cambiar primero de lenguaje.
Herramientas: permitir que el modelo llame a métodos empresariales
Muchos equipos se quedan atascados aquí, no en el modelo en sí, sino en cómo este determina qué método de Java debe llamar. La anotación @Tool se encarga de eso: basta con marcar un método con ella para que el framework lo convierta en un JSON Schema que el modelo pueda leer.
Comience por las dependencias. El módulo compatible con OpenAI es solo una opción. Pasar a Azure, Ollama o Qwen generalmente implica reemplazar la implementación del modelo de chat, mientras que el código de negocio permanece prácticamente igual.
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-skills</artifactId>
<version>1.19.0-beta29</version>
</dependency>
A continuación, una categoría de herramientas de orden. Nada exótico: métodos comunes de Java cuyos parámetros y valores de retorno están descritos claramente.
El código requiere Java 17 o una versión más reciente. En un proyecto Maven sencillo, active la bandera del compilador -parameters; de lo contrario, un nombre de parámetro como el marcador de posición de pregunta podría compilarse como arg0 y no tener significado alguno para el modelo. Spring y Quarkus se encargan de esto de forma predeterminada.
class OrderTools {
@Tool("Look up the status of an order by ID")
String queryOrder(@P("Order ID") String orderId) {
return "Order " + orderId + " has shipped and should arrive tomorrow";
}
@Tool("Refund the specified order")
String refund(@P("Order ID") String orderId, @P("Refund reason") String reason) {
return "Order " + orderId + " refund initiated. Reason: " + reason;
}
}
Luego, se conectan las herramientas a una interfaz. El cliente simplemente lo solicita, y el modelo decide si debe buscar un pedido o iniciar un reembolso.
interface CustomerServiceAssistant {
@UserMessage("You are an e-commerce customer service assistant. Answer questions about orders and refunds. The customer's question is {{question}}")
String answer(String question);
}
CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class)
.chatModel(model)
.tools(new OrderTools())
.chatMemory(MessageWindowChatMemory.withMaxMessages(20))
.build();
String reply = assistant.answer("Where is order 20260907001?");
Este es el ámbito en el que los desarrolladores de Java se sienten como en casa, ya que escribir un punto de extremo de consulta y exponer un método @Tool a un modelo resultan ser prácticamente lo mismo.
Agentes: una interfaz más un ámbito compartido
La orquestación se reduce a dos conceptos principales. La anotación @Agent se aplica a un método en lugar de a una clase, y AgenticScope transporta los resultados de un agente al siguiente.
Tomemos tres agentes pequeños. Nunca se llaman entre sí directamente. Cada uno escribe su resultado en una variable compartida y el siguiente lo recoge.
public interface DraftWriter {
@UserMessage("""
You are a technical editor. Write an opening of no more than 200 words on the topic {{topic}}.
Return only the opening, with no explanation.
""")
@Agent(outputKey = "draft", description = "Writes an opening based on a topic")
String write(@V("topic") String topic);
}
public interface Sharpener {
@UserMessage("""
You are a reviewer. Make the following opening more specific and remove filler.
The opening is {{draft}}.
Return only the revised text.
""")
@Agent(outputKey = "draft", description = "Makes the opening more specific")
String polish(@V("draft") String draft);
}
public interface TitlePicker {
@UserMessage("""
Write 3 headlines suitable for a technical blog for the following content, one per line.
The content is {{draft}}.
""")
@Agent(outputKey = "titles", description = "Writes headlines for the content")
String pickTitle(@V("draft") String draft);
}
Cada agente almacena su salida en la variable denominada outputKey. El agente siguiente lee esa variable a través de @V, y la cadena se convierte en una tubería de procesamiento.
DraftWriter writer = AgenticServices
.agentBuilder(DraftWriter.class)
.chatModel(model)
.build();
Sharpener sharpener = AgenticServices
.agentBuilder(Sharpener.class)
.chatModel(model)
.build();
TitlePicker titlePicker = AgenticServices
.agentBuilder(TitlePicker.class)
.chatModel(model)
.build();
UntypedAgent editor = AgenticServices
.sequenceBuilder()
.subAgents(writer, sharpener, titlePicker)
.outputKey("titles")
.build();
Object result = editor.invoke(Map.of("topic", "Building agents with LangChain4j"));
La ventaja es que la orquestación se convierte en componentes reutilizables. La redacción, el pulido y la asignación de títulos pueden probarse por separado y reordenarse. Los patrones de bucle, paralelo y condicional también vienen incluidos en el framework; por lo tanto, para seguir perfeccionando el resultado hasta que cumpla con ciertos estándares de calidad, utilice loopBuilder junto con un agente de puntuación y una condición de salida.
Habilidades: instrucciones cargadas bajo demanda
Las habilidades de LangChain4j se asemejan más a los paquetes de habilidades de Claude Code que a cualquier tipo de anotación. Una habilidad es un directorio que contiene un archivo SKILL.md. Por defecto, el modelo no ve el texto completo. Solo cuando determina que una tarea coincide, llama a activate_skill para cargar el contenido, lo que mantiene el contexto inicial reducido y hace que cada llamada sea más económica.
Comience creando un archivo de habilidad:
skills/refund/SKILL.md
---
name: refund
description: Handle refund requests. Look up the order, check the conditions, then either refund or escalate to a human.
---
When a customer requests a refund:
1. First call queryOrder to check the order status.
2. Only call refund when the order has shipped or is complete.
3. If the order is still awaiting payment, tell the customer to cancel it.
4. If the amount exceeds 500, escalate to a human instead of refunding directly.
Luego, asocie las habilidades con el servicio. El modo de herramienta es la ruta oficialmente recomendada, y el modelo solo puede acceder a las herramientas que haya registrado, sin acceso al sistema de archivos ni a la shell.
List<FileSystemSkill> loadedSkills = FileSystemSkillLoader.loadSkills(Path.of("skills"));
Skills skillSet = Skills.from(loadedSkills);
CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class)
.chatModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(20))
.tools(new OrderTools())
.toolProvider(skillSet.toolProvider())
.systemMessage("You have access to the following skills. When a request relates to one of them, activate it first.\n"
+ skillSet.formatAvailableSkills())
.build();
La alternativa es el modo shell, que proporciona al modelo una herramienta run_shell_command. Es útil para prototipos, pero en entornos de producción se debe usar con precaución: las órdenes se ejecutan directamente en el host sin ningún entorno aislado. A menos que los datos de entrada sean completamente confiables, es mejor no utilizarlo.
Pausar y reanudar flujos de trabajo largos
En un entorno empresarial, el caso complicado no tiene una respuesta sencilla, sino un flujo de trabajo que se interrumpe en medio de docenas de pasos. La persistencia y la aprobación humana en LangChain4j están diseñadas precisamente para esto. Para que un flujo de trabajo sea recuperable, su interfaz debe extender AgenticScopeAccess y se debe colocar @MemoryId en el método, lo que activa un ámbito persistente.
public interface OrderWorkflow extends AgenticScopeAccess {
@Agent
String processOrder(@MemoryId String orderId, @V("order") String orderDetails);
}
Luego se realizan tres pasos en secuencia: validación, aprobación humana y envío. El paso de aprobación devuelve un SuspendedResponse, por lo que el sistema guarda el estado actual y libera el hilo en lugar de retenerlo.
AgenticScopeAction validateOrder = AgenticServices.agentAction(scope -> {
String order = scope.readState("order", "");
scope.writeState("validated_order", "VALIDATED: " + order);
});
HumanInTheLoop approvalGate = AgenticServices.humanInTheLoopBuilder()
.description("Large orders require manual approval")
.outputKey("approval")
.responseProvider(scope -> new SuspendedResponse<>("manager-approval"))
.build();
AgenticScopeAction shipOrder = AgenticServices.agentAction(scope -> {
String validated = scope.readState("validated_order", "");
String approval = scope.readState("approval", "");
scope.writeState("result", "Order processed " + approval);
});
OrderWorkflow workflow = AgenticServices.sequenceBuilder(OrderWorkflow.class)
.subAgents(validateOrder, approvalGate, shipOrder)
.outputKey("result")
.build();
El llamante gestiona la suspensión mediante ResultWithAgenticScope. Una vez que un administrador lo aprueba, el flujo de trabajo continúa desde su punto de control.
ResultWithAgenticScope<String> result =
workflow.processOrder("order-10001", "1000 units");
if (result.suspended()) {
result = result.completePendingResponse("Manager approved");
}
El estado en memoria por sí solo no sobrevivirá a un reinicio, por lo que se debe implementar un AgenticScopeStore respaldado por Redis, una base de datos o el sistema de archivos. El framework realiza puntos de control después de que cada agente termine y registra la posición del planificador, de modo que los pasos ya completados no se repitan al recuperarse.
AgenticScopePersister.setStore(new MyAgenticScopeStore());
Parece algo sin importancia, pero esto es lo que diferencia a un agente de demostración de uno en producción. Las interrupciones, los reinicios y las demoras en la aprobación son eventos comunes, no casos excepcionales.
A2A: agentes remotos como elementos locales
Dentro de los diferentes equipos, el agente de riesgo puede ejecutarse en un servicio mientras que el servicio de atención al cliente pertenece a otro grupo. A2A actúa como el vínculo entre estos agentes remotos: al incluir el módulo langchain4j-agentic-a2a, el código de orquestación trata a un agente distante de la misma manera que trataría a uno dentro del mismo proceso.
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic-a2a</artifactId>
<version>1.19.0</version>
</dependency>
Declare una interfaz local que apunte al servicio A2A remoto.
public interface RemoteRiskAgent {
@Agent
String check(@V("transaction") String transaction);
}
UntypedAgent riskAgent = AgenticServices
.a2aBuilder("https://risk.internal/a2a", RemoteRiskAgent.class)
.outputKey("risk_result")
.build();
En chats de varias conversaciones, escriba contextId y taskId nuevamente en el ámbito compartido: el servidor los genera en la primera conversación y el cliente los devuelve en la segunda.
public interface ChatAgent {
@A2AClientAgent(a2aServerUrl = "http://localhost:8080", outputKey = "response")
ResultWithAgenticScope<String> chat(
@V("question") String question,
@A2AContextId @V("contextId") String contextId,
@A2ATaskId @V("taskId") String taskId);
}
ResultWithAgenticScope<String> first = chatAgent.chat("Hello", null, null);
String contextId = (String) first.agenticScope().readState("contextId");
String taskId = (String) first.agenticScope().readState("taskId");
ResultWithAgenticScope<String> second = chatAgent.chat("Continue", contextId, taskId);
La cooperación entre múltiples agentes ya no requiere que todo esté en un único repositorio. Los equipos pueden ser dueños y operar el servicio A2A para sus agentes respectivos, mientras que una capa superior une todas las partes.
Consejos para los equipos de Java
Comience de forma limitada. Omita el agente de operaciones completamente autónomo y cree un agente de soporte que verifique los pedidos y emita reembolsos; luego añada capas de habilidades y aprobación humana encima.
Mantenga las herramientas y las habilidades separadas. Las herramientas representan lo que el código puede hacer. Las habilidades son el procedimiento paso a paso que se presenta al modelo. Mantenga las herramientas pequeñas y estables, y permita que las habilidades cambien con frecuencia al igual que las reglas comerciales, sin que una interfiera con la otra.
Agregue capacidad de persistencia desde el principio para flujos de trabajo críticos. Si un flujo incluye esperas de aprobación, llamadas remotas o pasos que duran más de unas pocas decenas de segundos, configure un AgenticScopeStore de antemano, ya que implementar mecanismos de recuperación posterior suele costar más.
La ventaja que ofrece LangChain4j es que los agentes permiten volver a trabajar con la ingeniería habitual en Java. No hay ninguna característica destacada en particular que lo haga único. Las interfaces, la inyección de dependencias, las pruebas, el empaquetado y la supervisión se mantienen intactos, y la IA se convierte en una capacidad más en lugar de una razón para reconstruir todo desde cero.