본문 바로가기

개발/AI

Spring AI로 Pi 스타일 에이전트 하네스 만들기 (0) - init

pi/packages/coding-agent at main · earendil-works/pi

 

pi/packages/coding-agent at main · earendil-works/pi

AI agent toolkit: unified LLM API, agent loop, TUI, coding agent CLI - earendil-works/pi

github.com

 

오픈클로의 core 였던 PI 에이전트를 Spring AI로 만들어보려고 한다.

Spring 개발자가 이미 알고 있는 방식으로, 작은 단위부터 하나씩 쌓아보는 것이 목표다.

이번 단계의 목표는 딱 하나다.

실행 가능한 빈 Spring Boot 애플리케이션 만들기

 

처음 보면 “이걸 굳이 한 편으로 나눌 필요가 있나?” 싶을 수 있다.

예전 같았으면 바로 API 하나 만들고, ChatClient 붙이고, tool calling까지 한 번에 넣으려고 했을 것 같다.

 

그런데 에이전트 런타임을 만들 때는 시작이 꽤 중요하다고 생각했다.
처음부터 너무 많은 걸 넣어버리면 나중에 어디서부터 복잡해졌는지 알기 어렵다.

 

왜 굳이 Java/Spring인가

요즘 AI Agent 예제를 찾아보면 Python이나 TypeScript가 훨씬 많다.

LangChain, LangGraph, CrewAI, Vercel AI SDK 같은 생태계도 대부분 그쪽에서 먼저 나온다.

그래서 “에이전트 만들 거면 그냥 Python이나 TypeScript로 하는 게 맞지 않나?”라는 생각이 들 수 있다.

그런데 실제 회사 백엔드 시스템을 생각해보면 이야기가 조금 달라진다.

많은 서비스는 여전히 Java/Spring으로 운영된다.
인증, 권한, 결제, 정산, 주문, 관리자 시스템, 배치, 내부 API 같은 것들은 Spring 기반인 경우가 많다.

그러면 중요한 질문은 단순히 이게 아니다.

LLM을 한 번 호출할 수 있는가?

 

이건 사실 어렵지 않다. API key 넣고 HTTP 요청 보내면 된다.

내가 더 궁금했던 질문은

 

 

AI 기능을 기존 백엔드 구조 안에 어떻게 안전하게 넣을 것인가?

 

이 관점으로 보면 Spring은 꽤 좋은 출발점이다.

Spring 개발자는 이미 HTTP API를 만들고, 설정을 분리하고, 인증과 권한을 다루고, 외부 API를 호출하고, 테스트를 작성하고, 운영 지표를 확인하는 방식에 익숙하다.

 

AI Agent도 완전히 다른 세상이라기 보다는, 기존 백엔드 애플리케이션 안에 “LLM이라는 의사결정 엔진”이 들어온 형태로 볼 수 있다.

 

예를 들면 이런 식이다.

  • 사용자의 요청을 받는다.
  • 현재 세션이나 상태를 읽는다.
  • LLM에게 판단을 맡긴다.
  • 필요하면 도구를 실행한다.
  • 실행 결과를 다시 모델에게 전달한다.
  • 최종 응답이나 이벤트를 사용자에게 돌려준다.

이 흐름 자체는 낯설지만, 쪼개서 보면 백엔드 개발자가 이미 다뤄온 것들과 비슷하다.

HTTP API, 설정, 보안, 외부 호출, 이벤트 스트리밍, 로그, 테스트, 장애 대응.
결국 에이전트도 운영되는 애플리케이션이다.

M0에서 할 일

이번 단계에서 만드는 것은 아주 작다.

build.gradle.kts
Application main class
application.yml
기본 health endpoint
테스트 실행 구조
README

 

Spring 프로젝트로 치면 처음부터 주문, 결제, 쿠폰, 정산, 관리자까지 다 만들지 않는 것과 같다. 먼저 애플리케이션이 뜨고, 테스트가 돌고, health check가 되는 상태를 만든다. 그다음에 도메인을 붙인다.

에이전트도 똑같이 가보려고 한다.

 

초기 Gradle 설정은 다음 기준으로 잡았다.

Java 21
Spring Boot 4.1.0
Spring AI BOM 2.0.0
Spring WebFlux
Spring Boot Actuator


WebFlux는 나중에 SSE event stream을 붙이기 위한 기반이고, Actuator는 M0에서 `/actuator/health` smoke test를 확인하기 위해 둔다.

 

패키지 구조도 처음엔 단순하게

처음 패키지 구조는 욕심내지 않는다.

src/main/java/com/example/pispringai/
  PiSpringAiApplication.java

src/test/java/com/example/pispringai/
  PiSpringAiApplicationTests.java

 

application.yml

설정도 최소한만 둔다.

spring:
  application:
    name: spring-ai-pi
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:}

agent:
  workspace:
    root: .
    max-read-bytes: 200000
    max-directory-entries: 200
    max-grep-matches: 100
  tools:
    write-enabled-by-default: false
    bash-enabled-by-default: false
    bash-timeout-seconds: 30
    max-tool-output-chars: 20000
  session:
    root: .pi-spring-ai/sessions
  model:
    provider: openai
    name: gpt-4.1-mini

 

중요한 점은 아직 OpenAI starter를 붙이지 않았고, API key도 필수가 아니라는 것이다.
`OPENAI_API_KEY`가 없어도 빌드와 테스트는 통과해야 한다.

 

로컬 개발자는 API key 없이도 빌드와 테스트를 실행할 수 있어야 한다.

 

LLM 호출이 필요한 기능은 나중에 fake나 stub으로 대체할 수 있어야 한다.
적어도 기본 테스트가 실제 모델 호출에 묶이면 안 된다.

왜 API key 없이 테스트되어야 할까

AI 프로젝트에서 생각보다 자주 생기는 문제가 있다.

테스트가 실제 LLM API를 호출하는 것이다.

처음에는 편하다.
“진짜 호출되네?” 하고 바로 확인할 수 있다.

그런데 시간이 지나면 문제가 된다.

테스트를 돌릴 때마다 비용이 든다.
네트워크 상태에 따라 실패할 수 있다.
CI에서는 API key가 없어서 깨질 수 있다.
모델 응답이 매번 조금씩 달라져서 테스트가 불안정해질 수 있다.

이러면 테스트가 더 이상 안전망이 아니라 불안 요소가 된다.

그래서 M0부터 기준을 정해둔다.

기본 테스트는 deterministic 해야 한다.
LLM이 필요한 테스트는 fake 또는 stub으로 대체한다.
외부 API가 없어도 프로젝트의 기본 품질은 확인할 수 있어야 한다.

 

아직은 별것 아닌 원칙처럼 보이지만, 나중에 AgentRuntime, Tool, Session이 들어오기 시작하면 이 차이가 커질 것 같다.

에이전트는 기본적으로 비결정적인 요소가 많다.
모델 응답도 변하고, tool 실행 결과도 변하고, 외부 상태도 변한다.

그러니 최소한 우리가 통제할 수 있는 영역은 최대한 deterministic 하게 가져가야 한다.

health endpoint를 넣는 이유

M0에서 Actuator health endpoint를 열어두는 것도 작은 습관에 가깝다.

/actuator/health

 

이런 엔드포인트는 기능 개발 단계에서는 별로 중요해 보이지 않는다.
하지만 백엔드 애플리케이션을 운영한다고 생각하면 가장 기본적인 확인 지점이다.

애플리케이션이 떴는지, 죽었는지, 최소한의 상태 확인이 가능한지.

나중에 이 프로젝트가 실제 agent runtime 형태로 커진다면 health check는 더 중요해질 수 있다.

예를 들어 나중에는 이런 것들을 확인할 수도 있다.

  • 애플리케이션이 떠 있는가
  • 세션 저장소에 접근 가능한가
  • tool 실행 환경이 준비되어 있는가
  • 모델 provider 설정이 유효한가
  • 외부 API timeout이 너무 길지 않은가

지금은 그냥 애플리케이션이 살아 있는지 확인할 수 있는 정도면 충분하다.

완료 기준

이번 단계의 완료 기준은 단순하다.

- 기준 문서와 AGENTS.md가 저장소에서 추적된다.
- 애플리케이션이 실행된다.
- /actuator/health를 확인할 수 있다.
- ./gradlew test가 API key 없이 통과한다.
- README에 실행 방법과 다음 단계가 적혀 있다.
- 아직 agent/tool/session 코드는 없다.

 

마지막 항목이 오히려 중요하다.

“아직 agent/tool/session 코드가 없다.”

M0의 성공은 기능을 많이 넣는 것이 아니다.
오히려 넣지 않기로 한 것을 넣지 않는 것이다.

처음부터 tool calling을 붙이고 싶고, 세션 저장을 만들고 싶고, MCP도 붙여보고 싶겠지만 일단 참는다.

이 프로젝트는 작은 단위로 쌓아가는 것이 목적이다.

마무리

M0는 기능을 만드는 단계라기보다 바닥을 까는 단계다.

Spring Boot 애플리케이션이 뜨고, 테스트가 돌고, health check가 되고, API key 없이도 로컬에서 확인 가능한 상태.

정말 별것 없어 보이지만, 이 상태가 있어야 다음 단계가 편해진다.

건물을 지을 때 첫날부터 인테리어를 하지 않는 것처럼, 에이전트도 첫날부터 tool calling을 붙이지 않는다.

먼저 실행 가능한 작은 Spring Boot 애플리케이션을 만든다.

다음 단계에서는 아주 작은 Chat API를 하나 붙여볼 예정이다.

 

[codex] Bootstrap Spring AI project by dd3ok · Pull Request #1 · dd3ok/spring-pi-agent