ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SpringAI] Tool Calling (2) - DB 접근
    AI 2025. 6. 20. 21:50

    SpringAI 기술과 LLM을 활용해서 데이터베이스 접근을 해봤습니다.

    ChatClient(Ollama) + LLM(llama 3.2) + SpringAI Tool로 구성했습니다.

     

    사용자 더미데이터

     

    application.properties

    spring.application.name=ai-dataaccess
    
    spring.ai.ollama.base-url=http://localhost:11434
    spring.ai.ollama.chat.model=llama3.2
    
    spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    spring.datasource.url=jdbc:log4jdbc:mysql://127.0.0.1:3306/test_db
    spring.datasource.username=root
    spring.datasource.password=3473
    
    spring.jpa.hibernate.ddl-auto=validate
    spring.jpa.show-sql=true
    spring.jpa.properties.hibernate.format_sql=true
    
    logging.level.org.hibernate.SQL=debug
    logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace

    ollama chat client 및 datasource 설정입니다.

     

    1. 메서드 툴 생성

    @Component
    @RequiredArgsConstructor
    public class UserTool {
    
        private final UserService userService;
    
        @Tool(description = "Get all users")
        public List<User> getAllUsers() {
            return userService.findAll();
        }
    }

     

    2. Tool 등록

    @Configuration
    public class AiConfig {
    
        @Bean
        public ChatClient chatClient(OllamaChatModel chatModel, 
                                     UserTool userTool) {
            return ChatClient.builder(chatModel)
                    .defaultSystem("""
                            You are a read-only assistant.
                            Never perform insert, update, or delete actions.
                            If such requests occur, respond using the read-only tool.
                            If there's no data, tell me there isn't.
                           """)
                    .defaultTools(userTool)
                    .build();
        }
    }

    모델이 tool을 사용할 수 있도록 등록합니다.

     

    3. ChatClient Controller 등록

    @RestController
    @RequestMapping("/api/chat")
    @RequiredArgsConstructor
    public class ChatController {
    
        private final ChatClient chatClient;
    
        @PostMapping
        public String chat(@RequestBody PromptDto promptDto) {
            return chatClient.prompt(promptDto.prompt()).call().content();
        }
    }

    사용자가 채팅을 통해 데이터를 가져올 수 있도록 컨트롤러를 생성합니다.

     

    4. Ollama 로컬 서버 실행

    ollama serve

     

    Ollama 로컬 서버를 실행하지 않는다면, 다음과 같은 예외가 발생합니다.

     

    java.nio.channels.ClosedChannelException: null
    	at java.base/sun.nio.ch.SocketChannelImpl.ensureOpen(SocketChannelImpl.java:195) ~[na:na]
    	at java.base/sun.nio.ch.SocketChannelImpl.beginConnect(SocketChannelImpl.java:760) ~[na:na]
    	at java.base/sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:848) ~[na:na]
    	at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$0(PlainHttpConnection.java:183) ~[java.net.http:na]
    	at java.base/java.security.AccessController.doPrivileged(AccessController.java:569) ~[na:na]
    	at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:185) ~[java.net.http:na]
    	at java.net.http/jdk.internal.net.http.PlainHttpConnection.checkRetryConnect(PlainHttpConnection.java:230) ~[java.net.http:na]
    	at java.net.http/jdk.internal.net.http.PlainHttpConnection.lambda$connectAsync$1(PlainHttpConnection.java:206) ~[java.net.http:na]
    	at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na]
    	at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na]
    	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
    	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1773) ~[na:na]
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
    	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]

     

    5. Postman으로 테스트

     

    위와 같이 LLM은 tool이 반환한 데이터(JSON)를 기반으로 임의로 후처리하여 문자열 형태로 사용자에게 응답해줍니다. 이를 여러가지 방식으로 반환하는 예시를 보여드리겠습니다.

     

    Return Direct

    응답이 매번 다른 이유는, Tool calling 기본 동작이 다음과 같기 때문입니다.

     

     

    하지만 returnDirect attribute를 사용한다면 사용자에게 직접 반환하도록 수정할 수 있습니다.

    참고) @Tool attributes

    • name: Tool 이름. 지정하지 않으면 메서드 이름이 사용됨. AI 모델은 Tool을 호출할 때 name 값을 사용하기 때문에 고유한 값이어야 한다.
    • description: 모델이 Tool을 호출하는 시점과 방법을 이해하는 데 사용할 수 있는 Tool에 대한 설명. 생략시 메서드 이름으로 유추하지만 모델이 Tool의 목적과 사용 밥법을 판단하는데 굉장히 중요한 역할을 하기 때문에 자세하게 명시하는 것이 좋다.
    • returnDirect: Tool 실행 결과를 클라이언트로 바로 반환할지 모델로 반환할지 여부. 한 번에 여러 Tool을 요청하는 경우, returnDirect 어트리뷰트는 true로 설정되어야 한다. 그러지 않으면 결과가 모델로 전송됨.
    • resultConverter: AI 모델에게 보내기 위해서 Tool 실행 결과를 String 객체로 바꿔주는 ToolCallResultConverter 인터페이스의 구현체.

     

    메서드 툴 수정

    @Tool(
            description = "Get all users",
            returnDirect = true
    )
    public List<User> getAllUsers() {
        return userService.findAll();
    }

     

    returnDirect를 추가하면, 아래 사진과 같이 사용자에게 데이터가 바로 전달되도록 동작이 바뀝니다.

     

     

    따라서, 응답이 JSON 문자열로 바뀝니다.

     

     

    Result Converter

    더 나아가서, ToolCallResultConverter 인터페이스를 구현해서 응답 포맷을 지정할 수도 있습니다. 아래는 구현체와 ID로 사용자를 찾는 메서드 툴 예시입니다.

     

    UserToolCallResultConverter.java

    public class UserToolCallResultConverter implements ToolCallResultConverter {
    
        @Override
        public String convert(Object result, Type returnType) {
            if (result instanceof User user) {
                return String.format("사용자 이름은 %s이고, 이메일은 %s입니다.", user.getName(), user.getEmail());
            }
            return "";
        }
    }

     

    UserTool.java

    @Tool(
            description = "Get a user by their id",
            returnDirect = true,
            resultConverter = UserToolCallResultConverter.class
    )
    public User getUserById(Long id) {
        return userService.findById(id)
                .orElseThrow(() -> new NoSuchElementException("해당 ID의 유저가 없습니다."));
    }

     

    응답


    Github

    https://github.com/ehddnr3473/Spring-AI-DataAccess


    참고

    Spring doc

    (https://docs.spring.io/spring-ai/reference/api/tools.html)

    'AI' 카테고리의 다른 글

    [SpringAI] Tool Calling (1)  (0) 2025.05.24
    [SpringAI] 간단한 Ollama 채팅 클라이언트 만들기  (0) 2025.05.22
    Ollama 설치 및 실행  (0) 2025.05.16

    댓글

Designed by Tistory.