Most RAG demos stop at one search. You embed a question, pull top-k chunks, paste them into a prompt, and hope the first hit was enough. That works for FAQ pages. It fails when the first query is vague, the right facts are split across documents, or the model needs a second, more precise lookup.
Solon AI calls the upgrade Agent RAG: retrieval is no longer a fixed pre-step. It becomes a tool the model can call zero or more times inside a ReAct loop.
This article sticks to official Solon v4.0.3 APIs from the RAG and Agent docs.
Passive RAG vs Agent RAG
| Mode | Flow | Who decides to search? | Retry if context is weak? |
|---|---|---|---|
| Passive RAG | search → augment → generate | Your code, always once | No |
| Agent RAG | Thought → tool search → Observation → … | The model | Yes, multi-round |
Passive RAG is still useful. Agent RAG is what you want when answers need iterative retrieval.
1. Build a small knowledge base
Same building blocks as classic Solon RAG: embedding model, storable repository, load + split + save.
import org.noear.solon.ai.embedding.EmbeddingModel;
import org.noear.solon.ai.rag.Document;
import org.noear.solon.ai.rag.loader.TextLoader;
import org.noear.solon.ai.rag.repository.InMemoryRepository;
import org.noear.solon.ai.rag.splitter.RegexTextSplitter;
import org.noear.solon.ai.rag.splitter.SplitterPipeline;
import org.noear.solon.ai.rag.splitter.TokenSizeTextSplitter;
import java.net.URI;
import java.util.List;
EmbeddingModel embeddingModel = EmbeddingModel.of(embeddingApiUrl)
.apiKey(embeddingApiKey)
.model(embeddingModelName)
.build();
InMemoryRepository repository = new InMemoryRepository(embeddingModel);
// Load → split → save
List<Document> documents = new SplitterPipeline()
.next(new RegexTextSplitter("\n\n"))
.next(new TokenSizeTextSplitter(500))
.split(new TextLoader(URI.create("https://example.com/policy.txt")).load());
repository.save(documents);
InMemoryRepository is fine for demos. Production can swap in Milvus, Redis, Elasticsearch, Qdrant, and other repository plugins without changing the Agent wiring below.
2. Passive RAG still has a clean path
When one search is enough, keep the pipeline explicit:
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.message.ChatMessage;
List<Document> context = repository.search(question);
ChatMessage prompt = ChatMessage.ofUserAugment(question, context);
String answer = chatModel.prompt(prompt).call().getMessage().getContent();
Or let the repository do the one-shot augment:
ChatMessage prompt = repository.promptAugment(question);
This is the classic “retrieve then generate” path. No agent required.
3. Turn the repository into a tool
Agent RAG starts when search becomes a callable tool. Official docs introduce RepositoryTool for that (available since Solon AI 3.10.1):
import org.noear.solon.ai.rag.RepositoryTool;
RepositoryTool repositoryTool = new RepositoryTool(repository);
Stage A — ChatModel with tools
The model decides whether to search at all:
ChatModel chatModel = ChatModel.of(chatApiUrl)
.apiKey(chatApiKey)
.model(chatModelName)
.defaultToolAdd(repositoryTool)
.build();
chatModel.prompt("What is the refund window for lost packages?").call();
Stage B — ReActAgent for multi-hop retrieval
For harder questions, upgrade to ReAct so the model can search, rethink, and search again:
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.session.InMemoryAgentSession;
ReActAgent agent = ReActAgent.of(chatModel)
.defaultToolAdd(repositoryTool)
.sessionWindowSize(8)
.maxTurns(10)
.build();
String answer = agent.prompt(
"Customer paid 158 yuan. Package marked lost at Shanghai hub. "
+ "What policy applies and what should support do next?"
)
.session(InMemoryAgentSession.of("support_user_42"))
.call()
.getContent();
That is the heart of Agent RAG in Solon:
- Knowledge lives in a
Repository -
RepositoryToolexposes it as a tool -
ReActAgentruns Thought → Action → Observation until it has enough evidence
4. Mix knowledge search with business tools
Real support agents need more than docs. They also need order APIs, logistics APIs, and compensation actions. Solon tools are just ToolProviders — knowledge tools and domain tools hang on the same agent.
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.tool.AbsToolProvider;
import org.noear.solon.annotation.Param;
public class OrderTools extends AbsToolProvider {
@ToolMapping(description = "Look up order amount and tracking number by order id")
public String get_order(@Param(description = "order id") String orderId) {
// call your order service
return "{\"orderId\":\"" + orderId + "\",\"amount\":158.0,\"trackNo\":\"track_123\"}";
}
}
public class LogisticTools extends AbsToolProvider {
@ToolMapping(description = "Get logistics status by tracking number")
public String get_logistic_status(@Param(description = "tracking number") String trackNo) {
return "{\"status\":\"lost\",\"info\":\"lost at Shanghai hub\"}";
}
}
ReActAgent agent = ReActAgent.of(chatModel)
.defaultToolAdd(repositoryTool) // policy / FAQ knowledge
.defaultToolAdd(new OrderTools())
.defaultToolAdd(new LogisticTools())
.modelOptions(o -> o.temperature(0.0))
.maxTurns(10)
.build();
Typical loop the model may invent:
- Search policy for “lost package refund”
- Call
get_order - Call
get_logistic_status - Answer with both policy text and operational next step
No custom orchestrator graph required for this pattern.
5. Keep multi-turn memory under control
Agent RAG often spans several user turns. Solon separates session storage from window size:
ReActAgent agent = ReActAgent.of(chatModel)
.defaultToolAdd(repositoryTool)
.sessionWindowSize(5) // only recent messages go to the LLM
.build();
// Isolate users by session id
agent.prompt("Continue from the last ticket.")
.session(InMemoryAgentSession.of("user_123"))
.call();
Built-in session backends (official comparison):
| Class | Storage | When to use |
|---|---|---|
InMemoryAgentSession |
Local map | Dev / tests |
FileAgentSession |
Local files | CLI / single-node tools |
RedisAgentSession |
Redis | Multi-node production |
sessionWindowSize is short-term history for the model. Long knowledge still lives in the repository, not in the chat transcript.
6. Optional hybrid senses
When internal docs are not enough, official Agent RAG samples also attach web/code tools from the skill ecosystem:
ReActAgent agent = ReActAgent.of(chatModel)
.defaultToolAdd(repositoryTool)
.defaultToolAdd(new CodeSearchTool())
.defaultToolAdd(new WebsearchTool())
.defaultToolAdd(new WebfetchTool())
.build();
Use this carefully in production: each extra tool increases tool-selection noise. Start with repository + domain APIs; add web tools only when the product needs live external facts.
A practical decision guide
| Situation | Prefer |
|---|---|
| Fixed FAQ, one clear query | Passive RAG (search + ofUserAugment) |
| Vague question, multi-hop facts | Agent RAG (RepositoryTool + ReActAgent) |
| Docs + order/logistics actions | Agent RAG + AbsToolProvider tools |
| Long chat with same user |
AgentSession + sessionWindowSize
|
| Clustered app servers | RedisAgentSession |
What not to invent
- Do not fake a custom
implements Toolprotocol for knowledge search whenRepositoryToolalready wraps the repository. - Do not put entire policy PDFs into the system prompt “for simplicity” — load, split, and store them.
- Do not confuse Harness memory (coding-agent long-term facts) with business RAG. For product Q&A, start from
Repository+ Agent tools.
Official references (Solon v4.0.3)
- Agent RAG (passive → active)
- Repository
- Document load & split
- EmbeddingModel
- Session & memory
- ReActAgent config
- E-commerce after-sales ReAct sample
Takeaway
In Solon, Agent RAG is not a second product. It is a composition rule:
Repository (facts) + RepositoryTool (search action) + ReActAgent (decision loop).
Start passive when one search is enough. Flip the repository into a tool when the model must decide if, when, and how to retrieve — and keep domain tools on the same agent when answers must become actions.
Top comments (0)