Spring AI is a Spring project for building applications that use generative-AI models. It provides portable Java interfaces for model access and Spring Boot auto-configuration for model providers, vector stores, tool calling, conversation memory, retrieval augmented generation, observability, evaluation, and the Model Context Protocol.
Spring AI does not contain a language model. It connects an application to a remote or local model:
Spring application
-> Spring AI ChatClient / model API
-> provider SDK or HTTP API
-> OpenAI, Anthropic, Google, Amazon, Azure, Ollama, or another providerIts role is similar to Spring Data’s role for databases: application code uses common concepts while provider adapters handle transport and provider-specific details. The abstraction reduces coupling, but providers are not identical. Model names, token limits, structured-output behavior, tool support, safety controls, and request options still vary.
This article uses Spring AI 2.0 APIs. Artifact names and properties changed during earlier milestone and 1.x releases, so examples copied from old articles may not compile unchanged.
What Spring AI Provides #
The project includes:
ChatModelandChatClientfor synchronous and streaming conversations- Portable embedding, image, and other model APIs where providers support them
- Mapping model output into Java objects
- Tool calling from a model into application methods
- An Advisor chain for reusable request and response behavior
- Bounded conversation memory with pluggable repositories
- A portable
VectorStoreAPI and metadata-filter expressions - Document readers, transformers, and writers for RAG ingestion
- RAG Advisors for retrieval and prompt augmentation
- Micrometer-based observations for model and vector-store operations
- Evaluators for checking relevance and factual support
- MCP clients and servers
- Spring Boot starters and auto-configuration
The low-level model APIs remain available when an application needs direct
control. ChatClient is normally the more convenient application-facing API:
it provides a fluent interface similar to RestClient and manages patterns
such as Advisors, structured conversion, and automatic tool execution.
Start a Project #
The easiest path is Spring Initializr:
- Select a Spring Boot version compatible with the desired Spring AI release.
- Add Spring Web or Spring Reactive Web according to the application.
- Add one Spring AI model provider.
- Add Actuator for production observability.
- Add a vector-store integration only when the application needs retrieval.
Spring Initializr keeps the Spring Boot and Spring AI dependency set compatible. For an existing Maven project, import the Spring AI BOM and add a model starter:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>The Gradle equivalent is:
dependencies {
implementation platform("org.springframework.ai:spring-ai-bom:2.0.0")
implementation "org.springframework.ai:spring-ai-starter-model-openai"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-actuator"
}Do not specify versions on individual Spring AI modules when using the BOM.
Starter names identify the capability and provider. For example,
spring-ai-starter-model-openai configures OpenAI model clients. Other
providers have their own model starters, and vector databases have separate
vector-store starters. Use the reference page for that provider rather than
guessing an artifact name from an older release.
Configure a Provider #
For OpenAI, the minimum configuration is an API key:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}Set the secret outside source control:
export OPENAI_API_KEY='...'Select a model explicitly in each environment rather than depending forever on a starter’s default:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: ${CHAT_MODEL}
temperature: 0.2Model identifiers change independently of Spring AI. Injecting CHAT_MODEL
through deployment configuration makes such changes visible and reversible.
Other providers use different property prefixes and authentication. A local Ollama deployment, for example, needs a reachable Ollama server and a locally available model rather than a cloud API key.
Never:
- commit an API key to
application.yml; - accept a provider key from an untrusted request;
- log authorization headers;
- expose unrestricted model proxy endpoints; or
- assume provider-side retention and training policies are the same.
Use a secret manager, workload identity, or the platform’s credential mechanism. Review what user and business data may leave the application before enabling a remote provider.
Make the First Chat Call #
When one ChatModel is auto-configured, Spring Boot supplies a
ChatClient.Builder. Build the client once as an application bean or service
dependency:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class AssistantService {
private final ChatClient chatClient;
public AssistantService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
You are a concise product-support assistant.
State uncertainty instead of inventing an answer.
""")
.build();
}
public String answer(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}Expose it through an ordinary Spring MVC controller:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/assistant")
public class AssistantController {
private final AssistantService assistant;
public AssistantController(AssistantService assistant) {
this.assistant = assistant;
}
@PostMapping
public AnswerResponse answer(@RequestBody QuestionRequest request) {
return new AnswerResponse(assistant.answer(request.question()));
}
}
record QuestionRequest(String question) {}
record AnswerResponse(String answer) {}Validate request size and content as with any other API. A model call can be slow and expensive, so also apply authentication, authorization, rate limits, timeouts, concurrency limits, and cancellation.
Prompt Roles #
A chat prompt can contain different message roles:
- System: Application-level behavior and constraints
- User: The current user’s request
- Assistant: Previous model responses
- Tool: Results returned from application tools
Keep trusted instructions in system content. User input must remain user content; concatenating it into a system template gives untrusted text more authority and makes prompt injection easier.
Prompt templates can insert parameters:
String answer = chatClient.prompt()
.system("""
Answer questions about {product}.
If the supplied context is insufficient, say so.
""")
.user(user -> user
.text("Question: {question}")
.param("question", question))
.call()
.content();Template substitution is not a security boundary. Delimiters and instructions can improve model behavior, but they do not turn arbitrary text into trusted data.
ChatModel and ChatClient
#
ChatModel is the portable low-level interface:
ChatResponse response = chatModel.call(
new Prompt("Explain transaction isolation")
);It accepts a Prompt and returns a ChatResponse, exposing generations,
metadata, finish reasons, and usage information.
ChatClient adds a fluent application API:
ChatClientResponse response = chatClient.prompt()
.user("Explain transaction isolation")
.call()
.chatClientResponse();Use content() when only text is needed. Use the richer response when the
application needs metadata or Advisor context.
The core chat types have distinct responsibilities:
| Type | Responsibility |
|---|---|
ChatClient | Application-facing fluent API that assembles requests, runs Advisors, and can manage tool-calling loops |
ChatModel | Provider-facing model interface that sends a Prompt to an LLM and returns a ChatResponse |
ChatOptions | Per-request settings such as model, temperature, maximum output tokens, and provider-specific controls |
Prompt | Complete model input: an ordered list of messages plus ChatOptions |
Message | One system, user, assistant, or tool-result entry with content and metadata |
ChatResponse | Complete model result, including generations and response metadata |
Generation | One candidate output, normally containing an AssistantMessage |
ChatMemory | Conversation storage keyed by conversation ID; an Advisor or application code must load it into a prompt |
ToolCallback | Registered executable capability with a name, description, input schema, and invocation behavior |
The relationship is:
ChatClient
-> Prompt(messages + ChatOptions)
-> Advisor chain
-> ChatModel
-> provider API and LLM
-> ChatResponseProvider-neutral options cover common settings, while provider-specific option classes expose capabilities unavailable in the common interface. Depending on provider behavior, request options may include:
- model;
- temperature;
- maximum output tokens;
- stop sequences;
- response format;
- tool definitions;
- sampling controls; and
- provider-specific safety or reasoning settings.
Do not assume the same numeric option has identical semantics across models. Even two versions from one provider can behave differently.
Reading the Fluent Call #
For this request:
ChatResponse response = chatClient.prompt()
.advisors(spec -> spec.param(
ChatMemory.CONVERSATION_ID,
conversationId
))
.messages(new UserMessage(message))
.options(chatOptions)
.call()
.chatResponse();prompt()starts a request specification and inherits client defaults.advisors(...)supplies request-scoped Advisor configuration, such as the conversation ID used by a memory Advisor.messages(...)adds the current messages.options(...)selects request-specific model settings.call()executes synchronously and blocks until the model and any automatic tool-calling loop complete or fail.chatResponse()extracts the completed Spring AI response; it does not make another provider call.
Use stream() rather than call() when the caller should consume a reactive
response as it arrives.
Streaming #
Reactive applications can stream generated fragments:
import reactor.core.publisher.Flux;
public Flux<String> streamAnswer(String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}Streaming improves time to first output, not necessarily total completion time or cost. The application must handle:
- client disconnects and cancellation;
- partial UTF-8 or Markdown constructs;
- errors after output has begun;
- backpressure;
- moderation of partial content; and
- the inability to replace already displayed text cleanly.
Do not collect a stream into a large in-memory string merely to call an API designed for streaming.
Structured Output #
Business code should not parse prose with regular expressions. ChatClient
can map a response into a Java type:
record TicketTriage(
String category,
int priority,
String explanation
) {}
TicketTriage triage = chatClient.prompt()
.system("""
Classify the support ticket.
Priority must be an integer from 1 to 5.
""")
.user(ticketText)
.call()
.entity(TicketTriage.class);Spring AI derives a schema or format instructions and converts the response. When the model and provider support native structured output, Spring AI can use the provider’s schema facility for stronger conformance.
Structured output is still untrusted input. Validate:
- required fields;
- numeric ranges;
- enum membership;
- length limits;
- cross-field invariants; and
- whether the result is permitted to trigger an action.
A valid JSON object can be factually wrong or maliciously influenced. Schema validation solves shape, not truth.
Tool Calling #
Tool calling lets a model request that application code perform an operation. The model does not execute Java directly. It returns a structured tool request; Spring AI validates and invokes a registered callback, sends the result back to the model, and obtains the next response.
Define tools with annotations:
import java.math.BigDecimal;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class OrderTools {
private final OrderService orders;
public OrderTools(OrderService orders) {
this.orders = orders;
}
@Tool(description = "Look up the current status of an order")
public OrderStatus findOrder(
@ToolParam(description = "Canonical order ID") String orderId) {
return orders.status(orderId);
}
}Make the tool available to one request:
String answer = chatClient.prompt()
.user("Where is order 8f27?")
.tools(orderTools)
.call()
.content();An annotated tool is an ordinary Java method. It does not become an HTTP
endpoint, and the remote model never accesses the JVM. Spring AI discovers the
method, derives a JSON schema from its signature and annotations, and wraps it
as a ToolCallback.
Tools are not made available merely because @Tool appears on a method. They
must be registered with a request or client. For example, callbacks can be
discovered explicitly and installed as client defaults:
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
ToolCallback[] callbacks = ToolCallbacks.from(orderTools);
ChatClient toolClient = builder
.defaultToolCallbacks(callbacks)
.build();ToolCallbacks.from(...) uses reflection during callback construction to find
annotated methods and inspect their parameter and return types. Tool selection
does not use reflection: on each model request, Spring AI sends the registered
tool names, descriptions, and input schemas to the provider. The external LLM
may return a structured request such as:
{
"name": "findOrder",
"arguments": {
"orderId": "8f27"
}
}Spring AI matches the name to the local callback, converts the JSON arguments to Java values, invokes the method in the application JVM, serializes its result as a tool-result message, and sends that result to the model. The model then produces the user-facing answer. The caller normally invokes only the application’s chat HTTP endpoint and does not call the tool method directly.
Startup is commonly when an application constructs callback objects and
builds a ChatClient, but the model receives the available tool schemas on
each relevant request. The model does not inspect the application or remember
its tool catalog permanently.
Descriptions and parameter schemas tell the model when and how to request the tool. They are not authorization rules. The tool method must enforce the authenticated user’s access independently:
model asks for findOrder("8f27")
-> application validates identity and authorization
-> application calls OrderService
-> result returns to modelUse request-scoped tools for privileged capabilities. Default tools are
available to every request built from that ChatClient, which is dangerous for
destructive or tenant-specific operations.
Separate read-only tools from actions. For actions such as refunds, account changes, messages, purchases, or deletion:
- Validate all arguments.
- Recheck authorization in application code.
- Make operations idempotent.
- Require explicit user confirmation near execution.
- Record an audit event.
- Return a clear result instead of relying on the model to infer success.
Calling ChatModel directly with tool definitions does not automatically
execute returned tool calls. ChatClient installs the tool-calling Advisor for
the automatic loop; lower-level users must control that loop themselves.
Tool Calling Versus Deterministic Control Flow #
Tool calling normally adds model and network latency. A request that uses a tool usually needs:
model call to select a tool
-> tool execution
-> model call to interpret the resultCallback lookup and reflective method invocation are generally negligible compared with those provider round trips and with any remote service called by the tool.
Use ordinary Java routing when the request already has a stable typed contract and a known workflow. Model-selected tools are useful when a conversational request is ambiguous, can be phrased many ways, or may require a dynamic combination of capabilities. A robust design lets the model interpret or propose an action while deterministic code remains responsible for validation, authorization, transaction boundaries, retries, idempotency, and execution of high-value operations.
Advisors #
Advisors encapsulate behavior that surrounds a model call. They can modify a request, block it, call the next Advisor, inspect the response, and share context. Common uses include:
- conversation memory;
- RAG;
- tool execution;
- safety filters;
- logging;
- tenant context;
- caching; and
- custom policy enforcement.
Configure default Advisors on a client:
ChatClient client = builder
.defaultAdvisors(firstAdvisor, secondAdvisor)
.build();Or add them to one request:
client.prompt()
.user(question)
.advisors(requestAdvisor)
.call()
.content();Advisor order matters. Lower order values process the request first and the response last, so the chain behaves like nested middleware:
request -> A -> B -> model
response <- A <- B <- modelAn Advisor that records the final response may therefore need different order from one that validates the raw model response. Avoid Advisors with hidden network calls or ambiguous ordering; they make latency and failures difficult to reason about.
Conversation Memory #
Language models do not remember previous HTTP requests by themselves. Chat memory selects previous messages and adds them to the next prompt.
Spring AI’s ChatMemory abstraction supports bounded message history.
MessageWindowChatMemory retains a configurable window and evicts old
messages while preserving the current system message:
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
ChatClient client = builder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();Every request using a memory Advisor must provide a conversation ID:
String answer = client.prompt()
.user(question)
.advisors(advisor -> advisor.param(
ChatMemory.CONVERSATION_ID,
conversationId
))
.call()
.content();There is no safe universal default conversation ID. Derive it from an authenticated tenant, user, and conversation, and verify ownership on every request. A caller-chosen ID without authorization can expose another user’s history.
Memory repositories include in-memory and persistent implementations. In-memory storage is suitable for development but is lost on restart and is not shared among application replicas.
Chat memory is prompt context, not an authoritative business record. A bounded window forgets old content; a model can misinterpret remembered messages; and retaining conversations creates privacy and deletion obligations.
Embeddings and Vector Stores #
An embedding model converts content into a numeric vector whose geometry represents semantic similarity:
"How do I reset my password?"
-> embedding model
-> [0.013, -0.207, ...]A vector store keeps vectors together with source text and metadata. A query is embedded with the same compatible model, and nearest-neighbor search returns semantically related documents.
Spring AI’s EmbeddingModel and VectorStore APIs hide common provider
operations. Vector-store implementations exist for PostgreSQL/PGVector,
Elasticsearch, OpenSearch, Redis, MongoDB Atlas, Neo4j, Cassandra, Milvus,
Pinecone, Qdrant, Weaviate, and other systems.
Portability has limits:
- vector dimensions and distance functions must match the embedding model;
- indexes and filtering capabilities vary;
- metadata types differ;
- hybrid text/vector search is provider-specific;
- consistency and deletion semantics vary; and
- changing embedding models normally requires re-embedding the corpus.
Store the embedding model identifier and content version with each indexing generation. Do not mix incompatible vectors in one index.
Retrieval Augmented Generation #
Retrieval augmented generation, or RAG, supplies selected application documents to a model at request time:
documents
-> read
-> clean and split
-> embed
-> vector store
user question
-> embed
-> retrieve relevant chunks
-> add chunks to prompt
-> chat model
-> answerRAG does not train the model. It changes the context of one request.
Ingest Documents #
Spring AI’s ETL model uses:
DocumentReaderto load source material;DocumentTransformerto clean or split it; andDocumentWriter, commonly aVectorStore, to persist it.
A simplified ingestion flow is:
List<Document> documents = documentReader.get();
List<Document> chunks = tokenTextSplitter.apply(documents);
vectorStore.add(chunks);Chunking controls retrieval quality. Chunks that are too large waste context and mix topics; chunks that are too small lose necessary context. Preserve metadata such as document ID, title, tenant, version, section, and access classification.
Ingestion must be repeatable. Define how updated sources replace old chunks, how deleted sources are removed, and how partial indexing failures are retried.
Retrieve at Chat Time #
For straightforward vector-based question answering, add a
QuestionAnswerAdvisor:
ChatClient ragClient = builder
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.build();The Advisor searches the store and augments the prompt with matching documents.
More elaborate flows can use RetrievalAugmentationAdvisor to customize query
rewriting, retrieval, joining, filtering, and context augmentation.
Always apply authorization filters during retrieval, before content reaches the model:
tenant_id = authenticated tenant
AND visibility permits authenticated user
AND document version is activeAsking the model to ignore unauthorized chunks is not access control.
RAG can still hallucinate. Require citations where appropriate, preserve source identifiers, return “insufficient evidence” when retrieval is weak, and measure retrieval recall separately from answer quality.
Model Context Protocol #
The Model Context Protocol, or MCP, standardizes how AI applications discover and use external tools and resources. Spring AI supports both sides:
- an MCP client can consume capabilities from MCP servers;
- an MCP server can expose Spring application capabilities to MCP clients.
MCP is useful when tools should be shared across several AI applications or languages. Direct Spring AI tool callbacks are simpler when the capability exists only inside one application.
Treat an MCP server as a remote privileged API. Authenticate connections, authorize every operation, constrain filesystem and network access, validate tool arguments, and do not automatically trust a server merely because it speaks MCP.
Observability #
Spring AI uses Spring’s observability infrastructure and Micrometer observations around supported operations. With Actuator and an observability registry, an application can measure model calls, tool calls, embeddings, and vector-store operations.
Useful operational signals include:
- request count and error rate;
- latency and time to first streamed token;
- input and output token usage;
- rate-limit responses and retries;
- model and provider;
- tool-call count, latency, and failures;
- retrieval latency and result count; and
- estimated monetary cost.
Keep metric tags bounded. Never use prompt text, user IDs, conversation IDs, or tool arguments as metric tags.
Prompt and completion content is sensitive and high-cardinality. Spring AI does not make it safe merely by exposing an observation hook. Content logging should be disabled by default, tightly access-controlled when enabled, redacted, and subject to short retention.
Propagate trace context through model, tool, database, and vector-store calls so a slow user request can be investigated end to end. See Micrometer Application Observability and Spring Boot Actuator for the underlying instrumentation and management model.
Error Handling and Resilience #
Model APIs fail in ordinary distributed-system ways:
- timeouts and connection failures;
- authentication errors;
- rate limits;
- provider outages;
- unavailable models;
- context-length rejection;
- malformed structured output;
- safety-policy rejection; and
- successful responses with unusable content.
Set an end-to-end deadline shorter than the caller’s timeout. Retry only transient failures, with bounded exponential backoff and jitter. A retry can incur another charge and can repeat a tool side effect, so action tools must be idempotent.
Do not silently turn a model failure into an invented success response. Surface an explicit unavailable or incomplete result consistent with the application’s API contract.
Fallback models are not transparent. A different model may have different tool, schema, safety, context-window, latency, and quality behavior. Test a fallback as a separate supported execution path.
Limit concurrent model calls. Blocking servlet requests can exhaust request threads when provider latency rises; reactive calls can still overwhelm provider quotas and downstream tools without explicit concurrency controls.
Testing and Evaluation #
Traditional unit tests should cover deterministic code without contacting a provider:
- prompt construction;
- validation;
- authorization;
- tool argument handling;
- Advisor order;
- metadata filters;
- output conversion; and
- failure mapping.
Use fakes or controlled model implementations at application boundaries. Do not mock so deeply that the test only verifies a fluent call chain.
Provider integration tests should be a smaller, separately tagged suite because they are slower, cost money, require credentials, and can vary over time.
Exact string equality is usually the wrong assertion for generated prose. Instead test:
- required facts or fields;
- schema validity;
- forbidden content;
- citation support;
- tool selection;
- groundedness against supplied documents;
- latency and token budgets; and
- quality over a versioned evaluation dataset.
Spring AI supplies evaluators such as relevance and fact-checking evaluators. These can help at scale, but an LLM judge is also probabilistic and biased. Calibrate it against human-reviewed examples rather than treating it as ground truth.
Record the provider, model version, prompt version, tool set, retrieval configuration, and dataset version with evaluation results. Without them, a quality regression cannot be reproduced.
Production Architecture #
Keep AI integration behind an application service rather than spreading
ChatClient calls across controllers:
HTTP or messaging adapter
-> application policy and authorization
-> AI service
-> prompt templates
-> Advisors
-> tools
-> retrieval
-> ChatClient
-> validated domain resultThis boundary makes it possible to:
- change providers;
- enforce budgets;
- version prompts;
- add moderation;
- test without network calls;
- audit actions;
- centralize timeouts and retries; and
- prevent model text from becoming an implicit domain command.
For high-value actions, use the model to propose a typed intent and let deterministic application code validate and execute it. The model should not be the final authority for permissions, money movement, deletion, legal decisions, or safety-critical behavior.
Prompt Injection #
Any text read by the model can contain adversarial instructions:
- user messages;
- web pages;
- uploaded documents;
- database content;
- tool results; and
- retrieved RAG chunks.
Mitigations include:
- Keep authorization outside the model.
- Expose only the minimum tools required for a request.
- Separate data from trusted instructions.
- Validate tool parameters and model output.
- Filter retrieved documents by access policy.
- Require confirmation for consequential actions.
- Restrict outbound network and filesystem access.
- Assume model-visible secrets can be disclosed.
No system prompt can guarantee that an untrusted model input will be ignored.
Cost Control #
Cost is affected by input tokens, output tokens, model choice, retries, RAG context, conversation history, tool loops, and evaluation calls.
Control it with:
- bounded input and output sizes;
- bounded memory windows;
- selective RAG chunks;
- per-user and per-tenant quotas;
- concurrency limits;
- cached deterministic results where safe;
- cheaper models for classification or evaluation;
- alerts on token and monetary budgets; and
- explicit limits on tool-call iterations.
Measure actual usage rather than estimating from character count alone.
An Onboarding Sequence #
A safe incremental adoption path is:
- Plain chat: Configure one model and make one non-streaming call.
- Typed output: Map the answer to a validated Java record.
- Observability: Add latency, error, usage, and cost monitoring.
- Evaluation: Create a versioned set of representative inputs and expected properties.
- Read-only tools: Expose one narrowly authorized lookup.
- RAG: Ingest a small curated corpus with source metadata and access filters.
- Memory: Add bounded, tenant-isolated conversation state only if the product needs it.
- Actions: Add idempotency, confirmation, and auditing before enabling side effects.
- MCP: Introduce it when capabilities genuinely need to be shared outside the application.
At every step, test provider failures, hostile input, cost limits, and data handling before adding another abstraction.
Common Mistakes #
- Using an outdated starter name. Match examples to the Spring AI release.
- Committing provider keys. Load credentials from a secret mechanism.
- Treating model output as trusted. Parse, validate, authorize, and audit.
- Giving every request every tool. Scope tools to the current operation.
- Using conversation IDs without ownership checks. This can leak chat history across users or tenants.
- Putting unauthorized documents into a RAG prompt. Filter before model access.
- Logging prompts by default. They may contain secrets and personal data.
- Retrying action tools blindly. A repeated call can repeat its side effect.
- Testing exact prose only. Evaluate structure, facts, safety, and task success.
- Assuming provider portability is perfect. Test every supported model path.
- Starting with autonomous actions. Begin with read-only, observable behavior.
- Calling the model from every layer. Centralize policy in an AI service.
Spring AI gives Spring developers familiar dependency injection, auto-configuration, portable interfaces, middleware-style Advisors, and Micrometer observations around AI workloads. The difficult parts remain application concerns: defining trustworthy tools, protecting data, evaluating quality, controlling cost, and ensuring that probabilistic output cannot bypass deterministic business and security rules.
Comments
Read or comment on GitHub (sign-in required to comment)