CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/langchain4j-ai-agent

Build AI agents with LangChain4j - basic agent, memory, tools/MCP, agentic workflows, guardrails, and observability

90

2.90x
Quality

90%

Does it follow best practices?

Impact

90%

2.90x

Average score across 3 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/langchain4j-agent/

name:
langchain4j-agent
description:
Build AI agents with LangChain4j and Quarkus. Use when creating chat agents, adding memory, tool calling, MCP integration, multi-agent workflows, guardrails, or observability with LangChain4j.

Build AI Agents with LangChain4j

Step-by-step guide for building AI agents using LangChain4j 1.13+ and Quarkus. Covers basic agents, memory, tools, MCP, agentic workflows, guardrails, and observability.

1. Project Setup

Maven BOM

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-bom</artifactId>
      <version>1.13.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Core Dependencies (use with BOM, omit version)

ArtifactPurpose
dev.langchain4j:langchain4jCore framework
dev.langchain4j:langchain4j-open-aiOpenAI models
dev.langchain4j:langchain4j-anthropicAnthropic Claude models
dev.langchain4j:langchain4j-mcp-clientMCP client support
dev.langchain4j:langchain4j-agenticMulti-agent orchestration

Quarkus Extensions (groupId: io.quarkiverse.langchain4j)

ArtifactPurpose
quarkus-langchain4j-openaiOpenAI + Quarkus integration
quarkus-langchain4j-anthropicAnthropic + Quarkus integration
quarkus-langchain4j-mcpMCP + Quarkus integration

Minimum JDK: 17

Checkpoint: Run mvn compile to confirm dependencies resolve before proceeding.


2. Basic Agent with AiServices

Quarkus (CDI-managed, primary)

import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService
public interface Agent {
    @SystemMessage("You are a personal AI assistant")
    String chat(String message);
}

// Inject and use:
@Inject Agent agent;
String response = agent.chat("Hello!");

application.properties:

quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY}
quarkus.langchain4j.anthropic.chat-model.model-name=claude-sonnet-4-20250514

Plain Java (without Quarkus)

import dev.langchain4j.service.AiServices;

public interface Agent {
    @SystemMessage("You are a personal AI assistant")
    String chat(String message);
}

Agent agent = AiServices.builder(Agent.class)
    .chatModel(model)
    .build();

Return Types

  • String -- raw LLM text
  • POJOs, enums, boolean, int -- structured extraction
  • Result<T> -- wraps any type with TokenUsage, sources, tool executions
  • TokenStream -- streaming token-by-token

Checkpoint: Verify agent.chat("Hello!") returns a non-null response before adding memory or tools.


3. Chat Memory

MessageWindowChatMemory (sliding window)

import dev.langchain4j.memory.chat.MessageWindowChatMemory;

// Quarkus:
@RegisterAiService(
    chatMemoryProviderSupplier = MyMemoryProvider.class
)
public interface Agent {
    String chat(@MemoryId String userId, String message);
}

// Plain Java — per-user memory:
Agent agent = AiServices.builder(Agent.class)
    .chatModel(model)
    .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
        .id(memoryId)
        .maxMessages(20)
        .chatMemoryStore(store)  // optional for persistence
        .build())
    .build();

ChatMemoryStore Interface (Persistence) { .api }

import dev.langchain4j.store.memory.chat.ChatMemoryStore;

public interface ChatMemoryStore {
    List<ChatMessage> getMessages(Object memoryId);
    void updateMessages(Object memoryId, List<ChatMessage> messages);
    void deleteMessages(Object memoryId);
}

4. Tool Calling

Tool Annotation { .api }

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;

@ApplicationScoped  // CDI bean for Quarkus
public class CalendarTools {

    @Tool("Get meetings for a specific date")
    public String getMeetings(
        @P("Date in YYYY-MM-DD format") String date
    ) {
        return calendarService.getMeetings(date);
    }

    @Tool("Create a new calendar event")
    public String createEvent(
        @P("Event title") String title,
        @P("Start time in ISO format") String startTime,
        @P("Duration in minutes") int durationMinutes
    ) {
        return calendarService.create(title, startTime, durationMinutes);
    }
}

Register Tools

// Quarkus:
@RegisterAiService(tools = CalendarTools.class)
public interface Agent { String chat(String message); }

// Plain Java:
Agent agent = AiServices.builder(Agent.class)
    .chatModel(model)
    .tools(new CalendarTools())
    .build();

Dynamic Tools with ToolProvider { .api }

import dev.langchain4j.service.tool.ToolProvider;

ToolProvider provider = (ToolProviderRequest request) ->
    ToolProviderResult.builder()
        .add(toolSpec, toolExecutor)
        .build();

Agent agent = AiServices.builder(Agent.class)
    .chatModel(model)
    .toolProvider(provider)
    .build();

Checkpoint: Confirm tool methods are invoked by asking the agent a question that requires them (e.g., "What meetings do I have today?") and checking logs for tool execution before adding MCP.


5. MCP Integration

MCP Client Setup

import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpToolProvider;
import dev.langchain4j.mcp.client.transport.StdioMcpTransport;

// 1. Create transport (see options below)
McpTransport transport = StdioMcpTransport.builder()
    .command(List.of("developer-events-mcp"))
    .build();

// 2. Create MCP client
McpClient mcpClient = DefaultMcpClient.builder()
    .transport(transport)
    .build();

// 3. Create tool provider — optionally filter tool names
McpToolProvider toolProvider = McpToolProvider.builder()
    .mcpClients(mcpClient)
    .filterToolNames("search_cfps_by_keyword", "list_open_cfps")  // optional
    .build();

// 4. Use with AiServices
Agent agent = AiServices.builder(Agent.class)
    .chatModel(model)
    .toolProvider(toolProvider)
    .build();

Transport Options

// STDIO (local subprocess)
StdioMcpTransport.builder()
    .command(List.of("npx", "@modelcontextprotocol/server-everything"))
    .build();

// Streamable HTTP (remote, recommended)
StreamableHttpMcpTransport.builder()
    .url("http://localhost:3001/mcp")
    .build();

// Docker
DockerMcpTransport.builder()
    .image("mcp/time")
    .build();

Quarkus MCP

application.properties:

quarkus.langchain4j.mcp.events.transport-type=stdio
quarkus.langchain4j.mcp.events.command=developer-events-mcp
import io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;

@RegisterAiService
public interface Agent {
    @McpToolBox("events")  // use tools from "events" MCP server
    String chat(String message);
}

6. Agentic Workflows

Dependency: dev.langchain4j:langchain4j-agentic

Single Agent

import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.Agent;

public interface EmailAgent {
    @Agent(description = "Handles email-related requests", outputKey = "emailResult")
    String handleEmail(@V("request") String request);
}

UntypedAgent emailAgent = AgenticServices.agentBuilder(EmailAgent.class)
    .chatModel(model)
    .tools(new EmailTools())
    .outputKey("emailResult")
    .build();

Workflow Types

// Sequential
UntypedAgent pipeline = AgenticServices.sequenceBuilder()
    .subAgents(classifierAgent, handlerAgent, responseAgent)
    .outputKey("finalResult")
    .build();

// Parallel
UntypedAgent parallel = AgenticServices.parallelBuilder(PlannerAgent.class)
    .subAgents(emailAgent, calendarAgent, weatherAgent)
    .executor(Executors.newFixedThreadPool(3))
    .output(scope -> scope.readState("emailResult", "") + "\n" + scope.readState("calResult", ""))
    .outputKey("combinedResult")
    .build();

// Conditional
UntypedAgent conditional = AgenticServices.conditionalBuilder()
    .subAgents(scope -> "email".equals(scope.readState("category")), emailAgent)
    .subAgents(scope -> "calendar".equals(scope.readState("category")), calendarAgent)
    .build();

// Loop
UntypedAgent loop = AgenticServices.loopBuilder()
    .subAgents(drafterAgent, reviewerAgent)
    .maxIterations(3)
    .exitCondition(scope -> scope.readState("approved", false))
    .build();

Supervisor (LLM-Based Orchestration)

import dev.langchain4j.agentic.SupervisorAgent;
import dev.langchain4j.agentic.SupervisorResponseStrategy;

SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
    .chatModel(plannerModel)
    .subAgents(emailAgent, calendarAgent, webSearchAgent)
    .supervisorContext("Route user requests to the appropriate specialist agent")
    .responseStrategy(SupervisorResponseStrategy.SUMMARY)
    .build();

String result = supervisor.invoke("Schedule a meeting with John next Tuesday");

Response strategies: LAST (default), SUMMARY, SCORED

AgenticScope (Shared State) { .api }

scope.writeState("category", "email");
String cat = scope.readState("category", "unknown");

// Type-safe keys
public static class EmailResult implements TypedKey<String> {}
scope.writeState(new EmailResult(), "Done");

Declarative Annotations

@SequentialAgent
public interface Pipeline {
    @AgentMember ClassifierAgent classifier();
    @AgentMember EmailAgent emailHandler();
    @AgentMember CalendarAgent calendarHandler();
}

Checkpoint: Invoke each sub-agent independently with .invoke() and confirm correct output before composing into a pipeline or supervisor.


7. Guardrails

Status: EXPERIMENTAL

Input Guardrail { .api }

import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailResult;

public class PromptInjectionGuard implements InputGuardrail {
    @Override
    public InputGuardrailResult validate(UserMessage userMessage) {
        if (userMessage.singleText().toLowerCase().contains("ignore all previous")) {
            return fatal("Prompt injection detected");
        }
        return success();
    }
}

Result methods: success(), successWith(alteredMessage), failure(reason), fatal(reason)

Output Guardrail { .api }

import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrailResult;

public class ToxicityGuard implements OutputGuardrail {
    @Override
    public OutputGuardrailResult validate(AiMessage response) {
        if (isToxic(response.text())) {
            return reprompt("Response was toxic", "Please rephrase politely");
        }
        return success();
    }
}

Result methods: success(), successWith(rewritten), failure(reason), fatal(reason), retry(reason), reprompt(reason, newPrompt)

Apply Guardrails

import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.guardrail.OutputGuardrails;

@InputGuardrails(PromptInjectionGuard.class)
@OutputGuardrails(value = ToxicityGuard.class, maxRetries = 3)
public interface Agent {
    String chat(String message);
}

8. Observability

AgentListener { .api }

import dev.langchain4j.agentic.AgentListener;

public class LoggingListener implements AgentListener {
    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        log.info("Agent starting: {}", request.agentName());
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        log.info("Agent done: {} ({}ms, {} tokens)",
            response.agentName(),
            response.durationMs(),
            response.tokenUsage().totalTokenCount());
    }

    @Override
    public void beforeToolExecution(BeforeToolExecution exec) {
        log.info("Calling tool: {}", exec.toolName());
    }

    @Override
    public boolean inheritedBySubagents() { return true; }
}

AgentMonitor (HTML Reports)

import dev.langchain4j.agentic.AgentMonitor;

AgentMonitor monitor = AgentMonitor.create();

UntypedAgent agent = AgenticServices.agentBuilder(MyAgent.class)
    .chatModel(model)
    .listener(monitor)
    .build();

agent.invoke("Do something");

String html = monitor.generateReport();
Files.writeString(Path.of("agent-report.html"), html);

9. Complete Quarkus Agent Example

This end-to-end example wires together memory, tools, MCP, and guardrails.

pom.xml (key dependencies, use BOM):

<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-anthropic</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-mcp</artifactId>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-agentic</artifactId>
</dependency>

Agent + tools + guardrail:

@RegisterAiService(tools = LocalTools.class)
@InputGuardrails(PromptInjectionGuard.class)
public interface PersonalAgent {
    @SystemMessage("You are a personal AI assistant. Help with calendar, email, and tasks.")
    @McpToolBox("events")
    String chat(@MemoryId String userId, @UserMessage String message);
}

@ApplicationScoped
public class LocalTools {
    @Tool("Get the current date and time")
    public String getCurrentTime() { return LocalDateTime.now().toString(); }
}

@ApplicationScoped
public class PromptInjectionGuard implements InputGuardrail {
    @Override
    public InputGuardrailResult validate(UserMessage msg) {
        if (msg.singleText().toLowerCase().contains("ignore all previous"))
            return fatal("Blocked");
        return success();
    }
}

application.properties:

quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY}
quarkus.langchain4j.anthropic.chat-model.model-name=claude-sonnet-4-20250514
quarkus.langchain4j.mcp.events.transport-type=stdio
quarkus.langchain4j.mcp.events.command=developer-events-mcp

REST endpoint:

@Path("/chat")
@ApplicationScoped
public class ChatResource {
    @Inject PersonalAgent agent;

    @POST
    public String chat(@QueryParam("userId") String userId, String message) {
        return agent.chat(userId, message);
    }
}

skills

langchain4j-agent

tile.json