Every test drives the real Spring AI advisor classes (ToolCallingAdvisor,
ToolSearchToolCallingAdvisor) against a hand-written ScriptedChatModel that queues
real ChatResponse objects instead of calling a live LLM -- confirmed viable because
ChatModel has exactly one abstract method, call(Prompt) (checked with javap).
Covers:
- The plain call/execute/recall loop (WeatherTools, an ordinary @Tool method)
- @Tool(returnDirect = true) skipping the second model round trip entirely
(ServerStatusTools)
- ToolContext: excluded from the model-facing JSON schema (verified against the
real generated schema), still delivered to the tool from caller-supplied data
(UserContextTools)
- A 230-tool synthetic library across six fake domains, generated via
FunctionToolCallback.builder(...) (LargeToolLibrary)
- ToolSearchToolCallingAdvisor + RegexToolIndex: one tool ("toolSearchTool")
offered on the first call instead of 230, with real tool-count and
character-footprint measurements taken off the actual outgoing prompts
Findings recorded in the module's Javadoc rather than silently worked around:
- ToolCallingAdvisor only engages when the request's Prompt carries
ToolCallingChatOptions, built from ChatModel.getOptions().mutate() (not
getDefaultOptions(), a separate default method the request-building path never
calls) -- confirmed by disassembling ToolCallingAdvisor.adviseCall and
DefaultChatClientUtils
- The Tool Search Advisor's own tool is named "toolSearchTool" (camelCase), not
"tool_search_tool" -- confirmed via @Tool(name=...) in the decompiled class
- Its session ID comes from ChatClientRequest.context() (AdvisorSpec.param), not
from ChatClient.toolContext(Map) -- confirmed by disassembling
ToolSearchToolCallingAdvisor.initializeSession
- RegexToolIndex matches on verb/noun substrings, not semantic relevance -- a
real captured search for "look up an invoice" returned 5 lookup_-named tools
across three unrelated domains alongside the one actually wanted
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
60 lines
4.2 KiB
Markdown
60 lines
4.2 KiB
Markdown
# tool-calling
|
|
|
|
`@Tool` methods, `ToolCallingAdvisor` (the advisor-layer replacement for Spring AI 1.x's
|
|
per-`ChatModel` tool-calling loop), `returnDirect`, `ToolContext`, and `ToolSearchToolCallingAdvisor`
|
|
for progressive disclosure across a 230-tool synthetic library -- with no live model anywhere in
|
|
the test suite. Every test drives the real advisor classes against a hand-written `ScriptedChatModel`
|
|
that queues real `ChatResponse` objects, so what gets asserted is the advisor's real behaviour, not
|
|
a description of it.
|
|
|
|
Companion code for [Tool Calling in Spring AI 2.0](https://ankurm.com/spring-ai-2-0-tool-calling/)
|
|
on [ankurm.com](https://ankurm.com).
|
|
|
|
## Versions
|
|
|
|
| Component | Version |
|
|
|---|---|
|
|
| Spring Boot | 4.1.1 |
|
|
| Spring AI | 2.0.1 |
|
|
| `spring-ai-tool-search-advisor` / `spring-ai-tool-search-tool` | 2.0.1 (separately versioned artifacts, not covered by `spring-ai-bom` -- see the article's "going deeper" note) |
|
|
| Java | 25 (LTS) |
|
|
|
|
## Quickstart
|
|
|
|
```bash
|
|
./scripts/run-all.sh
|
|
```
|
|
|
|
Runs the full test suite. No API key, no network access, no Docker -- every test builds a
|
|
`ChatClient` around `ScriptedChatModel` (a queue of pre-programmed `ChatResponse`s) instead of a
|
|
real LLM. Output lands in `output/`.
|
|
|
|
## What's here
|
|
|
|
| File | What it does |
|
|
|---|---|
|
|
| [`tools/WeatherTools.java`](src/main/java/com/ankurm/toolcalling/tools/WeatherTools.java) | The baseline: an ordinary `@Tool` method, no special attributes. |
|
|
| [`tools/ServerStatusTools.java`](src/main/java/com/ankurm/toolcalling/tools/ServerStatusTools.java) | `@Tool(returnDirect = true)` -- the tool's own return value skips the second model round trip entirely. |
|
|
| [`tools/UserContextTools.java`](src/main/java/com/ankurm/toolcalling/tools/UserContextTools.java) | A `ToolContext`-typed parameter -- excluded from the model-facing JSON schema, still delivered at call time. |
|
|
| [`config/LargeToolLibrary.java`](src/main/java/com/ankurm/toolcalling/config/LargeToolLibrary.java) | Generates 230 synthetic `ToolCallback`s across six fake business domains via `FunctionToolCallback.builder(...)`, so the search-advisor section has something realistic to search. |
|
|
| [`config/ChatClientFactory.java`](src/main/java/com/ankurm/toolcalling/config/ChatClientFactory.java) | The exact `ChatClient` construction shared by production wiring (`ChatClientConfig`) and every test -- including the `ToolCallingChatOptions` base-options fix documented in its Javadoc, found by disassembling `ToolCallingAdvisor.adviseCall`. |
|
|
| [`config/ChatClientConfig.java`](src/main/java/com/ankurm/toolcalling/config/ChatClientConfig.java) | Production Spring `@Bean` wiring around a real (autoconfigured) `ChatModel`. |
|
|
| [`support/ScriptedChatModel.java`](src/test/java/com/ankurm/toolcalling/support/ScriptedChatModel.java) | A hand-written `ChatModel` (only `call(Prompt)` is abstract -- confirmed with `javap`) that returns a queued script of responses instead of calling a real API. |
|
|
|
|
## Output files
|
|
|
|
| File | What it captures |
|
|
|---|---|
|
|
| `output/01-plain-tool-call-round-trip.txt` | Two model calls for one tool question: the tool-call request, the tool result fed back, the final answer. |
|
|
| `output/02-return-direct-skips-second-round.txt` | `returnDirect = true`: exactly one model call, the tool's own JSON returned unparaphrased. |
|
|
| `output/03-tool-context-hidden-from-schema.txt` | `my_account`'s real JSON schema (no `userId` property anywhere in it) next to the real answer, built from a `userId` the caller supplied out-of-band. |
|
|
| `output/04-tool-search-progressive-disclosure.txt` | Tool counts actually offered to the model, read straight off `ToolCallingChatOptions.getToolCallbacks()`: 1 before searching, 6 after. |
|
|
| `output/05-tool-description-footprint.txt` | Measured character-count comparison between the full 230-tool definition text and `toolSearchTool`'s definition alone. |
|
|
| `output/06-tool-library-size.txt` | The exact size and per-domain breakdown of the synthetic library, asserted by a test rather than hand-counted. |
|
|
|
|
## Requirements
|
|
|
|
Nothing beyond the JDK and Maven. `spring-ai-starter-model-openai` is on the classpath for the
|
|
production `ChatClientConfig` bean to compile and (optionally) run against a real key via
|
|
`OPENAI_API_KEY`, but the test suite never constructs it.
|