Add langchain4j-interop: Spring AI 2.0 and LangChain4j 1.22 side by side
Same order assistant in both frameworks against a local fake OpenAI server: starter bean collision, wire differences, tool failure and loop bounds, observability meters, MCP-first tools, and a compile check of the 2025 article's snippets and Maven coordinates. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01G8ikz8xdWuTP5yun8DZ1hk
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Both starters in one Spring Boot 4.1.1 application
|
||||
|
||||
Spring AI: spring-ai-starter-model-openai 2.0.1. LangChain4j: langchain4j-open-ai-spring-boot4-starter 1.22.0-beta32.
|
||||
|
||||
1. Both starters, nothing else:
|
||||
FAILED: BeanDefinitionOverrideException: bean 'openAiChatModel' is defined twice, by org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration and by dev.langchain4j.openai.spring.OpenAiAutoConfiguration
|
||||
|
||||
2. spring.autoconfigure.exclude=dev.langchain4j.openai.spring.OpenAiAutoConfiguration:
|
||||
context started
|
||||
bean chatClientBuilder -> org.springframework.ai.chat.client.ChatClient$Builder
|
||||
bean openAiChatModel -> org.springframework.ai.openai.OpenAiChatModel
|
||||
beans of type dev.langchain4j.model.chat.ChatModel: 0
|
||||
beans of type org.springframework.ai.chat.model.ChatModel: 1
|
||||
|
||||
3. spring.main.allow-bean-definition-overriding=true:
|
||||
context started
|
||||
bean openAiChatModel -> org.springframework.ai.openai.OpenAiChatModel
|
||||
bean chatClientBuilder -> org.springframework.ai.chat.client.ChatClient$Builder
|
||||
beans of type dev.langchain4j.model.chat.ChatModel: 0
|
||||
beans of type org.springframework.ai.chat.model.ChatModel: 1
|
||||
@@ -0,0 +1,41 @@
|
||||
# The same order assistant in Spring AI and in LangChain4j, one question: "Where is ORD-1001?"
|
||||
|
||||
Spring AI : ChatClient.builder(model).defaultSystem(..).defaultTools(new SpringAiTools(orders))
|
||||
LangChain4j: AiServices.builder(Support.class).chatModel(model).tools(new Lc4jTools(orders))
|
||||
|
||||
Spring AI answer : Done: "Order ORD-1001: Status=SHIPPED, ETA=2026-10-14"
|
||||
LangChain4j answer : Done: Order ORD-1001: Status=SHIPPED, ETA=2026-10-14
|
||||
HTTP requests sent : Spring AI 2, LangChain4j 2. Tool body executed: Spring AI 1, LangChain4j 1.
|
||||
|
||||
What differs on the wire (computed from the four bodies below):
|
||||
top-level fields : Spring AI [messages, model, tools], LangChain4j [messages, model, stream, tools]
|
||||
tool schema : Spring AI has $schema=true additionalProperties=true; LangChain4j has $schema=false additionalProperties=false
|
||||
tool result message: Spring AI sent "\"Order ORD-1001: Status=SHIPPED, ETA=2026-10-14\"", LangChain4j sent "Order ORD-1001: Status=SHIPPED, ETA=2026-10-14"
|
||||
|
||||
--- Spring AI, request 1
|
||||
fields: [messages, model, tools]
|
||||
system: "You are a customer support agent. Use the tools for order questions. Be concise."
|
||||
user: "Where is ORD-1001?"
|
||||
tool getOrderStatus - Get current order status by order ID params {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"orderId":{"type":"string","description":"The unique identifier of the order"}},"required":["orderId"],"additionalProperties":false}
|
||||
|
||||
--- Spring AI, request 2
|
||||
fields: [messages, model, tools]
|
||||
system: "You are a customer support agent. Use the tools for order questions. Be concise."
|
||||
user: "Where is ORD-1001?"
|
||||
assistant: tool_call getOrderStatus {"orderId":"ORD-1001"}
|
||||
tool: ""Order ORD-1001: Status=SHIPPED, ETA=2026-10-14""
|
||||
tool getOrderStatus - Get current order status by order ID params {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"orderId":{"type":"string","description":"The unique identifier of the order"}},"required":["orderId"],"additionalProperties":false}
|
||||
|
||||
--- LangChain4j, request 1
|
||||
fields: [messages, model, stream, tools]
|
||||
system: "You are a customer support agent. Use the tools for order questions. Be concise."
|
||||
user: "Where is ORD-1001?"
|
||||
tool getOrderStatus - Get current order status by order ID params {"type":"object","properties":{"orderId":{"type":"string","description":"The unique identifier of the order"}},"required":["orderId"]}
|
||||
|
||||
--- LangChain4j, request 2
|
||||
fields: [messages, model, stream, tools]
|
||||
system: "You are a customer support agent. Use the tools for order questions. Be concise."
|
||||
user: "Where is ORD-1001?"
|
||||
assistant: tool_call getOrderStatus {"orderId":"ORD-1001"}
|
||||
tool: "Order ORD-1001: Status=SHIPPED, ETA=2026-10-14"
|
||||
tool getOrderStatus - Get current order status by order ID params {"type":"object","properties":{"orderId":{"type":"string","description":"The unique identifier of the order"}},"required":["orderId"]}
|
||||
@@ -0,0 +1,19 @@
|
||||
# A tool that throws (ORD-500 raises IllegalStateException: order database unavailable)
|
||||
|
||||
Case 1: a well-behaved model. It asks for the tool once, then answers from whatever the tool message says.
|
||||
Spring AI : 2 requests, tool body ran 1 time(s). Tool message the model received: "order database unavailable"
|
||||
answer: Done: order database unavailable
|
||||
LangChain4j : 2 requests, tool body ran 1 time(s). Tool message the model received: "order database unavailable"
|
||||
answer: Done: order database unavailable
|
||||
|
||||
Case 2: an insistent model. It asks for the tool again after every result. The server stops it with an HTTP 500 after 30 requests.
|
||||
Spring AI : 31 requests, tool body ran 30 time(s)
|
||||
exception InternalServerException: 500: cutoff reached
|
||||
LangChain4j : 31 requests, tool body ran 30 time(s)
|
||||
exception InternalServerException (root: HttpException): {"error":{"message":"cutoff reached"}}
|
||||
|
||||
Case 3: the same insistent model, each framework asked to stop after 5 tool rounds.
|
||||
Spring AI (ToolCallingAdvisor + our MaxToolRounds(5) checker): 6 requests, tool body ran 5 time(s)
|
||||
answer:
|
||||
LangChain4j (maxSequentialToolsInvocations(5)) : 6 requests, tool body ran 5 time(s)
|
||||
exception RuntimeException: Something is wrong, exceeded 5 tool calling round trips (maxToolCallingRoundTrips)
|
||||
@@ -0,0 +1,27 @@
|
||||
# What each framework records for one question that uses one tool (2 model requests)
|
||||
|
||||
Spring AI 2.0.1: model and client given an ObservationRegistry plus DefaultMeterObservationHandler and ChatModelMeterObservationHandler (the two Spring Boot registers for you).
|
||||
gen_ai.client.operation [TIMER] error=none, gen_ai.operation.name=chat, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=gpt-6.1-sol, gen_ai.system=openai
|
||||
gen_ai.client.operation.active [LONG_TASK_TIMER] gen_ai.operation.name=chat, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=none, gen_ai.system=openai
|
||||
gen_ai.client.token.usage [COUNTER] gen_ai.operation.name=chat, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=gpt-6.1-sol, gen_ai.system=openai, gen_ai.token.type=input
|
||||
gen_ai.client.token.usage [COUNTER] gen_ai.operation.name=chat, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=gpt-6.1-sol, gen_ai.system=openai, gen_ai.token.type=output
|
||||
gen_ai.client.token.usage [COUNTER] gen_ai.operation.name=chat, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=gpt-6.1-sol, gen_ai.system=openai, gen_ai.token.type=total
|
||||
okhttp.requests [TIMER] error=none, outcome=SUCCESS
|
||||
okhttp.requests.active [LONG_TASK_TIMER] outcome=UNKNOWN
|
||||
spring.ai.advisor [TIMER] error=none, gen_ai.operation.name=framework, gen_ai.system=spring_ai, spring.ai.kind=advisor
|
||||
spring.ai.advisor.active [LONG_TASK_TIMER] gen_ai.operation.name=framework, gen_ai.system=spring_ai, spring.ai.kind=advisor
|
||||
spring.ai.chat.client [TIMER] error=none, gen_ai.operation.name=framework, gen_ai.system=spring_ai, spring.ai.kind=chat_client
|
||||
spring.ai.chat.client.active [LONG_TASK_TIMER] gen_ai.operation.name=framework, gen_ai.system=spring_ai, spring.ai.kind=chat_client
|
||||
spring.ai.tool [TIMER] error=none, gen_ai.operation.name=execute_tool, gen_ai.system=spring_ai, spring.ai.kind=tool_call
|
||||
spring.ai.tool.active [LONG_TASK_TIMER] gen_ai.operation.name=execute_tool, gen_ai.system=spring_ai, spring.ai.kind=tool_call
|
||||
|
||||
LangChain4j 1.22.0, no listener registered: 0 meters.
|
||||
|
||||
LangChain4j 1.22.0 with TracingChatInterceptor, the listener from the original article (it still compiles and runs):
|
||||
llm.request.latency [TIMER] model=gpt-6.1-sol
|
||||
llm.tokens.input [COUNTER] model=gpt-6.1-sol
|
||||
llm.tokens.output [COUNTER] model=gpt-6.1-sol
|
||||
|
||||
LangChain4j 1.22.0 with the shipped MicrometerMetricsChatModelListener (langchain4j-micrometer-metrics 1.22.0-beta32):
|
||||
gen_ai.client.token.usage [DISTRIBUTION_SUMMARY] gen_ai.operation.name=chat, gen_ai.provider.name=openai, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=gpt-6.1-sol, gen_ai.token.type=input
|
||||
gen_ai.client.token.usage [DISTRIBUTION_SUMMARY] gen_ai.operation.name=chat, gen_ai.provider.name=openai, gen_ai.request.model=gpt-6.1-sol, gen_ai.response.model=gpt-6.1-sol, gen_ai.token.type=output
|
||||
@@ -0,0 +1,12 @@
|
||||
# The order tool served once over MCP (Streamable HTTP), used from Spring AI and from LangChain4j
|
||||
|
||||
MCP server: Spring AI 2.0.1 spring-ai-starter-mcp-server-webmvc, one @McpTool method, on port 33237.
|
||||
Spring AI client : McpClient.sync(HttpClientStreamableHttpTransport) -> SyncMcpToolCallbackProvider -> ChatClient.defaultToolCallbacks
|
||||
LangChain4j client: DefaultMcpClient + StreamableHttpMcpTransport -> McpToolProvider -> AiServices.toolProvider
|
||||
|
||||
Spring AI answer : Done: [{"text":"Order ORD-1002: Status=PROCESSING, ETA=2026-10-18"}] (2 requests to the model)
|
||||
tool offered to the model: tool getOrderStatus - Get current order status by order ID params {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"orderId":{"type":"string","description":"The unique identifier of the order"}},"required":["orderId"]}
|
||||
LangChain4j answer : Done: Order ORD-1002: Status=PROCESSING, ETA=2026-10-18 (2 requests to the model)
|
||||
tool offered to the model: tool getOrderStatus - Get current order status by order ID params {"type":"object","properties":{"orderId":{"type":"string","description":"The unique identifier of the order"}},"required":["orderId"]}
|
||||
|
||||
Times the one tool method on the server ran: 1 for Spring AI, then 1 more for LangChain4j.
|
||||
@@ -0,0 +1,56 @@
|
||||
# The original article's code, compiled against Spring AI 2.0.1 and LangChain4j 1.22.0 (one file per API line)
|
||||
|
||||
OldCallAroundAdvisor: DOES NOT COMPILE
|
||||
OldCallAroundAdvisor.java:1: error: cannot find symbol
|
||||
symbol: class AdvisedRequest
|
||||
location: package org.springframework.ai.chat.client.advisor.api
|
||||
OldCallAroundAdvisor.java:2: error: cannot find symbol
|
||||
symbol: class AdvisedResponse
|
||||
location: package org.springframework.ai.chat.client.advisor.api
|
||||
OldChatService: compiles
|
||||
OldDocumentGetContent: DOES NOT COMPILE
|
||||
OldDocumentGetContent.java:6: error: cannot find symbol
|
||||
symbol: method getContent()
|
||||
location: variable d of type Document
|
||||
OldLangChain4jBuilder: DOES NOT COMPILE
|
||||
OldLangChain4jBuilder.java:9: error: cannot find symbol
|
||||
symbol: method chatLanguageModel(OpenAiChatModel)
|
||||
location: class AiServices<Agent>
|
||||
OldMessageGetContent: DOES NOT COMPILE
|
||||
OldMessageGetContent.java:6: error: cannot find symbol
|
||||
symbol: method getContent()
|
||||
location: variable m of type Message
|
||||
OldMeterRegistryCustomizer: DOES NOT COMPILE
|
||||
OldMeterRegistryCustomizer.java:1: error: package org.springframework.boot.actuate.autoconfigure.metrics does not exist
|
||||
OldMeterRegistryCustomizer.java:5: error: cannot find symbol
|
||||
symbol: class MeterRegistryCustomizer
|
||||
location: class OldMeterRegistryCustomizer
|
||||
OldPdfConfig: compiles
|
||||
OldSearchRequest: DOES NOT COMPILE
|
||||
OldSearchRequest.java:6: error: cannot find symbol
|
||||
symbol: method query(String)
|
||||
location: class SearchRequest
|
||||
OldTokenTextSplitter: DOES NOT COMPILE
|
||||
OldTokenTextSplitter.java:5: error: no suitable constructor found for TokenTextSplitter(int,int,int,int,boolean)
|
||||
|
||||
# The original article's Maven coordinates, looked up on Maven Central
|
||||
|
||||
org/springframework/ai/spring-ai-openai-spring-boot-starter: latest release none, latest of any kind 1.0.0-M6
|
||||
org/springframework/ai/spring-ai-pgvector-store-spring-boot-starter: latest release none, latest of any kind 1.0.0-M6
|
||||
org/springframework/ai/spring-ai-starter-model-openai: latest release 2.0.1, latest of any kind 2.1.0-M1
|
||||
org/springframework/ai/spring-ai-starter-vector-store-pgvector: latest release 2.0.1, latest of any kind 2.1.0-M1
|
||||
dev/langchain4j/langchain4j-spring-boot-starter: latest release 0.36.2, latest of any kind 1.22.0-beta32
|
||||
dev/langchain4j/langchain4j-open-ai: latest release 1.22.0, latest of any kind 1.22.0
|
||||
dev/langchain4j/langchain4j-spring-boot4-starter: latest release none, latest of any kind 1.22.0-beta32
|
||||
|
||||
# Class names the original article used, searched in the jars this build resolves (mvn dependency:build-classpath)
|
||||
|
||||
TokenBudgetAdvisor: not found in any jar
|
||||
RequestResponseAdvisor: not found in any jar
|
||||
CallAroundAdvisor: not found in any jar
|
||||
CallAdvisor: found
|
||||
ToolCallingAdvisor: found
|
||||
AdvisedRequest: not found in any jar
|
||||
ChatClientRequest: found
|
||||
EmbeddingClient: not found in any jar
|
||||
EmbeddingModel: found
|
||||
Reference in New Issue
Block a user