티스토리 뷰
LangChain4j를 활용해 Tool 기반 에이전트를 설계하다 보면, Tool 메서드의 반환값을 어떤 형태로 설계하는 것이 바람직한지 고민하게 됩니다. 특히 단순 문자열 반환과 구조화된 DTO 반환 사이의 선택은 단순한 취향 문제가 아니라, LLM 응답 품질과 후처리 가능성에 직접적인 영향을 미치는 설계 요소로 이어집니다.
이 글에서는 LangChain4j 1.10.0 기준으로 Tool 반환값이 내부적으로 어떻게 처리되는지 살펴보고, Java record 기반 DTO 설계가 어떤 장점을 가지는지 정리합니다. 또한 Tool 반환값의 구조가 LLM의 응답 품질에 어떤 차이를 만들어내는지, 실제 사용 경험을 바탕으로 정리해 보고자 합니다.
LangChain4j Tool 반환값 처리 방식의 기본 동작
LangChain4j 공식 문서와 소스 코드를 기준으로 보면, @Tool 메서드의 반환값은 비교적 명확한 규칙에 따라 처리됩니다.
반환 타입이 void인 경우, LangChain4j는 Tool 실행 결과를 "Success"라는 문자열로 간주하여 LLM에게 전달합니다. 이 경우 Tool 호출 자체의 성공 여부만 의미를 가지며, 실제 데이터 전달에는 적합하지 않은 형태입니다.
반환 타입이 String인 경우에는 개발자가 반환한 문자열이 그대로 Tool 실행 결과로 전달됩니다. 이 방식은 간단한 설명이나 자연어 메시지를 전달하기에는 편리하지만, 데이터 구조를 명확히 표현하기에는 한계가 있습니다.
그 외의 객체 타입을 반환하는 경우에는, LangChain4j 내부에서 JSON 직렬화를 수행한 뒤 해당 JSON 문자열을 Tool 실행 결과로 LLM에게 전달합니다. 이 동작은 ToolExecutionResultMessage 구현을 통해 확인할 수 있으며, 반환 객체의 필드 구조가 그대로 LLM 입력 컨텍스트에 포함됩니다.
이 지점에서 Tool 반환값 설계의 방향성이 자연스럽게 갈립니다. 사람이 읽기 쉬운 문자열을 반환할 것인지, 아니면 LLM이 해석하기 쉬운 구조화된 데이터를 반환할 것인지에 대한 선택입니다.
String 반환 방식의 한계
String 반환 방식은 구현이 단순하다는 장점이 있습니다. 빠르게 실험하거나, 결과를 단순 요약 형태로 전달하는 경우에는 충분히 실용적인 선택이 될 수 있습니다.
다만 실제로 LLM에게 의미 있는 후속 작업을 기대하는 경우, 문자열 기반 반환은 몇 가지 한계를 드러냅니다. 문자열 내부에 포함된 숫자나 통계 값은 LLM 입장에서 명확한 의미 구조를 가지지 못하며, 동일한 의미를 여러 방식으로 해석할 가능성이 커집니다. 이로 인해 Markdown 테이블이나 Mermaid 차트와 같이 구조를 요구하는 출력 포맷에서는 응답 품질이 불안정해지는 경향을 보였습니다.
또한 문자열은 Tool 호출 결과를 다시 다른 Tool 입력으로 재사용하기 어렵습니다. 데이터의 의미 단위가 분리되어 있지 않기 때문에, 체이닝된 Tool 설계에서는 자연스럽게 병목이 발생합니다.
구조화된 DTO 반환의 설계 방향
이러한 한계를 보완하기 위해, Tool 반환값을 명확한 구조를 가진 DTO로 설계하는 접근이 실무에서는 더 안정적으로 느껴졌습니다. LangChain4j가 반환 객체를 JSON으로 직렬화한다는 점을 고려하면, 반환 DTO는 곧 LLM에게 전달되는 데이터 스키마 역할을 하게 됩니다.
Java 환경에서는 record를 활용한 DTO 설계가 특히 적합합니다. 불변 객체라는 특성 덕분에 Tool 실행 결과의 의미가 명확해지고, 직렬화 대상 필드 또한 의도적으로 제한할 수 있습니다.
예를 들어 통계 정보를 반환하는 Tool의 경우, 총 개수, 평균, 최소·최대 값과 같은 필드를 명시적으로 포함한 StatisticsDto 형태로 반환하면, LLM은 해당 JSON을 기반으로 자연스럽게 요약 문장이나 표 형태의 출력을 생성합니다. 단어 빈도 분석 결과 역시 WordFrequencyDto와 같이 의미 단위가 분리된 구조로 제공할 경우, Markdown 리스트나 차트로의 변환이 훨씬 안정적으로 이루어졌습니다.
여러 종류의 데이터를 함께 반환해야 하는 경우에는, 이를 하나의 결과 컨텍스트로 묶은 DataCollectionResultDto 형태가 유용했습니다. 이처럼 반환 DTO 자체가 “이 Tool이 어떤 의미의 데이터를 제공하는지”를 설명하는 문서 역할을 하게 됩니다.
LLM의 JSON 해석과 출력 포맷팅 흐름
구조화된 DTO를 반환하면, LLM은 Tool 실행 결과로 전달된 JSON을 입력 컨텍스트로 받아들입니다. 이 JSON은 단순한 문자열이 아니라, 키-값 구조를 가진 데이터로 인식됩니다.
이 상태에서 LLM에게 “이 데이터를 기반으로 Markdown 표를 생성하라”거나 “Mermaid 차트로 시각화하라”는 요청을 하면, 문자열 기반 반환에 비해 훨씬 일관된 결과를 얻을 수 있었습니다. 이는 LLM이 각 필드의 의미를 명확히 구분할 수 있기 때문으로 보입니다.
특히 Mermaid 차트와 같이 문법 오류에 민감한 출력에서는, 구조화된 입력이 응답 안정성에 큰 차이를 만들어냈습니다. 필드명이 곧 데이터의 의미를 설명하는 역할을 하면서, LLM이 임의로 해석을 보완하거나 추측할 필요가 줄어들기 때문입니다.
Tool 반환값과 LLM 응답 품질의 상관관계
실무에서 여러 형태의 Tool 반환값을 비교해 보면, 반환값의 구조화 수준이 LLM 응답 품질과 상당한 상관관계를 가진다는 점을 체감하게 됩니다.
String 반환은 빠른 구현과 단순한 대화 흐름에는 적합하지만, 응답의 일관성과 재사용성 측면에서는 아쉬움이 남습니다. 반면 DTO 기반 JSON 반환은 초기 설계 비용이 조금 더 들지만, LLM의 응답 품질과 예측 가능성을 높이는 데 기여합니다.
이 차이는 단순히 출력이 “예쁘게” 보이느냐의 문제가 아니라, LLM을 하나의 추론 엔진으로 활용할 수 있느냐의 문제로 이어집니다. Tool 반환값이 명확할수록, LLM은 그 위에서 더 안정적인 추론과 포맷 변환을 수행할 수 있습니다.
정리하며
LangChain4j의 Tool 반환값 설계는 단순한 구현 선택처럼 보이지만, 실제로는 LLM 활용 전략 전반에 영향을 미치는 요소입니다. 공식 문서와 구현을 기준으로 보면, LangChain4j는 구조화된 반환값을 자연스럽게 활용할 수 있도록 설계되어 있으며, 이를 어떻게 사용하는지는 전적으로 개발자의 선택에 달려 있습니다.
개인적으로는, Tool이 “데이터를 제공하는 역할”을 가진다면 가능한 한 DTO 기반 반환을 선택하는 쪽이 장기적으로 안정적이라는 인상을 받았습니다. 반대로 단순한 상태 변경이나 트리거 역할의 Tool이라면, void 또는 간단한 문자열 반환도 충분히 의미를 가질 수 있습니다.
'TECH AND AI' 카테고리의 다른 글
- Total
- Today
- Yesterday
- DB 인덱스 성능
- Eager Initialization
- Double-Checked Locking
- Spring Batch
- 백엔드 성능 튜닝
- 백엔드 성능 설계
- DB 트랜잭션
- TTL 설계
- Cache Aside
- Enum 기반 싱글톤
- Cache Avalanche
- Hot Key 문제
- 캐시 장애
- 트래픽 처리
- 캐시 성능 비교
- Java Performance
- mybatis
- Cache Penetration
- Redis 성능 개선
- 스레드 생명주기
- 캐시와 인덱스
- InterruptedException
- 백엔드 성능
- 동시성처리
- Redis vs DB
- Redis 캐시 전략
- spring batch 5
- 트랜잭션 관리
- 백엔드 아키텍처
- Initialization-on-Demand Holder Idiom
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

