
AI 앱 개발의 최소 구성|입력·기억·도구 실행을 분리하여 설계하기
요약
AI 앱 개발 시 모델 중심이 아닌 입력, 문맥, 판단, 실행의 경계를 분리하여 설계하는 최소 구성 방법을 제안합니다. Node.js를 활용해 추가 패키지 없이도 유지보수가 용이한 파이프라인 구축 원리를 설명합니다.
핵심 포인트
- 모델을 중심이 아닌 구조화된 응답을 반환하는 부품으로 취급
- 입력과 모델 출력을 명확한 계약(Contract) 형태로 정의
- 입력, 문맥, 판단, 도구, 출력의 단계를 분리하여 설계
- 실제 모델 대신 결정론적인 DemoModel로 로직 우선 검증
AI 앱을 만들려고 하면, "우선 모델부터 연결하자"라고 생각하게 됩니다. 대화 이력도, 장기 기억도, 도구 실행도 필요해지면서, 어느샌가 첫걸음이 상당히 커지게 됩니다.
하지만 처음에는 더 작아도 괜찮습니다. 저는 모델 선정보다 먼저 "어디서 실패했는지 알 수 있는 구성"을 만드는 것이 나중에 수정하기 더 쉽다고 생각합니다.
이 기사에서는 입력·기억·판단·실행을 분리한 최소 구성을, 추가 패키지 없는 Node.js Demo로 함께 구축해 봅니다.
TL;DR
- AI 앱 개발은 처음부터 채팅 UI나 장기 기억을 정교하게 만들기보다,
입력 → 문맥 → 판단 → 도구 → 출력
을 별도의 경계로 나누는 것이 수정하기 쉽습니다.
- 모델의 응답은 문장이 아니라,
reply
와
tool
의 두 종류로 고정합니다. 쓰기 계열 도구는 모델의 판단만으로는 실행하지 않습니다.
- 최초의 동작 확인에는 실제 모델을 사용하지 않습니다. 결정론적인
DemoModel
으로, 확인 대기와 실행 완료의 2가지 경로를 먼저 테스트합니다.
이 기사에서 다루는 범위
개인용 AI 어시스턴트를 주제로, 최소한의 실행 파이프라인을 만듭니다. 예를 들어 Macaron이 공개한 personal agent라는 체험으로부터는, "과거의 취향을 문맥에 더하고, 필요하다면 행동으로 연결한다"라는 사용자 측의 요구사항을 설정할 수 있습니다.
단, 여기서 구현하는 것은 독립된 학습용 Demo입니다. Macaron의 내부 구성을 설명하거나, 공개된 UI로부터 기술 방식을 추측하는 것이 아닙니다.
검증 환경은 Node.js v24.14.0이며, 추가 패키지는 없습니다. 모델 API로의 접속은 마지막까지 수행하지 않습니다.
최소 구성은 모델이 아니라 경계로부터 결정한다
첫 AI 앱을 하나의 함수로 만들면, 입력문, 대화 이력, 모델 출력, 외부 API, 저장 처리가 같은 곳에 모입니다. 작동하는 동안에는 짧아 보이지만, 실패했을 때 원인을 분리할 수 없습니다.
이번 최소 구성에서는 처리를 다음 6가지로 나눕니다.
flowchart LR
A[Input Contract] --> B[Context Builder]
B --> C[Model Adapter]
...
여기서 중요한 것은 모델을 중심에 두지 않는 것입니다. 모델은 "다음에 무엇을 하고 싶은가"를 구조화하여 반환하는 부품으로 한정합니다. 실행 권한, 기억의 읽기/쓰기, 에러 분류는 앱 측에 남겨둡니다.
입력과 모델 출력을 계약(Contract)으로 만들기
자연어를 그대로 모든 레이어로 전달하면, 어디서 망가졌는지 추적할 수 없습니다. 입구에서는 requestId, userId, text, confirmed만 받습니다.
모델의 응답도 reply 또는 tool로 고정합니다.
export function parseRequest(value) {
if (!value || typeof value !== "object") throw new Error("INPUT_INVALID");
const { requestId, userId, text, confirmed = false } = value;
...
실제 운영 환경에서는 수동 검증을 JSON Schema나 검증 라이브러리로 대체할 수 있습니다. JSON Schema의 required와 additionalProperties를 사용하면, 필수 항목과 불필요한 항목을 계약으로서 다룰 수 있습니다.
"JSON이 반환되었다"는 것만으로는 부족합니다. 허용된 형태의 JSON인지 여부를 도구 실행보다 앞서 확정 짓습니다.
기억은 필요한 파편만 읽어온다
이 Demo에서는 기억을 쓰지 않습니다. 우선 읽기만으로 한정하여, userId에 대응하는 작은 설정을 문맥에 추가합니다.
const profiles = new Map([
["u-1", {
timezone: "Asia/Tokyo",
...
대화 로그를 통째로 전달하지 않고, 이번 판단에 필요한 키만 반환하는 형태입니다. 장기 기억을 늘리기 전에, 취득 범위, 출처, 갱신 권한을 결정할 여지가 남습니다.
도구 선택과 실행 권한을 분리하기
모델이 create_task를 선택하더라도, 그대로 쓰지 않습니다. 도구 등록표에서 존재를 확인하고, 상태를 변경하는 조작에는 confirmed를 요구합니다.
const registry = {
create_task: {
mutates: true,
...
높은 영향력을 가진 작업 전에 사람의 승인을 두는 사고방식은 OWASP의 Excessive Agency 대책과도 일치합니다. 승인은 prompt의 부탁이 아니라, executeTool의 조건으로서 구현합니다.
실제 모델 전에 결정론적인 모델을 두기
처음부터 모델 API를 호출하면 구조적인 문제와 출력의 변동성을 동시에 디버깅해야 합니다. 그래서 입력에 タスク (태스크)가 포함되어 있으면 도구를 선택하는 DemoModel을 준비합니다.
export const demoModel = {
async decide({ input, memory }) {
if (!input.text.includes("タスク")) {
...
이것은 AI의 대용품이 아닙니다. 모델 이외의 배선을, 동일한 입력이라면 동일한 결과가 나오는 상태로 테스트하기 위한 부품입니다.
오케스트레이터는 순서만 가진다
오케스트레이터에 비즈니스 로직을 넣지 않고, 각 경계를 순서대로 호출합니다. 모든 로그에 동일한 requestId를 남깁니다.
import {
parseDecision,
parseRequest,
...
이 로그는 간이 버전이지만 입력, 기억, 판단, 실행을 별도의 stage에서 추적할 수 있습니다. 규모가 커지면 요청 경로를 추적하는 trace, 개별 이벤트를 남기는 log, 건수나 시간을 보는 metric으로 옮길 수 있습니다. OpenTelemetry가 나누고 있는 signals 또한 이러한 역할 분담을 따릅니다.
예외를 무시하지 않고 INPUT_INVALID, MODEL_OUTPUT_INVALID, TOOL_DENIED, TOOL_FAILED로 나눈 것도 재시도 대상을 바꾸기 위해서입니다. 처리되지 않은 예외를 trace 상의 event와 error status로 남기는 방법은 OpenTelemetry의 예외 기록 규약에 정리되어 있습니다.
확인 대기와 실행 완료를 모두 테스트하기
마지막으로, 동일한 요청을 confirmed 값만 바꿔서 두 번 실행합니다.
import { demoModel } from "./demo-model.mjs";
import { run } from "./orchestrator.mjs";
const base = {
...
6개의 파일을 동일한 src 디렉토리에 두고, 다음 명령어로 실행합니다.
node src/index.mjs
첫 번째는 도구를 실행하지 않고 확인 대기 상태가 됩니다. 두 번째에만 태스크를 생성합니다.
{"requestId":"req-001","stage":"tool.finished","status":"needs_confirmation"}
{ status: 'needs_confirmation', call: { name: 'create_task', ... } }
{"requestId":"req-002","stage":"tool.finished","status":"completed"}
...
여기서 확인하고 싶은 것은 답변 문장의 똑똑함이 아닙니다. 미확인 상태의 쓰기 작업이 멈추고, 확인 후에만 동일한 도구가 동작한다는 점입니다.
실제 모델로 교체할 때 유지되는 경계
실제 모델로 연결할 때는 demoModel.decide()와 동일한 반환값을 만드는 adapter만 교체합니다. 나머지 5개는 그대로 유지합니다.
- 입력 검증은 모델 호출 전에 수행한다
- 기억은 검색 결과를 그대로 신뢰하지 않고, 용도에 필요한 항목으로 압축한다
- 모델 출력은 schema 검증 후에만 다음 단계로 넘긴다
- 도구의 허가와 확인은 앱 측에서 판정한다
- tool error와 model error를 나누어 기록한다
프롬프트를 수정하여 정밀도를 높이는 작업과 권한 및 저장 처리를 안전하게 만드는 작업은 별개입니다. 이 둘을 합치면 모델을 변경할 때마다 실행 경계까지 흔들리게 됩니다.
요약
AI 앱 개발의 최소 구성은 기능의 수가 적은 것이 아니라, 실패 지점을 분리할 수 있는 것이라고 생각합니다.
이번 Demo에서는 입력, 기억, 모델 판단, 도구 허가, 실행, 로그를 별도의 책임으로 나누었습니다. 장기 기억 쓰기, 여러 도구의 연속 실행, 재시도, 영속 DB는 아직 구현되지 않았습니다. 필요해진 기능만 기존의 경계에 추가할 수 있습니다.
처음에는 DemoModel 상태로 확인 대기와 실행 완료라는 두 가지 경로를 직접 통과시켜 보세요. 그 부분이 안정된 후에 실제 모델로 교체하면, 모델의 문제와 앱의 문제를 분리하여 추적할 수 있습니다.
참고 문헌
토론 (Discussion)

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기